mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
fix: remove the suggest model in provider page
This commit is contained in:
@@ -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" }),
|
||||
]));
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user