diff --git a/src/cli/__snapshots__/model-fallback.test.ts.snap b/src/cli/__snapshots__/model-fallback.test.ts.snap index 3e1b2f50ce..88f86f97ba 100644 --- a/src/cli/__snapshots__/model-fallback.test.ts.snap +++ b/src/cli/__snapshots__/model-fallback.test.ts.snap @@ -4137,3 +4137,587 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is }, } `; + +exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strings when only Vercel AI Gateway is available 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", + "agents": { + "atlas": { + "fallback_models": [ + { + "model": "vercel/moonshotai/kimi-k2.5", + }, + { + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + { + "model": "vercel/minimax/minimax-m2.7", + }, + ], + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + "explore": { + "fallback_models": [ + { + "model": "vercel/xai/grok-code-fast-1", + }, + { + "model": "vercel/minimax/minimax-m2.7", + }, + { + "model": "vercel/anthropic/claude-haiku-4.5", + }, + { + "model": "vercel/openai/gpt-5-nano", + }, + ], + "model": "vercel/minimax/minimax-m2.7-highspeed", + }, + "hephaestus": { + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + "librarian": { + "fallback_models": [ + { + "model": "vercel/minimax/minimax-m2.7-highspeed", + }, + { + "model": "vercel/anthropic/claude-haiku-4.5", + }, + { + "model": "vercel/openai/gpt-5-nano", + }, + ], + "model": "vercel/minimax/minimax-m2.7", + }, + "metis": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.4", + "variant": "high", + }, + { + "model": "vercel/zai/glm-5", + }, + ], + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + "momus": { + "fallback_models": [ + { + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + { + "model": "vercel/google/gemini-3.1-pro-preview", + "variant": "high", + }, + { + "model": "vercel/zai/glm-5", + }, + ], + "model": "vercel/openai/gpt-5.4", + "variant": "xhigh", + }, + "multimodal-looker": { + "fallback_models": [ + { + "model": "vercel/moonshotai/kimi-k2.5", + }, + { + "model": "vercel/zai/glm-4.6v", + }, + { + "model": "vercel/openai/gpt-5-nano", + }, + ], + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + "oracle": { + "fallback_models": [ + { + "model": "vercel/google/gemini-3.1-pro-preview", + "variant": "high", + }, + { + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + { + "model": "vercel/zai/glm-5", + }, + ], + "model": "vercel/openai/gpt-5.4", + "variant": "high", + }, + "prometheus": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.4", + "variant": "high", + }, + { + "model": "vercel/zai/glm-5", + }, + { + "model": "vercel/google/gemini-3.1-pro-preview", + }, + ], + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + "sisyphus": { + "fallback_models": [ + { + "model": "vercel/moonshotai/kimi-k2.5", + }, + { + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + { + "model": "vercel/zai/glm-5", + }, + ], + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + "sisyphus-junior": { + "fallback_models": [ + { + "model": "vercel/moonshotai/kimi-k2.5", + }, + { + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + { + "model": "vercel/minimax/minimax-m2.7", + }, + ], + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + }, + "categories": { + "artistry": { + "fallback_models": [ + { + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + { + "model": "vercel/openai/gpt-5.4", + }, + ], + "model": "vercel/google/gemini-3.1-pro-preview", + "variant": "high", + }, + "deep": { + "fallback_models": [ + { + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + { + "model": "vercel/google/gemini-3.1-pro-preview", + "variant": "high", + }, + ], + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + "quick": { + "fallback_models": [ + { + "model": "vercel/anthropic/claude-haiku-4.5", + }, + { + "model": "vercel/google/gemini-3-flash", + }, + { + "model": "vercel/minimax/minimax-m2.7", + }, + { + "model": "vercel/openai/gpt-5-nano", + }, + ], + "model": "vercel/openai/gpt-5.4-mini", + }, + "ultrabrain": { + "fallback_models": [ + { + "model": "vercel/google/gemini-3.1-pro-preview", + "variant": "high", + }, + { + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + { + "model": "vercel/zai/glm-5", + }, + ], + "model": "vercel/openai/gpt-5.4", + "variant": "xhigh", + }, + "unspecified-high": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.3-codex", + "variant": "medium", + }, + { + "model": "vercel/moonshotai/kimi-k2.5", + }, + { + "model": "vercel/google/gemini-3-flash", + }, + { + "model": "vercel/minimax/minimax-m2.7", + }, + ], + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + "unspecified-low": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.3-codex", + "variant": "medium", + }, + { + "model": "vercel/moonshotai/kimi-k2.5", + }, + { + "model": "vercel/google/gemini-3-flash", + }, + { + "model": "vercel/minimax/minimax-m2.7", + }, + ], + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + "visual-engineering": { + "fallback_models": [ + { + "model": "vercel/zai/glm-5", + }, + { + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + ], + "model": "vercel/google/gemini-3.1-pro-preview", + "variant": "high", + }, + "writing": { + "fallback_models": [ + { + "model": "vercel/moonshotai/kimi-k2.5", + }, + { + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + { + "model": "vercel/minimax/minimax-m2.7", + }, + ], + "model": "vercel/google/gemini-3-flash", + }, + }, +} +`; + +exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strings with isMax20 flag 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json", + "agents": { + "atlas": { + "fallback_models": [ + { + "model": "vercel/moonshotai/kimi-k2.5", + }, + { + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + { + "model": "vercel/minimax/minimax-m2.7", + }, + ], + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + "explore": { + "fallback_models": [ + { + "model": "vercel/xai/grok-code-fast-1", + }, + { + "model": "vercel/minimax/minimax-m2.7", + }, + { + "model": "vercel/anthropic/claude-haiku-4.5", + }, + { + "model": "vercel/openai/gpt-5-nano", + }, + ], + "model": "vercel/minimax/minimax-m2.7-highspeed", + }, + "hephaestus": { + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + "librarian": { + "fallback_models": [ + { + "model": "vercel/minimax/minimax-m2.7-highspeed", + }, + { + "model": "vercel/anthropic/claude-haiku-4.5", + }, + { + "model": "vercel/openai/gpt-5-nano", + }, + ], + "model": "vercel/minimax/minimax-m2.7", + }, + "metis": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.4", + "variant": "high", + }, + { + "model": "vercel/zai/glm-5", + }, + ], + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + "momus": { + "fallback_models": [ + { + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + { + "model": "vercel/google/gemini-3.1-pro-preview", + "variant": "high", + }, + { + "model": "vercel/zai/glm-5", + }, + ], + "model": "vercel/openai/gpt-5.4", + "variant": "xhigh", + }, + "multimodal-looker": { + "fallback_models": [ + { + "model": "vercel/moonshotai/kimi-k2.5", + }, + { + "model": "vercel/zai/glm-4.6v", + }, + { + "model": "vercel/openai/gpt-5-nano", + }, + ], + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + "oracle": { + "fallback_models": [ + { + "model": "vercel/google/gemini-3.1-pro-preview", + "variant": "high", + }, + { + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + { + "model": "vercel/zai/glm-5", + }, + ], + "model": "vercel/openai/gpt-5.4", + "variant": "high", + }, + "prometheus": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.4", + "variant": "high", + }, + { + "model": "vercel/zai/glm-5", + }, + { + "model": "vercel/google/gemini-3.1-pro-preview", + }, + ], + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + "sisyphus": { + "fallback_models": [ + { + "model": "vercel/moonshotai/kimi-k2.5", + }, + { + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + { + "model": "vercel/zai/glm-5", + }, + ], + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + "sisyphus-junior": { + "fallback_models": [ + { + "model": "vercel/moonshotai/kimi-k2.5", + }, + { + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + { + "model": "vercel/minimax/minimax-m2.7", + }, + ], + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + }, + "categories": { + "artistry": { + "fallback_models": [ + { + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + { + "model": "vercel/openai/gpt-5.4", + }, + ], + "model": "vercel/google/gemini-3.1-pro-preview", + "variant": "high", + }, + "deep": { + "fallback_models": [ + { + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + { + "model": "vercel/google/gemini-3.1-pro-preview", + "variant": "high", + }, + ], + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + "quick": { + "fallback_models": [ + { + "model": "vercel/anthropic/claude-haiku-4.5", + }, + { + "model": "vercel/google/gemini-3-flash", + }, + { + "model": "vercel/minimax/minimax-m2.7", + }, + { + "model": "vercel/openai/gpt-5-nano", + }, + ], + "model": "vercel/openai/gpt-5.4-mini", + }, + "ultrabrain": { + "fallback_models": [ + { + "model": "vercel/google/gemini-3.1-pro-preview", + "variant": "high", + }, + { + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + { + "model": "vercel/zai/glm-5", + }, + ], + "model": "vercel/openai/gpt-5.4", + "variant": "xhigh", + }, + "unspecified-high": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.4", + "variant": "high", + }, + { + "model": "vercel/zai/glm-5", + }, + { + "model": "vercel/moonshotai/kimi-k2.5", + }, + ], + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + "unspecified-low": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.3-codex", + "variant": "medium", + }, + { + "model": "vercel/moonshotai/kimi-k2.5", + }, + { + "model": "vercel/google/gemini-3-flash", + }, + { + "model": "vercel/minimax/minimax-m2.7", + }, + ], + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + "visual-engineering": { + "fallback_models": [ + { + "model": "vercel/zai/glm-5", + }, + { + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + ], + "model": "vercel/google/gemini-3.1-pro-preview", + "variant": "high", + }, + "writing": { + "fallback_models": [ + { + "model": "vercel/moonshotai/kimi-k2.5", + }, + { + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + { + "model": "vercel/minimax/minimax-m2.7", + }, + ], + "model": "vercel/google/gemini-3-flash", + }, + }, +} +`; diff --git a/src/cli/cli-installer.telemetry.test.ts b/src/cli/cli-installer.telemetry.test.ts index c4bdee6525..c1b8eb8ac7 100644 --- a/src/cli/cli-installer.telemetry.test.ts +++ b/src/cli/cli-installer.telemetry.test.ts @@ -22,6 +22,7 @@ describe("runCliInstaller telemetry isolation", () => { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, }), spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true), spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.4.0"), diff --git a/src/cli/cli-installer.test.ts b/src/cli/cli-installer.test.ts index 38514fcf06..3fbb21f20c 100644 --- a/src/cli/cli-installer.test.ts +++ b/src/cli/cli-installer.test.ts @@ -37,6 +37,7 @@ describe("runCliInstaller", () => { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, }), spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true), spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.3.9"), @@ -83,6 +84,7 @@ describe("runCliInstaller", () => { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, }), spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true), spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.4.0"), diff --git a/src/cli/cli-installer.ts b/src/cli/cli-installer.ts index 5db0155316..72d5d8d7c0 100644 --- a/src/cli/cli-installer.ts +++ b/src/cli/cli-installer.ts @@ -138,7 +138,8 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi !config.hasOpenAI && !config.hasGemini && !config.hasCopilot && - !config.hasOpencodeZen + !config.hasOpencodeZen && + !config.hasVercelAiGateway ) { printWarning("No model providers configured. Using opencode/big-pickle as fallback.") } diff --git a/src/cli/cli-program.ts b/src/cli/cli-program.ts index 15d3d04891..6d982e57bf 100644 --- a/src/cli/cli-program.ts +++ b/src/cli/cli-program.ts @@ -33,6 +33,7 @@ program .option("--zai-coding-plan ", "Z.ai Coding Plan subscription: no, yes (default: no)") .option("--kimi-for-coding ", "Kimi For Coding subscription: no, yes (default: no)") .option("--opencode-go ", "OpenCode Go subscription: no, yes (default: no)") + .option("--vercel-ai-gateway ", "Vercel AI Gateway: no, yes (default: no)") .option("--skip-auth", "Skip authentication setup hints") .addHelpText("after", ` Examples: @@ -40,14 +41,15 @@ Examples: $ bunx oh-my-opencode install --no-tui --claude=max20 --openai=yes --gemini=yes --copilot=no $ bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=yes --opencode-zen=yes -Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai > Kimi): +Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai > Kimi > Vercel): Claude Native anthropic/ models (Opus, Sonnet, Haiku) OpenAI Native openai/ models (GPT-5.4 for Oracle) Gemini Native google/ models (Gemini 3.1 Pro, Flash) Copilot github-copilot/ models (fallback) OpenCode Zen opencode/ models (opencode/claude-opus-4-6, etc.) - Z.ai zai-coding-plan/glm-5 (visual-engineering fallback) + Z.ai zai-coding-plan/glm-5 (visual-engineering fallback) Kimi kimi-for-coding/k2p5 (Sisyphus/Prometheus fallback) + Vercel vercel/ models (universal proxy, always last fallback) `) .action(async (options) => { const args: InstallArgs = { @@ -60,6 +62,7 @@ Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai > Kimi): zaiCodingPlan: options.zaiCodingPlan, kimiForCoding: options.kimiForCoding, opencodeGo: options.opencodeGo, + vercelAiGateway: options.vercelAiGateway, skipAuth: options.skipAuth ?? false, } const exitCode = await install(args) diff --git a/src/cli/config-manager/detect-current-config.ts b/src/cli/config-manager/detect-current-config.ts index f158e18e21..e1fb398233 100644 --- a/src/cli/config-manager/detect-current-config.ts +++ b/src/cli/config-manager/detect-current-config.ts @@ -12,6 +12,7 @@ function detectProvidersFromOmoConfig(): { hasZaiCodingPlan: boolean hasKimiForCoding: boolean hasOpencodeGo: boolean + hasVercelAiGateway: boolean } { const omoConfigPath = getOmoConfigPath() if (!existsSync(omoConfigPath)) { @@ -21,6 +22,7 @@ function detectProvidersFromOmoConfig(): { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, } } @@ -34,6 +36,7 @@ function detectProvidersFromOmoConfig(): { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, } } @@ -43,8 +46,9 @@ function detectProvidersFromOmoConfig(): { const hasZaiCodingPlan = configStr.includes('"zai-coding-plan/') const hasKimiForCoding = configStr.includes('"kimi-for-coding/') const hasOpencodeGo = configStr.includes('"opencode-go/') + const hasVercelAiGateway = configStr.includes('"vercel/') - return { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding, hasOpencodeGo } + return { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding, hasOpencodeGo, hasVercelAiGateway } } catch { return { hasOpenAI: true, @@ -52,6 +56,7 @@ function detectProvidersFromOmoConfig(): { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, } } } @@ -78,6 +83,7 @@ export function detectCurrentConfig(): DetectedConfig { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, } const { format, path } = detectConfigFormat() @@ -106,12 +112,13 @@ export function detectCurrentConfig(): DetectedConfig { const providers = openCodeConfig.provider as Record | undefined result.hasGemini = providers ? "google" in providers : false - const { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding, hasOpencodeGo } = detectProvidersFromOmoConfig() + const { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding, hasOpencodeGo, hasVercelAiGateway } = detectProvidersFromOmoConfig() result.hasOpenAI = hasOpenAI result.hasOpencodeZen = hasOpencodeZen result.hasZaiCodingPlan = hasZaiCodingPlan result.hasKimiForCoding = hasKimiForCoding result.hasOpencodeGo = hasOpencodeGo + result.hasVercelAiGateway = hasVercelAiGateway return result } diff --git a/src/cli/config-manager/generate-omo-config.test.ts b/src/cli/config-manager/generate-omo-config.test.ts index dbec67c00a..50fe4e3d8b 100644 --- a/src/cli/config-manager/generate-omo-config.test.ts +++ b/src/cli/config-manager/generate-omo-config.test.ts @@ -18,6 +18,7 @@ describe("generateOmoConfig - model fallback system", () => { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, } //#when @@ -42,6 +43,7 @@ describe("generateOmoConfig - model fallback system", () => { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, } //#when @@ -64,6 +66,7 @@ describe("generateOmoConfig - model fallback system", () => { hasZaiCodingPlan: true, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, } //#when @@ -71,7 +74,7 @@ describe("generateOmoConfig - model fallback system", () => { //#then expect((result.agents as Record).librarian.model).toBe("zai-coding-plan/glm-4.7") - expect((result.agents as Record).sisyphus.model).toBe("anthropic/claude-opus-4-6") + expect((result.agents as Record).sisyphus.model).toBe("anthropic/claude-opus-4.6") }) test("uses native OpenAI models when only ChatGPT available", () => { @@ -86,6 +89,7 @@ describe("generateOmoConfig - model fallback system", () => { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, } //#when @@ -110,6 +114,7 @@ describe("generateOmoConfig - model fallback system", () => { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, } //#when @@ -126,7 +131,7 @@ describe("generateOmoConfig - model fallback system", () => { }> //#then - expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-6") + expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4.6") expect(agents.sisyphus.fallback_models).toEqual([ { model: "openai/gpt-5.4", @@ -136,7 +141,7 @@ describe("generateOmoConfig - model fallback system", () => { expect(categories.deep.model).toBe("openai/gpt-5.4") expect(categories.deep.fallback_models).toEqual([ { - model: "anthropic/claude-opus-4-6", + model: "anthropic/claude-opus-4.6", variant: "max", }, ]) @@ -154,6 +159,7 @@ describe("generateOmoConfig - model fallback system", () => { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, } //#when @@ -175,6 +181,7 @@ describe("generateOmoConfig - model fallback system", () => { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, } //#when diff --git a/src/cli/config-manager/write-omo-config.test.ts b/src/cli/config-manager/write-omo-config.test.ts index 10ccf7a27c..7b7ba6409a 100644 --- a/src/cli/config-manager/write-omo-config.test.ts +++ b/src/cli/config-manager/write-omo-config.test.ts @@ -20,6 +20,7 @@ const installConfig: InstallConfig = { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, } function getRecord(value: unknown): Record { diff --git a/src/cli/install-validators.ts b/src/cli/install-validators.ts index be429eee66..695747edf7 100644 --- a/src/cli/install-validators.ts +++ b/src/cli/install-validators.ts @@ -40,6 +40,7 @@ export function formatConfigSummary(config: InstallConfig): string { lines.push(formatProvider("OpenCode Zen", config.hasOpencodeZen, "opencode/ models")) lines.push(formatProvider("Z.ai Coding Plan", config.hasZaiCodingPlan, "Librarian/Multimodal")) lines.push(formatProvider("Kimi For Coding", config.hasKimiForCoding, "Sisyphus/Prometheus fallback")) + lines.push(formatProvider("Vercel AI Gateway", config.hasVercelAiGateway, "universal proxy")) lines.push("") lines.push(color.dim("─".repeat(40))) @@ -153,6 +154,10 @@ export function validateNonTuiArgs(args: InstallArgs): { valid: boolean; errors: errors.push(`Invalid --kimi-for-coding value: ${args.kimiForCoding} (expected: no, yes)`) } + if (args.vercelAiGateway !== undefined && !["no", "yes"].includes(args.vercelAiGateway)) { + errors.push(`Invalid --vercel-ai-gateway value: ${args.vercelAiGateway} (expected: no, yes)`) + } + return { valid: errors.length === 0, errors } } @@ -167,6 +172,7 @@ export function argsToConfig(args: InstallArgs): InstallConfig { hasZaiCodingPlan: args.zaiCodingPlan === "yes", hasKimiForCoding: args.kimiForCoding === "yes", hasOpencodeGo: args.opencodeGo === "yes", + hasVercelAiGateway: args.vercelAiGateway === "yes", } } @@ -179,6 +185,7 @@ export function detectedToInitialValues(detected: DetectedConfig): { zaiCodingPlan: BooleanArg kimiForCoding: BooleanArg opencodeGo: BooleanArg + vercelAiGateway: BooleanArg } { let claude: ClaudeSubscription = "no" if (detected.hasClaude) { @@ -194,5 +201,6 @@ kimiForCoding: BooleanArg zaiCodingPlan: detected.hasZaiCodingPlan ? "yes" : "no", kimiForCoding: detected.hasKimiForCoding ? "yes" : "no", opencodeGo: detected.hasOpencodeGo ? "yes" : "no", + vercelAiGateway: detected.hasVercelAiGateway ? "yes" : "no", } } diff --git a/src/cli/model-fallback-types.ts b/src/cli/model-fallback-types.ts index b195f698a2..6daa243882 100644 --- a/src/cli/model-fallback-types.ts +++ b/src/cli/model-fallback-types.ts @@ -11,6 +11,7 @@ export interface ProviderAvailability { zai: boolean kimiForCoding: boolean opencodeGo: boolean + vercelAiGateway: boolean isMaxPlan: boolean } diff --git a/src/cli/model-fallback.test.ts b/src/cli/model-fallback.test.ts index 09d002bcb0..f1df3cc0ac 100644 --- a/src/cli/model-fallback.test.ts +++ b/src/cli/model-fallback.test.ts @@ -16,6 +16,7 @@ function createConfig(overrides: Partial = {}): InstallConfig { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, ...overrides, } } @@ -607,6 +608,74 @@ describe("generateModelConfig", () => { }) }) + describe("Vercel AI Gateway provider", () => { + test("uses vercel/ model strings when only Vercel AI Gateway is available", () => { + // #given only Vercel AI Gateway is available + const config = createConfig({ hasVercelAiGateway: true }) + + // #when generateModelConfig is called + const result = generateModelConfig(config) + + // #then should use vercel// format + expect(result).toMatchSnapshot() + }) + + test("uses vercel/ model strings with isMax20 flag", () => { + // #given Vercel AI Gateway is available with Max 20 plan + const config = createConfig({ hasVercelAiGateway: true, isMax20: true }) + + // #when generateModelConfig is called + const result = generateModelConfig(config) + + // #then should use higher capability models via gateway + expect(result).toMatchSnapshot() + }) + + test("explore uses vercel/minimax/minimax-m2.7-highspeed when only gateway available", () => { + // #given only Vercel AI Gateway is available + const config = createConfig({ hasVercelAiGateway: true }) + + // #when generateModelConfig is called + const result = generateModelConfig(config) + + // #then explore should use gateway-routed minimax (preferred over claude-haiku) + expect(result.agents?.explore?.model).toBe("vercel/minimax/minimax-m2.7-highspeed") + }) + + test("librarian uses vercel/minimax/minimax-m2.7 when only gateway available", () => { + // #given only Vercel AI Gateway is available + const config = createConfig({ hasVercelAiGateway: true }) + + // #when generateModelConfig is called + const result = generateModelConfig(config) + + // #then librarian should use gateway-routed minimax (preferred over claude-haiku) + expect(result.agents?.librarian?.model).toBe("vercel/minimax/minimax-m2.7") + }) + + test("Hephaestus is created when only Vercel AI Gateway is available", () => { + // #given only Vercel AI Gateway is available + const config = createConfig({ hasVercelAiGateway: true }) + + // #when generateModelConfig is called + const result = generateModelConfig(config) + + // #then hephaestus should be created with gateway-routed gpt-5.4 + expect(result.agents?.hephaestus?.model).toBe("vercel/openai/gpt-5.4") + }) + + test("native providers take priority over gateway", () => { + // #given Claude and Vercel AI Gateway are both available + const config = createConfig({ hasClaude: true, hasVercelAiGateway: true }) + + // #when generateModelConfig is called + const result = generateModelConfig(config) + + // #then should prefer native anthropic over gateway + expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-opus-4.6") + }) + }) + describe("schema URL", () => { test("always includes correct schema URL", () => { // #given any config diff --git a/src/cli/model-fallback.ts b/src/cli/model-fallback.ts index 5dabb9fc1a..088c4515e1 100644 --- a/src/cli/model-fallback.ts +++ b/src/cli/model-fallback.ts @@ -105,7 +105,8 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig { avail.copilot || avail.zai || avail.kimiForCoding || - avail.opencodeGo + avail.opencodeGo || + avail.vercelAiGateway if (!hasAnyProvider) { return { $schema: SCHEMA_URL, @@ -130,6 +131,8 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig { agentConfig = { model: "opencode-go/minimax-m2.7" } } else if (avail.zai) { agentConfig = { model: ZAI_MODEL } + } else if (avail.vercelAiGateway) { + agentConfig = { model: "vercel/minimax/minimax-m2.7" } } if (agentConfig) { agents[role] = attachAllFallbackModels(agentConfig, req.fallbackChain, avail) @@ -147,6 +150,8 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig { agentConfig = { model: "opencode-go/minimax-m2.7" } } else if (avail.copilot) { agentConfig = { model: "github-copilot/gpt-5-mini" } + } else if (avail.vercelAiGateway) { + agentConfig = { model: "vercel/minimax/minimax-m2.7-highspeed" } } else { agentConfig = { model: "opencode/gpt-5-nano" } } diff --git a/src/cli/openai-only-model-catalog.test.ts b/src/cli/openai-only-model-catalog.test.ts index 9d516dfddf..da544156cf 100644 --- a/src/cli/openai-only-model-catalog.test.ts +++ b/src/cli/openai-only-model-catalog.test.ts @@ -14,6 +14,7 @@ function createConfig(overrides: Partial = {}): InstallConfig { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, ...overrides, } } diff --git a/src/cli/provider-availability.ts b/src/cli/provider-availability.ts index 6d17422f7f..f9ac8189b8 100644 --- a/src/cli/provider-availability.ts +++ b/src/cli/provider-availability.ts @@ -13,6 +13,7 @@ export function toProviderAvailability(config: InstallConfig): ProviderAvailabil zai: config.hasZaiCodingPlan, kimiForCoding: config.hasKimiForCoding, opencodeGo: config.hasOpencodeGo, + vercelAiGateway: config.hasVercelAiGateway, isMaxPlan: config.isMax20, } } @@ -27,6 +28,7 @@ export function isProviderAvailable(provider: string, availability: ProviderAvai "zai-coding-plan": availability.zai, "kimi-for-coding": availability.kimiForCoding, "opencode-go": availability.opencodeGo, + vercel: availability.vercelAiGateway, } return mapping[provider] ?? false } diff --git a/src/cli/provider-model-id-transform.test.ts b/src/cli/provider-model-id-transform.test.ts index ddfdd6e2f2..e87fc12ff8 100644 --- a/src/cli/provider-model-id-transform.test.ts +++ b/src/cli/provider-model-id-transform.test.ts @@ -201,6 +201,116 @@ describe("transformModelForProvider", () => { }) }) + describe("vercel provider", () => { + test("prepends anthropic/ and applies anthropic transform for claude models", () => { + // #given vercel provider and claude-opus-4-6 model + // #when transformModelForProvider is called + const result = transformModelForProvider("vercel", "claude-opus-4-6") + + // #then should produce anthropic/claude-opus-4.6 + expect(result).toBe("anthropic/claude-opus-4.6") + }) + + test("prepends anthropic/ and applies anthropic transform for claude-sonnet", () => { + // #given vercel provider and claude-sonnet-4-6 model + // #when transformModelForProvider is called + const result = transformModelForProvider("vercel", "claude-sonnet-4-6") + + // #then should produce anthropic/claude-sonnet-4.6 + expect(result).toBe("anthropic/claude-sonnet-4.6") + }) + + test("prepends anthropic/ and applies anthropic transform for claude-haiku", () => { + // #given vercel provider and claude-haiku-4-5 model + // #when transformModelForProvider is called + const result = transformModelForProvider("vercel", "claude-haiku-4-5") + + // #then should produce anthropic/claude-haiku-4.5 + expect(result).toBe("anthropic/claude-haiku-4.5") + }) + + test("prepends openai/ for gpt models", () => { + // #given vercel provider and gpt-5.4 model + // #when transformModelForProvider is called + const result = transformModelForProvider("vercel", "gpt-5.4") + + // #then should produce openai/gpt-5.4 + expect(result).toBe("openai/gpt-5.4") + }) + + test("prepends google/ and applies google transform for gemini models", () => { + // #given vercel provider and gemini-3.1-pro model + // #when transformModelForProvider is called + const result = transformModelForProvider("vercel", "gemini-3.1-pro") + + // #then should produce google/gemini-3.1-pro-preview + expect(result).toBe("google/gemini-3.1-pro-preview") + }) + + test("prepends google/ without -preview for gemini-3-flash", () => { + // #given vercel provider and gemini-3-flash model + // #when transformModelForProvider is called + const result = transformModelForProvider("vercel", "gemini-3-flash") + + // #then should produce google/gemini-3-flash (gateway does not use -preview for this model) + expect(result).toBe("google/gemini-3-flash") + }) + + test("prepends xai/ for grok models", () => { + // #given vercel provider and grok-code-fast-1 model + // #when transformModelForProvider is called + const result = transformModelForProvider("vercel", "grok-code-fast-1") + + // #then should produce xai/grok-code-fast-1 + expect(result).toBe("xai/grok-code-fast-1") + }) + + test("delegates to sub-provider when model already has sub-provider prefix", () => { + // #given vercel provider and anthropic/claude-opus-4-6 (already prefixed) + // #when transformModelForProvider is called + const result = transformModelForProvider("vercel", "anthropic/claude-opus-4-6") + + // #then should apply anthropic transform within the prefix + expect(result).toBe("anthropic/claude-opus-4.6") + }) + + test("prepends minimax/ for minimax models", () => { + // #given vercel provider and minimax-m2.7 model + // #when transformModelForProvider is called + const result = transformModelForProvider("vercel", "minimax-m2.7") + + // #then should produce minimax/minimax-m2.7 + expect(result).toBe("minimax/minimax-m2.7") + }) + + test("prepends moonshotai/ for kimi models", () => { + // #given vercel provider and kimi-k2.5 model + // #when transformModelForProvider is called + const result = transformModelForProvider("vercel", "kimi-k2.5") + + // #then should produce moonshotai/kimi-k2.5 + expect(result).toBe("moonshotai/kimi-k2.5") + }) + + test("prepends zai/ for glm models", () => { + // #given vercel provider and glm-5 model + // #when transformModelForProvider is called + const result = transformModelForProvider("vercel", "glm-5") + + // #then should produce zai/glm-5 + expect(result).toBe("zai/glm-5") + }) + + test("passes through unknown models without sub-provider prefix", () => { + // #given vercel provider and an unknown model name + // #when transformModelForProvider is called + const result = transformModelForProvider("vercel", "big-pickle") + + // #then should pass through unchanged + expect(result).toBe("big-pickle") + }) + }) + describe("unknown provider", () => { test("passes model through unchanged for unknown provider", () => { // #given unknown provider and any model diff --git a/src/cli/tui-install-prompts.ts b/src/cli/tui-install-prompts.ts index c2e5a3b49d..3d5fb45841 100644 --- a/src/cli/tui-install-prompts.ts +++ b/src/cli/tui-install-prompts.ts @@ -110,6 +110,16 @@ export async function promptInstallConfig(detected: DetectedConfig): Promise { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, }), spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true), spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.3.9"), @@ -92,6 +93,7 @@ describe("runTuiInstaller", () => { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, }), spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true), spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.4.0"), @@ -105,6 +107,7 @@ describe("runTuiInstaller", () => { hasZaiCodingPlan: false, hasKimiForCoding: false, hasOpencodeGo: false, + hasVercelAiGateway: false, }), spyOn(configManager, "addPluginToOpenCodeConfig").mockResolvedValue({ success: true, diff --git a/src/cli/tui-installer.ts b/src/cli/tui-installer.ts index 4272557c0e..ef4b9088a0 100644 --- a/src/cli/tui-installer.ts +++ b/src/cli/tui-installer.ts @@ -77,7 +77,7 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi ) } - if (!config.hasClaude && !config.hasOpenAI && !config.hasGemini && !config.hasCopilot && !config.hasOpencodeZen) { + if (!config.hasClaude && !config.hasOpenAI && !config.hasGemini && !config.hasCopilot && !config.hasOpencodeZen && !config.hasVercelAiGateway) { p.log.warn("No model providers configured. Using opencode/big-pickle as fallback.") } diff --git a/src/cli/types.ts b/src/cli/types.ts index a8f785cb05..c61f96bd41 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -11,6 +11,7 @@ export interface InstallArgs { zaiCodingPlan?: BooleanArg kimiForCoding?: BooleanArg opencodeGo?: BooleanArg + vercelAiGateway?: BooleanArg skipAuth?: boolean } @@ -24,6 +25,7 @@ export interface InstallConfig { hasZaiCodingPlan: boolean hasKimiForCoding: boolean hasOpencodeGo: boolean + hasVercelAiGateway: boolean } export interface ConfigMergeResult { @@ -44,4 +46,5 @@ export interface DetectedConfig { hasZaiCodingPlan: boolean hasKimiForCoding: boolean hasOpencodeGo: boolean + hasVercelAiGateway: boolean } diff --git a/src/shared/model-requirements.test.ts b/src/shared/model-requirements.test.ts index bb110f554b..324176b579 100644 --- a/src/shared/model-requirements.test.ts +++ b/src/shared/model-requirements.test.ts @@ -35,12 +35,12 @@ describe("AGENT_MODEL_REQUIREMENTS", () => { expect(sisyphus.requiresAnyModel).toBe(true) const primary = sisyphus.fallbackChain[0] - expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode"]) + expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode", "vercel"]) expect(primary.model).toBe("claude-opus-4-6") expect(primary.variant).toBe("max") const second = sisyphus.fallbackChain[1] - expect(second.providers).toEqual(["opencode-go"]) + expect(second.providers).toEqual(["opencode-go", "vercel"]) expect(second.model).toBe("kimi-k2.5") const third = sisyphus.fallbackChain[2] @@ -132,19 +132,19 @@ describe("AGENT_MODEL_REQUIREMENTS", () => { expect(multimodalLooker.fallbackChain).toHaveLength(4) const primary = multimodalLooker.fallbackChain[0] - expect(primary.providers).toEqual(["openai", "opencode"]) + expect(primary.providers).toEqual(["openai", "opencode", "vercel"]) expect(primary.model).toBe("gpt-5.4") expect(primary.variant).toBe("medium") const secondary = multimodalLooker.fallbackChain[1] - expect(secondary.providers).toEqual(["opencode-go"]) + expect(secondary.providers).toEqual(["opencode-go", "vercel"]) expect(secondary.model).toBe("kimi-k2.5") const tertiary = multimodalLooker.fallbackChain[2] expect(tertiary.model).toBe("glm-4.6v") const last = multimodalLooker.fallbackChain[3] - expect(last.providers).toEqual(["openai", "github-copilot", "opencode"]) + expect(last.providers).toEqual(["openai", "github-copilot", "opencode", "vercel"]) expect(last.model).toBe("gpt-5-nano") }) @@ -160,7 +160,7 @@ describe("AGENT_MODEL_REQUIREMENTS", () => { const primary = prometheus.fallbackChain[0] expect(primary.model).toBe("claude-opus-4-6") - expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode"]) + expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode", "vercel"]) expect(primary.variant).toBe("max") }) @@ -176,12 +176,12 @@ describe("AGENT_MODEL_REQUIREMENTS", () => { const primary = metis.fallbackChain[0] expect(primary.model).toBe("claude-opus-4-6") - expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode"]) + expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode", "vercel"]) expect(primary.variant).toBe("max") const openAiFallback = metis.fallbackChain.find((entry) => entry.providers.includes("openai")) expect(openAiFallback).toEqual({ - providers: ["openai", "github-copilot", "opencode"], + providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4", variant: "high", }) @@ -223,7 +223,7 @@ describe("AGENT_MODEL_REQUIREMENTS", () => { const tertiary = atlas.fallbackChain[2] expect(tertiary).toEqual({ - providers: ["openai", "github-copilot", "opencode"], + providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4", variant: "medium", }) @@ -245,7 +245,7 @@ describe("AGENT_MODEL_REQUIREMENTS", () => { // then expect(openAiFallback).toEqual({ - providers: ["openai", "github-copilot", "opencode"], + providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4", variant: "medium", }) @@ -261,7 +261,7 @@ describe("AGENT_MODEL_REQUIREMENTS", () => { // #when - accessing hephaestus requirement // #then - requiresProvider includes openai, github-copilot, venice, and opencode expect(hephaestus).toBeDefined() - expect(hephaestus.requiresProvider).toEqual(["openai", "github-copilot", "venice", "opencode"]) + expect(hephaestus.requiresProvider).toEqual(["openai", "github-copilot", "venice", "opencode", "vercel"]) expect(hephaestus.requiresModel).toBeUndefined() }) @@ -415,12 +415,12 @@ describe("CATEGORY_MODEL_REQUIREMENTS", () => { const primary = unspecifiedHigh.fallbackChain[0] expect(primary.model).toBe("claude-opus-4-6") expect(primary.variant).toBe("max") - expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode"]) + expect(primary.providers).toEqual(["anthropic", "github-copilot", "opencode", "vercel"]) const secondary = unspecifiedHigh.fallbackChain[1] expect(secondary.model).toBe("gpt-5.4") expect(secondary.variant).toBe("high") - expect(secondary.providers).toEqual(["openai", "github-copilot", "opencode"]) + expect(secondary.providers).toEqual(["openai", "github-copilot", "opencode", "vercel"]) }) test("artistry has valid fallbackChain with gemini-3.1-pro as primary", () => { diff --git a/src/shared/model-requirements.ts b/src/shared/model-requirements.ts index 5a1889eae3..b9ac989ab5 100644 --- a/src/shared/model-requirements.ts +++ b/src/shared/model-requirements.ts @@ -21,11 +21,11 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { sisyphus: { fallbackChain: [ { - providers: ["anthropic", "github-copilot", "opencode"], + providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-opus-4-6", variant: "max", }, - { providers: ["opencode-go"], model: "kimi-k2.5" }, + { providers: ["opencode-go", "vercel"], model: "kimi-k2.5" }, { providers: ["kimi-for-coding"], model: "k2p5" }, { providers: [ @@ -35,11 +35,12 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { "firmware", "ollama-cloud", "aihubmix", + "vercel", ], model: "kimi-k2.5", }, - { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.4", variant: "medium" }, - { providers: ["zai-coding-plan", "opencode"], model: "glm-5" }, + { providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4", variant: "medium" }, + { providers: ["zai-coding-plan", "opencode", "vercel"], model: "glm-5" }, { providers: ["opencode"], model: "big-pickle" }, ], requiresAnyModel: true, @@ -47,73 +48,73 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { hephaestus: { fallbackChain: [ { - providers: ["openai", "github-copilot", "venice", "opencode"], + providers: ["openai", "github-copilot", "venice", "opencode", "vercel"], model: "gpt-5.4", variant: "medium", }, ], - requiresProvider: ["openai", "github-copilot", "venice", "opencode"], + requiresProvider: ["openai", "github-copilot", "venice", "opencode", "vercel"], }, oracle: { fallbackChain: [ { - providers: ["openai", "github-copilot", "opencode"], + providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4", variant: "high", }, { - providers: ["google", "github-copilot", "opencode"], + providers: ["google", "github-copilot", "opencode", "vercel"], model: "gemini-3.1-pro", variant: "high", }, { - providers: ["anthropic", "github-copilot", "opencode"], + providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-opus-4-6", variant: "max", }, - { providers: ["opencode-go"], model: "glm-5" }, + { providers: ["opencode-go", "vercel"], model: "glm-5" }, ], }, librarian: { fallbackChain: [ - { providers: ["opencode-go"], model: "minimax-m2.7" }, - { providers: ["opencode"], model: "minimax-m2.7-highspeed" }, - { providers: ["anthropic", "opencode"], model: "claude-haiku-4-5" }, - { providers: ["opencode"], model: "gpt-5-nano" }, + { providers: ["opencode-go", "vercel"], model: "minimax-m2.7" }, + { providers: ["opencode", "vercel"], model: "minimax-m2.7-highspeed" }, + { providers: ["anthropic", "opencode", "vercel"], model: "claude-haiku-4-5" }, + { providers: ["opencode", "vercel"], model: "gpt-5-nano" }, ], }, explore: { fallbackChain: [ - { providers: ["github-copilot", "xai"], model: "grok-code-fast-1" }, - { providers: ["opencode-go"], model: "minimax-m2.7-highspeed" }, - { providers: ["opencode"], model: "minimax-m2.7" }, - { providers: ["anthropic", "opencode"], model: "claude-haiku-4-5" }, - { providers: ["opencode"], model: "gpt-5-nano" }, + { providers: ["github-copilot", "xai", "vercel"], model: "grok-code-fast-1" }, + { providers: ["opencode-go", "vercel"], model: "minimax-m2.7-highspeed" }, + { providers: ["opencode", "vercel"], model: "minimax-m2.7" }, + { providers: ["anthropic", "opencode", "vercel"], model: "claude-haiku-4-5" }, + { providers: ["opencode", "vercel"], model: "gpt-5-nano" }, ], }, "multimodal-looker": { fallbackChain: [ - { providers: ["openai", "opencode"], model: "gpt-5.4", variant: "medium" }, - { providers: ["opencode-go"], model: "kimi-k2.5" }, - { providers: ["zai-coding-plan"], model: "glm-4.6v" }, - { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5-nano" }, + { providers: ["openai", "opencode", "vercel"], model: "gpt-5.4", variant: "medium" }, + { providers: ["opencode-go", "vercel"], model: "kimi-k2.5" }, + { providers: ["zai-coding-plan", "vercel"], model: "glm-4.6v" }, + { providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5-nano" }, ], }, prometheus: { fallbackChain: [ { - providers: ["anthropic", "github-copilot", "opencode"], + providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-opus-4-6", variant: "max", }, { - providers: ["openai", "github-copilot", "opencode"], + providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4", variant: "high", }, - { providers: ["opencode-go"], model: "glm-5" }, + { providers: ["opencode-go", "vercel"], model: "glm-5" }, { - providers: ["google", "github-copilot", "opencode"], + providers: ["google", "github-copilot", "opencode", "vercel"], model: "gemini-3.1-pro", }, ], @@ -121,61 +122,61 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { metis: { fallbackChain: [ { - providers: ["anthropic", "github-copilot", "opencode"], + providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-opus-4-6", variant: "max", }, { - providers: ["openai", "github-copilot", "opencode"], + providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4", variant: "high", }, - { providers: ["opencode-go"], model: "glm-5" }, + { providers: ["opencode-go", "vercel"], model: "glm-5" }, { providers: ["kimi-for-coding"], model: "k2p5" }, ], }, momus: { fallbackChain: [ { - providers: ["openai", "github-copilot", "opencode"], + providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4", variant: "xhigh", }, { - providers: ["anthropic", "github-copilot", "opencode"], + providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-opus-4-6", variant: "max", }, { - providers: ["google", "github-copilot", "opencode"], + providers: ["google", "github-copilot", "opencode", "vercel"], model: "gemini-3.1-pro", variant: "high", }, - { providers: ["opencode-go"], model: "glm-5" }, + { providers: ["opencode-go", "vercel"], model: "glm-5" }, ], }, atlas: { fallbackChain: [ - { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-6" }, - { providers: ["opencode-go"], model: "kimi-k2.5" }, + { providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-sonnet-4-6" }, + { providers: ["opencode-go", "vercel"], model: "kimi-k2.5" }, { - providers: ["openai", "github-copilot", "opencode"], + providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4", variant: "medium", }, - { providers: ["opencode-go"], model: "minimax-m2.7" }, + { providers: ["opencode-go", "vercel"], model: "minimax-m2.7" }, ], }, "sisyphus-junior": { fallbackChain: [ - { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-6" }, - { providers: ["opencode-go"], model: "kimi-k2.5" }, + { providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-sonnet-4-6" }, + { providers: ["opencode-go", "vercel"], model: "kimi-k2.5" }, { - providers: ["openai", "github-copilot", "opencode"], + providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4", variant: "medium", }, - { providers: ["opencode-go"], model: "minimax-m2.7" }, + { providers: ["opencode-go", "vercel"], model: "minimax-m2.7" }, { providers: ["opencode"], model: "big-pickle" }, ], }, @@ -185,54 +186,54 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { "visual-engineering": { fallbackChain: [ { - providers: ["google", "github-copilot", "opencode"], + providers: ["google", "github-copilot", "opencode", "vercel"], model: "gemini-3.1-pro", variant: "high", }, - { providers: ["zai-coding-plan", "opencode"], model: "glm-5" }, + { providers: ["zai-coding-plan", "opencode", "vercel"], model: "glm-5" }, { - providers: ["anthropic", "github-copilot", "opencode"], + providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-opus-4-6", variant: "max", }, - { providers: ["opencode-go"], model: "glm-5" }, + { providers: ["opencode-go", "vercel"], model: "glm-5" }, { providers: ["kimi-for-coding"], model: "k2p5" }, ], }, ultrabrain: { fallbackChain: [ { - providers: ["openai", "opencode"], + providers: ["openai", "opencode", "vercel"], model: "gpt-5.4", variant: "xhigh", }, { - providers: ["google", "github-copilot", "opencode"], + providers: ["google", "github-copilot", "opencode", "vercel"], model: "gemini-3.1-pro", variant: "high", }, { - providers: ["anthropic", "github-copilot", "opencode"], + providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-opus-4-6", variant: "max", }, - { providers: ["opencode-go"], model: "glm-5" }, + { providers: ["opencode-go", "vercel"], model: "glm-5" }, ], }, deep: { fallbackChain: [ { - providers: ["openai", "github-copilot", "venice", "opencode"], + providers: ["openai", "github-copilot", "venice", "opencode", "vercel"], model: "gpt-5.4", variant: "medium", }, { - providers: ["anthropic", "github-copilot", "opencode"], + providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-opus-4-6", variant: "max", }, { - providers: ["google", "github-copilot", "opencode"], + providers: ["google", "github-copilot", "opencode", "vercel"], model: "gemini-3.1-pro", variant: "high", }, @@ -241,72 +242,72 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { artistry: { fallbackChain: [ { - providers: ["google", "github-copilot", "opencode"], + providers: ["google", "github-copilot", "opencode", "vercel"], model: "gemini-3.1-pro", variant: "high", }, { - providers: ["anthropic", "github-copilot", "opencode"], + providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-opus-4-6", variant: "max", }, - { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.4" }, + { providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4" }, ], requiresModel: "gemini-3.1-pro", }, quick: { fallbackChain: [ { - providers: ["openai", "github-copilot", "opencode"], + providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4-mini", }, { - providers: ["anthropic", "github-copilot", "opencode"], + providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-haiku-4-5", }, { - providers: ["google", "github-copilot", "opencode"], + providers: ["google", "github-copilot", "opencode", "vercel"], model: "gemini-3-flash", }, - { providers: ["opencode-go"], model: "minimax-m2.7" }, - { providers: ["opencode"], model: "gpt-5-nano" }, + { providers: ["opencode-go", "vercel"], model: "minimax-m2.7" }, + { providers: ["opencode", "vercel"], model: "gpt-5-nano" }, ], }, "unspecified-low": { fallbackChain: [ { - providers: ["anthropic", "github-copilot", "opencode"], + providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-sonnet-4-6", }, { - providers: ["openai", "opencode"], + providers: ["openai", "opencode", "vercel"], model: "gpt-5.3-codex", variant: "medium", }, - { providers: ["opencode-go"], model: "kimi-k2.5" }, + { providers: ["opencode-go", "vercel"], model: "kimi-k2.5" }, { - providers: ["google", "github-copilot", "opencode"], + providers: ["google", "github-copilot", "opencode", "vercel"], model: "gemini-3-flash", }, - { providers: ["opencode-go"], model: "minimax-m2.7" }, + { providers: ["opencode-go", "vercel"], model: "minimax-m2.7" }, ], }, "unspecified-high": { fallbackChain: [ { - providers: ["anthropic", "github-copilot", "opencode"], + providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-opus-4-6", variant: "max", }, { - providers: ["openai", "github-copilot", "opencode"], + providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4", variant: "high", }, - { providers: ["zai-coding-plan", "opencode"], model: "glm-5" }, + { providers: ["zai-coding-plan", "opencode", "vercel"], model: "glm-5" }, { providers: ["kimi-for-coding"], model: "k2p5" }, - { providers: ["opencode-go"], model: "glm-5" }, - { providers: ["opencode"], model: "kimi-k2.5" }, + { providers: ["opencode-go", "vercel"], model: "glm-5" }, + { providers: ["opencode", "vercel"], model: "kimi-k2.5" }, { providers: [ "opencode", @@ -315,6 +316,7 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { "firmware", "ollama-cloud", "aihubmix", + "vercel", ], model: "kimi-k2.5", }, @@ -323,15 +325,15 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { writing: { fallbackChain: [ { - providers: ["google", "github-copilot", "opencode"], + providers: ["google", "github-copilot", "opencode", "vercel"], model: "gemini-3-flash", }, - { providers: ["opencode-go"], model: "kimi-k2.5" }, + { providers: ["opencode-go", "vercel"], model: "kimi-k2.5" }, { - providers: ["anthropic", "github-copilot", "opencode"], + providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-sonnet-4-6", }, - { providers: ["opencode-go"], model: "minimax-m2.7" }, + { providers: ["opencode-go", "vercel"], model: "minimax-m2.7" }, ], }, }; diff --git a/src/shared/provider-model-id-transform.ts b/src/shared/provider-model-id-transform.ts index 38fdf869d4..c01942a0fd 100644 --- a/src/shared/provider-model-id-transform.ts +++ b/src/shared/provider-model-id-transform.ts @@ -1,24 +1,58 @@ +function inferSubProvider(model: string): string | undefined { + if (model.startsWith("claude-")) return "anthropic" + if (model.startsWith("gpt-")) return "openai" + if (model.startsWith("gemini-")) return "google" + if (model.startsWith("grok-")) return "xai" + if (model.startsWith("minimax-")) return "minimax" + if (model.startsWith("kimi-")) return "moonshotai" + if (model.startsWith("glm-")) return "zai" + return undefined +} + +const CLAUDE_VERSION_DOT = /claude-(\w+)-(\d+)-(\d+)/g +const GEMINI_31_PRO_PREVIEW = /gemini-3\.1-pro(?!-)/g +const GEMINI_3_FLASH_PREVIEW = /gemini-3-flash(?!-)/g + +function claudeVersionDot(model: string): string { + return model.replace(CLAUDE_VERSION_DOT, "claude-$1-$2.$3") +} + +function applyGatewayTransforms(model: string): string { + return claudeVersionDot(model) + .replace(GEMINI_31_PRO_PREVIEW, "gemini-3.1-pro-preview") +} + export function transformModelForProvider(provider: string, model: string): string { - if (provider === "github-copilot") { + // Vercel AI Gateway expects / (e.g. anthropic/claude-opus-4.6). + // Canonical names in model-requirements.ts may be bare (claude-opus-4-6) or + // already prefixed (anthropic/claude-opus-4-6). Both need gateway-specific transforms. + if (provider === "vercel") { + // Already prefixed — transform only the model part + const slashIndex = model.indexOf("/") + if (slashIndex !== -1) { + const subProvider = model.substring(0, slashIndex) + const subModel = model.substring(slashIndex + 1) + return `${subProvider}/${applyGatewayTransforms(subModel)}` + } + // Bare name — infer sub-provider from model prefix (claude- → anthropic, etc.) + const subProvider = inferSubProvider(model) + if (subProvider) { + return `${subProvider}/${applyGatewayTransforms(model)}` + } return model - .replace("claude-opus-4-6", "claude-opus-4.6") - .replace("claude-sonnet-4-6", "claude-sonnet-4.6") - .replace("claude-sonnet-4-5", "claude-sonnet-4.5") - .replace("claude-haiku-4-5", "claude-haiku-4.5") - .replace("claude-sonnet-4", "claude-sonnet-4") - .replace(/gemini-3\.1-pro(?!-)/g, "gemini-3.1-pro-preview") - .replace(/gemini-3-flash(?!-)/g, "gemini-3-flash-preview") + } + if (provider === "github-copilot") { + return claudeVersionDot(model) + .replace(GEMINI_31_PRO_PREVIEW, "gemini-3.1-pro-preview") + .replace(GEMINI_3_FLASH_PREVIEW, "gemini-3-flash-preview") } if (provider === "google") { return model - .replace(/gemini-3\.1-pro(?!-)/g, "gemini-3.1-pro-preview") - .replace(/gemini-3-flash(?!-)/g, "gemini-3-flash-preview") + .replace(GEMINI_31_PRO_PREVIEW, "gemini-3.1-pro-preview") + .replace(GEMINI_3_FLASH_PREVIEW, "gemini-3-flash-preview") } if (provider === "anthropic") { - return model - .replace("claude-opus-4-6", "claude-opus-4.6") - .replace("claude-sonnet-4-6", "claude-sonnet-4.6") - .replace("claude-haiku-4-5", "claude-haiku-4.5") + return claudeVersionDot(model) } return model }