diff --git a/README.md b/README.md index da309a6..ddb6e21 100644 --- a/README.md +++ b/README.md @@ -112,8 +112,7 @@ opencode run "Hello" --model=google/antigravity-claude-opus-4-6-thinking --varia | Model | Variants | Notes | |-------|----------|-------| -| `antigravity-gemini-3-pro` | low, high | Gemini 3 Pro with thinking | -| `antigravity-gemini-3.1-pro` | low, high | Gemini 3.1 Pro with thinking (rollout-dependent) | +| `antigravity-gemini-3.1-pro` | low, high | Gemini 3.1 Pro with thinking | | `antigravity-gemini-3-flash` | minimal, low, medium, high | Gemini 3 Flash with thinking | | `antigravity-claude-sonnet-4-6` | — | Claude Sonnet 4.6 | | `antigravity-claude-opus-4-6-thinking` | low, max | Claude Opus 4.6 with extended thinking | @@ -125,9 +124,8 @@ opencode run "Hello" --model=google/antigravity-claude-opus-4-6-thinking --varia | `gemini-2.5-flash` | Gemini 2.5 Flash | | `gemini-2.5-pro` | Gemini 2.5 Pro | | `gemini-3-flash-preview` | Gemini 3 Flash (preview) | -| `gemini-3-pro-preview` | Gemini 3 Pro (preview) | -| `gemini-3.1-pro-preview` | Gemini 3.1 Pro (preview, rollout-dependent) | -| `gemini-3.1-pro-preview-customtools` | Gemini 3.1 Pro Preview Custom Tools (preview, rollout-dependent) | +| `gemini-3.1-pro-preview` | Gemini 3.1 Pro (preview) | +| `gemini-3.1-pro-preview-customtools` | Gemini 3.1 Pro Preview Custom Tools (preview) | > **Routing Behavior:** > - **Antigravity-first (default):** Gemini models use Antigravity quota across accounts. @@ -155,15 +153,6 @@ Add this to your `~/.config/opencode/opencode.json`: "provider": { "google": { "models": { - "antigravity-gemini-3-pro": { - "name": "Gemini 3 Pro (Antigravity)", - "limit": { "context": 1048576, "output": 65535 }, - "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }, - "variants": { - "low": { "thinkingLevel": "low" }, - "high": { "thinkingLevel": "high" } - } - }, "antigravity-gemini-3.1-pro": { "name": "Gemini 3.1 Pro (Antigravity)", "limit": { "context": 1048576, "output": 65535 }, @@ -213,11 +202,6 @@ Add this to your `~/.config/opencode/opencode.json`: "limit": { "context": 1048576, "output": 65536 }, "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } }, - "gemini-3-pro-preview": { - "name": "Gemini 3 Pro Preview (Gemini CLI)", - "limit": { "context": 1048576, "output": 65535 }, - "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } - }, "gemini-3.1-pro-preview": { "name": "Gemini 3.1 Pro Preview (Gemini CLI)", "limit": { "context": 1048576, "output": 65535 }, @@ -422,7 +406,7 @@ If you encounter errors during a session: { "google_auth": false, "agents": { - "frontend-ui-ux-engineer": { "model": "google/antigravity-gemini-3-pro" }, + "frontend-ui-ux-engineer": { "model": "google/antigravity-gemini-3.1-pro" }, "document-writer": { "model": "google/antigravity-gemini-3-flash" } } } @@ -560,7 +544,7 @@ Disable built-in auth and override agent models in `oh-my-opencode.json`: { "google_auth": false, "agents": { - "frontend-ui-ux-engineer": { "model": "google/antigravity-gemini-3-pro" }, + "frontend-ui-ux-engineer": { "model": "google/antigravity-gemini-3.1-pro" }, "document-writer": { "model": "google/antigravity-gemini-3-flash" }, "multimodal-looker": { "model": "google/antigravity-gemini-3-flash" } } diff --git a/src/plugin/config/models.test.ts b/src/plugin/config/models.test.ts index fd5aecc..3f7b954 100644 --- a/src/plugin/config/models.test.ts +++ b/src/plugin/config/models.test.ts @@ -18,23 +18,16 @@ describe("OPENCODE_MODEL_DEFINITIONS", () => { "antigravity-claude-opus-4-6-thinking", "antigravity-claude-sonnet-4-6", "antigravity-gemini-3-flash", - "antigravity-gemini-3-pro", "antigravity-gemini-3.1-pro", "gemini-2.5-flash", "gemini-2.5-pro", "gemini-3-flash-preview", - "gemini-3-pro-preview", "gemini-3.1-pro-preview", "gemini-3.1-pro-preview-customtools", ]); }); it("defines Gemini 3 variants for Antigravity models", () => { - expect(getModel("antigravity-gemini-3-pro").variants).toEqual({ - low: { thinkingLevel: "low" }, - high: { thinkingLevel: "high" }, - }); - expect(getModel("antigravity-gemini-3.1-pro").variants).toEqual({ low: { thinkingLevel: "low" }, high: { thinkingLevel: "high" }, @@ -54,4 +47,11 @@ describe("OPENCODE_MODEL_DEFINITIONS", () => { max: { thinkingConfig: { thinkingBudget: 32768 } }, }); }); + + it("does not expose stale unavailable model IDs", () => { + expect(OPENCODE_MODEL_DEFINITIONS["antigravity-gemini-3-pro"]).toBeUndefined(); + expect(OPENCODE_MODEL_DEFINITIONS["gemini-3-pro-preview"]).toBeUndefined(); + expect(Object.keys(OPENCODE_MODEL_DEFINITIONS).some((name) => name.includes("claude-sonnet-4-5"))).toBe(false); + expect(Object.keys(OPENCODE_MODEL_DEFINITIONS).some((name) => name.includes("claude-opus-4-5"))).toBe(false); + }); }); diff --git a/src/plugin/config/models.ts b/src/plugin/config/models.ts index 641d2e1..b929371 100644 --- a/src/plugin/config/models.ts +++ b/src/plugin/config/models.ts @@ -38,15 +38,6 @@ const DEFAULT_MODALITIES: ModelModalities = { }; export const OPENCODE_MODEL_DEFINITIONS: OpencodeModelDefinitions = { - "antigravity-gemini-3-pro": { - name: "Gemini 3 Pro (Antigravity)", - limit: { context: 1048576, output: 65535 }, - modalities: DEFAULT_MODALITIES, - variants: { - low: { thinkingLevel: "low" }, - high: { thinkingLevel: "high" }, - }, - }, "antigravity-gemini-3.1-pro": { name: "Gemini 3.1 Pro (Antigravity)", limit: { context: 1048576, output: 65535 }, @@ -96,11 +87,6 @@ export const OPENCODE_MODEL_DEFINITIONS: OpencodeModelDefinitions = { limit: { context: 1048576, output: 65536 }, modalities: DEFAULT_MODALITIES, }, - "gemini-3-pro-preview": { - name: "Gemini 3 Pro Preview (Gemini CLI)", - limit: { context: 1048576, output: 65535 }, - modalities: DEFAULT_MODALITIES, - }, "gemini-3.1-pro-preview": { name: "Gemini 3.1 Pro Preview (Gemini CLI)", limit: { context: 1048576, output: 65535 }, diff --git a/src/plugin/config/updater.test.ts b/src/plugin/config/updater.test.ts index 83634ed..68a26c7 100644 --- a/src/plugin/config/updater.test.ts +++ b/src/plugin/config/updater.test.ts @@ -66,8 +66,9 @@ describe("updateOpencodeConfig", () => { // Old model should be replaced expect(writtenConfig.provider.google.models["old-model"]).toBeUndefined(); // New models should be present - expect(writtenConfig.provider.google.models["antigravity-gemini-3-pro"]).toBeDefined(); + expect(writtenConfig.provider.google.models["antigravity-gemini-3.1-pro"]).toBeDefined(); expect(writtenConfig.provider.google.models["antigravity-claude-sonnet-4-6"]).toBeDefined(); + expect(writtenConfig.provider.google.models["antigravity-gemini-3-pro"]).toBeUndefined(); }); test("preserves non-google provider sections", async () => { @@ -235,7 +236,7 @@ describe("updateOpencodeConfig", () => { expect(writtenConfig.plugin).toContain("other-plugin"); expect(writtenConfig.plugin).toContain("opencode-antigravity-auth@latest"); expect(writtenConfig.provider.google.region).toBe("us-central1"); - expect(writtenConfig.provider.google.models["antigravity-gemini-3-pro"]).toBeDefined(); + expect(writtenConfig.provider.google.models["antigravity-gemini-3.1-pro"]).toBeDefined(); }); test("prefers existing opencode.jsonc when using default config path", async () => { diff --git a/src/plugin/request.test.ts b/src/plugin/request.test.ts index 8694cfe..c6977f5 100644 --- a/src/plugin/request.test.ts +++ b/src/plugin/request.test.ts @@ -1036,7 +1036,7 @@ it("removes x-api-key header", () => { expect(result.effectiveModel).toBe("gemini-3-flash"); }); - it("transforms gemini-3-pro-preview to gemini-3-pro-low for antigravity headerStyle", () => { + it("transforms legacy gemini-3-pro-preview to gemini-3.1-pro-low for antigravity headerStyle", () => { const result = prepareAntigravityRequest( "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-preview:generateContent", { method: "POST", body: JSON.stringify({ contents: [] }) }, @@ -1045,7 +1045,7 @@ it("removes x-api-key header", () => { undefined, "antigravity" ); - expect(result.effectiveModel).toBe("gemini-3-pro-low"); + expect(result.effectiveModel).toBe("gemini-3.1-pro-low"); }); it("transforms gemini-3.1-pro-preview to gemini-3.1-pro-low for antigravity headerStyle", () => { @@ -1084,7 +1084,7 @@ it("removes x-api-key header", () => { expect(result.effectiveModel).toBe("gemini-3-flash-preview"); }); - it("transforms gemini-3-pro-low to gemini-3-pro-preview for gemini-cli headerStyle", () => { + it("transforms legacy gemini-3-pro-low to gemini-3.1-pro-preview for gemini-cli headerStyle", () => { const result = prepareAntigravityRequest( "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-low:generateContent", { method: "POST", body: JSON.stringify({ contents: [] }) }, @@ -1093,7 +1093,7 @@ it("removes x-api-key header", () => { undefined, "gemini-cli" ); - expect(result.effectiveModel).toBe("gemini-3-pro-preview"); + expect(result.effectiveModel).toBe("gemini-3.1-pro-preview"); }); it("transforms gemini-3.1-pro-low to gemini-3.1-pro-preview for gemini-cli headerStyle", () => { diff --git a/src/plugin/transform/model-resolver.test.ts b/src/plugin/transform/model-resolver.test.ts index e0b9b48..cc2d934 100644 --- a/src/plugin/transform/model-resolver.test.ts +++ b/src/plugin/transform/model-resolver.test.ts @@ -27,9 +27,9 @@ describe("resolveModelWithTier", () => { }); describe("Gemini 3 preview models (Issue #115)", () => { - it("gemini-3-pro-preview gets default thinkingLevel 'low' with antigravity quota", () => { + it("gemini-3-pro-preview resolves to Gemini 3.1 Pro preview", () => { const result = resolveModelWithTier("gemini-3-pro-preview"); - expect(result.actualModel).toBe("gemini-3-pro-preview"); + expect(result.actualModel).toBe("gemini-3.1-pro-preview"); expect(result.thinkingLevel).toBe("low"); // All Gemini models now default to antigravity expect(result.quotaPreference).toBe("antigravity"); @@ -60,6 +60,43 @@ describe("resolveModelWithTier", () => { }); }); + describe("legacy model aliases", () => { + it("antigravity-gemini-3-pro resolves to Gemini 3.1 Pro low", () => { + const result = resolveModelWithTier("antigravity-gemini-3-pro"); + expect(result.actualModel).toBe("gemini-3.1-pro-low"); + expect(result.thinkingLevel).toBe("low"); + expect(result.quotaPreference).toBe("antigravity"); + }); + + it("antigravity-gemini-3-pro-high preserves the high tier on Gemini 3.1 Pro", () => { + const result = resolveModelWithTier("antigravity-gemini-3-pro-high"); + expect(result.actualModel).toBe("gemini-3.1-pro-high"); + expect(result.thinkingLevel).toBe("high"); + expect(result.tier).toBe("high"); + }); + + it("antigravity-claude-sonnet-4-5 resolves to Claude Sonnet 4.6", () => { + const result = resolveModelWithTier("antigravity-claude-sonnet-4-5"); + expect(result.actualModel).toBe("claude-sonnet-4-6"); + expect(result.isThinkingModel).toBe(false); + expect(result.quotaPreference).toBe("antigravity"); + }); + + it("antigravity-claude-opus-4-5-thinking resolves to Claude Opus 4.6 Thinking", () => { + const result = resolveModelWithTier("antigravity-claude-opus-4-5-thinking"); + expect(result.actualModel).toBe("claude-opus-4-6-thinking"); + expect(result.thinkingBudget).toBe(32768); + expect(result.isThinkingModel).toBe(true); + }); + + it("claude-opus-4-5-thinking-low preserves the low tier on Claude Opus 4.6 Thinking", () => { + const result = resolveModelWithTier("claude-opus-4-5-thinking-low"); + expect(result.actualModel).toBe("claude-opus-4-6-thinking"); + expect(result.thinkingBudget).toBe(8192); + expect(result.tier).toBe("low"); + }); + }); + describe("cli_first quota preference", () => { it("prefers gemini-cli when cli_first is true and no prefix is set", () => { const result = resolveModelWithTier("gemini-3-flash", { cli_first: true }); @@ -91,16 +128,16 @@ describe("resolveModelWithTier", () => { }); describe("Antigravity Gemini 3 with tier suffix", () => { - it("antigravity-gemini-3-pro-low gets thinkingLevel from tier", () => { + it("legacy antigravity-gemini-3-pro-low resolves to Gemini 3.1 Pro low", () => { const result = resolveModelWithTier("antigravity-gemini-3-pro-low"); - expect(result.actualModel).toBe("gemini-3-pro-low"); + expect(result.actualModel).toBe("gemini-3.1-pro-low"); expect(result.thinkingLevel).toBe("low"); expect(result.quotaPreference).toBe("antigravity"); }); - it("antigravity-gemini-3-pro-high gets thinkingLevel from tier", () => { + it("legacy antigravity-gemini-3-pro-high resolves to Gemini 3.1 Pro high", () => { const result = resolveModelWithTier("antigravity-gemini-3-pro-high"); - expect(result.actualModel).toBe("gemini-3-pro-high"); + expect(result.actualModel).toBe("gemini-3.1-pro-high"); expect(result.thinkingLevel).toBe("high"); expect(result.quotaPreference).toBe("antigravity"); }); @@ -184,7 +221,7 @@ describe("resolveModelWithVariant", () => { it("falls back to tier resolution for Gemini 3 models", () => { const result = resolveModelWithVariant("gemini-3-pro-high"); - expect(result.actualModel).toBe("gemini-3-pro"); + expect(result.actualModel).toBe("gemini-3.1-pro"); expect(result.thinkingLevel).toBe("high"); expect(result.configSource).toBeUndefined(); }); @@ -204,7 +241,7 @@ describe("resolveModelWithVariant", () => { const result = resolveModelWithVariant("antigravity-gemini-3-pro", { thinkingBudget: 8000, }); - expect(result.actualModel).toBe("gemini-3-pro-low"); + expect(result.actualModel).toBe("gemini-3.1-pro-low"); expect(result.thinkingLevel).toBe("low"); expect(result.thinkingBudget).toBeUndefined(); expect(result.configSource).toBe("variant"); @@ -267,9 +304,9 @@ describe("Issue #103: resolveModelForHeaderStyle", () => { expect(result.quotaPreference).toBe("antigravity"); }); - it("transforms gemini-3-pro-preview to gemini-3-pro-low for antigravity", () => { + it("transforms legacy gemini-3-pro-preview to gemini-3.1-pro-low for antigravity", () => { const result = resolveModelForHeaderStyle("gemini-3-pro-preview", "antigravity"); - expect(result.actualModel).toBe("gemini-3-pro-low"); + expect(result.actualModel).toBe("gemini-3.1-pro-low"); expect(result.quotaPreference).toBe("antigravity"); }); @@ -293,9 +330,15 @@ describe("Issue #103: resolveModelForHeaderStyle", () => { expect(result.quotaPreference).toBe("gemini-cli"); }); - it("transforms gemini-3-pro-low to gemini-3-pro-preview for gemini-cli", () => { + it("transforms legacy gemini-3-pro-low to gemini-3.1-pro-preview for gemini-cli", () => { const result = resolveModelForHeaderStyle("gemini-3-pro-low", "gemini-cli"); - expect(result.actualModel).toBe("gemini-3-pro-preview"); + expect(result.actualModel).toBe("gemini-3.1-pro-preview"); + expect(result.quotaPreference).toBe("gemini-cli"); + }); + + it("transforms legacy gemini-3-pro-preview to gemini-3.1-pro-preview for gemini-cli", () => { + const result = resolveModelForHeaderStyle("gemini-3-pro-preview", "gemini-cli"); + expect(result.actualModel).toBe("gemini-3.1-pro-preview"); expect(result.quotaPreference).toBe("gemini-cli"); }); diff --git a/src/plugin/transform/model-resolver.ts b/src/plugin/transform/model-resolver.ts index dd635c3..9613d24 100644 --- a/src/plugin/transform/model-resolver.ts +++ b/src/plugin/transform/model-resolver.ts @@ -33,7 +33,7 @@ export const GEMINI_3_THINKING_LEVELS = ["minimal", "low", "medium", "high"] as * Model aliases - maps user-friendly names to API model names. * * Format: - * - Gemini 3 Pro variants: gemini-3-pro-{low,medium,high} + * - Gemini 3.1 Pro variants: gemini-3.1-pro-{low,high} * - Claude thinking variants: claude-{model}-thinking-{low,medium,high} * - Claude non-thinking: claude-{model} (no -thinking suffix) */ @@ -63,6 +63,17 @@ const TIER_REGEX = /-(minimal|low|medium|high)$/; const QUOTA_PREFIX_REGEX = /^antigravity-/i; const GEMINI_3_PRO_REGEX = /^gemini-3(?:\.\d+)?-pro/i; const GEMINI_3_FLASH_REGEX = /^gemini-3(?:\.\d+)?-flash/i; +const LEGACY_MODEL_ALIASES: Record = { + "antigravity-gemini-3-pro": "antigravity-gemini-3.1-pro", + "gemini-3-pro": "gemini-3.1-pro", + "gemini-3-pro-preview": "gemini-3.1-pro-preview", + "antigravity-claude-sonnet-4-5": "antigravity-claude-sonnet-4-6", + "claude-sonnet-4-5": "claude-sonnet-4-6", + "antigravity-claude-opus-4-5": "antigravity-claude-opus-4-6-thinking", + "claude-opus-4-5": "claude-opus-4-6-thinking", + "antigravity-claude-opus-4-5-thinking": "antigravity-claude-opus-4-6-thinking", + "claude-opus-4-5-thinking": "claude-opus-4-6-thinking", +}; // ANTIGRAVITY_ONLY_MODELS removed - all models now default to antigravity @@ -137,6 +148,20 @@ function isGemini3FlashModel(model: string): boolean { return GEMINI_3_FLASH_REGEX.test(model); } +function normalizeLegacyModel(model: string): string { + const tier = extractThinkingTierFromModel(model); + const baseName = tier ? model.replace(TIER_REGEX, "") : model; + const normalizedBase = LEGACY_MODEL_ALIASES[baseName.toLowerCase()]; + + if (!normalizedBase) { + return model; + } + if (!tier) { + return normalizedBase; + } + return supportsThinkingTiers(normalizedBase) ? `${normalizedBase}-${tier}` : normalizedBase; +} + /** * Resolves a model name with optional tier suffix and quota prefix to its actual API model name * and corresponding thinking configuration. @@ -149,8 +174,8 @@ function isGemini3FlashModel(model: string): boolean { * * Examples: * - "gemini-2.5-flash" → { quotaPreference: "antigravity" } - * - "gemini-3-pro-preview" → { quotaPreference: "antigravity" } - * - "antigravity-gemini-3-pro-high" → { quotaPreference: "antigravity", explicitQuota: true } + * - "gemini-3-pro-preview" → { actualModel: "gemini-3.1-pro-preview", quotaPreference: "antigravity" } + * - "antigravity-gemini-3-pro-high" → { actualModel: "gemini-3.1-pro-high", quotaPreference: "antigravity", explicitQuota: true } * - "claude-opus-4-6-thinking-medium" → { quotaPreference: "antigravity" } * * @param requestedModel - The model name from the request @@ -158,8 +183,9 @@ function isGemini3FlashModel(model: string): boolean { * @returns Resolved model with thinking configuration */ export function resolveModelWithTier(requestedModel: string, options: ModelResolverOptions = {}): ResolvedModel { - const isAntigravity = QUOTA_PREFIX_REGEX.test(requestedModel); - const modelWithoutQuota = requestedModel.replace(QUOTA_PREFIX_REGEX, ""); + const normalizedModel = normalizeLegacyModel(requestedModel); + const isAntigravity = QUOTA_PREFIX_REGEX.test(normalizedModel); + const modelWithoutQuota = normalizedModel.replace(QUOTA_PREFIX_REGEX, ""); const tier = extractThinkingTierFromModel(modelWithoutQuota); const baseName = tier ? modelWithoutQuota.replace(TIER_REGEX, "") : modelWithoutQuota; @@ -176,8 +202,8 @@ export function resolveModelWithTier(requestedModel: string, options: ModelResol const isGemini3 = modelWithoutQuota.toLowerCase().startsWith("gemini-3"); const skipAlias = isAntigravity && isGemini3; - // For Antigravity Gemini 3 Pro models without explicit tier, append default tier - // Antigravity API: gemini-3-pro requires tier suffix (gemini-3-pro-low/high) + // For Antigravity Gemini 3.1 Pro models without explicit tier, append default tier + // Antigravity API: gemini-3.1-pro requires tier suffix (gemini-3.1-pro-low/high) // gemini-3-flash uses bare name + thinkingLevel param // Pro defaults to -low unless an explicit tier is provided const isGemini3Pro = isGemini3ProModel(modelWithoutQuota); @@ -304,22 +330,23 @@ function budgetToGemini3Level(budget: number): "low" | "medium" | "high" { * * Issue #103: When quota fallback occurs, model names need to be transformed: * - gemini-3-flash-preview (gemini-cli) → gemini-3-flash (antigravity) - * - gemini-3-pro-preview (gemini-cli) → gemini-3-pro-low (antigravity) + * - gemini-3-pro-preview (gemini-cli) → gemini-3.1-pro-low (antigravity) * - gemini-3-flash (antigravity) → gemini-3-flash-preview (gemini-cli) */ export function resolveModelForHeaderStyle( requestedModel: string, headerStyle: "antigravity" | "gemini-cli" ): ResolvedModel { - const lower = requestedModel.toLowerCase(); + const normalizedModel = normalizeLegacyModel(requestedModel); + const lower = normalizedModel.toLowerCase(); const isGemini3 = lower.includes("gemini-3"); if (!isGemini3) { - return resolveModelWithTier(requestedModel); + return resolveModelWithTier(normalizedModel); } if (headerStyle === "antigravity") { - let transformedModel = requestedModel + let transformedModel = normalizedModel .replace(/-preview-customtools$/i, "") .replace(/-preview$/i, "") .replace(/^antigravity-/i, ""); @@ -338,7 +365,7 @@ export function resolveModelForHeaderStyle( } if (headerStyle === "gemini-cli") { - let transformedModel = requestedModel + let transformedModel = normalizedModel .replace(/^antigravity-/i, "") .replace(/-(low|medium|high)$/i, ""); @@ -353,7 +380,7 @@ export function resolveModelForHeaderStyle( }; } - return resolveModelWithTier(requestedModel); + return resolveModelWithTier(normalizedModel); } /** @@ -409,3 +436,4 @@ export function resolveModelWithVariant( configSource: "variant", }; } +