From 3583b544d01170d0352a49f3e4bfc8b5ecd2c953 Mon Sep 17 00:00:00 2001 From: Kenneth Wong Date: Thu, 3 Sep 2026 16:26:41 +0800 Subject: [PATCH] feat(models): add Claude Fable 5.1 and correct Sonnet 5 pricing Register `claude-fable-5-1` in the CLIProxy model catalog, the dashboard catalog, and the usage pricing registry. Pricing is taken from Anthropic's official pricing page: - Fable 5.1 base rates are $10/$50 per MTok with a $12.50 5m cache write, matching Fable 5. - Cache hits bill at 0.025x base input ($0.25/MTok) rather than the standard 0.1x multiplier. Anthropic applies that reduced rate only to Fable 5.1 and Mythos 5.1, so this entry cannot derive its cache rates from CACHE_READ_MULTIPLIER. This also corrects Claude Sonnet 5 from $3/$15 to $2/$10 (cache write $2.50, cache read $0.20). The launch introductory rate became the standard price and the increase scheduled for 2026-09-01 was cancelled, so the previous entry over-reported Sonnet 5 usage cost by 50%. Thinking on Fable 5.1 is always on and can only be steered through effort levels, so the catalog entry exposes the same `low`..`max` level surface as Fable 5 and Opus 5 instead of a manual token budget. The GitHub Copilot catalog is deliberately left unchanged, since Copilot availability for Fable 5.1 is not verified; surfacing it there would offer a model the backend may reject. Regenerates docs/reports/hardening-inventory.{json,md} so the ci-parity gate matches the source tree. --- docs/reports/hardening-inventory.json | 6 ++-- docs/reports/hardening-inventory.md | 4 +-- .../__tests__/thinking-validator.test.ts | 8 +++++ src/cliproxy/model-catalog.ts | 19 +++++++++++- src/web-server/model-pricing.ts | 31 +++++++++++++------ tests/unit/model-pricing.test.ts | 22 ++++++++++--- ui/src/lib/model-catalogs.ts | 14 ++++++++- ui/tests/unit/ui/lib/preset-utils.test.ts | 1 + 8 files changed, 83 insertions(+), 22 deletions(-) diff --git a/docs/reports/hardening-inventory.json b/docs/reports/hardening-inventory.json index 91e33acb..43bed65e 100644 --- a/docs/reports/hardening-inventory.json +++ b/docs/reports/hardening-inventory.json @@ -739,7 +739,7 @@ }, { "file": "src/web-server/model-pricing.ts", - "loc": 1127 + "loc": 1138 }, { "file": "src/cliproxy/config/generator.ts", @@ -778,8 +778,8 @@ "loc": 939 }, { - "file": "src/cliproxy/accounts/registry.ts", - "loc": 871 + "file": "src/cliproxy/model-catalog.ts", + "loc": 884 } ] } diff --git a/docs/reports/hardening-inventory.md b/docs/reports/hardening-inventory.md index 506b59c8..385cea08 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` | 1510 | | `src/cursor/cursor-executor.ts` | 1234 | -| `src/web-server/model-pricing.ts` | 1127 | +| `src/web-server/model-pricing.ts` | 1138 | | `src/cliproxy/config/generator.ts` | 1109 | | `src/cliproxy/auth/oauth-process.ts` | 1048 | | `src/cliproxy/config/env-builder.ts` | 1045 | @@ -103,5 +103,5 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}` | `src/cliproxy/quota/quota-manager.ts` | 954 | | `src/web-server/services/codex-dashboard-service.ts` | 940 | | `src/glmt/glmt-proxy.ts` | 939 | -| `src/cliproxy/accounts/registry.ts` | 871 | +| `src/cliproxy/model-catalog.ts` | 884 | diff --git a/src/cliproxy/__tests__/thinking-validator.test.ts b/src/cliproxy/__tests__/thinking-validator.test.ts index b6a47489..41be1a1a 100644 --- a/src/cliproxy/__tests__/thinking-validator.test.ts +++ b/src/cliproxy/__tests__/thinking-validator.test.ts @@ -60,6 +60,14 @@ describe('Thinking Validator', () => { expect(result.warning).toBeUndefined(); }); + it('should treat max as a distinct top tier on Claude Fable 5.1', () => { + // Fable 5.1 has always-on adaptive thinking steered only by effort level. + const result = validateThinking('claude', 'claude-fable-5-1', 'max'); + expect(result.valid).toBe(true); + expect(result.value).toBe('max'); + expect(result.warning).toBeUndefined(); + }); + it('should treat max as a distinct top tier on Claude Fable 5', () => { // Fable 5 shares Opus 4.8's adaptive thinking surface; max must remain // distinct from xhigh. diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index ff2d632b..509639bc 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -531,10 +531,27 @@ export const MODEL_CATALOG: Partial> = }, extendedContext: true, }, + { + id: 'claude-fable-5-1', + name: 'Claude Fable 5.1', + description: 'Most powerful model (1M context, 128K output)', + contextWindow: 1000000, + nativeImageInput: true, + // Thinking is always on and cannot be disabled: Anthropic rejects both + // `thinking.type: "disabled"` and manual budget_tokens with 400. Depth is + // steered only through effort levels (default `high`). + thinking: { + type: 'levels', + levels: ['low', 'medium', 'high', 'xhigh', 'max'], + maxLevel: 'max', + dynamicAllowed: true, + }, + extendedContext: true, + }, { id: 'claude-fable-5', name: 'Claude Fable 5', - description: 'Most powerful model', + description: 'Previous most powerful model', contextWindow: 1000000, nativeImageInput: true, // New tier above Opus. Same adaptive-thinking surface as Opus 4.8: diff --git a/src/web-server/model-pricing.ts b/src/web-server/model-pricing.ts index b4eec3e5..d1cdfacf 100644 --- a/src/web-server/model-pricing.ts +++ b/src/web-server/model-pricing.ts @@ -216,20 +216,21 @@ const PRICING_REGISTRY: Record = { cacheCreationPerMillion: 3.75, cacheReadPerMillion: 0.3, }, - // Claude Sonnet 5 ($3/$15) — latest Sonnet; shares the standard Sonnet rates. + // Claude Sonnet 5 ($2/$10) — the launch introductory rate is now the standard + // price; Anthropic cancelled the scheduled 2026-09-01 increase to $3/$15. // Registered explicitly so it is a known model rather than relying on the - // unknown-model fallback that coincidentally matches these rates. + // unknown-model fallback. 'claude-sonnet-5': { - inputPerMillion: 3.0, - outputPerMillion: 15.0, - cacheCreationPerMillion: 3.75, - cacheReadPerMillion: 0.3, + inputPerMillion: 2.0, + outputPerMillion: 10.0, + cacheCreationPerMillion: 2.5, + cacheReadPerMillion: 0.2, }, 'claude-sonnet-5-thinking': { - inputPerMillion: 3.0, - outputPerMillion: 15.0, - cacheCreationPerMillion: 3.75, - cacheReadPerMillion: 0.3, + inputPerMillion: 2.0, + outputPerMillion: 10.0, + cacheCreationPerMillion: 2.5, + cacheReadPerMillion: 0.2, }, // Claude 4 Opus ($15/$75) 'claude-4-opus-20250514': { @@ -360,6 +361,16 @@ const PRICING_REGISTRY: Record = { cacheCreationPerMillion: 12.5, cacheReadPerMillion: 1.0, }, + // Claude Fable 5.1 ($10/$50) — same tier and base rates as Fable 5, but cache + // hits are billed at 0.025x base input ($0.25/MTok) instead of the usual 0.1x. + // Anthropic applies that reduced multiplier only to Fable 5.1 and Mythos 5.1, + // so this entry cannot use CACHE_READ_MULTIPLIER-derived rates. + 'claude-fable-5-1': { + inputPerMillion: 10.0, + outputPerMillion: 50.0, + cacheCreationPerMillion: 12.5, + cacheReadPerMillion: 0.25, + }, // --------------------------------------------------------------------------- // OpenAI Models - Source: better-ccusage diff --git a/tests/unit/model-pricing.test.ts b/tests/unit/model-pricing.test.ts index 2df898fc..8202db38 100644 --- a/tests/unit/model-pricing.test.ts +++ b/tests/unit/model-pricing.test.ts @@ -277,12 +277,24 @@ describe('model-pricing', () => { expect(fable5.cacheReadPerMillion).toBe(1.0); }); + it('should return correct pricing for Claude Fable 5.1', () => { + // Same base rates as Fable 5, but cache hits bill at 0.025x base input + // ($0.25/MTok) rather than the standard 0.1x multiplier. + const fable51 = getModelPricing('claude-fable-5-1'); + expect(fable51.inputPerMillion).toBe(10.0); + expect(fable51.outputPerMillion).toBe(50.0); + expect(fable51.cacheCreationPerMillion).toBe(12.5); + expect(fable51.cacheReadPerMillion).toBe(0.25); + }); + it('should return correct pricing for Claude Sonnet 5', () => { + // The $2/$10 launch introductory rate became the standard price; the + // scheduled 2026-09-01 increase to $3/$15 was cancelled. const sonnet5 = getModelPricing('claude-sonnet-5'); - expect(sonnet5.inputPerMillion).toBe(3.0); - expect(sonnet5.outputPerMillion).toBe(15.0); - expect(sonnet5.cacheCreationPerMillion).toBe(3.75); - expect(sonnet5.cacheReadPerMillion).toBe(0.3); + expect(sonnet5.inputPerMillion).toBe(2.0); + expect(sonnet5.outputPerMillion).toBe(10.0); + expect(sonnet5.cacheCreationPerMillion).toBe(2.5); + expect(sonnet5.cacheReadPerMillion).toBe(0.2); }); it('should return Opus-tier pricing for Claude Opus 5', () => { @@ -430,7 +442,7 @@ describe('model-pricing', () => { cacheReadTokens: 1_000_000, }; const cost = calculateCost(usage, 'claude-sonnet-5'); - expect(cost).toBe(22.05); // 3 + 15 + 3.75 + 0.3 + expect(cost).toBe(14.7); // 2 + 10 + 2.5 + 0.2 }); it('should calculate fast-tier Claude Opus 4.8 cost (2x premium)', () => { diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts index 32c3d523..265e49e9 100644 --- a/ui/src/lib/model-catalogs.ts +++ b/ui/src/lib/model-catalogs.ts @@ -977,10 +977,22 @@ export const MODEL_CATALOGS: Record = { haiku: 'claude-haiku-4-5-20251001', }, }, + { + id: 'claude-fable-5-1', + name: 'Claude Fable 5.1', + description: 'Most powerful model', + extendedContext: true, + presetMapping: { + default: 'claude-fable-5-1', + opus: 'claude-fable-5-1', + sonnet: 'claude-sonnet-5', + haiku: 'claude-haiku-4-5-20251001', + }, + }, { id: 'claude-fable-5', name: 'Claude Fable 5', - description: 'Most powerful model', + description: 'Previous most powerful model', extendedContext: true, presetMapping: { default: 'claude-fable-5', diff --git a/ui/tests/unit/ui/lib/preset-utils.test.ts b/ui/tests/unit/ui/lib/preset-utils.test.ts index 06615ca0..d8ce4632 100644 --- a/ui/tests/unit/ui/lib/preset-utils.test.ts +++ b/ui/tests/unit/ui/lib/preset-utils.test.ts @@ -22,6 +22,7 @@ describe('claude preset utils', () => { expect(claudeCatalog.defaultModel).toBe('claude-sonnet-5'); expect(ids).toContain('claude-sonnet-5'); + expect(ids).toContain('claude-fable-5-1'); expect(ids).toContain('claude-fable-5'); expect(ids).toContain('claude-opus-5'); expect(ids).toContain('claude-opus-4-8');