diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index 4e135c0d..871cf760 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -91,18 +91,6 @@ export const MODEL_CATALOG: Partial> = // extendedContext: true, extendedContext: false, }, - { - id: 'claude-opus-4-5-thinking', - name: 'Claude Opus 4.5 Thinking', - description: 'Previous flagship, extended thinking', - thinking: { - type: 'budget', - min: 1024, - max: 100000, - zeroAllowed: false, - dynamicAllowed: true, - }, - }, { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', @@ -115,24 +103,6 @@ export const MODEL_CATALOG: Partial> = dynamicAllowed: true, }, }, - { - id: 'claude-sonnet-4-5-thinking', - name: 'Claude Sonnet 4.5 Thinking', - description: 'Balanced with extended thinking', - thinking: { - type: 'budget', - min: 1024, - max: 100000, - zeroAllowed: false, - dynamicAllowed: true, - }, - }, - { - id: 'claude-sonnet-4-5', - name: 'Claude Sonnet 4.5', - description: 'Fast and capable', - thinking: { type: 'none' }, - }, { id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro', diff --git a/src/cliproxy/model-id-normalizer.ts b/src/cliproxy/model-id-normalizer.ts index 23b587f0..4c94d105 100644 --- a/src/cliproxy/model-id-normalizer.ts +++ b/src/cliproxy/model-id-normalizer.ts @@ -23,6 +23,12 @@ const CLAUDE_DOTTED_THINKING_REGEX = const DEPRECATED_ANTIGRAVITY_SONNET_46_THINKING_REGEX = /claude-sonnet-4(?:[.-])6-thinking(?=(?:$|-|\[|\(|\/))/gi; const CANONICAL_ANTIGRAVITY_SONNET_46_MODEL = 'claude-sonnet-4-6'; +const DEPRECATED_ANTIGRAVITY_OPUS_45_THINKING_REGEX = + /claude-opus-4(?:[.-])5-thinking(?=(?:$|-|\[|\(|\/))/gi; +const CANONICAL_ANTIGRAVITY_OPUS_46_THINKING_MODEL = 'claude-opus-4-6-thinking'; +const DEPRECATED_ANTIGRAVITY_SONNET_45_THINKING_REGEX = + /claude-sonnet-4(?:[.-])5-thinking(?=(?:$|-|\[|\(|\/))/gi; +const DEPRECATED_ANTIGRAVITY_SONNET_45_REGEX = /claude-sonnet-4(?:[.-])5(?=(?:$|-|\[|\(|\/))/gi; const CODEX_EFFORT_SUFFIX_REGEX = /-(xhigh|high|medium)$/i; function trimModelId(model: string): string { @@ -86,10 +92,17 @@ export function normalizeClaudeDottedThinkingMajorMinor(model: string): string { * 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 - ); + return model + .replace( + DEPRECATED_ANTIGRAVITY_OPUS_45_THINKING_REGEX, + CANONICAL_ANTIGRAVITY_OPUS_46_THINKING_MODEL + ) + .replace(DEPRECATED_ANTIGRAVITY_SONNET_45_THINKING_REGEX, CANONICAL_ANTIGRAVITY_SONNET_46_MODEL) + .replace(DEPRECATED_ANTIGRAVITY_SONNET_45_REGEX, CANONICAL_ANTIGRAVITY_SONNET_46_MODEL) + .replace( + DEPRECATED_ANTIGRAVITY_SONNET_46_THINKING_REGEX, + CANONICAL_ANTIGRAVITY_SONNET_46_MODEL + ); } /** diff --git a/tests/unit/cliproxy/model-catalog-compat.test.ts b/tests/unit/cliproxy/model-catalog-compat.test.ts index 40154419..af51012f 100644 --- a/tests/unit/cliproxy/model-catalog-compat.test.ts +++ b/tests/unit/cliproxy/model-catalog-compat.test.ts @@ -3,16 +3,28 @@ import { findModel, supportsThinking } from '../../../src/cliproxy/model-catalog describe('model-catalog compatibility lookups', () => { it('finds agy Claude models using dotted major.minor IDs', () => { - const dottedThinking = findModel('agy', 'claude-sonnet-4.5-thinking'); - const dottedNonThinking = findModel('agy', 'claude-sonnet-4.5'); + const dottedThinking = findModel('agy', 'claude-opus-4.6-thinking'); + const dottedNonThinking = findModel('agy', 'claude-sonnet-4.6'); - expect(dottedThinking?.id).toBe('claude-sonnet-4-5-thinking'); - expect(dottedNonThinking?.id).toBe('claude-sonnet-4-5'); + expect(dottedThinking?.id).toBe('claude-opus-4-6-thinking'); + expect(dottedNonThinking?.id).toBe('claude-sonnet-4-6'); + }); + + it('maps legacy agy 4.5 IDs to canonical 4.6 catalog models', () => { + const legacyOpusThinking = findModel('agy', 'claude-opus-4.5-thinking'); + const legacySonnetThinking = findModel('agy', 'claude-sonnet-4.5-thinking'); + const legacySonnet = findModel('agy', 'claude-sonnet-4.5'); + + expect(legacyOpusThinking?.id).toBe('claude-opus-4-6-thinking'); + expect(legacySonnetThinking?.id).toBe('claude-sonnet-4-6'); + expect(legacySonnet?.id).toBe('claude-sonnet-4-6'); }); it('supports thinking checks for dotted agy model IDs', () => { expect(supportsThinking('agy', 'claude-opus-4.6-thinking')).toBe(true); - expect(supportsThinking('agy', 'claude-sonnet-4.5')).toBe(false); + expect(supportsThinking('agy', 'claude-sonnet-4.6')).toBe(true); + expect(supportsThinking('agy', 'claude-opus-4.5-thinking')).toBe(true); + expect(supportsThinking('agy', 'claude-sonnet-4.5')).toBe(true); }); it('maps legacy sonnet 4.6 thinking aliases to canonical agy model', () => { diff --git a/tests/unit/cliproxy/model-catalog.test.js b/tests/unit/cliproxy/model-catalog.test.js index 73c0e74e..87acb22a 100644 --- a/tests/unit/cliproxy/model-catalog.test.js +++ b/tests/unit/cliproxy/model-catalog.test.js @@ -69,20 +69,11 @@ describe('Model Catalog', () => { assert.strictEqual(MODEL_CATALOG.agy.defaultModel, 'claude-opus-4-6-thinking'); }); - it('includes Claude Opus 4.5 Thinking', () => { + it('includes Claude Opus 4.6 Thinking', () => { const { MODEL_CATALOG } = modelCatalog; - const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-5-thinking'); - assert(opus, 'Should include Claude Opus 4.5 Thinking'); - assert.strictEqual(opus.name, 'Claude Opus 4.5 Thinking'); - }); - - it('includes Claude Sonnet 4.5 Thinking', () => { - const { MODEL_CATALOG } = modelCatalog; - const sonnetThinking = MODEL_CATALOG.agy.models.find( - (m) => m.id === 'claude-sonnet-4-5-thinking' - ); - assert(sonnetThinking, 'Should include Claude Sonnet 4.5 Thinking'); - assert.strictEqual(sonnetThinking.name, 'Claude Sonnet 4.5 Thinking'); + const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-6-thinking'); + assert(opus, 'Should include Claude Opus 4.6 Thinking'); + assert.strictEqual(opus.name, 'Claude Opus 4.6 Thinking'); }); it('includes Claude Sonnet 4.6', () => { @@ -92,11 +83,12 @@ describe('Model Catalog', () => { assert.strictEqual(sonnet.name, 'Claude Sonnet 4.6'); }); - it('includes Claude Sonnet 4.5', () => { + it('does not include deprecated Claude 4.5 AGY models', () => { const { MODEL_CATALOG } = modelCatalog; - const sonnet = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-sonnet-4-5'); - assert(sonnet, 'Should include Claude Sonnet 4.5'); - assert.strictEqual(sonnet.name, 'Claude Sonnet 4.5'); + const ids = MODEL_CATALOG.agy.models.map((m) => m.id); + assert.strictEqual(ids.includes('claude-opus-4-5-thinking'), false); + assert.strictEqual(ids.includes('claude-sonnet-4-5-thinking'), false); + assert.strictEqual(ids.includes('claude-sonnet-4-5'), false); }); it('includes Gemini 3 Pro (free via Antigravity)', () => { @@ -108,9 +100,9 @@ describe('Model Catalog', () => { assert.strictEqual(gem3.tier, undefined, 'AGY models should not have paid tier'); }); - it('has 6 models total', () => { + it('has 3 models total', () => { const { MODEL_CATALOG } = modelCatalog; - assert.strictEqual(MODEL_CATALOG.agy.models.length, 6); + assert.strictEqual(MODEL_CATALOG.agy.models.length, 3); }); }); @@ -203,11 +195,11 @@ describe('Model Catalog', () => { }); describe('findModel', () => { - it('finds Claude Opus 4.5 Thinking in agy', () => { + it('finds Claude Opus 4.6 Thinking in agy', () => { const { findModel } = modelCatalog; - const model = findModel('agy', 'claude-opus-4-5-thinking'); + const model = findModel('agy', 'claude-opus-4-6-thinking'); assert(model, 'Should find model'); - assert.strictEqual(model.name, 'Claude Opus 4.5 Thinking'); + assert.strictEqual(model.name, 'Claude Opus 4.6 Thinking'); }); it('finds Gemini 2.5 Pro in gemini', () => { @@ -217,6 +209,16 @@ describe('Model Catalog', () => { assert.strictEqual(model.name, 'Gemini 2.5 Pro'); }); + it('maps legacy AGY 4.5 IDs to supported 4.6 models', () => { + const { findModel } = modelCatalog; + const legacyOpus = findModel('agy', 'claude-opus-4-5-thinking'); + const legacySonnetThinking = findModel('agy', 'claude-sonnet-4-5-thinking'); + const legacySonnet = findModel('agy', 'claude-sonnet-4-5'); + assert.strictEqual(legacyOpus?.id, 'claude-opus-4-6-thinking'); + assert.strictEqual(legacySonnetThinking?.id, 'claude-sonnet-4-6'); + assert.strictEqual(legacySonnet?.id, 'claude-sonnet-4-6'); + }); + it('returns undefined for unknown model', () => { const { findModel } = modelCatalog; const model = findModel('agy', 'unknown-model'); @@ -269,19 +271,17 @@ describe('Model Catalog', () => { }); describe('Thinking models ordering', () => { - it('Claude Opus 4.5 Thinking is not deprecated', () => { + it('Claude Opus 4.6 Thinking is not deprecated', () => { const { MODEL_CATALOG } = modelCatalog; - const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-5-thinking'); - assert(opus, 'Should include Claude Opus 4.5 Thinking'); + const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-6-thinking'); + assert(opus, 'Should include Claude Opus 4.6 Thinking'); assert.strictEqual(opus.deprecated, undefined, 'Should not be marked as deprecated'); }); - it('Claude Sonnet 4.5 Thinking is not deprecated', () => { + it('Claude Sonnet 4.6 is not deprecated', () => { const { MODEL_CATALOG } = modelCatalog; - const sonnetThinking = MODEL_CATALOG.agy.models.find( - (m) => m.id === 'claude-sonnet-4-5-thinking' - ); - assert(sonnetThinking, 'Should include Claude Sonnet 4.5 Thinking'); + const sonnetThinking = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-sonnet-4-6'); + assert(sonnetThinking, 'Should include Claude Sonnet 4.6'); assert.strictEqual( sonnetThinking.deprecated, undefined, @@ -294,31 +294,27 @@ describe('Model Catalog', () => { const models = MODEL_CATALOG.agy.models; // Find indices of thinking models - const opusIdx = models.findIndex((m) => m.id === 'claude-opus-4-5-thinking'); - const sonnetThinkingIdx = models.findIndex((m) => m.id === 'claude-sonnet-4-5-thinking'); + const opusIdx = models.findIndex((m) => m.id === 'claude-opus-4-6-thinking'); + const sonnetThinkingIdx = models.findIndex((m) => m.id === 'claude-sonnet-4-6'); - // Find indices of non-thinking models - const sonnetIdx = models.findIndex((m) => m.id === 'claude-sonnet-4-5'); + // Find indices of the remaining non-Claude model const geminiIdx = models.findIndex((m) => m.id === 'gemini-3-pro-preview'); - // Thinking models should come before non-thinking models - assert(opusIdx < sonnetIdx, 'Opus Thinking should be above non-thinking Sonnet'); - assert(opusIdx < geminiIdx, 'Opus Thinking should be above non-thinking Gemini'); - assert(sonnetThinkingIdx < sonnetIdx, 'Sonnet Thinking should be above non-thinking Sonnet'); - assert(sonnetThinkingIdx < geminiIdx, 'Sonnet Thinking should be above non-thinking Gemini'); + // Primary Claude choices should appear ahead of Gemini fallback. + assert(opusIdx < geminiIdx, 'Opus should be above Gemini'); + assert(sonnetThinkingIdx < geminiIdx, 'Sonnet should be above Gemini'); }); }); describe('isModelDeprecated', () => { - it('returns false for thinking models (no longer deprecated)', () => { + it('returns false for active AGY Claude models', () => { const { isModelDeprecated } = modelCatalog; - assert.strictEqual(isModelDeprecated('agy', 'claude-opus-4-5-thinking'), false); - assert.strictEqual(isModelDeprecated('agy', 'claude-sonnet-4-5-thinking'), false); + assert.strictEqual(isModelDeprecated('agy', 'claude-opus-4-6-thinking'), false); + assert.strictEqual(isModelDeprecated('agy', 'claude-sonnet-4-6'), false); }); it('returns false for non-deprecated models', () => { const { isModelDeprecated } = modelCatalog; - assert.strictEqual(isModelDeprecated('agy', 'claude-sonnet-4-5'), false); assert.strictEqual(isModelDeprecated('agy', 'gemini-3-pro-preview'), false); }); @@ -329,15 +325,15 @@ describe('Model Catalog', () => { }); describe('getModelDeprecationReason', () => { - it('returns undefined for thinking models (no longer deprecated)', () => { + it('returns undefined for active AGY Claude models', () => { const { getModelDeprecationReason } = modelCatalog; - assert.strictEqual(getModelDeprecationReason('agy', 'claude-opus-4-5-thinking'), undefined); - assert.strictEqual(getModelDeprecationReason('agy', 'claude-sonnet-4-5-thinking'), undefined); + assert.strictEqual(getModelDeprecationReason('agy', 'claude-opus-4-6-thinking'), undefined); + assert.strictEqual(getModelDeprecationReason('agy', 'claude-sonnet-4-6'), undefined); }); it('returns undefined for non-deprecated models', () => { const { getModelDeprecationReason } = modelCatalog; - assert.strictEqual(getModelDeprecationReason('agy', 'claude-sonnet-4-5'), undefined); + assert.strictEqual(getModelDeprecationReason('agy', 'gemini-3-pro-preview'), undefined); }); }); }); diff --git a/tests/unit/cliproxy/model-id-normalizer.test.ts b/tests/unit/cliproxy/model-id-normalizer.test.ts index 328db484..705e8dab 100644 --- a/tests/unit/cliproxy/model-id-normalizer.test.ts +++ b/tests/unit/cliproxy/model-id-normalizer.test.ts @@ -64,6 +64,13 @@ describe('model-id-normalizer', () => { expect(normalizeModelIdForProvider('claude-opus-4.6-thinking', 'agy')).toBe( 'claude-opus-4-6-thinking' ); + expect(normalizeModelIdForProvider('claude-opus-4.5-thinking', 'agy')).toBe( + 'claude-opus-4-6-thinking' + ); + expect(normalizeModelIdForProvider('claude-sonnet-4.5-thinking', 'agy')).toBe( + 'claude-sonnet-4-6' + ); + expect(normalizeModelIdForProvider('claude-sonnet-4.5', 'agy')).toBe('claude-sonnet-4-6'); expect(normalizeModelIdForProvider('claude-opus-4.6-thinking', 'gemini')).toBe( 'claude-opus-4.6-thinking' ); @@ -98,9 +105,9 @@ describe('model-id-normalizer', () => { describe('env normalization', () => { it('normalizes model env vars for antigravity only', () => { const input: NodeJS.ProcessEnv = { - ANTHROPIC_MODEL: 'claude-sonnet-4.6-thinking', - ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4.6-thinking', - ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4.6', + ANTHROPIC_MODEL: 'claude-sonnet-4.5-thinking', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4.5-thinking', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4.5', ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4.5', UNRELATED: 'keep-me', }; @@ -113,7 +120,7 @@ describe('model-id-normalizer', () => { expect(normalized.UNRELATED).toBe('keep-me'); const unchanged = normalizeModelEnvVarsForProvider(input, 'gemini'); - expect(unchanged.ANTHROPIC_MODEL).toBe('claude-sonnet-4.6-thinking'); + expect(unchanged.ANTHROPIC_MODEL).toBe('claude-sonnet-4.5-thinking'); expect(unchanged.UNRELATED).toBe('keep-me'); }); }); diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts index 0927ece4..87a902d0 100644 --- a/ui/src/lib/model-catalogs.ts +++ b/ui/src/lib/model-catalogs.ts @@ -26,17 +26,6 @@ export const MODEL_CATALOGS: Record = { haiku: 'claude-sonnet-4-6', }, }, - { - id: 'claude-opus-4-5-thinking', - name: 'Claude Opus 4.5 Thinking', - description: 'Previous flagship, extended thinking', - presetMapping: { - default: 'claude-opus-4-5-thinking', - opus: 'claude-opus-4-5-thinking', - sonnet: 'claude-sonnet-4-6', - haiku: 'claude-sonnet-4-6', - }, - }, { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', @@ -48,28 +37,6 @@ export const MODEL_CATALOGS: Record = { haiku: 'claude-sonnet-4-6', }, }, - { - id: 'claude-sonnet-4-5-thinking', - name: 'Claude Sonnet 4.5 Thinking', - description: 'Balanced with extended thinking', - presetMapping: { - default: 'claude-sonnet-4-5-thinking', - opus: 'claude-opus-4-6-thinking', - sonnet: 'claude-sonnet-4-5-thinking', - haiku: 'claude-sonnet-4-5', - }, - }, - { - id: 'claude-sonnet-4-5', - name: 'Claude Sonnet 4.5', - description: 'Fast and capable', - presetMapping: { - default: 'claude-sonnet-4-5', - opus: 'claude-opus-4-6-thinking', - sonnet: 'claude-sonnet-4-5', - haiku: 'claude-sonnet-4-5', - }, - }, { id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro',