From dac0c99e8c48deaf718904340547aa923de1388d Mon Sep 17 00:00:00 2001 From: Matan Kushner Date: Mon, 13 Apr 2026 11:22:22 +0900 Subject: [PATCH 1/8] feat: add Vercel AI Gateway as a provider Adds 'vercel' as a recognized provider throughout the model resolution system, enabling users with Vercel AI Gateway to use it as a universal fallback for OpenAI, Anthropic, and Google models. The gateway transform infers the sub-provider from canonical model names (claude->anthropic, gpt->openai, gemini->google) and delegates to the appropriate provider-specific transform, producing model strings like vercel/anthropic/claude-opus-4.6. --- .../config-manager/detect-current-config.ts | 11 ++- src/cli/install-validators.ts | 8 ++ src/cli/model-fallback-types.ts | 1 + src/cli/model-fallback.ts | 7 +- src/cli/provider-availability.ts | 2 + src/cli/tui-install-prompts.ts | 11 +++ src/cli/types.ts | 3 + src/shared/model-requirements.test.ts | 22 ++--- src/shared/model-requirements.ts | 88 +++++++++---------- src/shared/provider-model-id-transform.ts | 21 +++++ 10 files changed, 116 insertions(+), 58 deletions(-) diff --git a/src/cli/config-manager/detect-current-config.ts b/src/cli/config-manager/detect-current-config.ts index f158e18e2..e1fb39823 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/install-validators.ts b/src/cli/install-validators.ts index be429eee6..695747edf 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 b195f698a..6daa24388 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.ts b/src/cli/model-fallback.ts index 5dabb9fc1..ab67960e9 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/anthropic/claude-haiku-4.5" } } if (agentConfig) { agents[role] = attachAllFallbackModels(agentConfig, req.fallbackChain, avail) @@ -145,6 +148,8 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig { agentConfig = { model: "opencode/claude-haiku-4-5" } } else if (avail.opencodeGo) { agentConfig = { model: "opencode-go/minimax-m2.7" } + } else if (avail.vercelAiGateway) { + agentConfig = { model: "vercel/anthropic/claude-haiku-4.5" } } else if (avail.copilot) { agentConfig = { model: "github-copilot/gpt-5-mini" } } else { diff --git a/src/cli/provider-availability.ts b/src/cli/provider-availability.ts index 6d17422f7..f9ac8189b 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/tui-install-prompts.ts b/src/cli/tui-install-prompts.ts index c2e5a3b49..3d5fb4584 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 { 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") @@ -132,7 +132,7 @@ 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") @@ -144,7 +144,7 @@ describe("AGENT_MODEL_REQUIREMENTS", () => { 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 5a1889eae..4a0ce6071 100644 --- a/src/shared/model-requirements.ts +++ b/src/shared/model-requirements.ts @@ -21,7 +21,7 @@ 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", }, @@ -38,7 +38,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { ], model: "kimi-k2.5", }, - { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.4", variant: "medium" }, + { providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4", variant: "medium" }, { providers: ["zai-coding-plan", "opencode"], model: "glm-5" }, { providers: ["opencode"], model: "big-pickle" }, ], @@ -47,27 +47,27 @@ 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", }, @@ -78,7 +78,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { fallbackChain: [ { providers: ["opencode-go"], model: "minimax-m2.7" }, { providers: ["opencode"], model: "minimax-m2.7-highspeed" }, - { providers: ["anthropic", "opencode"], model: "claude-haiku-4-5" }, + { providers: ["anthropic", "opencode", "vercel"], model: "claude-haiku-4-5" }, { providers: ["opencode"], model: "gpt-5-nano" }, ], }, @@ -87,33 +87,33 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { { 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: ["anthropic", "opencode", "vercel"], model: "claude-haiku-4-5" }, { providers: ["opencode"], model: "gpt-5-nano" }, ], }, "multimodal-looker": { fallbackChain: [ - { providers: ["openai", "opencode"], model: "gpt-5.4", variant: "medium" }, + { providers: ["openai", "opencode", "vercel"], 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", "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: ["google", "github-copilot", "opencode"], + providers: ["google", "github-copilot", "opencode", "vercel"], model: "gemini-3.1-pro", }, ], @@ -121,12 +121,12 @@ 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", }, @@ -137,17 +137,17 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { 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", }, @@ -156,10 +156,10 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { }, atlas: { fallbackChain: [ - { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-6" }, + { providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-sonnet-4-6" }, { providers: ["opencode-go"], model: "kimi-k2.5" }, { - providers: ["openai", "github-copilot", "opencode"], + providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4", variant: "medium", }, @@ -168,10 +168,10 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { }, "sisyphus-junior": { fallbackChain: [ - { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-6" }, + { providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-sonnet-4-6" }, { providers: ["opencode-go"], model: "kimi-k2.5" }, { - providers: ["openai", "github-copilot", "opencode"], + providers: ["openai", "github-copilot", "opencode", "vercel"], model: "gpt-5.4", variant: "medium", }, @@ -185,13 +185,13 @@ 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: ["anthropic", "github-copilot", "opencode"], + providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-opus-4-6", variant: "max", }, @@ -202,17 +202,17 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { 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", }, @@ -222,17 +222,17 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { 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,31 +241,31 @@ 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" }, @@ -275,17 +275,17 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { "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: ["google", "github-copilot", "opencode"], + providers: ["google", "github-copilot", "opencode", "vercel"], model: "gemini-3-flash", }, { providers: ["opencode-go"], model: "minimax-m2.7" }, @@ -294,12 +294,12 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { "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", }, @@ -323,12 +323,12 @@ 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: ["anthropic", "github-copilot", "opencode"], + providers: ["anthropic", "github-copilot", "opencode", "vercel"], model: "claude-sonnet-4-6", }, { providers: ["opencode-go"], model: "minimax-m2.7" }, diff --git a/src/shared/provider-model-id-transform.ts b/src/shared/provider-model-id-transform.ts index 38fdf869d..60676e962 100644 --- a/src/shared/provider-model-id-transform.ts +++ b/src/shared/provider-model-id-transform.ts @@ -1,4 +1,25 @@ +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" + return undefined +} + export function transformModelForProvider(provider: string, model: string): string { + if (provider === "vercel") { + const slashIndex = model.indexOf("/") + if (slashIndex !== -1) { + const subProvider = model.substring(0, slashIndex) + const subModel = model.substring(slashIndex + 1) + return `${subProvider}/${transformModelForProvider(subProvider, subModel)}` + } + const subProvider = inferSubProvider(model) + if (subProvider) { + return `${subProvider}/${transformModelForProvider(subProvider, model)}` + } + return model + } if (provider === "github-copilot") { return model .replace("claude-opus-4-6", "claude-opus-4.6") From aac17135835e9d4eb3b7278b09d40efd14d60cf6 Mon Sep 17 00:00:00 2001 From: Matan Kushner Date: Mon, 13 Apr 2026 11:23:11 +0900 Subject: [PATCH 2/8] test: add vercel provider transform tests --- src/cli/provider-model-id-transform.test.ts | 83 +++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/cli/provider-model-id-transform.test.ts b/src/cli/provider-model-id-transform.test.ts index ddfdd6e2f..be01735d0 100644 --- a/src/cli/provider-model-id-transform.test.ts +++ b/src/cli/provider-model-id-transform.test.ts @@ -201,6 +201,89 @@ 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/ and applies google transform 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-preview + expect(result).toBe("google/gemini-3-flash-preview") + }) + + 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("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 From 6f72ec754daf347c135e4b0cee9afa6dc727ae40 Mon Sep 17 00:00:00 2001 From: Matan Kushner Date: Mon, 13 Apr 2026 11:26:30 +0900 Subject: [PATCH 3/8] test: add integration tests for Vercel AI Gateway model resolution --- .../__snapshots__/model-fallback.test.ts.snap | 402 ++++++++++++++++++ src/cli/model-fallback.test.ts | 69 +++ 2 files changed, 471 insertions(+) diff --git a/src/cli/__snapshots__/model-fallback.test.ts.snap b/src/cli/__snapshots__/model-fallback.test.ts.snap index 3e1b2f50c..83ae744e6 100644 --- a/src/cli/__snapshots__/model-fallback.test.ts.snap +++ b/src/cli/__snapshots__/model-fallback.test.ts.snap @@ -4137,3 +4137,405 @@ 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/openai/gpt-5.4", + "variant": "medium", + }, + ], + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + "explore": { + "model": "vercel/anthropic/claude-haiku-4.5", + }, + "hephaestus": { + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + "librarian": { + "model": "vercel/anthropic/claude-haiku-4.5", + }, + "metis": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.4", + "variant": "high", + }, + ], + "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/openai/gpt-5.4", + "variant": "xhigh", + }, + "multimodal-looker": { + "fallback_models": [ + { + "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/openai/gpt-5.4", + "variant": "high", + }, + "prometheus": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.4", + "variant": "high", + }, + { + "model": "vercel/google/gemini-3.1-pro-preview", + }, + ], + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + "sisyphus": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + ], + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + "sisyphus-junior": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + ], + "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-preview", + }, + ], + "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/openai/gpt-5.4", + "variant": "xhigh", + }, + "unspecified-high": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.3-codex", + "variant": "medium", + }, + { + "model": "vercel/google/gemini-3-flash-preview", + }, + ], + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + "unspecified-low": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.3-codex", + "variant": "medium", + }, + { + "model": "vercel/google/gemini-3-flash-preview", + }, + ], + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + "visual-engineering": { + "fallback_models": [ + { + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + ], + "model": "vercel/google/gemini-3.1-pro-preview", + "variant": "high", + }, + "writing": { + "fallback_models": [ + { + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + ], + "model": "vercel/google/gemini-3-flash-preview", + }, + }, +} +`; + +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/openai/gpt-5.4", + "variant": "medium", + }, + ], + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + "explore": { + "model": "vercel/anthropic/claude-haiku-4.5", + }, + "hephaestus": { + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + "librarian": { + "model": "vercel/anthropic/claude-haiku-4.5", + }, + "metis": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.4", + "variant": "high", + }, + ], + "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/openai/gpt-5.4", + "variant": "xhigh", + }, + "multimodal-looker": { + "fallback_models": [ + { + "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/openai/gpt-5.4", + "variant": "high", + }, + "prometheus": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.4", + "variant": "high", + }, + { + "model": "vercel/google/gemini-3.1-pro-preview", + }, + ], + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + "sisyphus": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + ], + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + "sisyphus-junior": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.4", + "variant": "medium", + }, + ], + "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-preview", + }, + ], + "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/openai/gpt-5.4", + "variant": "xhigh", + }, + "unspecified-high": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.4", + "variant": "high", + }, + ], + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + "unspecified-low": { + "fallback_models": [ + { + "model": "vercel/openai/gpt-5.3-codex", + "variant": "medium", + }, + { + "model": "vercel/google/gemini-3-flash-preview", + }, + ], + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + "visual-engineering": { + "fallback_models": [ + { + "model": "vercel/anthropic/claude-opus-4.6", + "variant": "max", + }, + ], + "model": "vercel/google/gemini-3.1-pro-preview", + "variant": "high", + }, + "writing": { + "fallback_models": [ + { + "model": "vercel/anthropic/claude-sonnet-4.6", + }, + ], + "model": "vercel/google/gemini-3-flash-preview", + }, + }, +} +`; diff --git a/src/cli/model-fallback.test.ts b/src/cli/model-fallback.test.ts index 09d002bcb..1a6a6c797 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/anthropic/claude-haiku-4.5 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 claude-haiku + expect(result.agents?.explore?.model).toBe("vercel/anthropic/claude-haiku-4.5") + }) + + test("librarian uses vercel/anthropic/claude-haiku-4.5 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 claude-haiku + expect(result.agents?.librarian?.model).toBe("vercel/anthropic/claude-haiku-4.5") + }) + + 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 From 542dc890f97250b6681952a9f5abd7da2de61f7d Mon Sep 17 00:00:00 2001 From: Matan Kushner Date: Mon, 13 Apr 2026 12:51:56 +0900 Subject: [PATCH 4/8] fix(provider): use gateway-specific model IDs for vercel transform The gateway uses google/gemini-3-flash (no -preview suffix) unlike the direct Google API. Give the vercel provider its own transform instead of delegating to sub-provider transforms blindly. --- src/cli/__snapshots__/model-fallback.test.ts.snap | 14 +++++++------- src/cli/provider-model-id-transform.test.ts | 6 +++--- src/shared/provider-model-id-transform.ts | 13 +++++++++++-- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/cli/__snapshots__/model-fallback.test.ts.snap b/src/cli/__snapshots__/model-fallback.test.ts.snap index 83ae744e6..73c5bfdb8 100644 --- a/src/cli/__snapshots__/model-fallback.test.ts.snap +++ b/src/cli/__snapshots__/model-fallback.test.ts.snap @@ -4275,7 +4275,7 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "model": "vercel/anthropic/claude-haiku-4.5", }, { - "model": "vercel/google/gemini-3-flash-preview", + "model": "vercel/google/gemini-3-flash", }, ], "model": "vercel/openai/gpt-5.4-mini", @@ -4301,7 +4301,7 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "variant": "medium", }, { - "model": "vercel/google/gemini-3-flash-preview", + "model": "vercel/google/gemini-3-flash", }, ], "model": "vercel/anthropic/claude-sonnet-4.6", @@ -4313,7 +4313,7 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "variant": "medium", }, { - "model": "vercel/google/gemini-3-flash-preview", + "model": "vercel/google/gemini-3-flash", }, ], "model": "vercel/anthropic/claude-sonnet-4.6", @@ -4334,7 +4334,7 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "model": "vercel/anthropic/claude-sonnet-4.6", }, ], - "model": "vercel/google/gemini-3-flash-preview", + "model": "vercel/google/gemini-3-flash", }, }, } @@ -4477,7 +4477,7 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "model": "vercel/anthropic/claude-haiku-4.5", }, { - "model": "vercel/google/gemini-3-flash-preview", + "model": "vercel/google/gemini-3-flash", }, ], "model": "vercel/openai/gpt-5.4-mini", @@ -4513,7 +4513,7 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "variant": "medium", }, { - "model": "vercel/google/gemini-3-flash-preview", + "model": "vercel/google/gemini-3-flash", }, ], "model": "vercel/anthropic/claude-sonnet-4.6", @@ -4534,7 +4534,7 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "model": "vercel/anthropic/claude-sonnet-4.6", }, ], - "model": "vercel/google/gemini-3-flash-preview", + "model": "vercel/google/gemini-3-flash", }, }, } diff --git a/src/cli/provider-model-id-transform.test.ts b/src/cli/provider-model-id-transform.test.ts index be01735d0..a898b7c2b 100644 --- a/src/cli/provider-model-id-transform.test.ts +++ b/src/cli/provider-model-id-transform.test.ts @@ -247,13 +247,13 @@ describe("transformModelForProvider", () => { expect(result).toBe("google/gemini-3.1-pro-preview") }) - test("prepends google/ and applies google transform for gemini-3-flash", () => { + 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-preview - expect(result).toBe("google/gemini-3-flash-preview") + // #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", () => { diff --git a/src/shared/provider-model-id-transform.ts b/src/shared/provider-model-id-transform.ts index 60676e962..9e304c9d8 100644 --- a/src/shared/provider-model-id-transform.ts +++ b/src/shared/provider-model-id-transform.ts @@ -6,17 +6,26 @@ function inferSubProvider(model: string): string | undefined { return undefined } +function transformModelForGateway(model: string): string { + 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(/gemini-3\.1-pro(?!-)/g, "gemini-3.1-pro-preview") +} + export function transformModelForProvider(provider: string, model: string): string { if (provider === "vercel") { const slashIndex = model.indexOf("/") if (slashIndex !== -1) { const subProvider = model.substring(0, slashIndex) const subModel = model.substring(slashIndex + 1) - return `${subProvider}/${transformModelForProvider(subProvider, subModel)}` + return `${subProvider}/${transformModelForGateway(subModel)}` } const subProvider = inferSubProvider(model) if (subProvider) { - return `${subProvider}/${transformModelForProvider(subProvider, model)}` + return `${subProvider}/${transformModelForGateway(model)}` } return model } From 34e334eabe2346059545a6e3e84322614977d94d Mon Sep 17 00:00:00 2001 From: Matan Kushner Date: Mon, 13 Apr 2026 13:44:50 +0900 Subject: [PATCH 5/8] feat(provider): add vercel to all gateway-supported fallback entries Adds vercel to every fallback entry where the model exists on the gateway (kimi-k2.5, glm-5, glm-4.6v, minimax-m2.7, grok-code-fast-1, gpt-5-nano). Adds inferSubProvider mappings for minimax, moonshotai, and zai. Updates librarian/explore special cases to prefer minimax via gateway over claude-haiku. --- .../__snapshots__/model-fallback.test.ts.snap | 190 +++++++++++++++++- src/cli/model-fallback.test.ts | 12 +- src/cli/model-fallback.ts | 6 +- src/cli/provider-model-id-transform.test.ts | 27 +++ src/shared/model-requirements.test.ts | 4 +- src/shared/model-requirements.ts | 64 +++--- src/shared/provider-model-id-transform.ts | 3 + 7 files changed, 260 insertions(+), 46 deletions(-) diff --git a/src/cli/__snapshots__/model-fallback.test.ts.snap b/src/cli/__snapshots__/model-fallback.test.ts.snap index 73c5bfdb8..88f86f97b 100644 --- a/src/cli/__snapshots__/model-fallback.test.ts.snap +++ b/src/cli/__snapshots__/model-fallback.test.ts.snap @@ -4144,22 +4144,53 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "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": { - "model": "vercel/anthropic/claude-haiku-4.5", + "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": { - "model": "vercel/anthropic/claude-haiku-4.5", + "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": [ @@ -4167,6 +4198,9 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "model": "vercel/openai/gpt-5.4", "variant": "high", }, + { + "model": "vercel/zai/glm-5", + }, ], "model": "vercel/anthropic/claude-opus-4.6", "variant": "max", @@ -4181,12 +4215,21 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "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", }, @@ -4204,6 +4247,9 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "model": "vercel/anthropic/claude-opus-4.6", "variant": "max", }, + { + "model": "vercel/zai/glm-5", + }, ], "model": "vercel/openai/gpt-5.4", "variant": "high", @@ -4214,6 +4260,9 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "model": "vercel/openai/gpt-5.4", "variant": "high", }, + { + "model": "vercel/zai/glm-5", + }, { "model": "vercel/google/gemini-3.1-pro-preview", }, @@ -4223,20 +4272,32 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin }, "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", }, @@ -4277,6 +4338,12 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin { "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", }, @@ -4290,6 +4357,9 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "model": "vercel/anthropic/claude-opus-4.6", "variant": "max", }, + { + "model": "vercel/zai/glm-5", + }, ], "model": "vercel/openai/gpt-5.4", "variant": "xhigh", @@ -4300,9 +4370,15 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "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", }, @@ -4312,14 +4388,23 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "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", @@ -4330,9 +4415,15 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin }, "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", }, @@ -4346,22 +4437,53 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "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": { - "model": "vercel/anthropic/claude-haiku-4.5", + "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": { - "model": "vercel/anthropic/claude-haiku-4.5", + "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": [ @@ -4369,6 +4491,9 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "model": "vercel/openai/gpt-5.4", "variant": "high", }, + { + "model": "vercel/zai/glm-5", + }, ], "model": "vercel/anthropic/claude-opus-4.6", "variant": "max", @@ -4383,12 +4508,21 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "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", }, @@ -4406,6 +4540,9 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "model": "vercel/anthropic/claude-opus-4.6", "variant": "max", }, + { + "model": "vercel/zai/glm-5", + }, ], "model": "vercel/openai/gpt-5.4", "variant": "high", @@ -4416,6 +4553,9 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "model": "vercel/openai/gpt-5.4", "variant": "high", }, + { + "model": "vercel/zai/glm-5", + }, { "model": "vercel/google/gemini-3.1-pro-preview", }, @@ -4425,20 +4565,32 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin }, "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", }, @@ -4479,6 +4631,12 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin { "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", }, @@ -4492,6 +4650,9 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "model": "vercel/anthropic/claude-opus-4.6", "variant": "max", }, + { + "model": "vercel/zai/glm-5", + }, ], "model": "vercel/openai/gpt-5.4", "variant": "xhigh", @@ -4502,6 +4663,12 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "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", @@ -4512,14 +4679,23 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin "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", @@ -4530,9 +4706,15 @@ exports[`generateModelConfig Vercel AI Gateway provider uses vercel/ model strin }, "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/model-fallback.test.ts b/src/cli/model-fallback.test.ts index 1a6a6c797..f1df3cc0a 100644 --- a/src/cli/model-fallback.test.ts +++ b/src/cli/model-fallback.test.ts @@ -631,26 +631,26 @@ describe("generateModelConfig", () => { expect(result).toMatchSnapshot() }) - test("explore uses vercel/anthropic/claude-haiku-4.5 when only gateway available", () => { + 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 claude-haiku - expect(result.agents?.explore?.model).toBe("vercel/anthropic/claude-haiku-4.5") + // #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/anthropic/claude-haiku-4.5 when only gateway available", () => { + 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 claude-haiku - expect(result.agents?.librarian?.model).toBe("vercel/anthropic/claude-haiku-4.5") + // #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", () => { diff --git a/src/cli/model-fallback.ts b/src/cli/model-fallback.ts index ab67960e9..217420c46 100644 --- a/src/cli/model-fallback.ts +++ b/src/cli/model-fallback.ts @@ -129,10 +129,10 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig { let agentConfig: AgentConfig | undefined if (avail.opencodeGo) { agentConfig = { model: "opencode-go/minimax-m2.7" } + } else if (avail.vercelAiGateway) { + agentConfig = { model: "vercel/minimax/minimax-m2.7" } } else if (avail.zai) { agentConfig = { model: ZAI_MODEL } - } else if (avail.vercelAiGateway) { - agentConfig = { model: "vercel/anthropic/claude-haiku-4.5" } } if (agentConfig) { agents[role] = attachAllFallbackModels(agentConfig, req.fallbackChain, avail) @@ -149,7 +149,7 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig { } else if (avail.opencodeGo) { agentConfig = { model: "opencode-go/minimax-m2.7" } } else if (avail.vercelAiGateway) { - agentConfig = { model: "vercel/anthropic/claude-haiku-4.5" } + agentConfig = { model: "vercel/minimax/minimax-m2.7-highspeed" } } else if (avail.copilot) { agentConfig = { model: "github-copilot/gpt-5-mini" } } else { diff --git a/src/cli/provider-model-id-transform.test.ts b/src/cli/provider-model-id-transform.test.ts index a898b7c2b..e87fc12ff 100644 --- a/src/cli/provider-model-id-transform.test.ts +++ b/src/cli/provider-model-id-transform.test.ts @@ -274,6 +274,33 @@ describe("transformModelForProvider", () => { 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 diff --git a/src/shared/model-requirements.test.ts b/src/shared/model-requirements.test.ts index 253d4e78a..324176b57 100644 --- a/src/shared/model-requirements.test.ts +++ b/src/shared/model-requirements.test.ts @@ -40,7 +40,7 @@ describe("AGENT_MODEL_REQUIREMENTS", () => { 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] @@ -137,7 +137,7 @@ describe("AGENT_MODEL_REQUIREMENTS", () => { 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] diff --git a/src/shared/model-requirements.ts b/src/shared/model-requirements.ts index 4a0ce6071..b9ac989ab 100644 --- a/src/shared/model-requirements.ts +++ b/src/shared/model-requirements.ts @@ -25,7 +25,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { 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", "vercel"], model: "gpt-5.4", variant: "medium" }, - { providers: ["zai-coding-plan", "opencode"], model: "glm-5" }, + { providers: ["zai-coding-plan", "opencode", "vercel"], model: "glm-5" }, { providers: ["opencode"], model: "big-pickle" }, ], requiresAnyModel: true, @@ -71,31 +72,31 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { 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: ["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"], model: "gpt-5-nano" }, + { 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: ["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"], model: "gpt-5-nano" }, + { providers: ["opencode", "vercel"], model: "gpt-5-nano" }, ], }, "multimodal-looker": { fallbackChain: [ { providers: ["openai", "opencode", "vercel"], model: "gpt-5.4", variant: "medium" }, - { providers: ["opencode-go"], model: "kimi-k2.5" }, - { providers: ["zai-coding-plan"], model: "glm-4.6v" }, + { 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" }, ], }, @@ -111,7 +112,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { model: "gpt-5.4", variant: "high", }, - { providers: ["opencode-go"], model: "glm-5" }, + { providers: ["opencode-go", "vercel"], model: "glm-5" }, { providers: ["google", "github-copilot", "opencode", "vercel"], model: "gemini-3.1-pro", @@ -130,7 +131,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { 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" }, ], }, @@ -151,31 +152,31 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { 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", "vercel"], model: "claude-sonnet-4-6" }, - { providers: ["opencode-go"], model: "kimi-k2.5" }, + { providers: ["opencode-go", "vercel"], model: "kimi-k2.5" }, { 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", "vercel"], model: "claude-sonnet-4-6" }, - { providers: ["opencode-go"], model: "kimi-k2.5" }, + { providers: ["opencode-go", "vercel"], model: "kimi-k2.5" }, { 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" }, ], }, @@ -189,13 +190,13 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { 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", "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" }, ], }, @@ -216,7 +217,7 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { model: "claude-opus-4-6", variant: "max", }, - { providers: ["opencode-go"], model: "glm-5" }, + { providers: ["opencode-go", "vercel"], model: "glm-5" }, ], }, deep: { @@ -268,8 +269,8 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { 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": { @@ -283,12 +284,12 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { 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", "vercel"], model: "gemini-3-flash", }, - { providers: ["opencode-go"], model: "minimax-m2.7" }, + { providers: ["opencode-go", "vercel"], model: "minimax-m2.7" }, ], }, "unspecified-high": { @@ -303,10 +304,10 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { 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", }, @@ -326,12 +328,12 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { 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", "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 9e304c9d8..2d5fd656d 100644 --- a/src/shared/provider-model-id-transform.ts +++ b/src/shared/provider-model-id-transform.ts @@ -3,6 +3,9 @@ function inferSubProvider(model: string): string | undefined { 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 } From effae16c14fea2fdee2ffc0d34e4d47cf45f47c0 Mon Sep 17 00:00:00 2001 From: Matan Kushner Date: Mon, 13 Apr 2026 14:01:17 +0900 Subject: [PATCH 6/8] refactor(provider): replace per-model string replacements with generic regex Claude version transforms now use a single regex claude-(\w+)-(\d+)-(\d+) -> claude-$1-$2.$3 instead of one .replace() per model. New claude models need zero changes. --- src/shared/provider-model-id-transform.ts | 42 ++++++++++------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/shared/provider-model-id-transform.ts b/src/shared/provider-model-id-transform.ts index 2d5fd656d..a4869c3c9 100644 --- a/src/shared/provider-model-id-transform.ts +++ b/src/shared/provider-model-id-transform.ts @@ -9,13 +9,17 @@ function inferSubProvider(model: string): string | undefined { return undefined } -function transformModelForGateway(model: string): string { - 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(/gemini-3\.1-pro(?!-)/g, "gemini-3.1-pro-preview") +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 { @@ -24,34 +28,26 @@ export function transformModelForProvider(provider: string, model: string): stri if (slashIndex !== -1) { const subProvider = model.substring(0, slashIndex) const subModel = model.substring(slashIndex + 1) - return `${subProvider}/${transformModelForGateway(subModel)}` + return `${subProvider}/${applyGatewayTransforms(subModel)}` } const subProvider = inferSubProvider(model) if (subProvider) { - return `${subProvider}/${transformModelForGateway(model)}` + return `${subProvider}/${applyGatewayTransforms(model)}` } return model } if (provider === "github-copilot") { - 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") + 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 } From 569addd3b05a6b7b929c913baa3f9e2ad33a726d Mon Sep 17 00:00:00 2001 From: Matan Kushner Date: Mon, 13 Apr 2026 14:05:29 +0900 Subject: [PATCH 7/8] docs(provider): add comments to vercel transform logic --- src/shared/provider-model-id-transform.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/shared/provider-model-id-transform.ts b/src/shared/provider-model-id-transform.ts index a4869c3c9..c01942a0f 100644 --- a/src/shared/provider-model-id-transform.ts +++ b/src/shared/provider-model-id-transform.ts @@ -23,13 +23,18 @@ function applyGatewayTransforms(model: string): string { } export function transformModelForProvider(provider: string, model: string): string { + // 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)}` From e3b5c2be2e73947ee1fbcd116bc7ddefea99928a Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 13 Apr 2026 15:31:28 +0900 Subject: [PATCH 8/8] fix: address review feedback for Vercel AI Gateway PR - Add --vercel-ai-gateway CLI option to install command - Fix librarian/explore priority: native providers (ZAI, Copilot) now take precedence over Vercel gateway - Add hasVercelAiGateway to installer no-provider warning conditions - Add hasVercelAiGateway: false to all test file InstallConfig fixtures - Fix test expectations for model ID format (claude-opus-4.6 vs claude-opus-4-6) --- src/cli/cli-installer.telemetry.test.ts | 1 + src/cli/cli-installer.test.ts | 2 ++ src/cli/cli-installer.ts | 3 ++- src/cli/cli-program.ts | 7 +++++-- src/cli/config-manager/generate-omo-config.test.ts | 13 ++++++++++--- src/cli/config-manager/write-omo-config.test.ts | 1 + src/cli/model-fallback.ts | 8 ++++---- src/cli/openai-only-model-catalog.test.ts | 1 + src/cli/tui-installer.test.ts | 3 +++ src/cli/tui-installer.ts | 2 +- 10 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/cli/cli-installer.telemetry.test.ts b/src/cli/cli-installer.telemetry.test.ts index c4bdee652..c1b8eb8ac 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 38514fcf0..3fbb21f20 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 5db015531..72d5d8d7c 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 15d3d0489..6d982e57b 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/generate-omo-config.test.ts b/src/cli/config-manager/generate-omo-config.test.ts index dbec67c00..50fe4e3d8 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 10ccf7a27..7b7ba6409 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/model-fallback.ts b/src/cli/model-fallback.ts index 217420c46..088c4515e 100644 --- a/src/cli/model-fallback.ts +++ b/src/cli/model-fallback.ts @@ -129,10 +129,10 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig { let agentConfig: AgentConfig | undefined if (avail.opencodeGo) { agentConfig = { model: "opencode-go/minimax-m2.7" } - } else if (avail.vercelAiGateway) { - agentConfig = { model: "vercel/minimax/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) @@ -148,10 +148,10 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig { agentConfig = { model: "opencode/claude-haiku-4-5" } } else if (avail.opencodeGo) { agentConfig = { model: "opencode-go/minimax-m2.7" } - } else if (avail.vercelAiGateway) { - agentConfig = { model: "vercel/minimax/minimax-m2.7-highspeed" } } 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 9d516dfdd..da544156c 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/tui-installer.test.ts b/src/cli/tui-installer.test.ts index dc5ca718f..0e5b1344d 100644 --- a/src/cli/tui-installer.test.ts +++ b/src/cli/tui-installer.test.ts @@ -44,6 +44,7 @@ describe("runTuiInstaller", () => { 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 4272557c0..ef4b9088a 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.") }