diff --git a/src/cliproxy/config/generator.ts b/src/cliproxy/config/generator.ts index dd4e65a7..61b565a5 100644 --- a/src/cliproxy/config/generator.ts +++ b/src/cliproxy/config/generator.ts @@ -35,8 +35,9 @@ export const CCS_CONTROL_PANEL_SECRET = 'ccs'; * v11: Migrated deprecated claude-sonnet-4-6-thinking aliases to claude-sonnet-4-6 * v12: Removed denylisted Antigravity Claude 4.5 aliases * v13: Removed aggressive Gemini alias expansion to reduce model list noise in Control Panel + * v14: Added Gemini 3.1 Flash Antigravity aliases for upcoming rollout compatibility */ -export const CLIPROXY_CONFIG_VERSION = 13; +export const CLIPROXY_CONFIG_VERSION = 14; interface OAuthModelAliasEntry { name: string; @@ -61,6 +62,7 @@ const DEFAULT_ANTIGRAVITY_ALIASES: OAuthModelAliasEntry[] = [ { name: 'gemini-3-pro-high', alias: 'gemini-3.1-pro-preview' }, { name: 'gemini-3-pro-high', alias: 'gemini-3.1-pro-preview-customtools' }, { name: 'gemini-3-flash', alias: 'gemini-3-flash-preview' }, + { name: 'gemini-3-flash', alias: 'gemini-3.1-flash-preview' }, { name: 'claude-sonnet-4-6', alias: 'claude-sonnet-4-6', fork: true }, // Backward compatibility: legacy sonnet thinking alias now routes to canonical model ID. { name: 'claude-sonnet-4-6-thinking', alias: 'claude-sonnet-4-6', fork: true }, diff --git a/src/cliproxy/executor/index.ts b/src/cliproxy/executor/index.ts index ec772b15..f6028662 100644 --- a/src/cliproxy/executor/index.ts +++ b/src/cliproxy/executor/index.ts @@ -716,7 +716,7 @@ export async function execClaudeWithCLIProxy( const issueUrl = getModelIssueUrl(provider, currentModel); console.error(''); console.error(warn(`${modelEntry?.name || currentModel} has known issues with Claude Code`)); - console.error(' Tool calls will fail. Use "gemini-3-pro-preview" instead.'); + console.error(' Tool calls will fail. Use "gemini-3.1-pro-preview" instead.'); if (issueUrl) { console.error(` Tracking: ${issueUrl}`); } diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index a9d7970e..729c8a86 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -11,6 +11,14 @@ import { migrateDeniedAntigravityModelAliases, normalizeModelIdForProvider, } from './model-id-normalizer'; +import { stripModelConfigurationSuffixes } from '../shared/extended-context-utils'; + +const GEMINI_MINOR_VERSION_COMPATIBILITY_IDS = Object.freeze({ + 'gemini-3-pro-preview': 'gemini-3.1-pro-preview', + 'gemini-3.1-pro-preview': 'gemini-3-pro-preview', + 'gemini-3-flash-preview': 'gemini-3.1-flash-preview', + 'gemini-3.1-flash-preview': 'gemini-3-flash-preview', +}); /** * Thinking support configuration for a model. @@ -108,9 +116,9 @@ export const MODEL_CATALOG: Partial> = }, }, { - id: 'gemini-3-pro-preview', - name: 'Gemini 3 Pro', - description: 'Google latest model via Antigravity', + id: 'gemini-3.1-pro-preview', + name: 'Gemini 3.1 Pro', + description: 'Google latest Gemini Pro model via Antigravity', thinking: { type: 'levels', levels: ['low', 'high'], dynamicAllowed: true }, extendedContext: true, }, @@ -122,10 +130,10 @@ export const MODEL_CATALOG: Partial> = defaultModel: 'gemini-2.5-pro', models: [ { - id: 'gemini-3-pro-preview', - name: 'Gemini 3 Pro', + id: 'gemini-3.1-pro-preview', + name: 'Gemini 3.1 Pro', tier: 'pro', - description: 'Latest model, requires paid Google account', + description: 'Latest Gemini Pro model, requires paid Google account', thinking: { type: 'levels', levels: ['low', 'high'], dynamicAllowed: true }, extendedContext: true, }, @@ -390,7 +398,7 @@ export function getProviderCatalog(provider: CLIProxyProvider): ProviderCatalog export function findModel(provider: CLIProxyProvider, modelId: string): ModelEntry | undefined { const catalog = MODEL_CATALOG[provider]; if (!catalog || !modelId) return undefined; - const normalizedId = modelId.trim().toLowerCase(); + const normalizedId = stripModelConfigurationSuffixes(modelId).trim().toLowerCase(); const providerNormalizedId = normalizeModelIdForProvider(normalizedId, provider) .trim() .toLowerCase(); @@ -404,6 +412,16 @@ export function findModel(provider: CLIProxyProvider, modelId: string): ModelEnt lookupCandidates.add(migratedProvider); } + for (const candidate of [...lookupCandidates]) { + const compatibilityId = + GEMINI_MINOR_VERSION_COMPATIBILITY_IDS[ + candidate as keyof typeof GEMINI_MINOR_VERSION_COMPATIBILITY_IDS + ]; + if (compatibilityId) { + lookupCandidates.add(compatibilityId); + } + } + return catalog.models.find((m) => lookupCandidates.has(m.id.toLowerCase())); } diff --git a/src/cliproxy/quota-fetcher-gemini-cli.ts b/src/cliproxy/quota-fetcher-gemini-cli.ts index f8364a91..c8034ebc 100644 --- a/src/cliproxy/quota-fetcher-gemini-cli.ts +++ b/src/cliproxy/quota-fetcher-gemini-cli.ts @@ -32,11 +32,16 @@ const GEMINI_CLI_GROUPS: Record< > = { 'gemini-flash-series': { label: 'Gemini Flash Series', - models: ['gemini-3-flash-preview', 'gemini-2.5-flash', 'gemini-2.5-flash-lite'], + models: [ + 'gemini-3-flash-preview', + 'gemini-3.1-flash-preview', + 'gemini-2.5-flash', + 'gemini-2.5-flash-lite', + ], }, 'gemini-pro-series': { label: 'Gemini Pro Series', - models: ['gemini-3-pro-preview', 'gemini-2.5-pro'], + models: ['gemini-3-pro-preview', 'gemini-3.1-pro-preview', 'gemini-2.5-pro'], }, }; diff --git a/src/web-server/model-pricing.ts b/src/web-server/model-pricing.ts index 935ecccf..9cf381e7 100644 --- a/src/web-server/model-pricing.ts +++ b/src/web-server/model-pricing.ts @@ -718,6 +718,10 @@ const MODEL_PRICING_ALIASES: Record = { 'qwen3-235b': 'qwen3-max', 'qwen3-vl-plus': 'qwen3.5-plus', 'qwen3-32b': 'qwen3.5-plus', + 'gemini-3.1-pro-preview': 'gemini-3-pro-preview', + 'gemini-3.1-pro-preview-customtools': 'gemini-3-pro-preview', + 'gemini-3-1-pro-preview': 'gemini-3-pro-preview', + 'gemini-3-1-pro-preview-customtools': 'gemini-3-pro-preview', }; // Default pricing for unknown models diff --git a/tests/unit/cliproxy/config-generator.test.js b/tests/unit/cliproxy/config-generator.test.js index 8830bff3..4445c807 100644 --- a/tests/unit/cliproxy/config-generator.test.js +++ b/tests/unit/cliproxy/config-generator.test.js @@ -640,6 +640,8 @@ auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth" const gemini31AliasLines = [ 'alias: gemini-3.1-pro-preview', 'alias: gemini-3.1-pro-preview-customtools', + 'alias: gemini-3.1-flash-preview', + 'alias: gemini-3.1-flash-preview-customtools', ]; for (const aliasLine of gemini31AliasLines) { diff --git a/tests/unit/cliproxy/model-catalog.test.js b/tests/unit/cliproxy/model-catalog.test.js index 0b2b47f2..748916dd 100644 --- a/tests/unit/cliproxy/model-catalog.test.js +++ b/tests/unit/cliproxy/model-catalog.test.js @@ -91,11 +91,11 @@ describe('Model Catalog', () => { assert.strictEqual(ids.includes('claude-sonnet-4-5'), false); }); - it('includes Gemini 3 Pro (free via Antigravity)', () => { + it('includes Gemini 3.1 Pro (free via Antigravity)', () => { const { MODEL_CATALOG } = modelCatalog; - const gem3 = MODEL_CATALOG.agy.models.find((m) => m.id === 'gemini-3-pro-preview'); - assert(gem3, 'Should include Gemini 3 Pro'); - assert.strictEqual(gem3.name, 'Gemini 3 Pro'); + const gem3 = MODEL_CATALOG.agy.models.find((m) => m.id === 'gemini-3.1-pro-preview'); + assert(gem3, 'Should include Gemini 3.1 Pro'); + assert.strictEqual(gem3.name, 'Gemini 3.1 Pro'); // AGY models are all free - no paid tier assert.strictEqual(gem3.tier, undefined, 'AGY models should not have paid tier'); }); @@ -139,11 +139,11 @@ describe('Model Catalog', () => { assert.strictEqual(MODEL_CATALOG.gemini.defaultModel, 'gemini-2.5-pro'); }); - it('includes Gemini 3 Pro with pro tier', () => { + it('includes Gemini 3.1 Pro with pro tier', () => { const { MODEL_CATALOG } = modelCatalog; - const gem3 = MODEL_CATALOG.gemini.models.find((m) => m.id === 'gemini-3-pro-preview'); - assert(gem3, 'Should include Gemini 3 Pro'); - assert.strictEqual(gem3.name, 'Gemini 3 Pro'); + const gem3 = MODEL_CATALOG.gemini.models.find((m) => m.id === 'gemini-3.1-pro-preview'); + assert(gem3, 'Should include Gemini 3.1 Pro'); + assert.strictEqual(gem3.name, 'Gemini 3.1 Pro'); assert.strictEqual(gem3.tier, 'pro'); }); @@ -246,6 +246,15 @@ describe('Model Catalog', () => { assert.strictEqual(legacySonnet?.id, 'claude-sonnet-4-6'); }); + it('treats Gemini 3 and 3.1 preview IDs as the same catalog family', () => { + const { findModel } = modelCatalog; + const legacyGemini = findModel('gemini', 'gemini-3-pro-preview'); + const currentGemini = findModel('gemini', 'gemini-3.1-pro-preview'); + + assert.strictEqual(legacyGemini?.id, 'gemini-3.1-pro-preview'); + assert.strictEqual(currentGemini?.id, 'gemini-3.1-pro-preview'); + }); + it('returns undefined for unknown model', () => { const { findModel } = modelCatalog; const model = findModel('agy', 'unknown-model'); @@ -325,7 +334,7 @@ describe('Model Catalog', () => { const sonnetThinkingIdx = models.findIndex((m) => m.id === 'claude-sonnet-4-6'); // Find indices of the remaining non-Claude model - const geminiIdx = models.findIndex((m) => m.id === 'gemini-3-pro-preview'); + const geminiIdx = models.findIndex((m) => m.id === 'gemini-3.1-pro-preview'); // Primary Claude choices should appear ahead of Gemini fallback. assert(opusIdx < geminiIdx, 'Opus should be above Gemini'); diff --git a/tests/unit/cliproxy/quota-fetcher-gemini-cli.test.ts b/tests/unit/cliproxy/quota-fetcher-gemini-cli.test.ts index e8cb4e46..66262b3c 100644 --- a/tests/unit/cliproxy/quota-fetcher-gemini-cli.test.ts +++ b/tests/unit/cliproxy/quota-fetcher-gemini-cli.test.ts @@ -168,6 +168,23 @@ describe('Gemini CLI Quota Fetcher', () => { expect(proBucket!.remainingFraction).toBe(0.9); }); + it('should recognize Gemini 3.1 preview IDs during the rollout', () => { + const rawBuckets = [ + { model_id: 'gemini-3.1-flash-preview', remaining_fraction: 0.7 }, + { model_id: 'gemini-3.1-pro-preview', remaining_fraction: 0.4 }, + ]; + + const buckets = buildGeminiCliBuckets(rawBuckets); + + const flashBucket = buckets.find((b) => b.label === 'Gemini Flash Series'); + const proBucket = buckets.find((b) => b.label === 'Gemini Pro Series'); + + expect(flashBucket).toBeDefined(); + expect(flashBucket!.modelIds).toContain('gemini-3.1-flash-preview'); + expect(proBucket).toBeDefined(); + expect(proBucket!.modelIds).toContain('gemini-3.1-pro-preview'); + }); + it('should handle camelCase API response', () => { const rawBuckets = [{ modelId: 'gemini-3-flash-preview', remainingFraction: 0.75 }]; diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts index 8d9f64a2..317950b2 100644 --- a/ui/src/lib/model-catalogs.ts +++ b/ui/src/lib/model-catalogs.ts @@ -6,6 +6,13 @@ import type { ProviderCatalog } from '@/components/cliproxy/provider-model-selector'; import { stripModelConfigurationSuffixes } from '@/lib/extended-context-utils'; +const GEMINI_MINOR_VERSION_COMPATIBILITY_IDS = Object.freeze({ + 'gemini-3-pro-preview': 'gemini-3.1-pro-preview', + 'gemini-3.1-pro-preview': 'gemini-3-pro-preview', + 'gemini-3-flash-preview': 'gemini-3.1-flash-preview', + 'gemini-3.1-flash-preview': 'gemini-3-flash-preview', +}); + /** Model catalog data - mirrors src/cliproxy/model-catalog.ts */ export const MODEL_CATALOGS: Record = { agy: { @@ -39,26 +46,26 @@ export const MODEL_CATALOGS: Record = { }, }, { - id: 'gemini-3-pro-preview', - name: 'Gemini 3 Pro', - description: 'Google latest model via Antigravity', + id: 'gemini-3.1-pro-preview', + name: 'Gemini 3.1 Pro', + description: 'Google latest Gemini Pro model via Antigravity', extendedContext: true, presetMapping: { - default: 'gemini-3-pro-preview', - opus: 'gemini-3-pro-preview', - sonnet: 'gemini-3-pro-preview', + default: 'gemini-3.1-pro-preview', + opus: 'gemini-3.1-pro-preview', + sonnet: 'gemini-3.1-pro-preview', haiku: 'gemini-3-flash-preview', }, }, { id: 'gemini-3-flash-preview', - name: 'Gemini 3 Flash', - description: 'Fast Gemini model via Antigravity', + name: 'Gemini Flash', + description: 'Fast Gemini model via Antigravity with 3/3.1 Flash rollout compatibility', extendedContext: true, presetMapping: { default: 'gemini-3-flash-preview', - opus: 'gemini-3-pro-preview', - sonnet: 'gemini-3-pro-preview', + opus: 'gemini-3.1-pro-preview', + sonnet: 'gemini-3.1-pro-preview', haiku: 'gemini-3-flash-preview', }, }, @@ -70,28 +77,28 @@ export const MODEL_CATALOGS: Record = { defaultModel: 'gemini-2.5-pro', models: [ { - id: 'gemini-3-pro-preview', - name: 'Gemini 3 Pro', + id: 'gemini-3.1-pro-preview', + name: 'Gemini 3.1 Pro', tier: 'paid', - description: 'Latest model, requires paid Google account', + description: 'Latest Gemini Pro model, requires paid Google account', extendedContext: true, presetMapping: { - default: 'gemini-3-pro-preview', - opus: 'gemini-3-pro-preview', - sonnet: 'gemini-3-pro-preview', + default: 'gemini-3.1-pro-preview', + opus: 'gemini-3.1-pro-preview', + sonnet: 'gemini-3.1-pro-preview', haiku: 'gemini-3-flash-preview', }, }, { id: 'gemini-3-flash-preview', - name: 'Gemini 3 Flash', + name: 'Gemini Flash', tier: 'paid', - description: 'Fast Gemini 3 model, requires paid Google account', + description: 'Fast Gemini model, requires paid Google account and tracks 3/3.1 Flash IDs', extendedContext: true, presetMapping: { default: 'gemini-3-flash-preview', - opus: 'gemini-3-pro-preview', - sonnet: 'gemini-3-pro-preview', + opus: 'gemini-3.1-pro-preview', + sonnet: 'gemini-3.1-pro-preview', haiku: 'gemini-3-flash-preview', }, }, @@ -560,7 +567,14 @@ export function findCatalogModel(provider: string, modelId: string) { if (!catalog) return undefined; const normalizedModelId = stripModelConfigurationSuffixes(modelId); - return catalog.models.find((model) => model.id === normalizedModelId); + const compatibilityModelId = + GEMINI_MINOR_VERSION_COMPATIBILITY_IDS[ + normalizedModelId.toLowerCase() as keyof typeof GEMINI_MINOR_VERSION_COMPATIBILITY_IDS + ]; + + return catalog.models.find( + (model) => model.id === normalizedModelId || model.id === compatibilityModelId + ); } export function supportsExtendedContext(provider: string, modelId: string): boolean { diff --git a/ui/tests/unit/ui/lib/preset-utils.test.ts b/ui/tests/unit/ui/lib/preset-utils.test.ts index 460c1e1f..ce3c189b 100644 --- a/ui/tests/unit/ui/lib/preset-utils.test.ts +++ b/ui/tests/unit/ui/lib/preset-utils.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { MODEL_CATALOGS } from '@/lib/model-catalogs'; +import { MODEL_CATALOGS, findCatalogModel } from '@/lib/model-catalogs'; import { applyDefaultPreset } from '@/lib/preset-utils'; describe('claude preset utils', () => { @@ -41,4 +41,16 @@ describe('claude preset utils', () => { ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001', }); }); + + it('keeps Gemini presets on 3.1 Pro while resolving 3/3.1 alias variants', () => { + const geminiCatalog = MODEL_CATALOGS.gemini; + const latestPro = geminiCatalog.models.find((model) => model.id === 'gemini-3.1-pro-preview'); + + expect(latestPro?.name).toBe('Gemini 3.1 Pro'); + expect(latestPro?.presetMapping?.default).toBe('gemini-3.1-pro-preview'); + expect(findCatalogModel('gemini', 'gemini-3-pro-preview')?.id).toBe('gemini-3.1-pro-preview'); + expect(findCatalogModel('gemini', 'gemini-3.1-flash-preview')?.id).toBe( + 'gemini-3-flash-preview' + ); + }); });