Merge pull request #1672 from KennethWKZ/feat/claude-opus-5

feat: support Claude Opus 5
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-07-29 12:39:56 -04:00
commit 558e727312
9 files changed
+89 -4

No files matched your search

+1 -1
View File
@@ -735,7 +735,7 @@
},
{
"file": "src/web-server/model-pricing.ts",
"loc": 1105
"loc": 1127
},
{
"file": "src/cliproxy/auth/oauth-process.ts",
+1 -1
View File
@@ -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 |
@@ -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);
+17
View File
@@ -499,6 +499,23 @@ export const MODEL_CATALOG: Partial<Record<CLIProxyProvider, ProviderCatalog>> =
},
extendedContext: true,
},
{
id: 'claude-opus-5',
name: 'Claude Opus 5',
description: 'Latest premium model',
nativeImageInput: true,
// 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: {
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',
+22
View File
@@ -331,6 +331,28 @@ const PRICING_REGISTRY: Record<string, ModelPricing> = {
fast: OPUS_48_FAST_RATES,
},
},
// 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,
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,
+17 -1
View File
@@ -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);
});
});
+12
View File
@@ -982,6 +982,18 @@ export const MODEL_CATALOGS: Record<string, ProviderCatalog> = {
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',
@@ -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(
<FlexibleModelSelector
label="Primary model"
@@ -84,6 +84,7 @@ describe('FlexibleModelSelector', () => {
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();
});
@@ -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');