fix: remove the suggest model in provider page

This commit is contained in:
2026-07-19 20:09:43 +07:00
parent c90f620694
commit 16eabec743
36 changed files with 146 additions and 771 deletions
+12 -41
View File
@@ -6,7 +6,6 @@ const getCustomModels = vi.fn();
const getProviderNodes = vi.fn();
const getUsers = vi.fn();
const getDeletedModels = vi.fn();
const getDisabledModels = vi.fn();
const requireUsageDashboardUser = vi.fn();
const getCapabilitiesForModel = vi.fn();
@@ -19,7 +18,6 @@ vi.mock("@/models", () => ({
vi.mock("@/lib/db", () => ({ getUsers, getDeletedModels }));
vi.mock("@/lib/disabledModelsDb", () => ({ getDisabledModels }));
vi.mock("@/lib/auth/currentUser", () => ({
requireUsageDashboardUser,
}));
@@ -27,7 +25,7 @@ vi.mock("open-sse/config/providerModels.js", () => ({
getModelsByProviderId: (providerId) => ({
alpha: [
{ id: "enabled", name: "Enabled model" },
{ id: "disabled", name: "Disabled model" },
{ id: "alternative", name: "Alternative model" },
],
beta: [{ id: "inactive", name: "Inactive provider model" }],
"orbit-provider": [
@@ -63,16 +61,14 @@ describe("GET /api/models/connected", () => {
getProviderNodes.mockReset();
getUsers.mockReset();
getDeletedModels.mockReset();
getDisabledModels.mockReset();
requireUsageDashboardUser.mockReset();
getCapabilitiesForModel.mockReset();
getModelAliases.mockResolvedValue({ "preferred-alpha": "alpha-alias/enabled" });
getDisabledModels.mockResolvedValue({ "alpha-alias": ["disabled"] });
getDeletedModels.mockResolvedValue({});
getCustomModels.mockResolvedValue([
{ providerAlias: "alpha-alias", id: "enabled", name: "Enabled model", type: "llm" },
{ providerAlias: "alpha-alias", id: "disabled", name: "Disabled model", type: "llm" },
{ providerAlias: "alpha-alias", id: "alternative", name: "Alternative model", type: "llm" },
{ providerAlias: "alpha-alias", id: "embedding", name: "Embedding model", type: "embedding" },
{ providerAlias: "beta-alias", id: "inactive", name: "Inactive provider model", type: "llm" },
]);
@@ -85,7 +81,7 @@ describe("GET /api/models/connected", () => {
]);
});
it("returns added models for a connected provider to an administrator, including disabled rows", async () => {
it("returns added models for a connected provider", async () => {
requireUsageDashboardUser.mockResolvedValue({ id: "admin", role: "admin" });
const response = await GET();
@@ -94,14 +90,12 @@ describe("GET /api/models/connected", () => {
expect(response.status).toBe(200);
expect(body.models).toEqual([
expect.objectContaining({
fullModel: "alpha-alias/disabled",
fullModel: "alpha-alias/alternative",
providerAlias: "alpha-alias",
disabled: true,
}),
expect.objectContaining({
fullModel: "alpha-alias/enabled",
alias: "preferred-alpha",
disabled: false,
caps: { vision: false, search: true, reasoning: true },
}),
]);
@@ -126,8 +120,7 @@ describe("GET /api/models/connected", () => {
isCustom: false,
}),
expect.objectContaining({
fullModel: "alpha-alias/disabled",
disabled: true,
fullModel: "alpha-alias/alternative",
isCustom: false,
}),
]));
@@ -171,16 +164,17 @@ describe("GET /api/models/connected", () => {
expect(body.models.filter((model) => model.fullModel === "orbit/claude-opus-4-8")).toHaveLength(1);
});
it("excludes disabled models for non-administrators", async () => {
it("returns the same catalog to non-administrators", async () => {
requireUsageDashboardUser.mockResolvedValue({ id: "member", role: "user" });
const response = await GET();
const body = await response.json();
expect(response.status).toBe(200);
expect(body.models).toEqual([
expect.objectContaining({ fullModel: "alpha-alias/enabled", disabled: false }),
]);
expect(body.models).toEqual(expect.arrayContaining([
expect.objectContaining({ fullModel: "alpha-alias/enabled" }),
expect.objectContaining({ fullModel: "alpha-alias/alternative" }),
]));
});
it("includes administrator-managed compatible-provider models for non-administrators", async () => {
@@ -224,39 +218,16 @@ describe("GET /api/models/connected", () => {
]));
});
it("does not expose disabled compatible-provider models to non-administrators", async () => {
const providerId = "anthropic-compatible-test-node";
requireUsageDashboardUser.mockResolvedValue({ id: "member", role: "user" });
getProviderConnections.mockResolvedValue([
{ provider: providerId, isActive: true, apiKey: "admin-secret", ownerId: "admin" },
]);
getProviderNodes.mockResolvedValue([
{ id: providerId, type: "anthropic-compatible", name: "Company Anthropic" },
]);
getCustomModels.mockResolvedValue([
{ providerAlias: providerId, id: "claude-company", type: "llm" },
]);
getDisabledModels.mockResolvedValue({ [providerId]: ["claude-company"] });
const response = await GET();
const body = await response.json();
expect(response.status).toBe(200);
expect(body.models).not.toEqual(expect.arrayContaining([
expect.objectContaining({ fullModel: `${providerId}/claude-company` }),
]));
});
it("does not expose permanently deleted models to administrators", async () => {
requireUsageDashboardUser.mockResolvedValue({ id: "admin", role: "admin" });
getDeletedModels.mockResolvedValue({ "alpha-alias": ["disabled", "enabled"] });
getDeletedModels.mockResolvedValue({ "alpha-alias": ["alternative", "enabled"] });
const response = await GET();
const body = await response.json();
expect(response.status).toBe(200);
expect(body.models).not.toEqual(expect.arrayContaining([
expect.objectContaining({ fullModel: "alpha-alias/disabled" }),
expect.objectContaining({ fullModel: "alpha-alias/alternative" }),
expect.objectContaining({ fullModel: "alpha-alias/enabled" }),
]));
});
+1 -5
View File
@@ -66,7 +66,7 @@ describe("DB Concurrency — atomic safety", () => {
expect(list.pagination.totalItems).toBeGreaterThanOrEqual(N);
}, 15000);
it("mixed concurrent: usage + details + connections + aliases", async () => {
it("mixed concurrent: usage, aliases, and connections", async () => {
const ops = [];
for (let i = 0; i < 50; i++) {
ops.push(db.saveRequestUsage({
@@ -74,16 +74,12 @@ describe("DB Concurrency — atomic safety", () => {
tokens: { prompt_tokens: 20 }, status: "ok",
}));
ops.push(db.setModelAlias(`a-${i}`, `target-${i}`));
ops.push(db.disableModels("openai", [`d-${i}`]));
}
await Promise.all(ops);
const aliases = await db.getModelAliases();
expect(Object.keys(aliases).filter((k) => k.startsWith("a-")).length).toBe(50);
const disabled = await db.getDisabledByProvider("openai");
expect(disabled.length).toBeGreaterThanOrEqual(50);
const stats = await db.getUsageStats("24h");
expect(stats.byProvider.anthropic.requests).toBe(50);
}, 30000);
+1 -23
View File
@@ -275,15 +275,6 @@ describe("DB SQLite layer — public API parity", () => {
expect(all.cursor).toEqual({ "gpt-5": "claude-3" });
});
it("disabledModels: add/remove per provider", async () => {
await sqliteDb.disableModels("openai", ["gpt-3", "gpt-4"]);
expect(await sqliteDb.getDisabledByProvider("openai")).toEqual(expect.arrayContaining(["gpt-3", "gpt-4"]));
await sqliteDb.enableModels("openai", ["gpt-3"]);
expect(await sqliteDb.getDisabledByProvider("openai")).toEqual(["gpt-4"]);
await sqliteDb.enableModels("openai", []);
expect(await sqliteDb.getDisabledByProvider("openai")).toEqual([]);
});
it("usage: saveRequestUsage + getUsageHistory + getUsageStats", async () => {
await sqliteDb.saveRequestUsage({
provider: "openai", model: "gpt-4", connectionId: "c1",
@@ -341,6 +332,7 @@ describe("DB SQLite layer — public API parity", () => {
expect(exported.settings).toBeDefined();
expect(Array.isArray(exported.providerConnections)).toBe(true);
expect(typeof exported.modelAliases).toBe("object");
expect(exported).not.toHaveProperty("disabledModels");
// Add marker, export, import a different payload, verify reset
await sqliteDb.setModelAlias("marker", "before");
@@ -353,20 +345,6 @@ describe("DB SQLite layer — public API parity", () => {
expect((await sqliteDb.getModelAliases()).marker).toBe("before");
});
it("exportDb / importDb preserves disabled model settings", async () => {
await sqliteDb.disableModels("backup-provider", ["backup-model"]);
const snapshot = await sqliteDb.exportDb();
expect(snapshot.disabledModels).toMatchObject({
"backup-provider": ["backup-model"],
});
await sqliteDb.enableModels("backup-provider", []);
await sqliteDb.importDb(snapshot);
expect(await sqliteDb.getDisabledByProvider("backup-provider")).toEqual(["backup-model"]);
});
it("pricing: user pricing merged with constants", async () => {
await sqliteDb.updatePricing({ openai: { "gpt-test": { input: 1, output: 2 } } });
const p = await sqliteDb.getPricing();
@@ -0,0 +1,51 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const isDeletedModel = vi.fn();
vi.mock("@/lib/db", () => ({ isDeletedModel }));
const { getDeletedModelResponse } = await import("../../src/sse/services/deletedModels.js");
describe("getDeletedModelResponse", () => {
beforeEach(() => {
isDeletedModel.mockReset();
});
it("allows a model that has not been permanently deleted", async () => {
isDeletedModel.mockResolvedValue(false);
await expect(getDeletedModelResponse("openai", "gpt-available")).resolves.toBeNull();
expect(isDeletedModel).toHaveBeenCalledWith("openai", "gpt-available");
});
it("blocks a permanently deleted model", async () => {
isDeletedModel.mockResolvedValue(true);
const response = await getDeletedModelResponse("openai", "gpt-deleted");
expect(response.status).toBe(404);
await expect(response.json()).resolves.toMatchObject({
error: {
code: "model_not_found",
message: "Model openai/gpt-deleted has been deleted by an administrator",
},
});
});
it("blocks permanently deleted thinking variants", async () => {
isDeletedModel.mockResolvedValue(true);
const response = await getDeletedModelResponse("codex", "gpt-deleted(high)");
expect(response.status).toBe(404);
expect(isDeletedModel).toHaveBeenCalledWith("codex", "gpt-deleted(high)");
});
it("fails closed when deleted-model storage cannot be read", async () => {
isDeletedModel.mockRejectedValue(new Error("database unavailable"));
const response = await getDeletedModelResponse("openai", "gpt-available");
expect(response.status).toBe(500);
});
});
@@ -1,85 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const getDisabledModels = vi.fn();
const getDeletedModels = vi.fn();
vi.mock("@/lib/disabledModelsDb", () => ({ getDisabledModels }));
vi.mock("@/lib/db", () => ({ getDeletedModels }));
vi.mock("@/shared/constants/providers", () => ({
getProviderAlias: (provider) => ({ openai: "oa", claude: "claude" })[provider] || provider,
}));
const { getDisabledModelResponse } = await import("../../src/sse/services/disabledModels.js");
describe("getDisabledModelResponse", () => {
beforeEach(() => {
getDisabledModels.mockReset();
getDeletedModels.mockReset();
getDeletedModels.mockResolvedValue({});
});
it("allows an enabled model", async () => {
getDisabledModels.mockResolvedValue({ oa: ["gpt-disabled"] });
await expect(getDisabledModelResponse("openai", "gpt-enabled")).resolves.toBeNull();
});
it("blocks a model disabled under the provider alias", async () => {
getDisabledModels.mockResolvedValue({ oa: ["gpt-disabled"] });
const response = await getDisabledModelResponse("openai", "gpt-disabled");
expect(response.status).toBe(404);
await expect(response.json()).resolves.toMatchObject({
error: {
code: "model_not_found",
message: "Model openai/gpt-disabled is disabled by an administrator",
},
});
});
it("blocks a model disabled under the provider ID", async () => {
getDisabledModels.mockResolvedValue({ openai: ["gpt-disabled"] });
const response = await getDisabledModelResponse("openai", "gpt-disabled");
expect(response.status).toBe(404);
});
it("blocks thinking variants when their base model is disabled", async () => {
getDisabledModels.mockResolvedValue({ codex: ["gpt-5.6-sol"] });
const response = await getDisabledModelResponse("codex", "gpt-5.6-sol(high)");
expect(response.status).toBe(404);
await expect(response.json()).resolves.toMatchObject({
error: {
code: "model_not_found",
message: "Model codex/gpt-5.6-sol(high) is disabled by an administrator",
},
});
});
it("blocks a permanently deleted model", async () => {
getDisabledModels.mockResolvedValue({});
getDeletedModels.mockResolvedValue({ oa: ["gpt-deleted"] });
const response = await getDisabledModelResponse("openai", "gpt-deleted");
expect(response.status).toBe(404);
await expect(response.json()).resolves.toMatchObject({
error: {
code: "model_not_found",
message: "Model openai/gpt-deleted has been deleted by an administrator",
},
});
});
it("fails closed when disabled-model storage cannot be read", async () => {
getDisabledModels.mockRejectedValue(new Error("database unavailable"));
const response = await getDisabledModelResponse("openai", "gpt-enabled");
expect(response.status).toBe(500);
});
});
-69
View File
@@ -1,69 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const getDisabledModels = vi.fn();
const disableModels = vi.fn();
const enableModels = vi.fn();
const requireAdminUser = vi.fn();
vi.mock("@/lib/disabledModelsDb", () => ({
getDisabledModels,
disableModels,
enableModels,
}));
vi.mock("@/lib/auth/currentUser", () => ({ requireAdminUser }));
const { GET, POST, DELETE } = await import("../../src/app/api/models/disabled/route.js");
describe("/api/models/disabled", () => {
beforeEach(() => {
getDisabledModels.mockReset();
disableModels.mockReset();
enableModels.mockReset();
requireAdminUser.mockReset();
});
it("keeps disabled model reads available for model selectors", async () => {
getDisabledModels.mockResolvedValue({ claude: ["claude-disabled"] });
const response = await GET(new Request("http://localhost/api/models/disabled?providerAlias=claude"));
expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual({ ids: ["claude-disabled"] });
expect(requireAdminUser).not.toHaveBeenCalled();
});
it("rejects a non-admin disable request", async () => {
requireAdminUser.mockRejectedValue(new Error("Forbidden"));
const response = await POST(new Request("http://localhost/api/models/disabled", {
method: "POST",
body: JSON.stringify({ providerAlias: "claude", ids: ["claude-disabled"] }),
}));
expect(response.status).toBe(403);
expect(disableModels).not.toHaveBeenCalled();
});
it("allows an admin to disable models", async () => {
requireAdminUser.mockResolvedValue({ role: "admin" });
const response = await POST(new Request("http://localhost/api/models/disabled", {
method: "POST",
body: JSON.stringify({ providerAlias: "claude", ids: ["claude-disabled"] }),
}));
expect(response.status).toBe(200);
expect(disableModels).toHaveBeenCalledWith("claude", ["claude-disabled"]);
});
it("rejects a non-admin enable request", async () => {
requireAdminUser.mockRejectedValue(new Error("Forbidden"));
const response = await DELETE(new Request("http://localhost/api/models/disabled?providerAlias=claude&id=claude-disabled", {
method: "DELETE",
}));
expect(response.status).toBe(403);
expect(enableModels).not.toHaveBeenCalled();
});
});
@@ -42,7 +42,6 @@ describe("permanent model deletion", () => {
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 },
@@ -131,7 +130,6 @@ describe("permanent model deletion", () => {
removedAliases: 1,
removedCustomModels: 1,
removedPricingEntries: 1,
removedDisabledModels: 1,
removedRequestDetails: 1,
updatedCliToolConfigs: 2,
updatedComboIds: [mixedCombo.id],
@@ -146,7 +144,6 @@ describe("permanent model deletion", () => {
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 },
});