From ee90dd26403e4366af7a4cdbf179e28f45887dc0 Mon Sep 17 00:00:00 2001 From: Kenneth Wong Date: Sun, 26 Jul 2026 00:37:10 +0800 Subject: [PATCH 1/4] feat: support claude opus 5 --- src/cliproxy/__tests__/model-catalog.test.js | 16 ++++++++++++++ src/cliproxy/model-catalog.ts | 17 +++++++++++++++ src/web-server/model-pricing.ts | 22 ++++++++++++++++++++ tests/unit/model-pricing.test.ts | 18 +++++++++++++++- ui/src/lib/model-catalogs.ts | 12 +++++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/cliproxy/__tests__/model-catalog.test.js b/src/cliproxy/__tests__/model-catalog.test.js index 4377ed32..ca020e79 100644 --- a/src/cliproxy/__tests__/model-catalog.test.js +++ b/src/cliproxy/__tests__/model-catalog.test.js @@ -236,6 +236,22 @@ describe('Model Catalog', () => { assert.strictEqual(fable5.extendedContext, true); }); + it('includes Claude Opus 5 with adaptive levels and extended context', () => { + const { MODEL_CATALOG } = modelCatalog; + const opus5 = MODEL_CATALOG.claude.models.find((m) => m.id === 'claude-opus-5'); + assert(opus5, 'Should include Claude Opus 5'); + assert.strictEqual(opus5.name, 'Claude Opus 5'); + // Opus 5 requires adaptive thinking (type: 'levels') like Sonnet 5 and Opus 4.8; + // manual budget_tokens is rejected with 400. Proxy reports zero + dynamic allowed. + assert.strictEqual(opus5.thinking.type, 'levels'); + assert.deepStrictEqual(opus5.thinking.levels, ['low', 'medium', 'high', 'xhigh', 'max']); + assert.strictEqual(opus5.thinking.maxLevel, 'max'); + assert.strictEqual(opus5.thinking.zeroAllowed, true); + assert.strictEqual(opus5.thinking.dynamicAllowed, true); + assert.strictEqual(opus5.nativeImageInput, true); + assert.strictEqual(opus5.extendedContext, true); + }); + it('retains previous 4.5 snapshot models for explicit selection', () => { const { MODEL_CATALOG } = modelCatalog; const ids = MODEL_CATALOG.claude.models.map((m) => m.id); diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index f300fe13..f46e42ed 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -499,6 +499,23 @@ export const MODEL_CATALOG: Partial> = }, extendedContext: true, }, + { + id: 'claude-opus-5', + name: 'Claude Opus 5', + description: 'Latest premium model', + nativeImageInput: true, + // Opus 5 (released 2026-07-14) uses adaptive thinking, matching Sonnet 5 + // and Opus 4.8: Anthropic accepts only effort levels; manual budget_tokens + // is rejected with 400. Proxy metadata reports zero + dynamic allowed. + thinking: { + type: 'levels', + levels: ['low', 'medium', 'high', 'xhigh', 'max'], + maxLevel: 'max', + zeroAllowed: true, + dynamicAllowed: true, + }, + extendedContext: true, + }, { id: 'claude-opus-4-8', name: 'Claude Opus 4.8', diff --git a/src/web-server/model-pricing.ts b/src/web-server/model-pricing.ts index c07f4e3a..da08755e 100644 --- a/src/web-server/model-pricing.ts +++ b/src/web-server/model-pricing.ts @@ -331,6 +331,28 @@ const PRICING_REGISTRY: Record = { fast: OPUS_48_FAST_RATES, }, }, + // Claude Opus 5 ($5/$25) — Claude 5 generation Opus (released 2026-07-14). + // Rates mirror Opus 4.8 (same Opus tier); fast mode reuses the 2x premium + // pending official Opus 5 numbers. Registered explicitly so it resolves to + // Opus-tier pricing instead of the unknown-model fallback. + 'claude-opus-5': { + inputPerMillion: 5.0, + outputPerMillion: 25.0, + cacheCreationPerMillion: 6.25, + cacheReadPerMillion: 0.5, + serviceTiers: { + fast: OPUS_48_FAST_RATES, + }, + }, + 'claude-opus-5-thinking': { + inputPerMillion: 5.0, + outputPerMillion: 25.0, + cacheCreationPerMillion: 6.25, + cacheReadPerMillion: 0.5, + serviceTiers: { + fast: OPUS_48_FAST_RATES, + }, + }, // Claude Fable 5 ($10/$50) — most powerful tier, above Opus 'claude-fable-5': { inputPerMillion: 10.0, diff --git a/tests/unit/model-pricing.test.ts b/tests/unit/model-pricing.test.ts index 9ce51248..2df898fc 100644 --- a/tests/unit/model-pricing.test.ts +++ b/tests/unit/model-pricing.test.ts @@ -285,10 +285,26 @@ describe('model-pricing', () => { expect(sonnet5.cacheReadPerMillion).toBe(0.3); }); + it('should return Opus-tier pricing for Claude Opus 5', () => { + // Opus 5 mirrors Opus 4.8 rates; date-stamped ids resolve via stripDateSuffix. + const opus5 = getModelPricing('claude-opus-5'); + expect(opus5.inputPerMillion).toBe(5.0); + expect(opus5.outputPerMillion).toBe(25.0); + expect(opus5.cacheCreationPerMillion).toBe(6.25); + expect(opus5.cacheReadPerMillion).toBe(0.5); + expect(getModelPricing('claude-opus-5-20270101').inputPerMillion).toBe(5.0); + }); + + it('should apply fast-tier pricing for Claude Opus 5', () => { + const opus5fast = getModelPricing('claude-opus-5', { serviceTier: 'fast' }); + expect(opus5fast.inputPerMillion).toBe(10.0); + expect(opus5fast.outputPerMillion).toBe(50.0); + }); + it('should not map unknown future model families onto known family pricing', () => { const fallback = getModelPricing('unknown-model-xyz'); - expect(getModelPricing('claude-opus-5-20270101')).toEqual(fallback); + expect(getModelPricing('claude-opus-6-20270101')).toEqual(fallback); expect(getModelPricing('gemini-3.2-pro')).toEqual(fallback); }); }); diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts index e6b03dcb..b209a653 100644 --- a/ui/src/lib/model-catalogs.ts +++ b/ui/src/lib/model-catalogs.ts @@ -982,6 +982,18 @@ export const MODEL_CATALOGS: Record = { haiku: 'claude-haiku-4-5-20251001', }, }, + { + id: 'claude-opus-5', + name: 'Claude Opus 5', + description: 'Latest premium model', + extendedContext: true, + presetMapping: { + default: 'claude-opus-5', + opus: 'claude-opus-5', + sonnet: 'claude-sonnet-5', + haiku: 'claude-haiku-4-5-20251001', + }, + }, { id: 'claude-opus-4-8', name: 'Claude Opus 4.8', From cd59c49ae88e117563f01db24fd70ee8c195f904 Mon Sep 17 00:00:00 2001 From: Kenneth Wong Date: Mon, 27 Jul 2026 19:41:19 +0800 Subject: [PATCH 2/4] chore(reports): refresh hardening inventory --- docs/reports/hardening-inventory.json | 2 +- docs/reports/hardening-inventory.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reports/hardening-inventory.json b/docs/reports/hardening-inventory.json index e110b57e..8bf3992d 100644 --- a/docs/reports/hardening-inventory.json +++ b/docs/reports/hardening-inventory.json @@ -735,7 +735,7 @@ }, { "file": "src/web-server/model-pricing.ts", - "loc": 1105 + "loc": 1127 }, { "file": "src/cliproxy/auth/oauth-process.ts", diff --git a/docs/reports/hardening-inventory.md b/docs/reports/hardening-inventory.md index 1fc5fb2f..0e8578b2 100644 --- a/docs/reports/hardening-inventory.md +++ b/docs/reports/hardening-inventory.md @@ -93,7 +93,7 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}` | `src/web-server/routes/cliproxy-auth-routes.ts` | 1531 | | `src/cliproxy/auth/oauth-handler.ts` | 1467 | | `src/cursor/cursor-executor.ts` | 1234 | -| `src/web-server/model-pricing.ts` | 1105 | +| `src/web-server/model-pricing.ts` | 1127 | | `src/cliproxy/auth/oauth-process.ts` | 1048 | | `src/web-server/routes/settings-routes.ts` | 1042 | | `src/cliproxy/config/generator.ts` | 1034 | From af4e4fb3531b5474cb2aa719cc2aaae6f112cfb0 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 29 Jul 2026 12:36:25 -0400 Subject: [PATCH 3/4] fix(cliproxy): correct Claude Opus 5 metadata --- src/cliproxy/model-catalog.ts | 2 +- src/web-server/model-pricing.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index f46e42ed..376cdfce 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -504,7 +504,7 @@ export const MODEL_CATALOG: Partial> = name: 'Claude Opus 5', description: 'Latest premium model', nativeImageInput: true, - // Opus 5 (released 2026-07-14) uses adaptive thinking, matching Sonnet 5 + // Opus 5 (released 2026-07-24) uses adaptive thinking, matching Sonnet 5 // and Opus 4.8: Anthropic accepts only effort levels; manual budget_tokens // is rejected with 400. Proxy metadata reports zero + dynamic allowed. thinking: { diff --git a/src/web-server/model-pricing.ts b/src/web-server/model-pricing.ts index da08755e..b4eec3e5 100644 --- a/src/web-server/model-pricing.ts +++ b/src/web-server/model-pricing.ts @@ -331,9 +331,9 @@ const PRICING_REGISTRY: Record = { fast: OPUS_48_FAST_RATES, }, }, - // Claude Opus 5 ($5/$25) — Claude 5 generation Opus (released 2026-07-14). - // Rates mirror Opus 4.8 (same Opus tier); fast mode reuses the 2x premium - // pending official Opus 5 numbers. Registered explicitly so it resolves to + // Claude Opus 5 ($5/$25) — Claude 5 generation Opus (released 2026-07-24). + // Rates mirror Opus 4.8 (same Opus tier); fast mode uses Anthropic's documented + // $10/$50 rates. Registered explicitly so it resolves to // Opus-tier pricing instead of the unknown-model fallback. 'claude-opus-5': { inputPerMillion: 5.0, From 0629b4a40573a41966da7218f5d9e64bd68b32be Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 29 Jul 2026 12:36:42 -0400 Subject: [PATCH 4/4] test(ui): cover Claude Opus 5 catalog visibility --- .../unit/components/cliproxy/provider-model-selector.test.tsx | 3 ++- ui/tests/unit/ui/lib/preset-utils.test.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) 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 c1c29da3..738fc00d 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 Sonnet 5 in the curated Claude picker', async () => { + it('surfaces current Claude models in the curated Claude picker', async () => { render( { await userEvent.click(screen.getByRole('button', { name: /select model/i })); expect(screen.getByRole('option', { name: /claude-sonnet-5/i })).toBeInTheDocument(); + expect(screen.getByRole('option', { name: /claude-opus-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(); }); diff --git a/ui/tests/unit/ui/lib/preset-utils.test.ts b/ui/tests/unit/ui/lib/preset-utils.test.ts index 68d12968..06615ca0 100644 --- a/ui/tests/unit/ui/lib/preset-utils.test.ts +++ b/ui/tests/unit/ui/lib/preset-utils.test.ts @@ -23,6 +23,7 @@ describe('claude preset utils', () => { expect(claudeCatalog.defaultModel).toBe('claude-sonnet-5'); expect(ids).toContain('claude-sonnet-5'); expect(ids).toContain('claude-fable-5'); + expect(ids).toContain('claude-opus-5'); expect(ids).toContain('claude-opus-4-8'); expect(ids).toContain('claude-opus-4-7'); expect(ids).toContain('claude-sonnet-4-6');