From c8a4b429906d0c9e08dc8ea27625676ff15c4bba Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 2 Jul 2026 10:31:42 -0400 Subject: [PATCH] feat: add Sonnet 5 to Claude provider catalog --- src/cliproxy/__tests__/model-catalog.test.js | 14 +++++++++- .../__tests__/thinking-validator.test.ts | 7 +++++ src/cliproxy/model-catalog.ts | 16 ++++++++++- ui/src/lib/model-catalogs.ts | 20 ++++++++++--- .../cliproxy/provider-model-selector.test.tsx | 28 +++++++++++++++++-- ui/tests/unit/ui/lib/preset-utils.test.ts | 15 +++++----- 6 files changed, 84 insertions(+), 16 deletions(-) diff --git a/src/cliproxy/__tests__/model-catalog.test.js b/src/cliproxy/__tests__/model-catalog.test.js index eae74a31..32f4cecd 100644 --- a/src/cliproxy/__tests__/model-catalog.test.js +++ b/src/cliproxy/__tests__/model-catalog.test.js @@ -132,7 +132,19 @@ describe('Model Catalog', () => { }); it('has correct default model', () => { const { MODEL_CATALOG } = modelCatalog; - assert.strictEqual(MODEL_CATALOG.claude.defaultModel, 'claude-sonnet-4-6'); + assert.strictEqual(MODEL_CATALOG.claude.defaultModel, 'claude-sonnet-5'); + }); + + it('includes Claude Sonnet 5 with adaptive levels and extended context', () => { + const { MODEL_CATALOG } = modelCatalog; + const sonnet = MODEL_CATALOG.claude.models.find((m) => m.id === 'claude-sonnet-5'); + assert(sonnet, 'Should include Claude Sonnet 5'); + assert.strictEqual(sonnet.name, 'Claude Sonnet 5'); + assert.strictEqual(sonnet.thinking.type, 'levels'); + assert.deepStrictEqual(sonnet.thinking.levels, ['low', 'medium', 'high', 'xhigh', 'max']); + assert.strictEqual(sonnet.thinking.maxLevel, 'max'); + assert.strictEqual(sonnet.nativeImageInput, true); + assert.strictEqual(sonnet.extendedContext, true); }); it('includes Claude Sonnet 4.6', () => { diff --git a/src/cliproxy/__tests__/thinking-validator.test.ts b/src/cliproxy/__tests__/thinking-validator.test.ts index 8dfd47c9..b6a47489 100644 --- a/src/cliproxy/__tests__/thinking-validator.test.ts +++ b/src/cliproxy/__tests__/thinking-validator.test.ts @@ -69,6 +69,13 @@ describe('Thinking Validator', () => { expect(result.warning).toBeUndefined(); }); + it('should treat max as a distinct top tier on Claude Sonnet 5', () => { + const result = validateThinking('claude', 'claude-sonnet-5', 'max'); + expect(result.valid).toBe(true); + expect(result.value).toBe('max'); + expect(result.warning).toBeUndefined(); + }); + it('should still alias max -> xhigh for models without a max level (backcompat)', () => { // Codex catalog uses ['low','medium','high','xhigh'] with maxLevel 'xhigh'. // User input "max" should map down to xhigh rather than be rejected. diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index 5c2ade81..8ba19731 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -367,8 +367,22 @@ export const MODEL_CATALOG: Partial> = claude: { provider: 'claude', displayName: 'Claude (Anthropic)', - defaultModel: 'claude-sonnet-4-6', + defaultModel: 'claude-sonnet-5', models: [ + { + id: 'claude-sonnet-5', + name: 'Claude Sonnet 5', + description: 'Latest Sonnet model', + nativeImageInput: true, + // Sonnet 5 uses adaptive thinking; manual budget_tokens is rejected with 400. + thinking: { + type: 'levels', + levels: ['low', 'medium', 'high', 'xhigh', 'max'], + maxLevel: 'max', + dynamicAllowed: true, + }, + extendedContext: true, + }, { id: 'claude-fable-5', name: 'Claude Fable 5', diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts index fa877db2..1d358ee7 100644 --- a/ui/src/lib/model-catalogs.ts +++ b/ui/src/lib/model-catalogs.ts @@ -854,8 +854,20 @@ export const MODEL_CATALOGS: Record = { claude: { provider: 'claude', displayName: 'Claude (Anthropic)', - defaultModel: 'claude-sonnet-4-6', + defaultModel: 'claude-sonnet-5', models: [ + { + id: 'claude-sonnet-5', + name: 'Claude Sonnet 5', + description: 'Latest Sonnet model', + extendedContext: true, + presetMapping: { + default: 'claude-sonnet-5', + opus: 'claude-opus-4-8', + sonnet: 'claude-sonnet-5', + haiku: 'claude-haiku-4-5-20251001', + }, + }, { id: 'claude-fable-5', name: 'Claude Fable 5', @@ -864,7 +876,7 @@ export const MODEL_CATALOGS: Record = { presetMapping: { default: 'claude-fable-5', opus: 'claude-fable-5', - sonnet: 'claude-sonnet-4-6', + sonnet: 'claude-sonnet-5', haiku: 'claude-haiku-4-5-20251001', }, }, @@ -876,7 +888,7 @@ export const MODEL_CATALOGS: Record = { presetMapping: { default: 'claude-opus-4-8', opus: 'claude-opus-4-8', - sonnet: 'claude-sonnet-4-6', + sonnet: 'claude-sonnet-5', haiku: 'claude-haiku-4-5-20251001', }, }, @@ -888,7 +900,7 @@ export const MODEL_CATALOGS: Record = { presetMapping: { default: 'claude-opus-4-7', opus: 'claude-opus-4-7', - sonnet: 'claude-sonnet-4-6', + sonnet: 'claude-sonnet-5', haiku: 'claude-haiku-4-5-20251001', }, }, diff --git a/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx b/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx index 0149a437..4c71ee7b 100644 --- a/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx +++ b/ui/tests/unit/components/cliproxy/provider-model-selector.test.tsx @@ -70,7 +70,7 @@ describe('FlexibleModelSelector', () => { expect(screen.queryByText(/All Models \(/i)).not.toBeInTheDocument(); }); - it('surfaces Claude Opus 4.7 in the curated Claude picker', async () => { + it('surfaces Claude Sonnet 5 in the curated Claude picker', async () => { render( { await userEvent.click(screen.getByRole('button', { name: /select model/i })); - expect(screen.getByRole('option', { name: /claude-opus-4-7/i })).toBeInTheDocument(); - expect(screen.getByRole('option', { name: /claude-opus-4-6/i })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: /claude-sonnet-5/i })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: /claude-opus-4-8/i })).toBeInTheDocument(); expect(screen.getByRole('option', { name: /claude-sonnet-4-6/i })).toBeInTheDocument(); }); @@ -129,6 +129,28 @@ describe('FlexibleModelSelector', () => { expect(onChange).toHaveBeenCalledWith('claude-sonnet-6'); }); + it('lets users enter a future direct Claude model ID from the CLIProxy picker', async () => { + const onChange = vi.fn(); + + render( + + ); + + await userEvent.click(screen.getByRole('button', { name: /select model/i })); + await userEvent.type(await screen.findByPlaceholderText('Search models...'), 'claude-sonnet-6'); + + expect(screen.getAllByText('Custom model').length).toBeGreaterThan(0); + await userEvent.click(screen.getByRole('option', { name: /claude-sonnet-6/i })); + + expect(onChange).toHaveBeenCalledWith('claude-sonnet-6'); + }); + it('preserves a filtered legacy value under the current-value fallback group', async () => { render( { vi.restoreAllMocks(); }); - it('keeps the claude catalog default on Sonnet 4.6 while exposing Opus 4.7/4.8 and Fable 5', () => { + it('keeps the claude catalog default on Sonnet 5 while exposing recent Claude models', () => { const claudeCatalog = MODEL_CATALOGS.claude; const ids = claudeCatalog.models.map((model) => model.id); - expect(claudeCatalog.defaultModel).toBe('claude-sonnet-4-6'); + expect(claudeCatalog.defaultModel).toBe('claude-sonnet-5'); + expect(ids).toContain('claude-sonnet-5'); expect(ids).toContain('claude-fable-5'); expect(ids).toContain('claude-opus-4-8'); expect(ids).toContain('claude-opus-4-7'); @@ -50,15 +51,15 @@ describe('claude preset utils', () => { const result = await applyDefaultPreset('claude'); - expect(result).toEqual({ success: true, presetName: 'Claude Sonnet 4.6' }); + expect(result).toEqual({ success: true, presetName: 'Claude Sonnet 5' }); const [, requestInit] = fetchMock.mock.calls[2] ?? []; const body = JSON.parse(String(requestInit?.body)); expect(body.settings.env).toMatchObject({ - ANTHROPIC_MODEL: 'claude-sonnet-4-6', - ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6', - ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6', + ANTHROPIC_MODEL: 'claude-sonnet-5', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-8', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-5', ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001', }); }); @@ -76,7 +77,7 @@ describe('claude preset utils', () => { const result = await applyDefaultPreset('claude', undefined, MODEL_CATALOGS.claude); - expect(result).toEqual({ success: true, presetName: 'Claude Sonnet 4.6' }); + expect(result).toEqual({ success: true, presetName: 'Claude Sonnet 5' }); expect(fetchMock).toHaveBeenCalledTimes(2); expect(fetchMock).toHaveBeenNthCalledWith(1, '/api/settings/auth/tokens/raw'); expect(fetchMock).toHaveBeenNthCalledWith(