From e5a0ab403459fdd14279e92476b80501967f4e46 Mon Sep 17 00:00:00 2001 From: once Date: Tue, 17 Feb 2026 21:15:38 +0900 Subject: [PATCH 1/3] fix: add google provider model transform for gemini-3-flash/pro preview suffix transformModelForProvider only handled github-copilot provider, leaving google provider models untransformed. This caused ProviderModelNotFoundError when google/gemini-3-flash was sent to the API (correct ID is gemini-3-flash-preview). Add google provider block with -preview suffix guard to prevent double transformation. --- .../__snapshots__/model-fallback.test.ts.snap | 88 ++++----- src/cli/provider-model-id-transform.test.ts | 167 ++++++++++++++++++ src/cli/provider-model-id-transform.ts | 8 + 3 files changed, 219 insertions(+), 44 deletions(-) create mode 100644 src/cli/provider-model-id-transform.test.ts diff --git a/src/cli/__snapshots__/model-fallback.test.ts.snap b/src/cli/__snapshots__/model-fallback.test.ts.snap index eb339823..9311d795 100644 --- a/src/cli/__snapshots__/model-fallback.test.ts.snap +++ b/src/cli/__snapshots__/model-fallback.test.ts.snap @@ -334,48 +334,48 @@ exports[`generateModelConfig single native provider uses Gemini models when only "model": "opencode/minimax-m2.5-free", }, "metis": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "momus": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "multimodal-looker": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, "oracle": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "prometheus": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", }, }, "categories": { "artistry": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "quick": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, "ultrabrain": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "unspecified-high": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, "unspecified-low": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, "visual-engineering": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "writing": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, }, } @@ -395,48 +395,48 @@ exports[`generateModelConfig single native provider uses Gemini models with isMa "model": "opencode/minimax-m2.5-free", }, "metis": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "momus": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "multimodal-looker": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, "oracle": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "prometheus": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", }, }, "categories": { "artistry": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "quick": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, "ultrabrain": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "unspecified-high": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", }, "unspecified-low": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, "visual-engineering": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "writing": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, }, } @@ -468,7 +468,7 @@ exports[`generateModelConfig all native providers uses preferred models from fal "variant": "medium", }, "multimodal-looker": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, "oracle": { "model": "openai/gpt-5.2", @@ -485,7 +485,7 @@ exports[`generateModelConfig all native providers uses preferred models from fal }, "categories": { "artistry": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "deep": { @@ -506,11 +506,11 @@ exports[`generateModelConfig all native providers uses preferred models from fal "model": "anthropic/claude-sonnet-4-6", }, "visual-engineering": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "writing": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, }, } @@ -542,7 +542,7 @@ exports[`generateModelConfig all native providers uses preferred models with isM "variant": "medium", }, "multimodal-looker": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, "oracle": { "model": "openai/gpt-5.2", @@ -559,7 +559,7 @@ exports[`generateModelConfig all native providers uses preferred models with isM }, "categories": { "artistry": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "deep": { @@ -581,11 +581,11 @@ exports[`generateModelConfig all native providers uses preferred models with isM "model": "anthropic/claude-sonnet-4-6", }, "visual-engineering": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "writing": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, }, } @@ -1230,10 +1230,10 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi "variant": "max", }, "multimodal-looker": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, "oracle": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "prometheus": { @@ -1247,14 +1247,14 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi }, "categories": { "artistry": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "quick": { "model": "anthropic/claude-haiku-4-5", }, "ultrabrain": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "unspecified-high": { @@ -1264,11 +1264,11 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi "model": "anthropic/claude-sonnet-4-6", }, "visual-engineering": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "writing": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, }, } @@ -1391,7 +1391,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe }, "categories": { "artistry": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "deep": { @@ -1412,11 +1412,11 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe "model": "anthropic/claude-sonnet-4-6", }, "visual-engineering": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "writing": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, }, } @@ -1465,7 +1465,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is }, "categories": { "artistry": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "deep": { @@ -1487,11 +1487,11 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is "model": "anthropic/claude-sonnet-4-6", }, "visual-engineering": { - "model": "google/gemini-3-pro", + "model": "google/gemini-3-pro-preview", "variant": "high", }, "writing": { - "model": "google/gemini-3-flash", + "model": "google/gemini-3-flash-preview", }, }, } diff --git a/src/cli/provider-model-id-transform.test.ts b/src/cli/provider-model-id-transform.test.ts new file mode 100644 index 00000000..08317afb --- /dev/null +++ b/src/cli/provider-model-id-transform.test.ts @@ -0,0 +1,167 @@ +import { describe, expect, test } from "bun:test" + +import { transformModelForProvider } from "./provider-model-id-transform" + +describe("transformModelForProvider", () => { + describe("github-copilot provider", () => { + test("transforms claude-opus-4-6 to claude-opus-4.6", () => { + // #given github-copilot provider and claude-opus-4-6 model + const provider = "github-copilot" + const model = "claude-opus-4-6" + + // #when transformModelForProvider is called + const result = transformModelForProvider(provider, model) + + // #then should transform to claude-opus-4.6 + expect(result).toBe("claude-opus-4.6") + }) + + test("transforms claude-sonnet-4-5 to claude-sonnet-4.5", () => { + // #given github-copilot provider and claude-sonnet-4-5 model + const provider = "github-copilot" + const model = "claude-sonnet-4-5" + + // #when transformModelForProvider is called + const result = transformModelForProvider(provider, model) + + // #then should transform to claude-sonnet-4.5 + expect(result).toBe("claude-sonnet-4.5") + }) + + test("transforms claude-haiku-4-5 to claude-haiku-4.5", () => { + // #given github-copilot provider and claude-haiku-4-5 model + const provider = "github-copilot" + const model = "claude-haiku-4-5" + + // #when transformModelForProvider is called + const result = transformModelForProvider(provider, model) + + // #then should transform to claude-haiku-4.5 + expect(result).toBe("claude-haiku-4.5") + }) + + test("transforms gemini-3-pro to gemini-3-pro-preview", () => { + // #given github-copilot provider and gemini-3-pro model + const provider = "github-copilot" + const model = "gemini-3-pro" + + // #when transformModelForProvider is called + const result = transformModelForProvider(provider, model) + + // #then should transform to gemini-3-pro-preview + expect(result).toBe("gemini-3-pro-preview") + }) + + test("transforms gemini-3-flash to gemini-3-flash-preview", () => { + // #given github-copilot provider and gemini-3-flash model + const provider = "github-copilot" + const model = "gemini-3-flash" + + // #when transformModelForProvider is called + const result = transformModelForProvider(provider, model) + + // #then should transform to gemini-3-flash-preview + expect(result).toBe("gemini-3-flash-preview") + }) + }) + + describe("google provider", () => { + test("transforms gemini-3-flash to gemini-3-flash-preview", () => { + // #given google provider and gemini-3-flash model + const provider = "google" + const model = "gemini-3-flash" + + // #when transformModelForProvider is called + const result = transformModelForProvider(provider, model) + + // #then should transform to gemini-3-flash-preview + expect(result).toBe("gemini-3-flash-preview") + }) + + test("transforms gemini-3-pro to gemini-3-pro-preview", () => { + // #given google provider and gemini-3-pro model + const provider = "google" + const model = "gemini-3-pro" + + // #when transformModelForProvider is called + const result = transformModelForProvider(provider, model) + + // #then should transform to gemini-3-pro-preview + expect(result).toBe("gemini-3-pro-preview") + }) + + test("passes through other gemini models unchanged", () => { + // #given google provider and gemini-2.5-flash model + const provider = "google" + const model = "gemini-2.5-flash" + + // #when transformModelForProvider is called + const result = transformModelForProvider(provider, model) + + // #then should pass through unchanged + expect(result).toBe("gemini-2.5-flash") + }) + + test("prevents double transformation of gemini-3-flash-preview", () => { + // #given google provider and gemini-3-flash-preview model (already transformed) + const provider = "google" + const model = "gemini-3-flash-preview" + + // #when transformModelForProvider is called + const result = transformModelForProvider(provider, model) + + // #then should NOT become gemini-3-flash-preview-preview + expect(result).toBe("gemini-3-flash-preview") + }) + + test("prevents double transformation of gemini-3-pro-preview", () => { + // #given google provider and gemini-3-pro-preview model (already transformed) + const provider = "google" + const model = "gemini-3-pro-preview" + + // #when transformModelForProvider is called + const result = transformModelForProvider(provider, model) + + // #then should NOT become gemini-3-pro-preview-preview + expect(result).toBe("gemini-3-pro-preview") + }) + + test("does not transform claude models for google provider", () => { + // #given google provider and claude-opus-4-6 model + const provider = "google" + const model = "claude-opus-4-6" + + // #when transformModelForProvider is called + const result = transformModelForProvider(provider, model) + + // #then should pass through unchanged (google doesn't use claude) + expect(result).toBe("claude-opus-4-6") + }) + }) + + describe("unknown provider", () => { + test("passes model through unchanged for unknown provider", () => { + // #given unknown provider and any model + const provider = "unknown-provider" + const model = "some-model" + + // #when transformModelForProvider is called + const result = transformModelForProvider(provider, model) + + // #then should pass through unchanged + expect(result).toBe("some-model") + }) + + test("passes gemini-3-flash through unchanged for unknown provider", () => { + // #given unknown provider and gemini-3-flash model + const provider = "unknown-provider" + const model = "gemini-3-flash" + + // #when transformModelForProvider is called + const result = transformModelForProvider(provider, model) + + // #then should pass through unchanged (no transformation for unknown provider) + expect(result).toBe("gemini-3-flash") + }) + }) +}) diff --git a/src/cli/provider-model-id-transform.ts b/src/cli/provider-model-id-transform.ts index b2a81fb0..0d4b2236 100644 --- a/src/cli/provider-model-id-transform.ts +++ b/src/cli/provider-model-id-transform.ts @@ -8,5 +8,13 @@ export function transformModelForProvider(provider: string, model: string): stri .replace("gemini-3-pro", "gemini-3-pro-preview") .replace("gemini-3-flash", "gemini-3-flash-preview") } + if (provider === "google") { + if (!model.endsWith("-preview")) { + return model + .replace("gemini-3-pro", "gemini-3-pro-preview") + .replace("gemini-3-flash", "gemini-3-flash-preview") + } + return model + } return model } From fec75535bab77f0972387ff4263ff45428da5b38 Mon Sep 17 00:00:00 2001 From: feelsodev Date: Tue, 17 Feb 2026 22:18:47 +0900 Subject: [PATCH 2/3] refactor: move transformModelForProvider to shared for runtime access Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- src/cli/provider-model-id-transform.ts | 21 +-------------------- src/shared/provider-model-id-transform.ts | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 20 deletions(-) create mode 100644 src/shared/provider-model-id-transform.ts diff --git a/src/cli/provider-model-id-transform.ts b/src/cli/provider-model-id-transform.ts index 0d4b2236..e6cb0623 100644 --- a/src/cli/provider-model-id-transform.ts +++ b/src/cli/provider-model-id-transform.ts @@ -1,20 +1 @@ -export function transformModelForProvider(provider: string, model: string): string { - 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-haiku-4-5", "claude-haiku-4.5") - .replace("claude-sonnet-4", "claude-sonnet-4") - .replace("gemini-3-pro", "gemini-3-pro-preview") - .replace("gemini-3-flash", "gemini-3-flash-preview") - } - if (provider === "google") { - if (!model.endsWith("-preview")) { - return model - .replace("gemini-3-pro", "gemini-3-pro-preview") - .replace("gemini-3-flash", "gemini-3-flash-preview") - } - return model - } - return model -} +export { transformModelForProvider } from "../shared/provider-model-id-transform" diff --git a/src/shared/provider-model-id-transform.ts b/src/shared/provider-model-id-transform.ts new file mode 100644 index 00000000..0a326f12 --- /dev/null +++ b/src/shared/provider-model-id-transform.ts @@ -0,0 +1,21 @@ +export function transformModelForProvider(provider: string, model: string): string { + 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-pro", "gemini-3-pro-preview") + .replace("gemini-3-flash", "gemini-3-flash-preview") + } + if (provider === "google") { + if (!model.endsWith("-preview")) { + return model + .replace("gemini-3-pro", "gemini-3-pro-preview") + .replace("gemini-3-flash", "gemini-3-flash-preview") + } + return model + } + return model +} From 4c7b81986ace2919fd365a854a7476bf51029772 Mon Sep 17 00:00:00 2001 From: feelsodev Date: Tue, 17 Feb 2026 22:18:56 +0900 Subject: [PATCH 3/3] fix: add google provider model transform across all resolution paths transformModelForProvider only handled github-copilot provider, leaving google provider models untransformed. This caused ProviderModelNotFoundError when google/gemini-3-flash was sent to the API (correct ID is gemini-3-flash-preview). Changes: - Add google provider to transformModelForProvider with idempotent regex negative lookahead to prevent double -preview suffix - Fix category-default path in model-resolution-pipeline when availableModels is empty but connected provider exists - Fix getFirstFallbackModel first-run path that constructed raw model IDs without transformation - Fix github-copilot provider gemini transforms to also use idempotent regex (was vulnerable to double-transform) - Extract transformModelForProvider to shared module (single source of truth, imported by cli and shared layers) - Add 20 new test cases: unit tests for both providers, runtime integration tests for category-default and fallback-chain paths, double-transform prevention for both providers --- src/agents/builtin-agents/model-resolution.ts | 5 +- src/cli/provider-model-id-transform.test.ts | 24 ++++++ src/shared/model-resolution-pipeline.ts | 13 ++- src/shared/model-resolver.test.ts | 81 ++++++++++++++++++- src/shared/provider-model-id-transform.ts | 11 +-- src/tools/delegate-task/model-selection.ts | 4 +- 6 files changed, 122 insertions(+), 16 deletions(-) diff --git a/src/agents/builtin-agents/model-resolution.ts b/src/agents/builtin-agents/model-resolution.ts index dd5f3266..f692c935 100644 --- a/src/agents/builtin-agents/model-resolution.ts +++ b/src/agents/builtin-agents/model-resolution.ts @@ -1,4 +1,5 @@ import { resolveModelPipeline } from "../../shared" +import { transformModelForProvider } from "../../shared/provider-model-id-transform" export function applyModelResolution(input: { uiSelectedModel?: string @@ -20,8 +21,10 @@ export function getFirstFallbackModel(requirement?: { }) { const entry = requirement?.fallbackChain?.[0] if (!entry || entry.providers.length === 0) return undefined + const provider = entry.providers[0] + const transformedModel = transformModelForProvider(provider, entry.model) return { - model: `${entry.providers[0]}/${entry.model}`, + model: `${provider}/${transformedModel}`, provenance: "provider-fallback" as const, variant: entry.variant, } diff --git a/src/cli/provider-model-id-transform.test.ts b/src/cli/provider-model-id-transform.test.ts index 08317afb..e13c7846 100644 --- a/src/cli/provider-model-id-transform.test.ts +++ b/src/cli/provider-model-id-transform.test.ts @@ -63,6 +63,30 @@ describe("transformModelForProvider", () => { // #then should transform to gemini-3-flash-preview expect(result).toBe("gemini-3-flash-preview") }) + + test("prevents double transformation of gemini-3-pro-preview", () => { + // #given github-copilot provider and gemini-3-pro-preview model (already transformed) + const provider = "github-copilot" + const model = "gemini-3-pro-preview" + + // #when transformModelForProvider is called + const result = transformModelForProvider(provider, model) + + // #then should NOT become gemini-3-pro-preview-preview + expect(result).toBe("gemini-3-pro-preview") + }) + + test("prevents double transformation of gemini-3-flash-preview", () => { + // #given github-copilot provider and gemini-3-flash-preview model (already transformed) + const provider = "github-copilot" + const model = "gemini-3-flash-preview" + + // #when transformModelForProvider is called + const result = transformModelForProvider(provider, model) + + // #then should NOT become gemini-3-flash-preview-preview + expect(result).toBe("gemini-3-flash-preview") + }) }) describe("google provider", () => { diff --git a/src/shared/model-resolution-pipeline.ts b/src/shared/model-resolution-pipeline.ts index 34d1c13b..6b39f9d5 100644 --- a/src/shared/model-resolution-pipeline.ts +++ b/src/shared/model-resolution-pipeline.ts @@ -2,6 +2,7 @@ import { log } from "./logger" import * as connectedProvidersCache from "./connected-providers-cache" import { fuzzyMatchModel } from "./model-availability" import type { FallbackEntry } from "./model-requirements" +import { transformModelForProvider } from "./provider-model-id-transform" export type ModelResolutionRequest = { intent?: { @@ -85,10 +86,13 @@ export function resolveModelPipeline( if (parts.length >= 2) { const provider = parts[0] if (connectedProviders.includes(provider)) { + const modelName = parts.slice(1).join("/") + const transformedModel = `${provider}/${transformModelForProvider(provider, modelName)}` log("Model resolved via category default (connected provider)", { - model: normalizedCategoryDefault, + model: transformedModel, + original: normalizedCategoryDefault, }) - return { model: normalizedCategoryDefault, provenance: "category-default", attempted } + return { model: transformedModel, provenance: "category-default", attempted } } } } @@ -108,10 +112,11 @@ export function resolveModelPipeline( for (const entry of fallbackChain) { for (const provider of entry.providers) { if (connectedSet.has(provider)) { - const model = `${provider}/${entry.model}` + const transformedModelId = transformModelForProvider(provider, entry.model) + const model = `${provider}/${transformedModelId}` log("Model resolved via fallback chain (connected provider)", { provider, - model: entry.model, + model: transformedModelId, variant: entry.variant, }) return { diff --git a/src/shared/model-resolver.test.ts b/src/shared/model-resolver.test.ts index 520c0c00..ea301a78 100644 --- a/src/shared/model-resolver.test.ts +++ b/src/shared/model-resolver.test.ts @@ -543,7 +543,8 @@ describe("resolveModelWithFallback", () => { const result = resolveModelWithFallback(input) // then - should use github-copilot (second provider) since google not connected - expect(result!.model).toBe("github-copilot/gemini-3-pro") + // model name is transformed to preview variant for github-copilot provider + expect(result!.model).toBe("github-copilot/gemini-3-pro-preview") expect(result!.source).toBe("provider-fallback") cacheSpy.mockRestore() }) @@ -795,8 +796,82 @@ describe("resolveModelWithFallback", () => { // when const result = resolveModelWithFallback(input) - // then - should use categoryDefaultModel since google is connected - expect(result!.model).toBe("google/gemini-3-pro") + // then - should use transformed categoryDefaultModel since google is connected + expect(result!.model).toBe("google/gemini-3-pro-preview") + expect(result!.source).toBe("category-default") + cacheSpy.mockRestore() + }) + + test("transforms gemini-3-flash in categoryDefaultModel for google connected provider", () => { + // given - google connected, category default uses gemini-3-flash + const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["google"]) + const input: ExtendedModelResolutionInput = { + categoryDefaultModel: "google/gemini-3-flash", + availableModels: new Set(), + systemDefaultModel: "anthropic/claude-sonnet-4-5", + } + + // when + const result = resolveModelWithFallback(input) + + // then - gemini-3-flash should be transformed to gemini-3-flash-preview + expect(result!.model).toBe("google/gemini-3-flash-preview") + expect(result!.source).toBe("category-default") + cacheSpy.mockRestore() + }) + + test("does not double-transform categoryDefaultModel already containing -preview", () => { + // given - category default already has -preview suffix + const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["google"]) + const input: ExtendedModelResolutionInput = { + categoryDefaultModel: "google/gemini-3-pro-preview", + availableModels: new Set(), + systemDefaultModel: "anthropic/claude-sonnet-4-5", + } + + // when + const result = resolveModelWithFallback(input) + + // then - should NOT become gemini-3-pro-preview-preview + expect(result!.model).toBe("google/gemini-3-pro-preview") + expect(result!.source).toBe("category-default") + cacheSpy.mockRestore() + }) + + test("transforms gemini-3-pro in fallback chain for google connected provider", () => { + // given - google connected, fallback chain has gemini-3-pro + const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["google"]) + const input: ExtendedModelResolutionInput = { + fallbackChain: [ + { providers: ["google", "github-copilot"], model: "gemini-3-pro" }, + ], + availableModels: new Set(), + systemDefaultModel: "anthropic/claude-sonnet-4-5", + } + + // when + const result = resolveModelWithFallback(input) + + // then - should transform to preview variant for google provider + expect(result!.model).toBe("google/gemini-3-pro-preview") + expect(result!.source).toBe("provider-fallback") + cacheSpy.mockRestore() + }) + + test("passes through non-gemini-3 models for google connected provider", () => { + // given - google connected, category default uses gemini-2.5-flash (no transform needed) + const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["google"]) + const input: ExtendedModelResolutionInput = { + categoryDefaultModel: "google/gemini-2.5-flash", + availableModels: new Set(), + systemDefaultModel: "anthropic/claude-sonnet-4-5", + } + + // when + const result = resolveModelWithFallback(input) + + // then - should pass through unchanged + expect(result!.model).toBe("google/gemini-2.5-flash") expect(result!.source).toBe("category-default") cacheSpy.mockRestore() }) diff --git a/src/shared/provider-model-id-transform.ts b/src/shared/provider-model-id-transform.ts index 0a326f12..5b8c810b 100644 --- a/src/shared/provider-model-id-transform.ts +++ b/src/shared/provider-model-id-transform.ts @@ -6,16 +6,13 @@ export function transformModelForProvider(provider: string, model: string): stri .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-pro", "gemini-3-pro-preview") - .replace("gemini-3-flash", "gemini-3-flash-preview") + .replace(/gemini-3-pro(?!-)/g, "gemini-3-pro-preview") + .replace(/gemini-3-flash(?!-)/g, "gemini-3-flash-preview") } if (provider === "google") { - if (!model.endsWith("-preview")) { - return model - .replace("gemini-3-pro", "gemini-3-pro-preview") - .replace("gemini-3-flash", "gemini-3-flash-preview") - } return model + .replace(/gemini-3-pro(?!-)/g, "gemini-3-pro-preview") + .replace(/gemini-3-flash(?!-)/g, "gemini-3-flash-preview") } return model } diff --git a/src/tools/delegate-task/model-selection.ts b/src/tools/delegate-task/model-selection.ts index 12987421..188e3e37 100644 --- a/src/tools/delegate-task/model-selection.ts +++ b/src/tools/delegate-task/model-selection.ts @@ -1,5 +1,6 @@ import type { FallbackEntry } from "../../shared/model-requirements" import { fuzzyMatchModel } from "../../shared/model-availability" +import { transformModelForProvider } from "../../shared/provider-model-id-transform" function normalizeModel(model?: string): string | undefined { const trimmed = model?.trim() @@ -38,7 +39,8 @@ export function resolveModelForDelegateTask(input: { const first = fallbackChain[0] const provider = first?.providers?.[0] if (provider) { - return { model: `${provider}/${first.model}`, variant: first.variant } + const transformedModelId = transformModelForProvider(provider, first.model) + return { model: `${provider}/${transformedModelId}`, variant: first.variant } } } else { for (const entry of fallbackChain) {