diff --git a/src/cliproxy/config/env-builder.ts b/src/cliproxy/config/env-builder.ts index f90a0691..15f03616 100644 --- a/src/cliproxy/config/env-builder.ts +++ b/src/cliproxy/config/env-builder.ts @@ -60,7 +60,11 @@ function stripCodexEffortSuffix(modelId: string): string { * * Returns true if migration was performed and file was updated. */ -function migrateDeprecatedModelNames(settingsPath: string, settings: ProviderSettings): boolean { +function migrateDeprecatedModelNames( + settingsPath: string, + provider: CLIProxyProvider, + settings: ProviderSettings +): boolean { if (!settings.env || typeof settings.env !== 'object') return false; let migrated = false; @@ -68,13 +72,36 @@ function migrateDeprecatedModelNames(settingsPath: string, settings: ProviderSet const value = settings.env[key]; if (typeof value !== 'string') continue; - // Check if the base model name (before any suffixes) uses the deprecated prefix - if (value.toLowerCase().startsWith(DEPRECATED_MODEL_PREFIX)) { - settings.env[key] = UPSTREAM_MODEL_PREFIX + value.slice(DEPRECATED_MODEL_PREFIX.length); + let canonical = value; + // Check if the base model name (before any suffixes) uses the deprecated prefix. + if (canonical.toLowerCase().startsWith(DEPRECATED_MODEL_PREFIX)) { + canonical = UPSTREAM_MODEL_PREFIX + canonical.slice(DEPRECATED_MODEL_PREFIX.length); + } + canonical = normalizeModelIdForProvider(canonical, provider); + + if (canonical !== value) { + settings.env[key] = canonical; migrated = true; } } + if (provider === 'agy' && Array.isArray(settings.presets)) { + for (const preset of settings.presets) { + if (!preset || typeof preset !== 'object') continue; + const presetRecord = preset as Record; + + for (const key of PRESET_MODEL_KEYS) { + const value = presetRecord[key]; + if (typeof value !== 'string') continue; + const canonical = normalizeModelIdForProvider(value, provider); + if (canonical !== value) { + presetRecord[key] = canonical; + migrated = true; + } + } + } + } + if (migrated) { try { fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', { mode: 0o600 }); @@ -409,7 +436,7 @@ export function getEffectiveEnvVars( if (settings.env && typeof settings.env === 'object') { // Migrate deprecated gemini-claude-* model names if present - migrateDeprecatedModelNames(expandedPath, settings); + migrateDeprecatedModelNames(expandedPath, provider, settings); // Migrate codex effort suffixes to canonical IDs if present migrateCodexEffortSuffixes(expandedPath, provider, settings); // Migrate legacy iFlow placeholders to supported model IDs @@ -444,7 +471,7 @@ export function getEffectiveEnvVars( if (settings.env && typeof settings.env === 'object') { // Migrate deprecated gemini-claude-* model names if present - migrateDeprecatedModelNames(settingsPath, settings); + migrateDeprecatedModelNames(settingsPath, provider, settings); // Migrate codex effort suffixes to canonical IDs if present migrateCodexEffortSuffixes(settingsPath, provider, settings); // Migrate legacy iFlow placeholders to supported model IDs @@ -584,7 +611,7 @@ export function getRemoteEnvVars( const content = fs.readFileSync(expandedPath, 'utf-8'); const settings: ProviderSettings = JSON.parse(content); if (settings.env && typeof settings.env === 'object') { - migrateDeprecatedModelNames(expandedPath, settings); + migrateDeprecatedModelNames(expandedPath, provider, settings); migrateCodexEffortSuffixes(expandedPath, provider, settings); migrateIFlowPlaceholderModel(expandedPath, provider, settings); userEnvVars = settings.env as Record; @@ -604,7 +631,7 @@ export function getRemoteEnvVars( const content = fs.readFileSync(settingsPath, 'utf-8'); const settings: ProviderSettings = JSON.parse(content); if (settings.env && typeof settings.env === 'object') { - migrateDeprecatedModelNames(settingsPath, settings); + migrateDeprecatedModelNames(settingsPath, provider, settings); migrateCodexEffortSuffixes(settingsPath, provider, settings); migrateIFlowPlaceholderModel(settingsPath, provider, settings); userEnvVars = settings.env as Record; diff --git a/src/cliproxy/config/generator.ts b/src/cliproxy/config/generator.ts index 0b88f6f0..89d80979 100644 --- a/src/cliproxy/config/generator.ts +++ b/src/cliproxy/config/generator.ts @@ -32,8 +32,9 @@ export const CCS_CONTROL_PANEL_SECRET = 'ccs'; * v8: Added Gemini 3.1 preview aliases for provider routing compatibility * v9: Added resilient alias compatibility expansion and cache-assisted alias enrichment * v10: Migrated deprecated gemini-claude-* aliases to upstream claude-* aliases + * v11: Migrated deprecated claude-sonnet-4-6-thinking aliases to claude-sonnet-4-6 */ -export const CLIPROXY_CONFIG_VERSION = 10; +export const CLIPROXY_CONFIG_VERSION = 11; interface OAuthModelAliasEntry { name: string; @@ -44,6 +45,8 @@ interface OAuthModelAliasEntry { const GEMINI_MINOR_COMPAT_RANGE = [1, 2, 3, 4, 5, 6, 7, 8, 9] as const; const DEPRECATED_ANTIGRAVITY_ALIAS_PREFIX = 'gemini-claude-'; const UPSTREAM_CLAUDE_ALIAS_PREFIX = 'claude-'; +const DEPRECATED_ANTIGRAVITY_SONNET_46_THINKING_ALIAS = 'claude-sonnet-4-6-thinking'; +const CANONICAL_ANTIGRAVITY_SONNET_46_ALIAS = 'claude-sonnet-4-6'; /** * Default Antigravity oauth-model-alias entries. @@ -58,7 +61,8 @@ const DEFAULT_ANTIGRAVITY_ALIASES: OAuthModelAliasEntry[] = [ { name: 'gemini-3-pro-high', alias: 'gemini-3.1-pro-preview-customtools' }, { name: 'gemini-3-flash', alias: 'gemini-3-flash-preview' }, { name: 'claude-sonnet-4-6', alias: 'claude-sonnet-4-6', fork: true }, - { name: 'claude-sonnet-4-6-thinking', alias: 'claude-sonnet-4-6-thinking', 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 }, { name: 'claude-sonnet-4-5', alias: 'claude-sonnet-4-5', fork: true }, { name: 'claude-sonnet-4-5-thinking', alias: 'claude-sonnet-4-5-thinking', fork: true }, { name: 'claude-opus-4-5-thinking', alias: 'claude-opus-4-5-thinking', fork: true }, @@ -107,10 +111,15 @@ function sanitizeYamlScalar(rawValue: string): string { function normalizeAntigravityAlias(rawAlias: string): string { const normalized = sanitizeYamlScalar(rawAlias); if (normalized.toLowerCase().startsWith(DEPRECATED_ANTIGRAVITY_ALIAS_PREFIX)) { - return ( - UPSTREAM_CLAUDE_ALIAS_PREFIX + normalized.slice(DEPRECATED_ANTIGRAVITY_ALIAS_PREFIX.length) - ); + const migratedPrefix = + UPSTREAM_CLAUDE_ALIAS_PREFIX + normalized.slice(DEPRECATED_ANTIGRAVITY_ALIAS_PREFIX.length); + return normalizeAntigravityAlias(migratedPrefix); } + + if (normalized.toLowerCase() === DEPRECATED_ANTIGRAVITY_SONNET_46_THINKING_ALIAS) { + return CANONICAL_ANTIGRAVITY_SONNET_46_ALIAS; + } + return normalized; } diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index eb6a87f5..4e135c0d 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -104,23 +104,17 @@ export const MODEL_CATALOG: Partial> = }, }, { - id: 'claude-sonnet-4-6-thinking', - name: 'Claude Sonnet 4.6 Thinking', - description: 'Latest Sonnet with extended thinking', + id: 'claude-sonnet-4-6', + name: 'Claude Sonnet 4.6', + description: 'Latest Sonnet with thinking budget support', thinking: { type: 'budget', min: 1024, - max: 128000, + max: 64000, zeroAllowed: true, dynamicAllowed: true, }, }, - { - id: 'claude-sonnet-4-6', - name: 'Claude Sonnet 4.6', - description: 'Latest Sonnet baseline', - thinking: { type: 'none' }, - }, { id: 'claude-sonnet-4-5-thinking', name: 'Claude Sonnet 4.5 Thinking', diff --git a/src/cliproxy/model-id-normalizer.ts b/src/cliproxy/model-id-normalizer.ts index 9e771780..9f83ec3c 100644 --- a/src/cliproxy/model-id-normalizer.ts +++ b/src/cliproxy/model-id-normalizer.ts @@ -20,6 +20,9 @@ type ProviderLike = CLIProxyProvider | string | null | undefined; const CLAUDE_DOTTED_VERSION_REGEX = /claude-(sonnet|opus|haiku)-(\d+)\.(\d+)(?=(?:$|-|\[|\(|\/))/gi; const CLAUDE_DOTTED_THINKING_REGEX = /claude-(sonnet|opus|haiku)-(\d+)\.(\d+)-thinking(?=(?:$|-|\[|\(|\/))/gi; +const DEPRECATED_ANTIGRAVITY_SONNET_46_THINKING_REGEX = + /claude-sonnet-4(?:[.-])6-thinking(?=(?:$|-|\[|\(|\/))/gi; +const CANONICAL_ANTIGRAVITY_SONNET_46_MODEL = 'claude-sonnet-4-6'; /** * Extract provider segment from `/api/provider/{provider}` request paths. @@ -62,6 +65,17 @@ export function normalizeClaudeDottedThinkingMajorMinor(model: string): string { ); } +/** + * Antigravity no longer exposes `claude-sonnet-4-6-thinking`. + * Canonicalize legacy aliases to `claude-sonnet-4-6` while preserving suffixes. + */ +export function normalizeDeprecatedAntigravityModelAliases(model: string): string { + return model.replace( + DEPRECATED_ANTIGRAVITY_SONNET_46_THINKING_REGEX, + CANONICAL_ANTIGRAVITY_SONNET_46_MODEL + ); +} + /** * Normalize model ID for a specific provider. * Antigravity requires hyphenated Claude major.minor model IDs. @@ -76,7 +90,8 @@ export function normalizeClaudeDottedThinkingMajorMinor(model: string): string { */ export function normalizeModelIdForProvider(model: string, provider: ProviderLike): string { if (!isAntigravityProvider(provider)) return model; - return normalizeClaudeDottedMajorMinor(model); + const normalizedDottedVersion = normalizeClaudeDottedMajorMinor(model); + return normalizeDeprecatedAntigravityModelAliases(normalizedDottedVersion); } /** @@ -87,7 +102,7 @@ export function normalizeModelIdForProvider(model: string, provider: ProviderLik * * @example * normalizeModelIdForRouting('claude-sonnet-4.6-thinking', null) - * // => 'claude-sonnet-4-6-thinking' + * // => 'claude-sonnet-4-6' * * @example * normalizeModelIdForRouting('claude-sonnet-4.6', null) @@ -95,9 +110,10 @@ export function normalizeModelIdForProvider(model: string, provider: ProviderLik */ export function normalizeModelIdForRouting(model: string, provider: ProviderLike): string { if (isAntigravityProvider(provider)) { - return normalizeClaudeDottedMajorMinor(model); + return normalizeModelIdForProvider(model, provider); } - return normalizeClaudeDottedThinkingMajorMinor(model); + const normalizedThinking = normalizeClaudeDottedThinkingMajorMinor(model); + return normalizeDeprecatedAntigravityModelAliases(normalizedThinking); } /** @@ -109,7 +125,7 @@ export function normalizeModelIdForRouting(model: string, provider: ProviderLike * { ANTHROPIC_MODEL: 'claude-sonnet-4.6-thinking' }, * 'agy' * ) - * // => { ANTHROPIC_MODEL: 'claude-sonnet-4-6-thinking' } + * // => { ANTHROPIC_MODEL: 'claude-sonnet-4-6' } */ export function normalizeModelEnvVarsForProvider( envVars: NodeJS.ProcessEnv, diff --git a/tests/unit/cliproxy/config-generator.test.js b/tests/unit/cliproxy/config-generator.test.js index d32269f1..84ebe19f 100644 --- a/tests/unit/cliproxy/config-generator.test.js +++ b/tests/unit/cliproxy/config-generator.test.js @@ -792,8 +792,8 @@ oauth-model-alias: const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8'); assert( - newConfig.includes('alias: claude-sonnet-4-6-thinking'), - 'Should include normalized upstream Claude alias' + newConfig.includes('alias: claude-sonnet-4-6'), + 'Should migrate deprecated sonnet thinking alias to canonical model ID' ); assert( !newConfig.includes('alias: gemini-claude-sonnet-4-6-thinking'), diff --git a/tests/unit/cliproxy/env-builder-provider-url.test.ts b/tests/unit/cliproxy/env-builder-provider-url.test.ts index 9c6f9002..b7bfd595 100644 --- a/tests/unit/cliproxy/env-builder-provider-url.test.ts +++ b/tests/unit/cliproxy/env-builder-provider-url.test.ts @@ -105,10 +105,17 @@ describe('getEffectiveEnvVars local provider URL normalization', () => { }); const env = getEffectiveEnvVars('agy', 8317, settingsPath); - expect(env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6-thinking'); + expect(env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6'); expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking'); - expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6-thinking'); + expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6'); expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5'); + + const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as { + env: Record; + }; + expect(persisted.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6'); + expect(persisted.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking'); + expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6'); }); it('migrates codex preset model mappings to canonical IDs', () => { diff --git a/tests/unit/cliproxy/model-catalog.test.js b/tests/unit/cliproxy/model-catalog.test.js index 3cd6d7e8..73c0e74e 100644 --- a/tests/unit/cliproxy/model-catalog.test.js +++ b/tests/unit/cliproxy/model-catalog.test.js @@ -85,15 +85,6 @@ describe('Model Catalog', () => { assert.strictEqual(sonnetThinking.name, 'Claude Sonnet 4.5 Thinking'); }); - it('includes Claude Sonnet 4.6 Thinking', () => { - const { MODEL_CATALOG } = modelCatalog; - const sonnetThinking = MODEL_CATALOG.agy.models.find( - (m) => m.id === 'claude-sonnet-4-6-thinking' - ); - assert(sonnetThinking, 'Should include Claude Sonnet 4.6 Thinking'); - assert.strictEqual(sonnetThinking.name, 'Claude Sonnet 4.6 Thinking'); - }); - it('includes Claude Sonnet 4.6', () => { const { MODEL_CATALOG } = modelCatalog; const sonnet = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-sonnet-4-6'); @@ -117,9 +108,9 @@ describe('Model Catalog', () => { assert.strictEqual(gem3.tier, undefined, 'AGY models should not have paid tier'); }); - it('has 7 models total', () => { + it('has 6 models total', () => { const { MODEL_CATALOG } = modelCatalog; - assert.strictEqual(MODEL_CATALOG.agy.models.length, 7); + assert.strictEqual(MODEL_CATALOG.agy.models.length, 6); }); }); diff --git a/tests/unit/cliproxy/model-id-normalizer.test.ts b/tests/unit/cliproxy/model-id-normalizer.test.ts index d13bca6b..af025fa5 100644 --- a/tests/unit/cliproxy/model-id-normalizer.test.ts +++ b/tests/unit/cliproxy/model-id-normalizer.test.ts @@ -44,13 +44,16 @@ describe('model-id-normalizer', () => { it('applies provider-aware routing normalization', () => { expect(normalizeModelIdForRouting('claude-sonnet-4.6-thinking', null)).toBe( - 'claude-sonnet-4-6-thinking' + 'claude-sonnet-4-6' ); expect(normalizeModelIdForRouting('claude-sonnet-4.6', null)).toBe('claude-sonnet-4.6'); expect(normalizeModelIdForRouting('claude-sonnet-4.6', 'agy')).toBe('claude-sonnet-4-6'); }); it('applies provider-only normalization for antigravity', () => { + expect(normalizeModelIdForProvider('claude-sonnet-4.6-thinking', 'agy')).toBe( + 'claude-sonnet-4-6' + ); expect(normalizeModelIdForProvider('claude-opus-4.6-thinking', 'agy')).toBe( 'claude-opus-4-6-thinking' ); @@ -71,7 +74,7 @@ describe('model-id-normalizer', () => { }; const normalized = normalizeModelEnvVarsForProvider(input, 'agy'); - expect(normalized.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6-thinking'); + expect(normalized.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6'); expect(normalized.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking'); expect(normalized.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6'); expect(normalized.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5'); diff --git a/tests/unit/cliproxy/tool-sanitization-proxy-integration.test.ts b/tests/unit/cliproxy/tool-sanitization-proxy-integration.test.ts index dd7dd2ac..32c591cb 100644 --- a/tests/unit/cliproxy/tool-sanitization-proxy-integration.test.ts +++ b/tests/unit/cliproxy/tool-sanitization-proxy-integration.test.ts @@ -201,9 +201,7 @@ describe('ToolSanitizationProxy Integration', () => { }); expect(lastRequest).not.toBeNull(); - expect((lastRequest!.body as Record).model).toBe( - 'claude-sonnet-4-6-thinking' - ); + expect((lastRequest!.body as Record).model).toBe('claude-sonnet-4-6'); } finally { proxy.stop(); } diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts index 0a8e98f3..0927ece4 100644 --- a/ui/src/lib/model-catalogs.ts +++ b/ui/src/lib/model-catalogs.ts @@ -22,7 +22,7 @@ export const MODEL_CATALOGS: Record = { presetMapping: { default: 'claude-opus-4-6-thinking', opus: 'claude-opus-4-6-thinking', - sonnet: 'claude-sonnet-4-6-thinking', + sonnet: 'claude-sonnet-4-6', haiku: 'claude-sonnet-4-6', }, }, @@ -33,25 +33,14 @@ export const MODEL_CATALOGS: Record = { presetMapping: { default: 'claude-opus-4-5-thinking', opus: 'claude-opus-4-5-thinking', - sonnet: 'claude-sonnet-4-6-thinking', - haiku: 'claude-sonnet-4-6', - }, - }, - { - id: 'claude-sonnet-4-6-thinking', - name: 'Claude Sonnet 4.6 Thinking', - description: 'Latest Sonnet with extended thinking', - presetMapping: { - default: 'claude-sonnet-4-6-thinking', - opus: 'claude-opus-4-6-thinking', - sonnet: 'claude-sonnet-4-6-thinking', + sonnet: 'claude-sonnet-4-6', haiku: 'claude-sonnet-4-6', }, }, { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', - description: 'Latest Sonnet baseline', + description: 'Latest Sonnet with thinking budget support', presetMapping: { default: 'claude-sonnet-4-6', opus: 'claude-opus-4-6-thinking',