import { describe, expect, test } from "bun:test"; import { type Api, Effort, type Model } from "@oh-my-pi/pi-ai"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; import { DEFAULT_MODEL_PER_PROVIDER } from "@oh-my-pi/pi-catalog/provider-models"; import { expandRoleAlias, extractExplicitThinkingSelector, filterAvailableModelsByEnabledPatterns, parseModelPattern, parseModelString, pickDefaultAvailableModel, resolveAgentModelPatterns, resolveAgentModelSelection, resolveAgentPrewalkPattern, resolveAllowedModels, resolveCliModel, resolveExplicitModelRole, resolveModelFromString, resolveModelOverride, resolveModelRoleValue, resolveModelScope, } from "@oh-my-pi/pi-coding-agent/config/model-resolver"; import { DEFAULT_MODEL_ROLE_ALIAS, LEGACY_MODEL_ROLE_ALIAS_PREFIX } from "@oh-my-pi/pi-coding-agent/config/model-roles"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; // Mock models for testing const mockModels: Model<"anthropic-messages">[] = [ buildModel({ id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", api: "anthropic-messages", provider: "anthropic", baseUrl: "https://api.anthropic.com", reasoning: true, thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High], }, input: ["text", "image"], cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, contextWindow: 200000, maxTokens: 8192, }), buildModel({ id: "gpt-4o", name: "GPT-4o", api: "anthropic-messages", // Using same type for simplicity provider: "openai", baseUrl: "https://api.openai.com", reasoning: false, input: ["text", "image"], cost: { input: 5, output: 15, cacheRead: 0.5, cacheWrite: 5 }, contextWindow: 128000, maxTokens: 4096, }), ]; // Mock OpenRouter models with colons in IDs const mockOpenRouterModels: Model[] = [ buildModel({ id: "qwen/qwen3-coder:exacto", name: "Qwen3 Coder Exacto", api: "anthropic-messages", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High], }, input: ["text"], cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, contextWindow: 128000, maxTokens: 8192, }), buildModel({ id: "openai/gpt-4o:extended", name: "GPT-4o Extended", api: "anthropic-messages", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: false, input: ["text", "image"], cost: { input: 5, output: 15, cacheRead: 0.5, cacheWrite: 5 }, contextWindow: 128000, maxTokens: 4096, }), buildModel({ id: "z-ai/glm-4.7", name: "GLM 4.7", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High], }, input: ["text"], cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, contextWindow: 128000, maxTokens: 8192, }), buildModel({ id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, input: ["text"], cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, contextWindow: 128000, maxTokens: 8192, }), ]; const mockMaxSuffixModels: Model[] = [ buildModel({ id: "coding-router", name: "NanoGPT Coding Router", api: "openai-completions", provider: "nanogpt", baseUrl: "https://nano-gpt.com/api/v1", reasoning: true, thinking: { mode: "effort", efforts: [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh], }, input: ["text"], cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192, }), buildModel({ id: "coding-router:max", name: "NanoGPT Coding Router Max", api: "openai-completions", provider: "nanogpt", baseUrl: "https://nano-gpt.com/api/v1", reasoning: false, input: ["text"], cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192, }), buildModel({ id: "coding-router:low", name: "NanoGPT Coding Router Low", api: "openai-completions", provider: "nanogpt", baseUrl: "https://nano-gpt.com/api/v1", reasoning: true, thinking: { mode: "effort", efforts: [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh], }, input: ["text"], cost: { input: 0.14, output: 0.28, cacheRead: 0.028, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192, }), ]; // Sibling models where one id is a prefix of the other AND the longer id embeds // a thinking-tier token (`-highspeed` contains `high`). Regression fixture for // the fuzzy match swallowing a `:high` thinking suffix into the longer id. const mockThinkingSuffixSiblingModels: Model<"openai-completions">[] = [ buildModel({ id: "kimi-for-coding", name: "K2.7 Code", api: "openai-completions", provider: "kimi-code", baseUrl: "https://api.kimi.com/coding/v1", reasoning: true, thinking: { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] }, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262144, maxTokens: 32000, }), buildModel({ id: "kimi-for-coding-highspeed", name: "K2.7 Code Highspeed", api: "openai-completions", provider: "kimi-code", baseUrl: "https://api.kimi.com/coding/v1", reasoning: true, thinking: { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] }, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 262144, maxTokens: 32000, }), ]; const mockAutoSuffixModels: Model[] = [ buildModel({ id: "runtime:auto", name: "Runtime Auto", api: "openai-completions", provider: "example", baseUrl: "https://example.com/api", reasoning: false, input: ["text"], cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192, }), ]; const mockProviderOverlapModels: Model<"anthropic-messages">[] = [ buildModel({ id: "kimi-k2.5", name: "Kimi K2.5", api: "anthropic-messages", provider: "kimi-code", baseUrl: "https://api.kimi.ai", reasoning: false, input: ["text"], cost: { input: 2, output: 6, cacheRead: 0.2, cacheWrite: 2 }, contextWindow: 128000, maxTokens: 8192, }), buildModel({ id: "moonshotai/kimi-k2.5", name: "Kimi K2.5 (OpenRouter)", api: "anthropic-messages", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: false, input: ["text"], cost: { input: 2.2, output: 6.2, cacheRead: 0.22, cacheWrite: 2.2 }, contextWindow: 128000, maxTokens: 8192, }), ]; const mockCodexOverlapModels: Model<"anthropic-messages">[] = [ buildModel({ id: "gpt-5.3-codex", name: "GPT-5.3 Codex", api: "anthropic-messages", provider: "openai-codex", baseUrl: "https://api.openai.com", reasoning: true, thinking: { mode: "effort", efforts: [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh], }, input: ["text"], cost: { input: 1.5, output: 6, cacheRead: 0.15, cacheWrite: 1.5 }, contextWindow: 200000, maxTokens: 8192, }), buildModel({ id: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark", api: "anthropic-messages", provider: "openai-codex", baseUrl: "https://api.openai.com", reasoning: true, thinking: { mode: "effort", efforts: [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh], }, input: ["text"], cost: { input: 1, output: 4, cacheRead: 0.1, cacheWrite: 1 }, contextWindow: 200000, maxTokens: 8192, }), ]; const mockMaxCapableModels: Model<"anthropic-messages">[] = [ buildModel({ id: "claude-opus-4-7", name: "Claude Opus 4.7", api: "anthropic-messages", provider: "anthropic", baseUrl: "https://api.anthropic.com", reasoning: true, thinking: { mode: "anthropic-adaptive", efforts: [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max], }, input: ["text", "image"], cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 }, contextWindow: 200000, maxTokens: 32000, }), ]; const openaiGpt55Models: Model[] = [ buildModel({ id: "gpt-5.5", name: "GPT-5.5", api: "openai-responses", provider: "openai", baseUrl: "https://api.openai.com", reasoning: true, thinking: { mode: "effort", efforts: [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh], }, input: ["text"], cost: { input: 1, output: 4, cacheRead: 0.1, cacheWrite: 1 }, contextWindow: 400000, maxTokens: 128000, }), buildModel({ id: "gpt-5.5", name: "GPT-5.5 Codex", api: "openai-codex-responses", provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api/codex/responses", reasoning: true, thinking: { mode: "effort", efforts: [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh], }, input: ["text"], cost: { input: 1, output: 4, cacheRead: 0.1, cacheWrite: 1 }, contextWindow: 400000, maxTokens: 128000, }), ]; function createBedrockDefaultModel(): Model<"bedrock-converse-stream"> { return buildModel({ id: "us.anthropic.claude-opus-4-8", name: "Claude Opus 4.8 (US)", api: "bedrock-converse-stream", provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", reasoning: true, input: ["text", "image"], cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, contextWindow: 1000000, maxTokens: 128000, }); } function createOpusModel(provider: string, id: string, name: string): Model<"anthropic-messages"> { return buildModel({ id, name, api: "anthropic-messages", provider, baseUrl: provider === "anthropic" ? "https://api.anthropic.com" : "https://api.githubcopilot.com", reasoning: true, thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh], }, input: ["text", "image"], cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 }, contextWindow: 200000, maxTokens: 32000, }); } const allModels = [...mockModels, ...mockOpenRouterModels, ...mockProviderOverlapModels, ...mockCodexOverlapModels]; describe("pickDefaultAvailableModel", () => { test("prefers Codex OAuth over plain OpenAI for the shared GPT default", () => { const result = pickDefaultAvailableModel(openaiGpt55Models); expect(result?.provider).toBe("openai-codex"); expect(result?.id).toBe("gpt-5.5"); }); test("keeps earlier unrelated provider defaults ahead of shared Codex defaults", () => { const anthropicDefault = buildModel({ id: DEFAULT_MODEL_PER_PROVIDER.anthropic, name: "Anthropic Default", api: "anthropic-messages", provider: "anthropic", baseUrl: "https://api.anthropic.com", reasoning: true, thinking: { mode: "budget", efforts: [Effort.Low, Effort.Medium, Effort.High], }, input: ["text"], cost: { input: 1, output: 4, cacheRead: 0.1, cacheWrite: 1 }, contextWindow: 200000, maxTokens: 8192, }); const result = pickDefaultAvailableModel([anthropicDefault, ...openaiGpt55Models]); expect(result?.provider).toBe("anthropic"); expect(result?.id).toBe(DEFAULT_MODEL_PER_PROVIDER.anthropic); }); test("uses the Zhipu Coding Plan login-validated model before newer z.ai defaults", () => { const zhipuGlm51 = buildModel({ id: "glm-5.1", name: "GLM-5.1", api: "openai-completions", provider: "zhipu-coding-plan", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 200000, maxTokens: 131072, }); const zhipuGlm52 = buildModel({ id: "glm-5.2", name: "GLM-5.2", api: "openai-completions", provider: "zhipu-coding-plan", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1000000, maxTokens: 131072, }); const zaiGlm52 = buildModel({ id: "glm-5.2", name: "GLM-5.2", api: "anthropic-messages", provider: "zai", baseUrl: "https://api.z.ai/api/anthropic", reasoning: true, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1000000, maxTokens: 131072, }); const result = pickDefaultAvailableModel([zhipuGlm51, zhipuGlm52, zaiGlm52]); expect(result?.provider).toBe("zhipu-coding-plan"); expect(result?.id).toBe("glm-5.1"); }); }); describe("parseModelPattern", () => { describe("simple patterns without colons", () => { test("exact match returns model with undefined thinking level", () => { const result = parseModelPattern("claude-sonnet-4-5", allModels); expect(result.model?.id).toBe("claude-sonnet-4-5"); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); expect(result.warning).toBeUndefined(); }); test("partial match returns best model with undefined thinking level", () => { const result = parseModelPattern("sonnet", allModels); expect(result.model?.id).toBe("claude-sonnet-4-5"); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); expect(result.warning).toBeUndefined(); }); test("no match returns undefined model and thinking level", () => { const result = parseModelPattern("nonexistent", allModels); expect(result.model).toBeUndefined(); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); expect(result.warning).toBeUndefined(); }); }); describe("patterns with valid thinking levels", () => { test("sonnet:high returns sonnet with high thinking level", () => { const result = parseModelPattern("sonnet:high", allModels); expect(result.model?.id).toBe("claude-sonnet-4-5"); expect(result.thinkingLevel).toBe(Effort.High); expect(result.warning).toBeUndefined(); }); test("gpt-4o:medium returns gpt-4o with medium thinking level", () => { const result = parseModelPattern("gpt-4o:medium", allModels); expect(result.model?.id).toBe("gpt-4o"); expect(result.thinkingLevel).toBe(Effort.Medium); expect(result.warning).toBeUndefined(); }); test("all valid thinking levels work", () => { const levels = [ "off", Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max, ] as const; for (const level of levels) { const result = parseModelPattern(`sonnet:${level}`, allModels); expect(result.model?.id).toBe("claude-sonnet-4-5"); expect(result.thinkingLevel).toBe(level); expect(result.warning).toBeUndefined(); } }); test("max parses as a real thinking level after the literal pattern misses", () => { const result = parseModelPattern("gpt-5.3-codex:max", allModels); expect(result.model?.id).toBe("gpt-5.3-codex"); expect(result.thinkingLevel).toBe(Effort.Max); expect(result.explicitThinkingLevel).toBe(true); expect(result.warning).toBeUndefined(); }); test("literal model ids ending in max win over the thinking suffix", () => { const result = parseModelPattern("nanogpt/coding-router:max", mockMaxSuffixModels); expect(result.model?.id).toBe("coding-router:max"); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); expect(result.warning).toBeUndefined(); }); test("fuzzy selectors preserve literal models ending in a thinking-level suffix", () => { const result = parseModelPattern("router:low", mockMaxSuffixModels); expect(result.model?.id).toBe("coding-router:low"); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); }); test("literal model ids ending in auto win over the auto sentinel alias", () => { const result = parseModelPattern("example/runtime:auto", mockAutoSuffixModels); expect(result.model?.id).toBe("runtime:auto"); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); expect(result.warning).toBeUndefined(); }); test("thinking suffix is stripped before fuzzy match, never absorbed into a longer sibling id", () => { // `kimi-for-coding:high` must resolve to the standard model at high effort, // not fuzzy-match `kimi-for-coding-highspeed` (issue #5151). const result = parseModelPattern("kimi-code/kimi-for-coding:high", mockThinkingSuffixSiblingModels); expect(result.model?.id).toBe("kimi-for-coding"); expect(result.thinkingLevel).toBe(Effort.High); expect(result.explicitThinkingLevel).toBe(true); expect(result.warning).toBeUndefined(); }); test("bare id thinking suffix is stripped before fuzzy match against a longer sibling", () => { const result = parseModelPattern("kimi-for-coding:high", mockThinkingSuffixSiblingModels); expect(result.model?.id).toBe("kimi-for-coding"); expect(result.thinkingLevel).toBe(Effort.High); expect(result.explicitThinkingLevel).toBe(true); }); test("the longer sibling still resolves exactly with its own thinking suffix", () => { const result = parseModelPattern("kimi-code/kimi-for-coding-highspeed:high", mockThinkingSuffixSiblingModels); expect(result.model?.id).toBe("kimi-for-coding-highspeed"); expect(result.thinkingLevel).toBe(Effort.High); expect(result.explicitThinkingLevel).toBe(true); }); }); describe("patterns with invalid thinking levels", () => { test("sonnet:random returns sonnet with undefined thinking level and warning", () => { const result = parseModelPattern("sonnet:random", allModels); expect(result.model?.id).toBe("claude-sonnet-4-5"); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); expect(result.warning).toContain("Invalid thinking level"); expect(result.warning).toContain("random"); }); test("gpt-4o:invalid returns gpt-4o with undefined thinking level and warning", () => { const result = parseModelPattern("gpt-4o:invalid", allModels); expect(result.model?.id).toBe("gpt-4o"); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); expect(result.warning).toContain("Invalid thinking level"); }); }); describe("OpenRouter models with colons in IDs", () => { test("qwen3-coder:exacto matches the model with undefined thinking level", () => { const result = parseModelPattern("qwen/qwen3-coder:exacto", allModels); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); expect(result.warning).toBeUndefined(); }); test("openrouter/qwen/qwen3-coder:exacto matches with provider prefix", () => { const result = parseModelPattern("openrouter/qwen/qwen3-coder:exacto", allModels); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); expect(result.model?.provider).toBe("openrouter"); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); expect(result.warning).toBeUndefined(); }); test("qwen3-coder:exacto:high matches model with high thinking level", () => { const result = parseModelPattern("qwen/qwen3-coder:exacto:high", allModels); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); expect(result.thinkingLevel).toBe(Effort.High); expect(result.explicitThinkingLevel).toBe(true); expect(result.warning).toBeUndefined(); }); test("openrouter/qwen/qwen3-coder:exacto:high matches with provider and thinking level", () => { const result = parseModelPattern("openrouter/qwen/qwen3-coder:exacto:high", allModels); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); expect(result.model?.provider).toBe("openrouter"); expect(result.thinkingLevel).toBe(Effort.High); expect(result.explicitThinkingLevel).toBe(true); expect(result.warning).toBeUndefined(); }); test("gpt-4o:extended matches the extended model with undefined thinking level", () => { const result = parseModelPattern("openai/gpt-4o:extended", allModels); expect(result.model?.id).toBe("openai/gpt-4o:extended"); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); expect(result.warning).toBeUndefined(); }); test("supports OpenRouter route suffixes that are not present in the catalog", () => { const result = parseModelPattern("openrouter/z-ai/glm-4.7-20251222:nitro", allModels); expect(result.model?.provider).toBe("openrouter"); expect(result.model?.id).toBe("z-ai/glm-4.7-20251222:nitro"); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); expect(result.warning).toBeUndefined(); }); test("supports OpenRouter route suffixes with an appended thinking level", () => { const result = parseModelPattern("openrouter/z-ai/glm-4.7-20251222:nitro:high", allModels); expect(result.model?.provider).toBe("openrouter"); expect(result.model?.id).toBe("z-ai/glm-4.7-20251222:nitro"); expect(result.thinkingLevel).toBe(Effort.High); expect(result.explicitThinkingLevel).toBe(true); expect(result.warning).toBeUndefined(); }); test("openrouter/:max applies max through the exact-selector path, not an OpenRouter route", () => { // `max` is a thinking-level suffix, never an OpenRouter route suffix: the request // must resolve the base model and carry max, not clone a literal `z-ai/glm-4.7:max`. const result = parseModelPattern("openrouter/z-ai/glm-4.7:max", allModels); expect(result.model?.provider).toBe("openrouter"); expect(result.model?.id).toBe("z-ai/glm-4.7"); expect(result.thinkingLevel).toBe(Effort.Max); expect(result.explicitThinkingLevel).toBe(true); }); }); describe("invalid thinking levels with OpenRouter models", () => { test("qwen3-coder:exacto:random returns model with undefined thinking level and warning", () => { const result = parseModelPattern("qwen/qwen3-coder:exacto:random", allModels); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); expect(result.warning).toContain("Invalid thinking level"); expect(result.warning).toContain("random"); }); test("qwen3-coder:exacto:high:random returns model with undefined thinking level and warning", () => { const result = parseModelPattern("qwen/qwen3-coder:exacto:high:random", allModels); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); expect(result.warning).toContain("Invalid thinking level"); expect(result.warning).toContain("random"); }); }); describe("edge cases", () => { test("empty pattern matches via partial matching", () => { // Empty string is included in all model IDs, so partial matching finds a match const result = parseModelPattern("", allModels); expect(result.model).not.toBeNull(); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); }); test("pattern ending with colon treats empty suffix as invalid", () => { const result = parseModelPattern("sonnet:", allModels); // Empty string after colon is not a valid thinking level // So it tries to match "sonnet:" which won't match, then tries "sonnet" expect(result.model?.id).toBe("claude-sonnet-4-5"); expect(result.warning).toContain("Invalid thinking level"); }); }); describe("preference logic", () => { test("prefers most recently used model when multiple providers match", () => { const result = parseModelPattern("k2.5", allModels, { usageOrder: ["kimi-code/kimi-k2.5"], }); expect(result.model?.provider).toBe("kimi-code"); }); test("prefers first-party providers over OpenRouter when no usage data exists", () => { const result = parseModelPattern("k2.5", allModels, { usageOrder: [] }); expect(result.model?.provider).toBe("kimi-code"); }); test("respects most recently used provider even if openrouter", () => { const result = parseModelPattern("k2.5", allModels, { usageOrder: ["openrouter/moonshotai/kimi-k2.5"], }); expect(result.model?.provider).toBe("openrouter"); expect(result.model?.id).toBe("moonshotai/kimi-k2.5"); }); }); }); describe("resolveModelRoleValue", () => { test("resolves @role: by expanding role alias before parsing thinking", () => { const settings = { getModelRole: (role: string) => (role === "smol" ? "openrouter/qwen/qwen3-coder:exacto" : undefined), } as NonNullable[2]>["settings"]; const result = resolveModelRoleValue("@smol:high", allModels, { settings }); expect(result.model?.provider).toBe("openrouter"); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); expect(result.thinkingLevel).toBe(Effort.High); expect(result.explicitThinkingLevel).toBe(true); }); test("resolves @role:max by expanding role alias before parsing thinking", () => { const settings = { getModelRole: (role: string) => (role === "smol" ? "openai-codex/gpt-5.3-codex" : undefined), } as NonNullable[2]>["settings"]; const result = resolveModelRoleValue("@smol:max", allModels, { settings }); expect(result.model?.provider).toBe("openai-codex"); expect(result.model?.id).toBe("gpt-5.3-codex"); // Role-value resolution clamps: gpt-5.3-codex's ladder tops out at xhigh. expect(result.thinkingLevel).toBe(Effort.XHigh); expect(result.explicitThinkingLevel).toBe(true); }); test("resolves @default through configured default role alias", () => { const settings = { getModelRole: (role: string) => (role === "default" ? "openrouter/qwen/qwen3-coder:exacto" : undefined), } as NonNullable[2]>["settings"]; const result = resolveModelRoleValue("@default", allModels, { settings }); expect(result.model?.provider).toBe("openrouter"); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); expect(result.thinkingLevel).toBeUndefined(); expect(result.explicitThinkingLevel).toBe(false); expect(result.warning).toBeUndefined(); }); test("splits direct comma fallback chains before parsing thinking selectors", () => { const result = resolveModelRoleValue("anthropic/claude-sonnet-4-5:off,openai/gpt-4o:off", allModels); expect(result.model?.provider).toBe("anthropic"); expect(result.model?.id).toBe("claude-sonnet-4-5"); expect(result.thinkingLevel).toBe("off"); expect(result.explicitThinkingLevel).toBe(true); expect(result.warning).toBeUndefined(); }); test("tries later direct comma fallback entries when earlier entries miss", () => { const result = resolveModelRoleValue("anthropic/missing:off,openai/gpt-4o:off", allModels); expect(result.model?.provider).toBe("openai"); expect(result.model?.id).toBe("gpt-4o"); expect(result.thinkingLevel).toBe("off"); expect(result.explicitThinkingLevel).toBe(true); expect(result.warning).toBeUndefined(); }); test("does not resolve exact codex role values to codex-spark via substring matching", () => { const providerQualified = resolveModelRoleValue("openai-codex/gpt-5.3-codex:xhigh", allModels); expect(providerQualified.model?.provider).toBe("openai-codex"); expect(providerQualified.model?.id).toBe("gpt-5.3-codex"); expect(providerQualified.thinkingLevel).toBe(Effort.XHigh); expect(providerQualified.explicitThinkingLevel).toBe(true); const idOnly = resolveModelRoleValue("gpt-5.3-codex:xhigh", allModels); expect(idOnly.model?.provider).toBe("openai-codex"); expect(idOnly.model?.id).toBe("gpt-5.3-codex"); expect(idOnly.thinkingLevel).toBe(Effort.XHigh); expect(idOnly.explicitThinkingLevel).toBe(true); }); test("clamps explicit thinking selectors from model metadata", () => { const result = resolveModelRoleValue("anthropic/claude-sonnet-4-5:xhigh", allModels); expect(result.model?.provider).toBe("anthropic"); expect(result.model?.id).toBe("claude-sonnet-4-5"); expect(result.thinkingLevel).toBe(Effort.High); expect(result.explicitThinkingLevel).toBe(true); }); test("passes max through unclamped when the model ladder includes it", () => { const result = resolveModelRoleValue("anthropic/claude-opus-4-7:max", mockMaxCapableModels); expect(result.model?.provider).toBe("anthropic"); expect(result.model?.id).toBe("claude-opus-4-7"); expect(result.thinkingLevel).toBe(Effort.Max); expect(result.explicitThinkingLevel).toBe(true); }); test("preserves an explicit :auto suffix as an explicit thinking selector", () => { const result = resolveModelRoleValue("anthropic/claude-sonnet-4-5:auto", allModels); expect(result.model?.provider).toBe("anthropic"); expect(result.model?.id).toBe("claude-sonnet-4-5"); expect(result.thinkingLevel).toBe("auto"); expect(result.explicitThinkingLevel).toBe(true); expect(result.warning).toBeUndefined(); }); test("does not clamp :auto against the model's supported efforts", () => { // claude-sonnet-4-5 caps at "high"; ensure auto isn't collapsed onto it // by resolveThinkingLevelForModel. const result = resolveModelRoleValue("anthropic/claude-sonnet-4-5:auto", allModels); expect(result.thinkingLevel).toBe("auto"); expect(result.explicitThinkingLevel).toBe(true); }); }); describe("resolveAgentPrewalkPattern", () => { test("agent definition alone decides: true → default target, pattern → custom, false/absent → off", () => { expect(resolveAgentPrewalkPattern({ agentPrewalk: true })).toBe("@smol"); expect(resolveAgentPrewalkPattern({ agentPrewalk: "@very-smol" })).toBe("@very-smol"); expect(resolveAgentPrewalkPattern({ agentPrewalk: false })).toBeUndefined(); expect(resolveAgentPrewalkPattern({})).toBeUndefined(); }); test("settings override wins over the agent definition", () => { expect(resolveAgentPrewalkPattern({ settingsOverride: "off", agentPrewalk: true })).toBeUndefined(); expect(resolveAgentPrewalkPattern({ settingsOverride: "off", agentPrewalk: "@very-smol" })).toBeUndefined(); expect(resolveAgentPrewalkPattern({ settingsOverride: "on", agentPrewalk: false })).toBe("@smol"); expect(resolveAgentPrewalkPattern({ settingsOverride: "openai/gpt-4o", agentPrewalk: false })).toBe( "openai/gpt-4o", ); }); test("override 'on' keeps the agent's custom target when one is defined", () => { expect(resolveAgentPrewalkPattern({ settingsOverride: "on", agentPrewalk: "@very-smol" })).toBe("@very-smol"); expect(resolveAgentPrewalkPattern({ settingsOverride: "on" })).toBe("@smol"); }); test("blank override falls through to the agent definition", () => { expect(resolveAgentPrewalkPattern({ settingsOverride: " ", agentPrewalk: true })).toBe("@smol"); expect(resolveAgentPrewalkPattern({ settingsOverride: "", agentPrewalk: false })).toBeUndefined(); }); }); describe("resolveAgentModelPatterns", () => { test("pairs the first non-empty source's role with its patterns, skipping aliases with no patterns", () => { const settings = Settings.isolated({ modelRoles: { empty: "", override: "openai/gpt-4o", definition: "anthropic/claude-sonnet-4-5", }, }); expect( resolveAgentModelSelection({ requestModel: "", settingsOverride: "@override", agentModel: ["@definition"], settings, }), ).toEqual({ patterns: ["openai/gpt-4o"], role: "override" }); expect( resolveAgentModelSelection({ requestModel: "@empty", settingsOverride: ",,", agentModel: ["@definition"], settings, }), ).toEqual({ patterns: ["anthropic/claude-sonnet-4-5"], role: "definition" }); // An explicit selector carries no role identity, so the child must not // capture the routing of a role that happens to name the same model. expect( resolveAgentModelSelection({ requestModel: "openai/gpt-4o", settingsOverride: "@override", agentModel: ["@definition"], settings, }), ).toEqual({ patterns: ["openai/gpt-4o"], role: undefined }); }); test("falls back to the active session model when @task is unset", () => { const settings = Settings.isolated({ modelRoles: { default: "anthropic/claude-sonnet-4-5" }, }); const result = resolveAgentModelPatterns({ agentModel: "@task", settings, activeModelPattern: "openai/gpt-4o", }); expect(result).toEqual(["openai/gpt-4o"]); }); test("uses the configured task role before falling back to the session model", () => { const settings = Settings.isolated({ modelRoles: { default: "openai/gpt-4o", task: "anthropic/claude-sonnet-4-5:high", }, }); const result = resolveAgentModelPatterns({ agentModel: "@task", settings, activeModelPattern: "openai/gpt-4o", }); expect(result).toEqual(["anthropic/claude-sonnet-4-5:high"]); }); test("accepts YAML list values for configured task role patterns", () => { const settings = Settings.isolated({ modelRoles: { task: ["anthropic/claude-sonnet-4-6", "zai/glm-5.2:high"], }, }); const result = resolveAgentModelPatterns({ agentModel: "@task", settings, }); expect(result).toEqual(["anthropic/claude-sonnet-4-6", "zai/glm-5.2:high"]); }); test("uses default for unconfigured smol, slow, and designer agent roles before priority defaults", () => { const settings = Settings.isolated({ modelRoles: { default: "local/llama" }, }); expect(resolveAgentModelPatterns({ agentModel: "@smol", settings })).toEqual(["local/llama"]); expect(resolveAgentModelPatterns({ agentModel: "@slow", settings })).toEqual(["local/llama"]); expect(resolveAgentModelPatterns({ agentModel: "@designer", settings })).toEqual(["local/llama"]); }); test("expands cross-role default aliases when inheriting for an unset role", () => { const settings = Settings.isolated({ modelRoles: { default: "@slow", slow: "anthropic/claude-sonnet-4-5" }, }); expect(resolveAgentModelPatterns({ agentModel: "@smol", settings })).toEqual(["anthropic/claude-sonnet-4-5"]); }); test("prefers configured designer role override over priority defaults", () => { const settings = Settings.isolated({ modelRoles: { default: "anthropic/claude-sonnet-4-5", designer: "openai/gpt-4o", }, }); const result = resolveAgentModelPatterns({ agentModel: "@designer", settings, }); expect(result).toEqual(["openai/gpt-4o"]); }); test("slow priority falls forward to Opus 4.8 before older Opus aliases", () => { const settings = Settings.isolated(); const patterns = resolveAgentModelPatterns({ agentModel: "@slow", settings }); const dottedRegistry = { getAvailable: () => [ createOpusModel("github-copilot", "claude-opus-4.7", "Claude Opus 4.7"), createOpusModel("github-copilot", "claude-opus-4.8", "Claude Opus 4.8"), ], } as Parameters[1]; const dotted = resolveModelOverride(patterns, dottedRegistry, settings); expect(dotted.model?.provider).toBe("github-copilot"); expect(dotted.model?.id).toBe("claude-opus-4.8"); const dashedRegistry = { getAvailable: () => [ createOpusModel("anthropic", "claude-opus-4-7", "Claude Opus 4.7"), createOpusModel("anthropic", "claude-opus-4-8", "Claude Opus 4.8"), ], } as Parameters[1]; const dashed = resolveModelOverride(patterns, dashedRegistry, settings); expect(dashed.model?.provider).toBe("anthropic"); expect(dashed.model?.id).toBe("claude-opus-4-8"); }); }); describe("resolveModelFromString", () => { test("falls back to pattern parsing for provider/model:thinking when strict provider+id miss", () => { const resolved = resolveModelFromString("openrouter/qwen/qwen3-coder:exacto:high", allModels); expect(resolved?.provider).toBe("openrouter"); expect(resolved?.id).toBe("qwen/qwen3-coder:exacto"); }); test("treats colon-containing model IDs without thinking suffix as exact IDs", () => { const resolved = resolveModelFromString("openrouter/qwen/qwen3-coder:exacto", allModels); expect(resolved?.provider).toBe("openrouter"); expect(resolved?.id).toBe("qwen/qwen3-coder:exacto"); }); }); describe("resolveModelOverride", () => { test("preserves explicit off and explicit-thinking metadata", () => { const registry = { getAvailable: () => allModels, } as Parameters[1]; const result = resolveModelOverride(["sonnet:off"], registry); expect(result.model?.id).toBe("claude-sonnet-4-5"); expect(result.thinkingLevel).toBe("off"); expect(result.explicitThinkingLevel).toBe(true); }); test("resolves colon-containing model IDs with appended thinking suffix", () => { const registry = { getAvailable: () => allModels, } as Parameters[1]; const result = resolveModelOverride(["openrouter/qwen/qwen3-coder:exacto:high"], registry); expect(result.model?.provider).toBe("openrouter"); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); expect(result.thinkingLevel).toBe(Effort.High); expect(result.explicitThinkingLevel).toBe(true); }); }); describe("resolveCliModel", () => { test("resolves --model provider/id without --provider", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< typeof resolveCliModel >[0]["modelRegistry"]; const result = resolveCliModel({ cliModel: "openai/gpt-4o", modelRegistry: registry, }); expect(result.error).toBeUndefined(); expect(result.model?.provider).toBe("openai"); expect(result.model?.id).toBe("gpt-4o"); }); test("prefers an authenticated provider for an unqualified exact model id", () => { const availableModels = openaiGpt55Models.filter(model => model.provider === "openai-codex"); const registry = { getAll: () => openaiGpt55Models, getAvailable: () => openaiGpt55Models }; const result = resolveCliModel({ cliModel: "gpt-5.5", modelRegistry: registry, availableModels, }); expect(result.error).toBeUndefined(); expect(result.model?.provider).toBe("openai-codex"); expect(result.model?.id).toBe("gpt-5.5"); }); test("prefers an authenticated provider for flat slashful ids whose prefix is a provider slug", () => { const mirror = (provider: string, baseUrl: string): Model<"anthropic-messages"> => buildModel({ id: "openai/gpt-oss-120b", name: "GPT-OSS 120B", api: "anthropic-messages", provider, baseUrl, reasoning: false, input: ["text"], cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, contextWindow: 128000, maxTokens: 4096, }); const catalogFirst = mirror("fireworks", "https://api.fireworks.ai"); const authenticated = mirror("openrouter", "https://openrouter.ai"); // "openai" is a real provider slug in the catalog, but it does not carry // this model — the selector is a flat aggregator id, not provider/id. const catalog = [...allModels, catalogFirst, authenticated]; const result = resolveCliModel({ cliModel: "openai/gpt-oss-120b", modelRegistry: { getAll: () => catalog, getAvailable: () => [authenticated] }, availableModels: [authenticated], }); expect(result.error).toBeUndefined(); expect(result.model?.provider).toBe("openrouter"); expect(result.model?.id).toBe("openai/gpt-oss-120b"); }); test("resolves bare configured role names from --model", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { task: "openai/gpt-4o" }, }); const result = resolveCliModel({ cliModel: "task", modelRegistry: registry, settings, }); expect(result.error).toBeUndefined(); expect(result.model?.provider).toBe("openai"); expect(result.model?.id).toBe("gpt-4o"); }); test("resolves bare configured role names with thinking suffixes", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { task: "anthropic/claude-sonnet-4-5" }, }); const result = resolveCliModel({ cliModel: "task:high", modelRegistry: registry, settings, }); expect(result.error).toBeUndefined(); expect(result.model?.id).toBe("claude-sonnet-4-5"); expect(result.thinkingLevel).toBe(Effort.High); expect(result.configuredPatterns).toEqual(["anthropic/claude-sonnet-4-5:high"]); }); test("preserves configured role fallback selectors for deferred resolution", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { task: "openrouter/z-ai/glm-4.7@cerebras,anthropic/claude-sonnet-4-5", }, }); const result = resolveCliModel({ cliModel: "task", modelRegistry: registry, settings, }); expect(result.configuredPatterns).toEqual(["openrouter/z-ai/glm-4.7@cerebras", "anthropic/claude-sonnet-4-5"]); }); test("reports when a configured role matches after unresolved candidates", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { task: "runtime-provider/runtime-model,anthropic/claude-sonnet-4-5", }, }); const result = resolveCliModel({ cliModel: "task", modelRegistry: registry, settings, }); expect(result.model?.provider).toBe("anthropic"); expect(result.configuredPatternIndex).toBe(1); expect(result.configuredPatterns).toEqual(["runtime-provider/runtime-model", "anthropic/claude-sonnet-4-5"]); }); test("does not fuzzy-match unresolved configured roles", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { sonnet: "runtime-provider/runtime-model" }, }); const result = resolveCliModel({ cliModel: "sonnet", modelRegistry: registry, settings, }); expect(result.model).toBeUndefined(); expect(result.configuredPatterns).toEqual(["runtime-provider/runtime-model"]); expect(result.error).toContain('Model "sonnet" not found'); }); test("keeps unknown --model names on the not-found path", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels }; const result = resolveCliModel({ cliModel: "not-a-model", modelRegistry: registry, }); expect(result.model).toBeUndefined(); expect(result.error).toContain('Model "not-a-model" not found'); }); test("prefers an exact model name over a same-named configured role", () => { const exactModel = buildModel({ id: "task", name: "Task", api: "anthropic-messages", provider: "openai", baseUrl: "https://api.openai.com", reasoning: false, input: ["text"], cost: { input: 5, output: 15, cacheRead: 0.5, cacheWrite: 5 }, contextWindow: 128000, maxTokens: 4096, }); const registry = { getAll: () => [...allModels, exactModel], getAvailable: () => [...allModels, exactModel] }; const settings = Settings.isolated({ modelRoles: { task: "anthropic/claude-sonnet-4-5" }, }); const result = resolveCliModel({ cliModel: "task", modelRegistry: registry, settings, }); expect(result.error).toBeUndefined(); expect(result.model).toBe(exactModel); const suffixed = resolveCliModel({ cliModel: "task:high", modelRegistry: registry, settings, }); expect(suffixed.error).toBeUndefined(); expect(suffixed.model).toBe(exactModel); expect(suffixed.thinkingLevel).toBe(Effort.High); }); test("configured role beats an unauthenticated catalog id collision (#6508)", () => { // A bundled `cursor/default` model has the bare id `default`, which collides // with the reserved `default` role selector. When the user has no Cursor // credentials the catalog entry is not authenticated, so it must not shadow // a configured, runnable `modelRoles.default`. const cursorDefault = buildModel({ id: "default", name: "Cursor Default", api: "anthropic-messages", provider: "cursor", baseUrl: "https://cursor.sh", reasoning: false, input: ["text"], cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, contextWindow: 128000, maxTokens: 4096, }); const registry = { getAll: () => [...allModels, cursorDefault], getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { default: "openai/gpt-4o" }, }); const result = resolveCliModel({ cliModel: "default", modelRegistry: registry, settings, }); expect(result.error).toBeUndefined(); expect(result.model?.provider).toBe("openai"); expect(result.model?.id).toBe("gpt-4o"); }); test("unauthenticated catalog id still resolves when no role matches", () => { // Without a configured role the same bare id must still reach the catalog // model (so `--model default` surfaces the usual "no API key" error rather // than a spurious not-found), confirming the fallback is only deferred. const cursorDefault = buildModel({ id: "default", name: "Cursor Default", api: "anthropic-messages", provider: "cursor", baseUrl: "https://cursor.sh", reasoning: false, input: ["text"], cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, contextWindow: 128000, maxTokens: 4096, }); const registry = { getAll: () => [...allModels, cursorDefault], getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: {} }); const result = resolveCliModel({ cliModel: "default", modelRegistry: registry, settings, }); expect(result.model?.provider).toBe("cursor"); expect(result.model?.id).toBe("default"); }); test("resolves configured custom, legacy, and default role aliases from --model", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { default: "openai/gpt-4o", fable: "anthropic/claude-sonnet-4-5:high", }, }); const canonical = resolveCliModel({ cliModel: "@fable", modelRegistry: registry, settings, }); const legacy = resolveCliModel({ cliModel: `${LEGACY_MODEL_ROLE_ALIAS_PREFIX}fable`, modelRegistry: registry, settings, }); const defaultRole = resolveCliModel({ cliModel: DEFAULT_MODEL_ROLE_ALIAS, modelRegistry: registry, settings, }); expect(canonical.error).toBeUndefined(); expect(canonical.model?.provider).toBe("anthropic"); expect(canonical.model?.id).toBe("claude-sonnet-4-5"); expect(canonical.thinkingLevel).toBe(Effort.High); expect(legacy).toEqual(canonical); expect(defaultRole.model?.provider).toBe("openai"); expect(defaultRole.model?.id).toBe("gpt-4o"); }); test("splits thinking suffixes and abbreviations off the * default alias", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels }; const settings = Settings.isolated({ modelRoles: { default: "anthropic/claude-sonnet-4-5" }, }); const explicit = resolveCliModel({ cliModel: `${DEFAULT_MODEL_ROLE_ALIAS}:high`, modelRegistry: registry, settings, }); const abbreviated = resolveCliModel({ cliModel: `${DEFAULT_MODEL_ROLE_ALIAS}:xhi`, modelRegistry: registry, settings, }); expect(explicit.error).toBeUndefined(); expect(explicit.model?.id).toBe("claude-sonnet-4-5"); expect(explicit.thinkingLevel).toBe(Effort.High); // `xhi` → xhigh via unique-prefix parsing, then clamped to the model ladder. expect(abbreviated.error).toBeUndefined(); expect(abbreviated.model?.id).toBe("claude-sonnet-4-5"); expect(abbreviated.thinkingLevel).toBe(Effort.High); }); test("resolves fuzzy patterns within an explicit provider", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< typeof resolveCliModel >[0]["modelRegistry"]; const result = resolveCliModel({ cliProvider: "openai", cliModel: "4o", modelRegistry: registry, }); expect(result.error).toBeUndefined(); expect(result.model?.provider).toBe("openai"); expect(result.model?.id).toBe("gpt-4o"); }); test("supports --model : (without explicit --thinking)", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< typeof resolveCliModel >[0]["modelRegistry"]; const result = resolveCliModel({ cliModel: "sonnet:high", modelRegistry: registry, }); expect(result.error).toBeUndefined(); expect(result.model?.id).toBe("claude-sonnet-4-5"); expect(result.thinkingLevel).toBe(Effort.High); }); test("prefers exact model id match over provider inference (OpenRouter-style ids)", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< typeof resolveCliModel >[0]["modelRegistry"]; const result = resolveCliModel({ cliModel: "openai/gpt-4o:extended", modelRegistry: registry, }); expect(result.error).toBeUndefined(); expect(result.model?.provider).toBe("openrouter"); expect(result.model?.id).toBe("openai/gpt-4o:extended"); }); test("does not strip invalid :suffix as thinking level in --model (fail fast)", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< typeof resolveCliModel >[0]["modelRegistry"]; const result = resolveCliModel({ cliProvider: "openai", cliModel: "gpt-4o:extended", modelRegistry: registry, }); expect(result.model).toBeUndefined(); expect(result.error).toContain("not found"); }); test("supports provider-prefixed OpenRouter route suffixes even when the base model is cataloged without them", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< typeof resolveCliModel >[0]["modelRegistry"]; const result = resolveCliModel({ cliModel: "openrouter/z-ai/glm-4.7-20251222:nitro", modelRegistry: registry, }); expect(result.error).toBeUndefined(); expect(result.model?.provider).toBe("openrouter"); expect(result.model?.id).toBe("z-ai/glm-4.7-20251222:nitro"); }); test("supports explicit OpenRouter provider with route suffixes that are not in the catalog", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< typeof resolveCliModel >[0]["modelRegistry"]; const result = resolveCliModel({ cliProvider: "openrouter", cliModel: "z-ai/glm-4.7-20251222:nitro", modelRegistry: registry, }); expect(result.error).toBeUndefined(); expect(result.model?.provider).toBe("openrouter"); expect(result.model?.id).toBe("z-ai/glm-4.7-20251222:nitro"); }); test("accepts Bedrock inference profile ARNs and preserves thinking suffixes", () => { const defaultBedrockModel = createBedrockDefaultModel(); const profileArn = "arn:aws:bedrock:us-east-2:1234567890:application-inference-profile/company-opus-48"; const baseResult = resolveCliModel({ cliProvider: "amazon-bedrock", cliModel: profileArn, modelRegistry: { getAll: () => [defaultBedrockModel], getAvailable: () => [defaultBedrockModel] }, }); const offResult = resolveCliModel({ cliProvider: "amazon-bedrock", cliModel: `${profileArn}:off`, modelRegistry: { getAll: () => [defaultBedrockModel], getAvailable: () => [defaultBedrockModel] }, }); expect(baseResult.error).toBeUndefined(); expect(baseResult.model?.provider).toBe("amazon-bedrock"); expect(baseResult.model?.api).toBe("bedrock-converse-stream"); expect(baseResult.model?.id).toBe(profileArn); expect(baseResult.model?.name).toBe("Bedrock inference profile"); expect(baseResult.model?.reasoning).toBe(false); expect(baseResult.model?.thinking).toBeUndefined(); expect(baseResult.model?.contextWindow).toBeNull(); expect(baseResult.model?.maxTokens).toBeNull(); expect(baseResult.thinkingLevel).toBeUndefined(); expect(offResult.error).toBeUndefined(); expect(offResult.model?.id).toBe(profileArn); expect(offResult.thinkingLevel).toBe("off"); }); test("returns a clear error when there are no models", () => { const registry = { getAll: () => [], getAvailable: () => [] } as unknown as Parameters< typeof resolveCliModel >[0]["modelRegistry"]; const result = resolveCliModel({ cliProvider: "openai", cliModel: "gpt-4o", modelRegistry: registry, }); expect(result.model).toBeUndefined(); expect(result.error).toContain("No models available"); }); test("resolves provider-prefixed fuzzy patterns (openrouter/qwen -> openrouter model)", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< typeof resolveCliModel >[0]["modelRegistry"]; const result = resolveCliModel({ cliModel: "openrouter/qwen", modelRegistry: registry, }); expect(result.error).toBeUndefined(); expect(result.model?.provider).toBe("openrouter"); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); }); test("prefers decomposed provider+id over flat id match when ambiguous", () => { // Simulates the zai/glm-5 bug: vercel-ai-gateway has id="zai/glm-5", // zai has id="glm-5". Input "zai/glm-5" should resolve to provider=zai. const ambiguousModels: Model<"anthropic-messages">[] = [ buildModel({ id: "zai/glm-5", name: "GLM-5 (Vercel)", api: "anthropic-messages", provider: "vercel-ai-gateway", baseUrl: "https://vercel.ai", reasoning: false, input: ["text"], cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, contextWindow: 128000, maxTokens: 4096, }), buildModel({ id: "glm-5", name: "GLM-5", api: "anthropic-messages", provider: "zai", baseUrl: "https://api.z.ai", reasoning: false, input: ["text"], cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, contextWindow: 128000, maxTokens: 4096, }), ]; const registry = { getAll: () => ambiguousModels, getAvailable: () => ambiguousModels } as unknown as Parameters< typeof resolveCliModel >[0]["modelRegistry"]; const result = resolveCliModel({ cliModel: "zai/glm-5", modelRegistry: registry, }); expect(result.error).toBeUndefined(); expect(result.model?.provider).toBe("zai"); expect(result.model?.id).toBe("glm-5"); }); }); describe("resolveModelScope", () => { test("does not coalesce explicit provider/id patterns to Codex (regression for enabledModels)", async () => { const scoped = await resolveModelScope(["openai/gpt-5.5"], { getAvailable: () => openaiGpt55Models, }); expect(scoped).toHaveLength(1); expect(scoped[0].model.provider).toBe("openai"); expect(scoped[0].model.id).toBe("gpt-5.5"); }); test("resolves role aliases in --models scope to the role's model with its thinking level", async () => { const settings = Settings.isolated({ modelRoles: { fable: "anthropic/claude-sonnet-4-5:high" }, }); const scoped = await resolveModelScope( ["@fable", "openai/gpt-4o"], { getAvailable: () => allModels }, undefined, settings, ); expect(scoped).toHaveLength(2); expect(scoped[0].model.id).toBe("claude-sonnet-4-5"); expect(scoped[0].thinkingLevel).toBe(Effort.High); expect(scoped[0].explicitThinkingLevel).toBe(true); expect(scoped[1].model.id).toBe("gpt-4o"); }); test("applies max thinking selectors to glob scopes when no literal max ids match", async () => { const registry = { getAvailable: () => mockCodexOverlapModels, }; const scoped = await resolveModelScope(["openai-codex/*:max"], registry); expect(scoped).toHaveLength(2); // Scoped levels clamp per model: max on an xhigh-ceiling ladder resolves to xhigh. expect(scoped.map(entry => entry.thinkingLevel)).toEqual([Effort.XHigh, Effort.XHigh]); expect(scoped.every(entry => entry.explicitThinkingLevel)).toBe(true); }); test("keeps max on glob scopes when the model ladder includes it", async () => { const registry = { getAvailable: () => mockMaxCapableModels, }; const scoped = await resolveModelScope(["anthropic/*:max"], registry); expect(scoped).toHaveLength(1); expect(scoped[0].thinkingLevel).toBe(Effort.Max); expect(scoped[0].explicitThinkingLevel).toBe(true); }); test("preserves literal :max in scoped-model globs", async () => { const registry = { getAvailable: () => mockMaxSuffixModels, }; const scoped = await resolveModelScope(["nanogpt/*:max"], registry); expect(scoped).toHaveLength(1); expect(scoped[0].model.id).toBe("coding-router:max"); expect(scoped[0].thinkingLevel).toBeUndefined(); expect(scoped[0].explicitThinkingLevel).toBe(false); }); }); describe("parseModelString", () => { test("parses standard provider/id format", () => { const result = parseModelString("anthropic/claude-sonnet-4-5"); expect(result).toEqual({ provider: "anthropic", id: "claude-sonnet-4-5" }); }); test("returns undefined for strings without a slash", () => { expect(parseModelString("claude-sonnet-4-5")).toBeUndefined(); expect(parseModelString("")).toBeUndefined(); expect(parseModelString("sonnet:high")).toBeUndefined(); }); test("returns undefined for strings starting with slash", () => { expect(parseModelString("/claude-sonnet-4-5")).toBeUndefined(); }); describe("thinking level suffix extraction", () => { test("extracts valid thinking level from provider/id:level", () => { const result = parseModelString("anthropic/claude-sonnet-4-5:high"); expect(result).toEqual({ provider: "anthropic", id: "claude-sonnet-4-5", thinkingLevel: Effort.High }); }); test("extracts all valid thinking levels", () => { const levels = ["off", Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh] as const; for (const level of levels) { const result = parseModelString(`anthropic/claude-sonnet-4-5:${level}`); expect(result?.id).toBe("claude-sonnet-4-5"); expect(result?.thinkingLevel).toBe(level); } }); test("does NOT strip invalid suffix — treats it as part of model ID", () => { const result = parseModelString("openrouter/qwen/qwen3-coder:exacto"); expect(result).toEqual({ provider: "openrouter", id: "qwen/qwen3-coder:exacto" }); }); test("handles model ID with colon followed by valid thinking level", () => { // e.g. "openrouter/qwen/qwen3-coder:exacto:high" — last colon is thinking level const result = parseModelString("openrouter/qwen/qwen3-coder:exacto:high"); expect(result).toEqual({ provider: "openrouter", id: "qwen/qwen3-coder:exacto", thinkingLevel: Effort.High, }); }); test("extracts max when explicitly enabled for provider id selectors", () => { const result = parseModelString("deepseek/deepseek-v4-pro:max", { allowMaxSuffix: true }); expect(result).toEqual({ provider: "deepseek", id: "deepseek-v4-pro", thinkingLevel: Effort.Max }); }); test("preserves literal max model ids when the caller can prove they exist", () => { const result = parseModelString("nanogpt/coding-router:max", { allowMaxSuffix: true, isLiteralModelId: (provider, id) => provider === "nanogpt" && id === "coding-router:max", }); expect(result).toEqual({ provider: "nanogpt", id: "coding-router:max" }); }); test("leaves :max attached to the model id unless the caller opts in via allowMaxSuffix", () => { // Without allowMaxSuffix, the strict suffix parser must not silently // reinterpret a literal `:max` id as a thinking suffix. const result = parseModelString("anthropic/claude-sonnet-4-5:max"); expect(result).toEqual({ provider: "anthropic", id: "claude-sonnet-4-5:max" }); }); test("leaves :auto attached to the model id unless the caller opts in via allowAutoAlias", () => { // Without allowAutoAlias, the strict suffix parser must not silently // reinterpret a literal `:auto` id as an auto-thinking selector. const result = parseModelString("example/runtime:auto"); expect(result).toEqual({ provider: "example", id: "runtime:auto" }); }); test("extracts auto sentinel when explicitly enabled for provider id selectors", () => { const result = parseModelString("openai/gpt-5:auto", { allowAutoAlias: true }); expect(result).toEqual({ provider: "openai", id: "gpt-5", thinkingLevel: "auto" }); }); test("preserves literal :auto model ids when the caller can prove they exist", () => { const result = parseModelString("example/runtime:auto", { allowAutoAlias: true, isLiteralModelId: (provider, id) => provider === "example" && id === "runtime:auto", }); expect(result).toEqual({ provider: "example", id: "runtime:auto" }); }); test("does not strip inherited object keys as thinking suffixes", () => { const result = parseModelString("anthropic/claude-sonnet-4-5:constructor"); expect(result).toEqual({ provider: "anthropic", id: "claude-sonnet-4-5:constructor" }); }); test("does not extract thinking level from model ID with invalid suffix", () => { const result = parseModelString("openrouter/openai/gpt-4o:extended"); // :extended is not a valid thinking level, so it stays as part of the ID expect(result).toEqual({ provider: "openrouter", id: "openai/gpt-4o:extended" }); }); test("handles empty suffix after colon", () => { const result = parseModelString("anthropic/claude-sonnet-4-5:"); // Empty string is not a valid thinking level, so colon stays as part of ID expect(result).toEqual({ provider: "anthropic", id: "claude-sonnet-4-5:" }); }); }); }); describe("resolveModelFromString", () => { test("applies max as a provider model selector alias after literal lookup misses", () => { const result = resolveModelFromString("nanogpt/coding-router:max", [mockMaxSuffixModels[0]]); expect(result?.provider).toBe("nanogpt"); expect(result?.id).toBe("coding-router"); }); test("preserves literal max provider model ids before alias parsing", () => { const result = resolveModelFromString("nanogpt/coding-router:max", mockMaxSuffixModels); expect(result?.provider).toBe("nanogpt"); expect(result?.id).toBe("coding-router:max"); }); test("preserves literal :auto provider model ids before alias parsing", () => { const result = resolveModelFromString("example/runtime:auto", mockAutoSuffixModels); expect(result?.provider).toBe("example"); expect(result?.id).toBe("runtime:auto"); }); }); describe("resolveExplicitModelRole", () => { test("extracts built-in, custom, legacy, default, and thinking-suffixed aliases before expansion", () => { const settings = Settings.isolated({ modelRoles: { reviewer: "openai/gpt-4o", }, }); expect(resolveExplicitModelRole("@task", settings)).toBe("task"); expect(resolveExplicitModelRole("pi/reviewer:high", settings)).toBe("reviewer"); expect(resolveExplicitModelRole("@reviewer:xhigh", settings)).toBe("reviewer"); expect(resolveExplicitModelRole("*:low", settings)).toBe("default"); }); test("does not infer a role from an explicit model selector", () => { const settings = Settings.isolated({ modelRoles: { reviewer: "openai/gpt-4o" } }); expect(resolveExplicitModelRole("openai/gpt-4o", settings)).toBeUndefined(); expect(resolveExplicitModelRole("openai/gpt-4o:high", settings)).toBeUndefined(); expect(resolveExplicitModelRole("openai/gpt-4o:max", settings)).toBeUndefined(); expect(resolveExplicitModelRole(["openai/gpt-4o", "@reviewer:high"], settings)).toBe("reviewer"); }); }); describe("expandRoleAlias", () => { test("expands @vision to configured vision role", () => { const settings = Settings.isolated(); settings.setModelRole("vision", "openai/gpt-4o"); expect(expandRoleAlias("@vision", settings)).toBe("openai/gpt-4o"); }); test("keeps @vision alias when vision role is unset", () => { const settings = Settings.isolated(); settings.setModelRole("default", "anthropic/claude-sonnet-4-5"); expect(expandRoleAlias("@vision", settings)).toBe("@vision"); }); }); describe("extractExplicitThinkingSelector", () => { test("does not carry max from literal role model ids", () => { const result = extractExplicitThinkingSelector("nanogpt/coding-router:max", undefined, { isLiteralModelId: (provider, id) => provider === "nanogpt" && id === "coding-router:max", }); expect(result).toBeUndefined(); }); test("treats max as an explicit selector when the model id is not literal", () => { const result = extractExplicitThinkingSelector("nanogpt/coding-router:max", undefined, { isLiteralModelId: () => false, }); expect(result).toBe(Effort.Max); }); test("treats max on pi role aliases as an explicit selector before expansion", () => { const settings = Settings.isolated(); settings.setModelRole("smol", "nanogpt/coding-router:max"); const result = extractExplicitThinkingSelector("@smol:max", settings, { isLiteralModelId: (provider, id) => provider === "nanogpt" && id === "coding-router:max", }); expect(result).toBe(Effort.Max); }); test("does not carry auto from literal role model ids", () => { const result = extractExplicitThinkingSelector("nanogpt/coding-router:auto", undefined, { isLiteralModelId: (provider, id) => provider === "nanogpt" && id === "coding-router:auto", }); expect(result).toBeUndefined(); }); test("treats auto as an explicit selector when the model id is not literal", () => { const result = extractExplicitThinkingSelector("openai/gpt-5:auto", undefined, { isLiteralModelId: () => false, }); expect(result).toBe("auto"); }); }); describe("provider routing selector (@upstream)", () => { const openRouterOnly = (model: Model | undefined): string[] | undefined => (model?.compat as { openRouterRouting?: { only?: string[] } } | undefined)?.openRouterRouting?.only; test("pins an OpenRouter model to one upstream via @slug", () => { const result = parseModelPattern("openrouter/z-ai/glm-4.7@cerebras", allModels); expect(result.model?.id).toBe("z-ai/glm-4.7"); expect(result.model?.provider).toBe("openrouter"); expect(result.upstream).toBe("cerebras"); expect(openRouterOnly(result.model)).toEqual(["cerebras"]); }); test("resolves @slug without an explicit provider prefix", () => { const result = parseModelPattern("z-ai/glm-4.7@cerebras", allModels); expect(result.model?.id).toBe("z-ai/glm-4.7"); expect(openRouterOnly(result.model)).toEqual(["cerebras"]); }); test("combines @slug with a trailing thinking level", () => { const result = parseModelPattern("openrouter/z-ai/glm-4.7@cerebras:high", allModels); expect(result.model?.id).toBe("z-ai/glm-4.7"); expect(result.thinkingLevel).toBe(Effort.High); expect(openRouterOnly(result.model)).toEqual(["cerebras"]); }); test("preserves @upstream when the slug also matches model tokens", () => { const result = parseModelPattern("openrouter/deepseek/deepseek-v4-pro@deepseek:high", allModels); expect(result.model?.id).toBe("deepseek/deepseek-v4-pro"); expect(result.thinkingLevel).toBe(Effort.High); expect(result.upstream).toBe("deepseek"); expect(openRouterOnly(result.model)).toEqual(["deepseek"]); }); test("routes Vercel AI Gateway models via vercelGatewayRouting", () => { const gatewayModel: Model<"openai-completions"> = buildModel({ id: "zai/glm-4.7", name: "GLM 4.7 (Gateway)", api: "openai-completions", provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh/v1", reasoning: true, input: ["text"], cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, contextWindow: 128000, maxTokens: 8192, }); const result = parseModelPattern("vercel-ai-gateway/zai/glm-4.7@cerebras", [gatewayModel]); expect(result.model?.id).toBe("zai/glm-4.7"); expect( (result.model?.compat as { vercelGatewayRouting?: { only?: string[] } } | undefined)?.vercelGatewayRouting ?.only, ).toEqual(["cerebras"]); expect(openRouterOnly(result.model)).toBeUndefined(); }); test("does not split a model id that legitimately ends in @ (Vertex)", () => { const vertexModel: Model<"anthropic-messages"> = buildModel({ id: "claude-opus-4-8@default", name: "Claude Opus 4.8", api: "anthropic-messages", provider: "google-vertex", baseUrl: "https://us-aiplatform.googleapis.com", reasoning: true, input: ["text"], cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 }, contextWindow: 200000, maxTokens: 32000, }); const result = parseModelPattern("claude-opus-4-8@default", [vertexModel]); expect(result.model?.id).toBe("claude-opus-4-8@default"); expect(result.upstream).toBeUndefined(); expect(openRouterOnly(result.model)).toBeUndefined(); }); test("keeps fuzzy matching a non-aggregator provider id that ends in @ (Vertex)", () => { const vertexModel: Model<"anthropic-messages"> = buildModel({ id: "claude-opus-4-8@default", name: "Claude Opus 4.8", api: "anthropic-messages", provider: "google-vertex", baseUrl: "https://us-aiplatform.googleapis.com", reasoning: true, input: ["text"], cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 }, contextWindow: 200000, maxTokens: 32000, }); // `opus@default` is a fuzzy provider-qualified pattern: the `@upstream` bypass must not // swallow it, because google-vertex is not an aggregator and the routing fallback would // never resolve it, leaving the selector unmatched. const result = parseModelPattern("google-vertex/opus@default", [vertexModel]); expect(result.model?.id).toBe("claude-opus-4-8@default"); expect(result.upstream).toBeUndefined(); expect(openRouterOnly(result.model)).toBeUndefined(); }); test("ignores @slug on a non-aggregator model (no silent routing)", () => { const result = parseModelPattern("gpt-4o@cerebras", allModels); expect(result.model).toBeUndefined(); }); test("resolveCliModel round-trips @upstream in the selector and carries compat", () => { const registry = { getAll: () => allModels, getAvailable: () => allModels } as unknown as Parameters< typeof resolveCliModel >[0]["modelRegistry"]; const result = resolveCliModel({ cliModel: "openrouter/z-ai/glm-4.7@cerebras", modelRegistry: registry }); expect(result.model?.id).toBe("z-ai/glm-4.7"); expect(result.selector).toBe("openrouter/z-ai/glm-4.7@cerebras"); expect(openRouterOnly(result.model)).toEqual(["cerebras"]); }); }); describe("filterAvailableModelsByEnabledPatterns", () => { const models = mockModels as Model[]; test("returns all models when patterns is empty", () => { expect(filterAvailableModelsByEnabledPatterns(models, [])).toEqual(models); }); test("resolves role aliases to the role's model when settings are provided", () => { const settings = Settings.isolated({ modelRoles: { fable: "anthropic/claude-sonnet-4-5:high" }, }); const result = filterAvailableModelsByEnabledPatterns(models, ["@fable"], settings); expect(result).toHaveLength(1); expect(result[0].id).toBe("claude-sonnet-4-5"); }); test("filters by exact provider/modelId", () => { const result = filterAvailableModelsByEnabledPatterns(models, ["anthropic/claude-sonnet-4-5"]); expect(result).toHaveLength(1); expect(result[0].id).toBe("claude-sonnet-4-5"); }); test("filters by bare model id matching across providers", () => { const result = filterAvailableModelsByEnabledPatterns(models, ["claude-sonnet-4-5"]); expect(result).toHaveLength(1); expect(result[0].provider).toBe("anthropic"); }); test("strips :thinkingLevel suffix before matching", () => { const result = filterAvailableModelsByEnabledPatterns(models, ["anthropic/claude-sonnet-4-5:high"]); expect(result).toHaveLength(1); expect(result[0].id).toBe("claude-sonnet-4-5"); }); test("preserves colon-bearing OpenRouter ids (suffix is not a thinking level)", () => { const openRouterModels = mockOpenRouterModels as Model[]; const result = filterAvailableModelsByEnabledPatterns(openRouterModels, ["openrouter/qwen/qwen3-coder:exacto"]); expect(result).toHaveLength(1); expect(result[0].id).toBe("qwen/qwen3-coder:exacto"); }); test("matches bare OpenRouter-style model id with slash but no provider prefix", () => { const openRouterModels = mockOpenRouterModels as Model[]; const result = filterAvailableModelsByEnabledPatterns(openRouterModels, ["qwen/qwen3-coder:exacto"]); expect(result).toHaveLength(1); expect(result[0].id).toBe("qwen/qwen3-coder:exacto"); expect(result[0].provider).toBe("openrouter"); }); test("evaluates glob patterns against provider/modelId", () => { const result = filterAvailableModelsByEnabledPatterns(models, ["anthropic/*"]); expect(result).toHaveLength(1); expect(result[0].provider).toBe("anthropic"); }); test("preserves literal :max in enabledModels globs", () => { const result = filterAvailableModelsByEnabledPatterns(mockMaxSuffixModels, ["nanogpt/*:max"]); expect(result).toHaveLength(1); expect(result[0].id).toBe("coding-router:max"); }); test("evaluates glob patterns against bare model id", () => { const result = filterAvailableModelsByEnabledPatterns(models, ["claude-*"]); expect(result).toHaveLength(1); expect(result[0].id).toBe("claude-sonnet-4-5"); }); test("applies glob and exact patterns together", () => { const result = filterAvailableModelsByEnabledPatterns(models, ["anthropic/*", "openai/gpt-4o"]); expect(result).toHaveLength(2); }); test("returns empty list when no pattern matches (misconfiguration)", () => { const result = filterAvailableModelsByEnabledPatterns(models, ["nonexistent-model"]); expect(result).toHaveLength(0); }); test("includes multiple patterns from different providers", () => { const result = filterAvailableModelsByEnabledPatterns(models, ["anthropic/claude-sonnet-4-5", "openai/gpt-4o"]); expect(result).toHaveLength(2); }); test("keeps synthetic Bedrock inference profile matches", () => { const bedrockModels = [createBedrockDefaultModel()]; const profileArn = "arn:aws:bedrock:us-east-2:1234567890:application-inference-profile/company-opus-48"; const result = filterAvailableModelsByEnabledPatterns(bedrockModels, [`amazon-bedrock/${profileArn}`]); expect(result).toHaveLength(1); expect(result[0].provider).toBe("amazon-bedrock"); expect(result[0].id).toBe(profileArn); expect(result[0].reasoning).toBe(false); }); test("resolveAllowedModels keeps synthetic Bedrock inference profile matches", async () => { const bedrockModels = [createBedrockDefaultModel()]; const profileArn = "arn:aws:bedrock:us-east-2:1234567890:application-inference-profile/company-opus-48"; const settings = Settings.isolated({ enabledModels: [profileArn] }); const result = await resolveAllowedModels( { getAvailable: () => bedrockModels, }, settings, ); expect(result).toHaveLength(1); expect(result[0].provider).toBe("amazon-bedrock"); expect(result[0].id).toBe(profileArn); expect(result[0].reasoning).toBe(false); }); test("does not coalesce explicit provider/id patterns to Codex (regression for enabledModels)", () => { const result = filterAvailableModelsByEnabledPatterns(openaiGpt55Models, ["openai/gpt-5.5"]); expect(result).toHaveLength(1); expect(result[0].provider).toBe("openai"); expect(result[0].id).toBe("gpt-5.5"); }); }); describe("effort-tier variant aliases", () => { const variantModels: Model[] = [ buildModel({ id: "gemini-3.5-flash", requestModelId: "gemini-3.5-flash-extra-low", name: "Gemini 3.5 Flash", api: "google-gemini-cli", provider: "google-antigravity", baseUrl: "https://daily-cloudcode-pa.googleapis.com", reasoning: true, thinking: { mode: "google-level", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High], effortRouting: { off: "gemini-3.5-flash-extra-low", [Effort.Minimal]: "gemini-3-flash-agent", [Effort.Low]: "gemini-3.5-flash-extra-low", [Effort.Medium]: "gemini-3.5-flash-extra-low", [Effort.High]: "gemini-3.5-flash-low", }, suppressWhenOff: true, }, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1_048_576, maxTokens: 65_535, }), // Live legacy model whose id is also a recycled alias of the family — // exact matches must keep winning while it exists. buildModel({ id: "gemini-3-flash", name: "Gemini 3 Flash", api: "google-gemini-cli", provider: "google-antigravity", baseUrl: "https://daily-cloudcode-pa.googleapis.com", reasoning: true, thinking: { mode: "google-level", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] }, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1_048_576, maxTokens: 65_535, }), // Auto-derived pair target on a provider without a hand table. buildModel({ id: "kimi-k2", name: "Kimi K2", api: "openai-completions", provider: "venice", baseUrl: "https://api.venice.ai/api/v1", reasoning: true, thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] }, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128_000, maxTokens: 8_192, }), ]; test("provider-qualified retired tier ids resolve to the collapsed model", () => { const result = parseModelPattern("google-antigravity/gemini-3.5-flash-low", variantModels); expect(result.model?.id).toBe("gemini-3.5-flash"); expect(result.thinkingLevel).toBeUndefined(); }); test("retired tier ids keep explicit :level suffixes", () => { const result = parseModelPattern("google-antigravity/gemini-3.5-flash-low:high", variantModels); expect(result.model?.id).toBe("gemini-3.5-flash"); expect(result.thinkingLevel).toBe(Effort.High); }); test("bare retired tier ids resolve through the alias table", () => { const result = parseModelPattern("gemini-3.5-flash-extra-low", variantModels); expect(result.model?.id).toBe("gemini-3.5-flash"); expect(result.model?.provider).toBe("google-antigravity"); }); test("live models always beat recycled aliases", () => { const result = parseModelPattern("google-antigravity/gemini-3-flash", variantModels); expect(result.model?.id).toBe("gemini-3-flash"); }); test("consumed X-thinking twins resolve via the grammar fallback", () => { expect(parseModelPattern("venice/kimi-k2-thinking", variantModels).model?.id).toBe("kimi-k2"); expect(parseModelPattern("kimi-k2-thinking", variantModels).model?.id).toBe("kimi-k2"); }); });