feat: add Sonnet 5 to Claude provider catalog

This commit is contained in:
Tam Nhu Tran committed 2026-07-02 10:31:42 -04:00
1 parent 631a515986
commit c8a4b42990
6 files changed
+84 -16

No files matched your search

+13 -1
View File
@@ -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', () => {
@@ -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.
+15 -1
View File
@@ -367,8 +367,22 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
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',
+16 -4
View File
@@ -854,8 +854,20 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
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<string, ProviderCatalog> = {
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<string, ProviderCatalog> = {
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<string, ProviderCatalog> = {
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',
},
},
@@ -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(
<FlexibleModelSelector
label="Primary model"
@@ -83,8 +83,8 @@ describe('FlexibleModelSelector', () => {
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(
<FlexibleModelSelector
label="Primary model"
value={undefined}
onChange={onChange}
catalog={MODEL_CATALOGS.claude}
allModels={[]}
/>
);
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(
<FlexibleModelSelector
+8 -7
View File
@@ -16,11 +16,12 @@ describe('claude preset utils', () => {
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(