import { describe, expect, it } from "vitest"; import { getModelEnvVars, getModelProvider, modelAliases, parseModel, providers, resolveCliModel, resolveDisplayAlias, resolveModelSlug, resolveOpenRouterModel, } from "./models.ts"; describe("parseModel", () => { it("parses provider/model format", () => { const result = parseModel("anthropic/claude-opus"); expect(result).toEqual({ provider: "anthropic", model: "claude-opus" }); }); it("handles nested slashes (openrouter format)", () => { const result = parseModel("openrouter/anthropic/claude-opus-4.6"); expect(result).toEqual({ provider: "openrouter", model: "anthropic/claude-opus-4.6" }); }); it("throws on invalid slug without slash", () => { expect(() => parseModel("invalid")).toThrow("invalid model slug"); }); }); describe("getModelProvider", () => { it("extracts provider from slug", () => { expect(getModelProvider("anthropic/claude-opus")).toBe("anthropic"); expect(getModelProvider("openai/gpt")).toBe("openai"); expect(getModelProvider("google/gemini-pro")).toBe("google"); }); }); describe("getModelEnvVars", () => { it("returns correct env vars for anthropic", () => { expect(getModelEnvVars("anthropic/claude-opus")).toEqual([ "ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN", ]); }); it("returns correct env vars for google (multiple)", () => { const envVars = getModelEnvVars("google/gemini-pro"); expect(envVars).toContain("GOOGLE_GENERATIVE_AI_API_KEY"); expect(envVars).toContain("GEMINI_API_KEY"); }); it("returns empty array for unknown provider", () => { expect(getModelEnvVars("unknown/model")).toEqual([]); }); it("returns empty env vars for free opencode models", () => { expect(getModelEnvVars("opencode/big-pickle")).toEqual([]); expect(getModelEnvVars("opencode/mimo-v2-pro-free")).toEqual([]); expect(getModelEnvVars("opencode/minimax-m2.5-free")).toEqual([]); }); it("still requires OPENCODE_API_KEY for non-free opencode models", () => { expect(getModelEnvVars("opencode/claude-opus")).toEqual(["OPENCODE_API_KEY"]); expect(getModelEnvVars("opencode/gpt-5-nano")).toEqual(["OPENCODE_API_KEY"]); }); }); describe("resolveModelSlug", () => { it("resolves known alias to concrete specifier", () => { const resolved = resolveModelSlug("anthropic/claude-opus"); expect(resolved).toBe("anthropic/claude-opus-4-7"); }); it("resolves openai alias", () => { const resolved = resolveModelSlug("openai/gpt"); expect(resolved).toBe("openai/gpt-5.5"); }); it("returns the raw resolve for deprecated aliases (does not walk fallback)", () => { expect(resolveModelSlug("openai/gpt-codex")).toBe("openai/gpt-5.3-codex"); }); it("returns undefined for unknown slug", () => { expect(resolveModelSlug("unknown/model")).toBeUndefined(); }); }); describe("resolveCliModel", () => { it("returns same as resolveModelSlug (models.dev specifier)", () => { const slug = "anthropic/claude-opus"; expect(resolveCliModel(slug)).toBe(resolveModelSlug(slug)); }); it("returns undefined for unknown slug", () => { expect(resolveCliModel("bogus/nope")).toBeUndefined(); }); it("walks fallback chain for deprecated deepseek aliases", () => { expect(resolveCliModel("deepseek/deepseek-reasoner")).toBe("deepseek/deepseek-v4-pro"); expect(resolveCliModel("deepseek/deepseek-chat")).toBe("deepseek/deepseek-v4-flash"); }); it("walks fallback chain for deprecated openai codex aliases", () => { expect(resolveCliModel("openai/gpt-codex")).toBe("openai/gpt-5.5"); expect(resolveCliModel("openai/gpt-codex-mini")).toBe("openai/gpt-5.4-mini"); expect(resolveCliModel("opencode/gpt-codex")).toBe("opencode/gpt-5.5"); expect(resolveCliModel("openrouter/gpt-codex")).toBe("openrouter/openai/gpt-5.5"); }); }); describe("resolveDisplayAlias", () => { it("returns the alias itself for a non-deprecated slug", () => { const alias = resolveDisplayAlias("anthropic/claude-opus"); expect(alias?.slug).toBe("anthropic/claude-opus"); expect(alias?.displayName).toBe("Claude Opus"); }); it("walks fallback chain to terminal alias for deprecated slug", () => { const alias = resolveDisplayAlias("openai/gpt-codex"); expect(alias?.slug).toBe("openai/gpt"); expect(alias?.displayName).toBe("GPT"); }); it("walks fallback chain for deepseek-reasoner -> deepseek-pro", () => { const alias = resolveDisplayAlias("deepseek/deepseek-reasoner"); expect(alias?.slug).toBe("deepseek/deepseek-pro"); expect(alias?.displayName).toBe("DeepSeek Pro"); }); it("returns undefined for unknown slug", () => { expect(resolveDisplayAlias("bogus/nope")).toBeUndefined(); }); }); describe("resolveOpenRouterModel", () => { it("returns the openrouter specifier for a non-deprecated alias", () => { expect(resolveOpenRouterModel("anthropic/claude-opus")).toBe( "openrouter/anthropic/claude-opus-4.7" ); }); it("walks fallback chain for deprecated deepseek aliases", () => { expect(resolveOpenRouterModel("deepseek/deepseek-reasoner")).toBe( "openrouter/deepseek/deepseek-v4-pro" ); expect(resolveOpenRouterModel("deepseek/deepseek-chat")).toBe( "openrouter/deepseek/deepseek-v4-flash" ); expect(resolveOpenRouterModel("openrouter/deepseek-chat")).toBe( "openrouter/deepseek/deepseek-v4-flash" ); }); it("walks fallback chain for deprecated openai codex aliases", () => { expect(resolveOpenRouterModel("openai/gpt-codex")).toBe("openrouter/openai/gpt-5.5"); expect(resolveOpenRouterModel("openai/gpt-codex-mini")).toBe("openrouter/openai/gpt-5.4-mini"); }); it("returns undefined for free opencode models with no openrouter equivalent", () => { expect(resolveOpenRouterModel("opencode/big-pickle")).toBeUndefined(); }); it("returns undefined for unknown slug", () => { expect(resolveOpenRouterModel("bogus/nope")).toBeUndefined(); }); }); describe("modelAliases registry", () => { it("has at least one model per provider", () => { for (const providerKey of Object.keys(providers)) { const providerModels = modelAliases.filter((a) => a.provider === providerKey); expect(providerModels.length).toBeGreaterThan(0); } }); it("has exactly one preferred model per provider", () => { for (const providerKey of Object.keys(providers)) { const preferred = modelAliases.filter((a) => a.provider === providerKey && a.preferred); expect(preferred.length, `${providerKey} should have exactly 1 preferred model`).toBe(1); } }); it("all slugs follow provider/model format", () => { for (const alias of modelAliases) { expect(alias.slug).toContain("/"); const parsed = parseModel(alias.slug); expect(parsed.provider).toBe(alias.provider); } }); it("all resolve values follow provider/model format", () => { for (const alias of modelAliases) { expect(alias.resolve).toContain("/"); } }); it("slugs are unique", () => { const slugs = modelAliases.map((a) => a.slug); expect(new Set(slugs).size).toBe(slugs.length); }); }); describe("providers registry", () => { it("every provider has envVars", () => { for (const [key, config] of Object.entries(providers)) { expect(config.envVars.length, `${key} should have env vars`).toBeGreaterThan(0); } }); it("every provider has a displayName", () => { for (const [key, config] of Object.entries(providers)) { expect(config.displayName, `${key} should have a displayName`).toBeTruthy(); } }); });