fix(models): update Gemini 3 to 3.1 Pro and add Kimi to writing category fallback

Closes #2065

Closes #1968
This commit is contained in:
YeonGyu-Kim
2026-02-26 21:01:05 +09:00
parent 502bcf76dd
commit 7a88f151df
25 changed files with 242 additions and 239 deletions
+65 -65
View File
@@ -10,7 +10,7 @@ describe("resolveModel", () => {
const input: ModelResolutionInput = {
userModel: "anthropic/claude-opus-4-6",
inheritedModel: "openai/gpt-5.2",
systemDefault: "google/gemini-3-pro",
systemDefault: "google/gemini-3.1-pro",
}
// when
@@ -25,7 +25,7 @@ describe("resolveModel", () => {
const input: ModelResolutionInput = {
userModel: undefined,
inheritedModel: "openai/gpt-5.2",
systemDefault: "google/gemini-3-pro",
systemDefault: "google/gemini-3.1-pro",
}
// when
@@ -40,14 +40,14 @@ describe("resolveModel", () => {
const input: ModelResolutionInput = {
userModel: undefined,
inheritedModel: undefined,
systemDefault: "google/gemini-3-pro",
systemDefault: "google/gemini-3.1-pro",
}
// when
const result = resolveModel(input)
// then
expect(result).toBe("google/gemini-3-pro")
expect(result).toBe("google/gemini-3.1-pro")
})
})
@@ -57,7 +57,7 @@ describe("resolveModel", () => {
const input: ModelResolutionInput = {
userModel: "",
inheritedModel: "openai/gpt-5.2",
systemDefault: "google/gemini-3-pro",
systemDefault: "google/gemini-3.1-pro",
}
// when
@@ -72,14 +72,14 @@ describe("resolveModel", () => {
const input: ModelResolutionInput = {
userModel: " ",
inheritedModel: "",
systemDefault: "google/gemini-3-pro",
systemDefault: "google/gemini-3.1-pro",
}
// when
const result = resolveModel(input)
// then
expect(result).toBe("google/gemini-3-pro")
expect(result).toBe("google/gemini-3.1-pro")
})
})
@@ -89,7 +89,7 @@ describe("resolveModel", () => {
const input: ModelResolutionInput = {
userModel: "anthropic/claude-opus-4-6",
inheritedModel: "openai/gpt-5.2",
systemDefault: "google/gemini-3-pro",
systemDefault: "google/gemini-3.1-pro",
}
// when
@@ -123,7 +123,7 @@ describe("resolveModelWithFallback", () => {
{ providers: ["anthropic", "github-copilot"], model: "claude-opus-4-6" },
],
availableModels: new Set(["anthropic/claude-opus-4-6", "github-copilot/claude-opus-4-6-preview"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -141,7 +141,7 @@ describe("resolveModelWithFallback", () => {
uiSelectedModel: "opencode/big-pickle",
userModel: "anthropic/claude-opus-4-6",
availableModels: new Set(["anthropic/claude-opus-4-6"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -158,7 +158,7 @@ describe("resolveModelWithFallback", () => {
uiSelectedModel: " ",
userModel: "anthropic/claude-opus-4-6",
availableModels: new Set(["anthropic/claude-opus-4-6"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -175,7 +175,7 @@ describe("resolveModelWithFallback", () => {
uiSelectedModel: "",
userModel: "anthropic/claude-opus-4-6",
availableModels: new Set(["anthropic/claude-opus-4-6"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -195,7 +195,7 @@ describe("resolveModelWithFallback", () => {
{ providers: ["anthropic", "github-copilot"], model: "claude-opus-4-6" },
],
availableModels: new Set(["anthropic/claude-opus-4-6", "github-copilot/claude-opus-4-6-preview"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -215,7 +215,7 @@ describe("resolveModelWithFallback", () => {
{ providers: ["anthropic"], model: "claude-opus-4-6" },
],
availableModels: new Set(["anthropic/claude-opus-4-6"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -234,7 +234,7 @@ describe("resolveModelWithFallback", () => {
{ providers: ["anthropic"], model: "claude-opus-4-6" },
],
availableModels: new Set(["anthropic/claude-opus-4-6"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -252,7 +252,7 @@ describe("resolveModelWithFallback", () => {
{ providers: ["anthropic"], model: "claude-opus-4-6" },
],
availableModels: new Set(["anthropic/claude-opus-4-6"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -271,7 +271,7 @@ describe("resolveModelWithFallback", () => {
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6" },
],
availableModels: new Set(["github-copilot/claude-opus-4-6-preview", "opencode/claude-opus-4-7"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -294,8 +294,8 @@ describe("resolveModelWithFallback", () => {
fallbackChain: [
{ providers: ["openai", "anthropic", "google"], model: "gpt-5.2" },
],
availableModels: new Set(["openai/gpt-5.2", "anthropic/claude-opus-4-6", "google/gemini-3-pro"]),
systemDefaultModel: "google/gemini-3-pro",
availableModels: new Set(["openai/gpt-5.2", "anthropic/claude-opus-4-6", "google/gemini-3.1-pro"]),
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -313,7 +313,7 @@ describe("resolveModelWithFallback", () => {
{ providers: ["anthropic", "opencode"], model: "gpt-5-nano" },
],
availableModels: new Set(["opencode/gpt-5-nano"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -331,7 +331,7 @@ describe("resolveModelWithFallback", () => {
{ providers: ["anthropic", "github-copilot"], model: "claude-opus" },
],
availableModels: new Set(["anthropic/claude-opus-4-6", "github-copilot/claude-opus-4-6-preview"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -346,7 +346,7 @@ describe("resolveModelWithFallback", () => {
// given
const input: ExtendedModelResolutionInput = {
availableModels: new Set(["anthropic/claude-opus-4-6"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -361,7 +361,7 @@ describe("resolveModelWithFallback", () => {
const input: ExtendedModelResolutionInput = {
fallbackChain: [],
availableModels: new Set(["anthropic/claude-opus-4-6"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -378,7 +378,7 @@ describe("resolveModelWithFallback", () => {
{ providers: ["anthropic"], model: "CLAUDE-OPUS" },
],
availableModels: new Set(["anthropic/claude-opus-4-6"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -397,7 +397,7 @@ describe("resolveModelWithFallback", () => {
{ providers: ["anthropic"], model: "claude-sonnet-4-6" },
],
availableModels: new Set(["opencode/glm-5", "anthropic/claude-sonnet-4-6"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -420,7 +420,7 @@ describe("resolveModelWithFallback", () => {
{ providers: ["zai-coding-plan"], model: "glm-5" },
],
availableModels: new Set(["zai-coding-plan/glm-5", "opencode/glm-5"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -438,7 +438,7 @@ describe("resolveModelWithFallback", () => {
{ providers: ["zai-coding-plan"], model: "glm-5", variant: "high" },
],
availableModels: new Set(["opencode/glm-5"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -457,7 +457,7 @@ describe("resolveModelWithFallback", () => {
{ providers: ["anthropic"], model: "claude-sonnet-4-6" },
],
availableModels: new Set(["anthropic/claude-sonnet-4-6"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -477,14 +477,14 @@ describe("resolveModelWithFallback", () => {
{ providers: ["anthropic"], model: "nonexistent-model" },
],
availableModels: new Set(["openai/gpt-5.2", "anthropic/claude-opus-4-6"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
const result = resolveModelWithFallback(input)
// then
expect(result!.model).toBe("google/gemini-3-pro")
expect(result!.model).toBe("google/gemini-3.1-pro")
expect(result!.source).toBe("system-default")
expect(logSpy).toHaveBeenCalledWith("No available model found in fallback chain, falling through to system default")
})
@@ -516,7 +516,7 @@ describe("resolveModelWithFallback", () => {
{ providers: ["anthropic", "openai"], model: "claude-opus-4-6" },
],
availableModels: new Set(),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -533,7 +533,7 @@ describe("resolveModelWithFallback", () => {
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["github-copilot"])
const input: ExtendedModelResolutionInput = {
fallbackChain: [
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro" },
],
availableModels: new Set(),
systemDefaultModel: "anthropic/claude-sonnet-4-6",
@@ -544,7 +544,7 @@ describe("resolveModelWithFallback", () => {
// then - should use github-copilot (second provider) since google not connected
// model name is transformed to preview variant for github-copilot provider
expect(result!.model).toBe("github-copilot/gemini-3-pro-preview")
expect(result!.model).toBe("github-copilot/gemini-3.1-pro-preview")
expect(result!.source).toBe("provider-fallback")
cacheSpy.mockRestore()
})
@@ -577,14 +577,14 @@ describe("resolveModelWithFallback", () => {
{ providers: ["anthropic"], model: "claude-opus-4-6" },
],
availableModels: new Set(),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
const result = resolveModelWithFallback(input)
// then - should fall through to system default
expect(result!.model).toBe("google/gemini-3-pro")
expect(result!.model).toBe("google/gemini-3.1-pro")
expect(result!.source).toBe("system-default")
cacheSpy.mockRestore()
})
@@ -593,14 +593,14 @@ describe("resolveModelWithFallback", () => {
// given
const input: ExtendedModelResolutionInput = {
availableModels: new Set(["openai/gpt-5.2"]),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
const result = resolveModelWithFallback(input)
// then
expect(result!.model).toBe("google/gemini-3-pro")
expect(result!.model).toBe("google/gemini-3.1-pro")
expect(result!.source).toBe("system-default")
})
})
@@ -627,20 +627,20 @@ describe("resolveModelWithFallback", () => {
test("tries all providers in first entry before moving to second entry", () => {
// given
const availableModels = new Set(["google/gemini-3-pro"])
const availableModels = new Set(["google/gemini-3.1-pro"])
// when
const result = resolveModelWithFallback({
fallbackChain: [
{ providers: ["openai", "anthropic"], model: "gpt-5.2" },
{ providers: ["google"], model: "gemini-3-pro" },
{ providers: ["google"], model: "gemini-3.1-pro" },
],
availableModels,
systemDefaultModel: "system/default",
})
// then
expect(result!.model).toBe("google/gemini-3-pro")
expect(result!.model).toBe("google/gemini-3.1-pro")
expect(result!.source).toBe("provider-fallback")
})
@@ -675,7 +675,7 @@ describe("resolveModelWithFallback", () => {
fallbackChain: [
{ providers: ["openai"], model: "gpt-5.2" },
{ providers: ["anthropic"], model: "claude-opus-4-6" },
{ providers: ["google"], model: "gemini-3-pro" },
{ providers: ["google"], model: "gemini-3.1-pro" },
],
availableModels,
systemDefaultModel: "system/default",
@@ -693,7 +693,7 @@ describe("resolveModelWithFallback", () => {
const input: ExtendedModelResolutionInput = {
userModel: "anthropic/claude-opus-4-6",
availableModels: new Set(),
systemDefaultModel: "google/gemini-3-pro",
systemDefaultModel: "google/gemini-3.1-pro",
}
// when
@@ -708,32 +708,32 @@ describe("resolveModelWithFallback", () => {
describe("categoryDefaultModel (fuzzy matching for category defaults)", () => {
test("applies fuzzy matching to categoryDefaultModel when userModel not provided", () => {
// given - gemini-3-pro is the category default, but only gemini-3-pro-preview is available
// given - gemini-3.1-pro is the category default, but only gemini-3.1-pro-preview is available
const input: ExtendedModelResolutionInput = {
categoryDefaultModel: "google/gemini-3-pro",
categoryDefaultModel: "google/gemini-3.1-pro",
fallbackChain: [
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro" },
],
availableModels: new Set(["google/gemini-3-pro-preview", "anthropic/claude-opus-4-6"]),
availableModels: new Set(["google/gemini-3.1-pro-preview", "anthropic/claude-opus-4-6"]),
systemDefaultModel: "anthropic/claude-sonnet-4-6",
}
// when
const result = resolveModelWithFallback(input)
// then - should fuzzy match gemini-3-pro → gemini-3-pro-preview
expect(result!.model).toBe("google/gemini-3-pro-preview")
// then - should fuzzy match gemini-3.1-pro → gemini-3.1-pro-preview
expect(result!.model).toBe("google/gemini-3.1-pro-preview")
expect(result!.source).toBe("category-default")
})
test("categoryDefaultModel uses exact match when available", () => {
// given - exact match exists
const input: ExtendedModelResolutionInput = {
categoryDefaultModel: "google/gemini-3-pro",
categoryDefaultModel: "google/gemini-3.1-pro",
fallbackChain: [
{ providers: ["google"], model: "gemini-3-pro" },
{ providers: ["google"], model: "gemini-3.1-pro" },
],
availableModels: new Set(["google/gemini-3-pro", "google/gemini-3-pro-preview"]),
availableModels: new Set(["google/gemini-3.1-pro", "google/gemini-3.1-pro-preview"]),
systemDefaultModel: "anthropic/claude-sonnet-4-6",
}
@@ -741,14 +741,14 @@ describe("resolveModelWithFallback", () => {
const result = resolveModelWithFallback(input)
// then - should use exact match
expect(result!.model).toBe("google/gemini-3-pro")
expect(result!.model).toBe("google/gemini-3.1-pro")
expect(result!.source).toBe("category-default")
})
test("categoryDefaultModel falls through to fallbackChain when no match in availableModels", () => {
// given - categoryDefaultModel has no match, but fallbackChain does
const input: ExtendedModelResolutionInput = {
categoryDefaultModel: "google/gemini-3-pro",
categoryDefaultModel: "google/gemini-3.1-pro",
fallbackChain: [
{ providers: ["anthropic"], model: "claude-opus-4-6" },
],
@@ -768,11 +768,11 @@ describe("resolveModelWithFallback", () => {
// given - both userModel and categoryDefaultModel provided
const input: ExtendedModelResolutionInput = {
userModel: "anthropic/claude-opus-4-6",
categoryDefaultModel: "google/gemini-3-pro",
categoryDefaultModel: "google/gemini-3.1-pro",
fallbackChain: [
{ providers: ["google"], model: "gemini-3-pro" },
{ providers: ["google"], model: "gemini-3.1-pro" },
],
availableModels: new Set(["google/gemini-3-pro-preview", "anthropic/claude-opus-4-6"]),
availableModels: new Set(["google/gemini-3.1-pro-preview", "anthropic/claude-opus-4-6"]),
systemDefaultModel: "system/default",
}
@@ -788,7 +788,7 @@ describe("resolveModelWithFallback", () => {
// given - no availableModels but connected provider cache exists
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["google"])
const input: ExtendedModelResolutionInput = {
categoryDefaultModel: "google/gemini-3-pro",
categoryDefaultModel: "google/gemini-3.1-pro",
availableModels: new Set(),
systemDefaultModel: "anthropic/claude-sonnet-4-6",
}
@@ -797,7 +797,7 @@ describe("resolveModelWithFallback", () => {
const result = resolveModelWithFallback(input)
// then - should use transformed categoryDefaultModel since google is connected
expect(result!.model).toBe("google/gemini-3-pro-preview")
expect(result!.model).toBe("google/gemini-3.1-pro-preview")
expect(result!.source).toBe("category-default")
cacheSpy.mockRestore()
})
@@ -824,7 +824,7 @@ describe("resolveModelWithFallback", () => {
// given - category default already has -preview suffix
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["google"])
const input: ExtendedModelResolutionInput = {
categoryDefaultModel: "google/gemini-3-pro-preview",
categoryDefaultModel: "google/gemini-3.1-pro-preview",
availableModels: new Set(),
systemDefaultModel: "anthropic/claude-sonnet-4-5",
}
@@ -832,18 +832,18 @@ describe("resolveModelWithFallback", () => {
// when
const result = resolveModelWithFallback(input)
// then - should NOT become gemini-3-pro-preview-preview
expect(result!.model).toBe("google/gemini-3-pro-preview")
// then - should NOT become gemini-3.1-pro-preview-preview
expect(result!.model).toBe("google/gemini-3.1-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
test("transforms gemini-3.1-pro in fallback chain for google connected provider", () => {
// given - google connected, fallback chain has gemini-3.1-pro
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["google"])
const input: ExtendedModelResolutionInput = {
fallbackChain: [
{ providers: ["google", "github-copilot"], model: "gemini-3-pro" },
{ providers: ["google", "github-copilot"], model: "gemini-3.1-pro" },
],
availableModels: new Set(),
systemDefaultModel: "anthropic/claude-sonnet-4-5",
@@ -853,7 +853,7 @@ describe("resolveModelWithFallback", () => {
const result = resolveModelWithFallback(input)
// then - should transform to preview variant for google provider
expect(result!.model).toBe("google/gemini-3-pro-preview")
expect(result!.model).toBe("google/gemini-3.1-pro-preview")
expect(result!.source).toBe("provider-fallback")
cacheSpy.mockRestore()
})