Compare commits
3 Commits
fix/backgr
...
fix/google
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c7b81986a | ||
|
|
fec75535ba | ||
|
|
e5a0ab4034 |
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
191
src/cli/provider-model-id-transform.test.ts
Normal file
191
src/cli/provider-model-id-transform.test.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
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")
|
||||
})
|
||||
|
||||
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", () => {
|
||||
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")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,12 +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")
|
||||
}
|
||||
return model
|
||||
}
|
||||
export { transformModelForProvider } from "../shared/provider-model-id-transform"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
18
src/shared/provider-model-id-transform.ts
Normal file
18
src/shared/provider-model-id-transform.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
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(?!-)/g, "gemini-3-pro-preview")
|
||||
.replace(/gemini-3-flash(?!-)/g, "gemini-3-flash-preview")
|
||||
}
|
||||
if (provider === "google") {
|
||||
return model
|
||||
.replace(/gemini-3-pro(?!-)/g, "gemini-3-pro-preview")
|
||||
.replace(/gemini-3-flash(?!-)/g, "gemini-3-flash-preview")
|
||||
}
|
||||
return model
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user