fix(cliproxy): align gemini 3.1 preset compatibility

This commit is contained in:
Tam Nhu Tran
2026-03-27 16:39:40 -04:00
parent fcc5fad601
commit 5ac91e0cac
10 changed files with 125 additions and 42 deletions
+3 -1
View File
@@ -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 },
+1 -1
View File
@@ -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}`);
}
+25 -7
View File
@@ -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<Record<CLIProxyProvider, ProviderCatalog>> =
},
},
{
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<Record<CLIProxyProvider, ProviderCatalog>> =
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()));
}
+7 -2
View File
@@ -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'],
},
};
+4
View File
@@ -718,6 +718,10 @@ const MODEL_PRICING_ALIASES: Record<string, string> = {
'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
@@ -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) {
+18 -9
View File
@@ -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');
@@ -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 }];
+35 -21
View File
@@ -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<string, ProviderCatalog> = {
agy: {
@@ -39,26 +46,26 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
},
},
{
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<string, ProviderCatalog> = {
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 {
+13 -1
View File
@@ -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'
);
});
});