import { describe, it, expect } from "vitest"; import { customModelKey, parseCustomModelKey, isCustomModelKey, parseModelListing, mergeCustomModels, normalizeBaseUrl, normalizeModelsPath, createCustomEndpointAdapter } from "@/lib/ai/providers/custom"; import { parseModelKey, isProviderId, PROVIDER_IDS, KEYED_PROVIDER_IDS } from "@/lib/ai/core/types"; import { getAdapter, PROVIDER_META } from "@/lib/ai/providers"; import { PROVIDERS, PROVIDER_ORDER } from "@/lib/client/providers"; describe("custom model keys", () => { it("round-trips ids that contain colons (Ollama tags)", () => { const key = customModelKey("cep_abc123", "llama3.1:8b-instruct-q4_K_M"); expect(key).toBe("custom/cep_abc123:llama3.1:8b-instruct-q4_K_M"); expect(parseCustomModelKey(key)).toEqual({ endpointId: "cep_abc123", modelId: "llama3.1:8b-instruct-q4_K_M" }); expect(isCustomModelKey(key)).toBe(true); }); it("keeps parseModelKey compatible: provider is `custom`, id keeps the endpoint prefix", () => { const parsed = parseModelKey("custom/cep_x:mistral-nemo"); expect(parsed).toEqual({ provider: "custom", id: "cep_x:mistral-nemo" }); expect(isProviderId("custom")).toBe(true); expect(PROVIDER_IDS).toContain("custom"); expect(KEYED_PROVIDER_IDS).not.toContain("custom"); }); it("rejects malformed keys", () => { expect(parseCustomModelKey("openai/gpt-5")).toBeNull(); expect(parseCustomModelKey("custom/noendpoint")).toBeNull(); expect(parseCustomModelKey("custom/cep_x:")).toBeNull(); expect(parseCustomModelKey("custom/:model")).toBeNull(); }); }); describe("exhaustive provider maps include `custom`", () => { it("has metadata, colors and a placeholder adapter", () => { expect(PROVIDER_META.custom.color).toBe("var(--p-custom)"); expect(PROVIDERS.custom.colorVar).toBe("var(--p-custom)"); expect(PROVIDER_ORDER).not.toContain("custom"); // keyed providers only in the key list expect(getAdapter("custom").id).toBe("custom"); }); it("placeholder adapter fails loudly instead of calling anything", async () => { const a = getAdapter("custom"); const v = await a.validateApiKey("x"); expect(v.ok).toBe(false); const events = []; for await (const ev of a.streamChat({ provider: "custom", model: "m", apiKey: "k", messages: [] })) events.push(ev); expect(events[0]).toMatchObject({ type: "error" }); }); }); describe("discovery parsing + merge", () => { it("understands OpenAI, bare-array and Ollama listings", () => { expect(parseModelListing({ data: [{ id: "a", owned_by: "me" }, { id: "b" }] }).map((m) => m.id)).toEqual(["a", "b"]); expect(parseModelListing(["x", { id: "y" }]).map((m) => m.id)).toEqual(["x", "y"]); expect(parseModelListing({ models: [{ name: "llama3:8b", size: 1 }, { model: "phi3" }] }).map((m) => m.id)).toEqual(["llama3:8b", "phi3"]); expect(parseModelListing({ nope: true })).toEqual([]); expect(parseModelListing(null)).toEqual([]); }); it("applies manual declarations as capability overrides and appends missing ids", () => { const cfg = { id: "cep_1", name: "My Ollama" }; const models = mergeCustomModels(cfg, [{ id: "llama3.2-vision:11b", owned_by: "library" }, { id: "qwen2.5:7b" }], [{ id: "llama3.2-vision:11b", vision: true, tools: true, contextTokens: 128_000 }, { id: "manual-only", displayName: "Manual", reasoning: true }]); expect(models.map((m) => m.key)).toEqual(["custom/cep_1:llama3.2-vision:11b", "custom/cep_1:qwen2.5:7b", "custom/cep_1:manual-only"]); const vision = models[0]; expect(vision.provider).toBe("custom"); expect(vision.capabilities.vision).toBe(true); expect(vision.capabilities.tools).toBe(true); expect(vision.limits?.contextTokens).toBe(128_000); expect(vision.metadata?.source).toBe("discovery"); expect(vision.pricing).toBeNull(); // never invent a price expect(models[1].capabilities.vision).toBe(false); expect(models[1].capabilities.tools).toBe(false); expect(models[2].displayName).toBe("Manual"); expect(models[2].capabilities.reasoning).toBe(true); expect(models[2].metadata?.source).toBe("manual"); expect(models[2].family).toBe("My Ollama"); }); it("normalizes base URLs and model paths", () => { expect(normalizeBaseUrl(" http://localhost:11434/v1/// ")).toBe("http://localhost:11434/v1"); expect(normalizeModelsPath("models")).toBe("/models"); expect(normalizeModelsPath(" ")).toBe(""); expect(normalizeModelsPath(null)).toBe(""); }); it("builds a per-endpoint adapter with id `custom` and the endpoint's name", () => { const a = createCustomEndpointAdapter({ id: "cep_2", name: "LM Studio", baseUrl: "http://127.0.0.1:1234/v1/", headers: { "X-Test": "1" } }); expect(a.id).toBe("custom"); expect(a.name).toBe("LM Studio"); const err = a.normalizeError(new Error("boom")); expect(err.provider).toBe("custom"); }); });