From ee90dd26403e4366af7a4cdbf179e28f45887dc0 Mon Sep 17 00:00:00 2001 From: Kenneth Wong Date: Sun, 26 Jul 2026 00:37:10 +0800 Subject: [PATCH] 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',