fix: update the logic for remove model in provider

This commit is contained in:
2026-07-19 18:32:14 +07:00
parent fa7cd62d58
commit 62975ab15b
25 changed files with 1209 additions and 172 deletions
+225
View File
@@ -0,0 +1,225 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const originalDataDir = process.env.DATA_DIR;
let tempDir;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "9router-model-delete-"));
process.env.DATA_DIR = tempDir;
delete global._dbAdapter;
delete global._pendingRequests;
delete global._pendingTimers;
delete global._recentRing;
vi.resetModules();
});
afterEach(() => {
try { global._dbAdapter?.instance?.close?.(); } catch {}
delete global._dbAdapter;
fs.rmSync(tempDir, { recursive: true, force: true });
if (originalDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = originalDataDir;
});
describe("permanent model deletion", () => {
it("removes dependent catalog, combo, pricing, and observability data while retaining usage history", async () => {
const db = await import("@/lib/db/index.js");
const providerId = "openai-compatible-cascade-test";
const providerPrefix = "cascade";
const modelId = "gpt-delete";
await db.createProviderNode({
id: providerId,
type: "openai-compatible",
name: "Cascade Test Provider",
prefix: providerPrefix,
baseUrl: "https://example.invalid/v1",
});
await db.setModelAlias("deleted-alias", `${providerPrefix}/${modelId}`);
await db.setModelAlias("keep-alias", `${providerPrefix}/gpt-keep`);
await db.addCustomModel({ providerAlias: providerPrefix, id: modelId, type: "llm" });
await db.addCustomModel({ providerAlias: providerPrefix, id: "gpt-keep", type: "llm" });
await db.disableModels(providerPrefix, [modelId]);
await db.updatePricing({
[providerPrefix]: {
[modelId]: { prompt: 1, completion: 2 },
"gpt-keep": { prompt: 3, completion: 4 },
},
});
const mixedCombo = await db.createCombo({
name: "cascade-mixed",
models: [`${providerPrefix}/${modelId}`, `${providerPrefix}/gpt-keep`],
});
const aliasCombo = await db.createCombo({
name: "cascade-alias-only",
models: ["deleted-alias"],
});
await db.updateSettings({
comboStrategies: {
[mixedCombo.id]: {
fallbackStrategy: "fusion",
judgeModel: "deleted-alias",
},
[aliasCombo.id]: { fallbackStrategy: "round-robin" },
},
});
const cliUser = await db.createUser({ username: "cascade-cli-user", password: "password", role: "user" });
await db.upsertCliToolConfig(cliUser.id, "codex", {
baseUrl: "http://127.0.0.1:20127",
codexModel: "deleted-alias",
codexThinking: "high",
});
await db.upsertCliToolConfig(cliUser.id, "cowork", {
baseUrl: "http://127.0.0.1:20127",
selectedModels: ["deleted-alias", `${providerPrefix}/gpt-keep`],
coworkThinking: { "deleted-alias": "high", [`${providerPrefix}/gpt-keep`]: "low" },
});
await db.saveRequestUsage({
timestamp: "2026-07-18T10:00:00.000Z",
provider: providerId,
model: modelId,
connectionId: "cascade-connection",
tokens: { prompt_tokens: 10, completion_tokens: 5 },
endpoint: "/v1/chat/completions",
});
await db.saveRequestUsage({
timestamp: "2026-07-18T10:01:00.000Z",
provider: providerPrefix,
model: `${modelId}(high)`,
connectionId: "cascade-connection",
tokens: { prompt_tokens: 20, completion_tokens: 10 },
endpoint: "/v1/chat/completions",
});
await db.saveRequestUsage({
timestamp: "2026-07-18T10:02:00.000Z",
provider: providerId,
model: "gpt-keep",
connectionId: "cascade-connection",
tokens: { prompt_tokens: 30, completion_tokens: 15 },
endpoint: "/v1/chat/completions",
});
await db.updateSettings({ enableObservability: true, observabilityBatchSize: 1 });
await db.saveRequestDetail({
id: "cascade-deleted-detail",
provider: providerPrefix,
model: modelId,
status: "ok",
request: {},
response: {},
});
await db.saveRequestDetail({
id: "cascade-keep-detail",
provider: providerId,
model: "gpt-keep",
status: "ok",
request: {},
response: {},
});
await new Promise((resolve) => setTimeout(resolve, 100));
const result = await db.deleteModelPermanently(providerId, modelId);
db.purgeRequestDetailBuffer(result.providerAliases, result.modelId);
expect(result).toMatchObject({
deleted: true,
removedAliases: 1,
removedCustomModels: 1,
removedPricingEntries: 1,
removedDisabledModels: 1,
removedRequestDetails: 1,
updatedCliToolConfigs: 2,
updatedComboIds: [mixedCombo.id],
deletedComboIds: [aliasCombo.id],
});
expect(result.providerAliases).toEqual(expect.arrayContaining([providerId, providerPrefix]));
expect(await db.isDeletedModel(providerId, modelId)).toBe(true);
expect(await db.isDeletedModel(providerId, `${modelId}(high)`)).toBe(true);
expect((await db.getDeletedModels())[providerId]).toContain(modelId);
expect(await db.getModelAliases()).toEqual({ "keep-alias": `${providerPrefix}/gpt-keep` });
expect(await db.getCustomModels()).toEqual([
expect.objectContaining({ providerAlias: providerPrefix, id: "gpt-keep" }),
]);
expect(await db.getDisabledByProvider(providerPrefix)).toEqual([]);
expect((await db.getPricing())[providerPrefix]).toEqual({
"gpt-keep": { prompt: 3, completion: 4 },
});
expect(await db.getComboById(mixedCombo.id)).toMatchObject({
models: [`${providerPrefix}/gpt-keep`],
});
expect(await db.getComboById(aliasCombo.id)).toBeNull();
expect((await db.getSettings()).comboStrategies).toEqual({
[mixedCombo.id]: { fallbackStrategy: "fusion" },
});
expect((await db.getCliToolConfig(cliUser.id, "codex")).config).toMatchObject({
codexModel: "",
codexThinking: "",
});
expect((await db.getCliToolConfig(cliUser.id, "cowork")).config).toMatchObject({
selectedModels: [`${providerPrefix}/gpt-keep`],
coworkThinking: { [`${providerPrefix}/gpt-keep`]: "low" },
});
const history = await db.getUsageHistory({});
expect(history).toEqual(expect.arrayContaining([
expect.objectContaining({ provider: providerId, model: modelId }),
expect.objectContaining({ provider: providerPrefix, model: `${modelId}(high)` }),
expect.objectContaining({ provider: providerId, model: "gpt-keep" }),
]));
expect(history).toHaveLength(3);
const stats = await db.getUsageStats("all");
expect(stats.totalRequests).toBe(3);
expect(stats.byModel).toMatchObject({
[`${modelId} (${providerId})`]: expect.objectContaining({ requests: 1, promptTokens: 10, completionTokens: 5 }),
[`${modelId}(high) (${providerPrefix})`]: expect.objectContaining({ requests: 1, promptTokens: 20, completionTokens: 10 }),
[`gpt-keep (${providerId})`]: expect.objectContaining({ requests: 1 }),
});
expect(await db.getRequestDetailById("cascade-deleted-detail")).toBeNull();
expect(await db.getRequestDetailById("cascade-keep-detail")).toMatchObject({ id: "cascade-keep-detail" });
await db.saveRequestUsage({
timestamp: "2026-07-18T10:03:00.000Z",
provider: providerId,
model: modelId,
tokens: { prompt_tokens: 100, completion_tokens: 100 },
});
await db.saveRequestDetail({
id: "cascade-deleted-detail-after-tombstone",
provider: providerId,
model: modelId,
status: "ok",
request: {},
response: {},
});
await new Promise((resolve) => setTimeout(resolve, 100));
const historyAfterTombstone = await db.getUsageHistory({});
expect(historyAfterTombstone).toHaveLength(4);
expect(historyAfterTombstone).toEqual(expect.arrayContaining([
expect.objectContaining({
provider: providerId,
model: modelId,
tokens: expect.objectContaining({ prompt_tokens: 100, completion_tokens: 100 }),
}),
]));
const statsAfterTombstone = await db.getUsageStats("all");
expect(statsAfterTombstone.totalRequests).toBe(4);
expect(statsAfterTombstone.byModel[`${modelId} (${providerId})`]).toMatchObject({
requests: 2,
promptTokens: 110,
completionTokens: 105,
});
expect(await db.getRequestDetailById("cascade-deleted-detail-after-tombstone")).toBeNull();
const backup = await db.exportDb();
expect(backup.deletedModels).toMatchObject({ [providerId]: [modelId] });
});
});