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
This commit is contained in:
@@ -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()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user