import { describe, it, expect, vi, afterEach } from "vitest"; // /v1/models is what external CLIs (Codex, Claude Code, aider) read. A capability // the user asserted on the dashboard must show up there, and it must win over the // name heuristic / live-catalog guess. const NODE_ID = "openai-compatible-chat-abc123"; vi.mock("@/lib/localDb", () => ({ getProviderConnections: vi.fn(async () => [ { provider: NODE_ID, isActive: true, providerSpecificData: { prefix: "relay", apiType: "chat", baseUrl: "https://relay.example.com/v1" }, }, ]), getCombos: vi.fn(async () => []), getCustomModels: vi.fn(async () => []), getModelAliases: vi.fn(async () => ({})), })); vi.mock("@/lib/disabledModelsDb", () => ({ getDisabledModels: vi.fn(async () => ({})), })); vi.mock("@/sse/services/auth.js", () => ({ getApiKeyRecord: vi.fn(async () => null), extractApiKey: vi.fn(() => null), })); vi.mock("@/sse/services/tokenRefresh", () => ({ updateProviderCredentials: vi.fn(async () => {}), })); afterEach(() => vi.clearAllMocks()); async function buildWithCustomModels(customModels) { const localDb = await import("@/lib/localDb"); localDb.getCustomModels.mockResolvedValue(customModels); const { buildModelsList } = await import("@/app/api/v1/models/route.js"); // skipDynamicFetch: a real GET /models against the node URL would be network I/O. return buildModelsList(["llm"], { skipDynamicFetch: true }); } describe("/v1/models honors stored per-model capabilities", () => { it("reports asserted vision:false over the name heuristic", async () => { const models = await buildWithCustomModels([ { providerAlias: NODE_ID, id: "qwen3-vl-plus", type: "llm", caps: { vision: false } }, ]); const entry = models.find((m) => m.id === "relay/qwen3-vl-plus"); expect(entry).toBeTruthy(); expect(entry.capabilities.vision).toBe(false); }); it("reports asserted reasoning:true and token limits", async () => { const models = await buildWithCustomModels([ { providerAlias: NODE_ID, id: "my-private-model", type: "llm", caps: { reasoning: true, contextWindow: 32000, maxOutput: 4096 }, }, ]); const entry = models.find((m) => m.id === "relay/my-private-model"); expect(entry.capabilities.reasoning).toBe(true); // Snake_case names are what clients actually read for compaction. expect(entry.context_length).toBe(32000); expect(entry.max_completion_tokens).toBe(4096); }); it("leaves models with no assertions to the heuristics", async () => { const models = await buildWithCustomModels([ { providerAlias: NODE_ID, id: "qwen3-vl-plus", type: "llm" }, ]); const entry = models.find((m) => m.id === "relay/qwen3-vl-plus"); expect(entry.capabilities.vision).toBe(true); }); });