mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
fix(cliproxy): align gemini 3.1 preset compatibility
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }];
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user