From 9211d59388dfdc62e1dd397a3bc11beb7afe2304 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 24 Nov 2025 16:30:56 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20feat:=20Claude=20Opus=204.5=20To?= =?UTF-8?q?ken=20Rates=20and=20Window=20Limits=20(#10653)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🤖 feat: Claude Opus 4.5 Token Rates and Window Limits - Introduced new model 'claude-opus-4-5' with defined prompt and completion values in tokenValues and cacheTokenValues. - Updated tests to validate prompt, completion, and cache rates for the new model. - Enhanced model name handling to accommodate variations for 'claude-opus-4-5' across different contexts. - Adjusted schemas to ensure correct max output token limits for the new model. * ci: Add tests for "prompt-caching" beta header in Claude Opus 4.5 models - Implemented tests to verify the addition of the "prompt-caching" beta header for the 'claude-opus-4-5' model and its variations. - Updated future-proofing logic to ensure correct max token limits for Claude 4.x and 5.x Opus models, adjusting defaults to 64K where applicable. - Enhanced existing tests to reflect changes in expected max token values for future Claude models. * chore: Remove redundant max output check for Anthropic settings - Eliminated the unnecessary check for ANTHROPIC_MAX_OUTPUT in the anthropicSettings schema, streamlining the logic for handling max output values. --- api/models/tx.js | 3 + api/models/tx.spec.js | 40 ++ api/utils/tokens.spec.js | 43 +++ .../api/src/endpoints/anthropic/llm.spec.ts | 75 +++- packages/api/src/utils/tokens.ts | 6 +- packages/data-provider/src/config.ts | 1 + packages/data-provider/src/schemas.spec.ts | 341 ++++++++++++++++++ packages/data-provider/src/schemas.ts | 13 +- 8 files changed, 508 insertions(+), 14 deletions(-) create mode 100644 packages/data-provider/src/schemas.spec.ts diff --git a/api/models/tx.js b/api/models/tx.js index 48d2801dbd..328f2c2d4d 100644 --- a/api/models/tx.js +++ b/api/models/tx.js @@ -136,6 +136,7 @@ const tokenValues = Object.assign( 'claude-3.7-sonnet': { prompt: 3, completion: 15 }, 'claude-haiku-4-5': { prompt: 1, completion: 5 }, 'claude-opus-4': { prompt: 15, completion: 75 }, + 'claude-opus-4-5': { prompt: 5, completion: 25 }, 'claude-sonnet-4': { prompt: 3, completion: 15 }, 'command-r': { prompt: 0.5, completion: 1.5 }, 'command-r-plus': { prompt: 3, completion: 15 }, @@ -238,8 +239,10 @@ const cacheTokenValues = { 'claude-3.5-haiku': { write: 1, read: 0.08 }, 'claude-3-5-haiku': { write: 1, read: 0.08 }, 'claude-3-haiku': { write: 0.3, read: 0.03 }, + 'claude-haiku-4-5': { write: 1.25, read: 0.1 }, 'claude-sonnet-4': { write: 3.75, read: 0.3 }, 'claude-opus-4': { write: 18.75, read: 1.5 }, + 'claude-opus-4-5': { write: 6.25, read: 0.5 }, }; /** diff --git a/api/models/tx.spec.js b/api/models/tx.spec.js index 7f11e4e466..b70f9572d0 100644 --- a/api/models/tx.spec.js +++ b/api/models/tx.spec.js @@ -1372,6 +1372,15 @@ describe('Claude Model Tests', () => { ); }); + it('should return correct prompt and completion rates for Claude Opus 4.5', () => { + expect(getMultiplier({ model: 'claude-opus-4-5', tokenType: 'prompt' })).toBe( + tokenValues['claude-opus-4-5'].prompt, + ); + expect(getMultiplier({ model: 'claude-opus-4-5', tokenType: 'completion' })).toBe( + tokenValues['claude-opus-4-5'].completion, + ); + }); + it('should handle Claude Haiku 4.5 model name variations', () => { const modelVariations = [ 'claude-haiku-4-5', @@ -1394,6 +1403,28 @@ describe('Claude Model Tests', () => { }); }); + it('should handle Claude Opus 4.5 model name variations', () => { + const modelVariations = [ + 'claude-opus-4-5', + 'claude-opus-4-5-20250420', + 'claude-opus-4-5-latest', + 'anthropic/claude-opus-4-5', + 'claude-opus-4-5/anthropic', + 'claude-opus-4-5-preview', + ]; + + modelVariations.forEach((model) => { + const valueKey = getValueKey(model); + expect(valueKey).toBe('claude-opus-4-5'); + expect(getMultiplier({ model, tokenType: 'prompt' })).toBe( + tokenValues['claude-opus-4-5'].prompt, + ); + expect(getMultiplier({ model, tokenType: 'completion' })).toBe( + tokenValues['claude-opus-4-5'].completion, + ); + }); + }); + it('should handle Claude 4 model name variations with different prefixes and suffixes', () => { const modelVariations = [ 'claude-sonnet-4', @@ -1440,6 +1471,15 @@ describe('Claude Model Tests', () => { ); }); + it('should return correct cache rates for Claude Opus 4.5', () => { + expect(getCacheMultiplier({ model: 'claude-opus-4-5', cacheType: 'write' })).toBe( + cacheTokenValues['claude-opus-4-5'].write, + ); + expect(getCacheMultiplier({ model: 'claude-opus-4-5', cacheType: 'read' })).toBe( + cacheTokenValues['claude-opus-4-5'].read, + ); + }); + it('should handle Claude 4 model cache rates with different prefixes and suffixes', () => { const modelVariations = [ 'claude-sonnet-4', diff --git a/api/utils/tokens.spec.js b/api/utils/tokens.spec.js index dd01f4cb07..c4589c610e 100644 --- a/api/utils/tokens.spec.js +++ b/api/utils/tokens.spec.js @@ -864,6 +864,15 @@ describe('Claude Model Tests', () => { ); }); + it('should return correct context length for Claude Opus 4.5', () => { + expect(getModelMaxTokens('claude-opus-4-5', EModelEndpoint.anthropic)).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-opus-4-5'], + ); + expect(getModelMaxTokens('claude-opus-4-5')).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-opus-4-5'], + ); + }); + it('should handle Claude Haiku 4.5 model name variations', () => { const modelVariations = [ 'claude-haiku-4-5', @@ -883,6 +892,25 @@ describe('Claude Model Tests', () => { }); }); + it('should handle Claude Opus 4.5 model name variations', () => { + const modelVariations = [ + 'claude-opus-4-5', + 'claude-opus-4-5-20250420', + 'claude-opus-4-5-latest', + 'anthropic/claude-opus-4-5', + 'claude-opus-4-5/anthropic', + 'claude-opus-4-5-preview', + ]; + + modelVariations.forEach((model) => { + const modelKey = findMatchingPattern(model, maxTokensMap[EModelEndpoint.anthropic]); + expect(modelKey).toBe('claude-opus-4-5'); + expect(getModelMaxTokens(model, EModelEndpoint.anthropic)).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-opus-4-5'], + ); + }); + }); + it('should match model names correctly for Claude Haiku 4.5', () => { const modelVariations = [ 'claude-haiku-4-5', @@ -898,6 +926,21 @@ describe('Claude Model Tests', () => { }); }); + it('should match model names correctly for Claude Opus 4.5', () => { + const modelVariations = [ + 'claude-opus-4-5', + 'claude-opus-4-5-20250420', + 'claude-opus-4-5-latest', + 'anthropic/claude-opus-4-5', + 'claude-opus-4-5/anthropic', + 'claude-opus-4-5-preview', + ]; + + modelVariations.forEach((model) => { + expect(matchModelName(model, EModelEndpoint.anthropic)).toBe('claude-opus-4-5'); + }); + }); + it('should handle Claude 4 model name variations with different prefixes and suffixes', () => { const modelVariations = [ 'claude-sonnet-4', diff --git a/packages/api/src/endpoints/anthropic/llm.spec.ts b/packages/api/src/endpoints/anthropic/llm.spec.ts index a203f50533..24fbef344b 100644 --- a/packages/api/src/endpoints/anthropic/llm.spec.ts +++ b/packages/api/src/endpoints/anthropic/llm.spec.ts @@ -122,6 +122,38 @@ describe('getLLMConfig', () => { }); }); + it('should add "prompt-caching" beta header for claude-opus-4-5 model', () => { + const modelOptions = { + model: 'claude-opus-4-5', + promptCache: true, + }; + const result = getLLMConfig('test-key', { modelOptions }); + const clientOptions = result.llmConfig.clientOptions; + expect(clientOptions?.defaultHeaders).toBeDefined(); + expect(clientOptions?.defaultHeaders).toHaveProperty('anthropic-beta'); + const defaultHeaders = clientOptions?.defaultHeaders as Record; + expect(defaultHeaders['anthropic-beta']).toBe('prompt-caching-2024-07-31'); + }); + + it('should add "prompt-caching" beta header for claude-opus-4-5 model formats', () => { + const modelVariations = [ + 'claude-opus-4-5', + 'claude-opus-4-5-20250420', + 'claude-opus-4.5', + 'anthropic/claude-opus-4-5', + ]; + + modelVariations.forEach((model) => { + const modelOptions = { model, promptCache: true }; + const result = getLLMConfig('test-key', { modelOptions }); + const clientOptions = result.llmConfig.clientOptions; + expect(clientOptions?.defaultHeaders).toBeDefined(); + expect(clientOptions?.defaultHeaders).toHaveProperty('anthropic-beta'); + const defaultHeaders = clientOptions?.defaultHeaders as Record; + expect(defaultHeaders['anthropic-beta']).toBe('prompt-caching-2024-07-31'); + }); + }); + it('should NOT include topK and topP for Claude-3.7 models with thinking enabled (decimal notation)', () => { const result = getLLMConfig('test-api-key', { modelOptions: { @@ -707,6 +739,7 @@ describe('getLLMConfig', () => { { model: 'claude-haiku-4-5-20251001', expectedMaxTokens: 64000 }, { model: 'claude-opus-4-1', expectedMaxTokens: 32000 }, { model: 'claude-opus-4-1-20250805', expectedMaxTokens: 32000 }, + { model: 'claude-opus-4-5', expectedMaxTokens: 64000 }, { model: 'claude-sonnet-4-20250514', expectedMaxTokens: 64000 }, { model: 'claude-opus-4-0', expectedMaxTokens: 32000 }, ]; @@ -771,6 +804,17 @@ describe('getLLMConfig', () => { }); }); + it('should default Claude Opus 4.5 model to 64K tokens', () => { + const testCases = ['claude-opus-4-5', 'claude-opus-4-5-20250420', 'claude-opus-4.5']; + + testCases.forEach((model) => { + const result = getLLMConfig('test-key', { + modelOptions: { model }, + }); + expect(result.llmConfig.maxTokens).toBe(64000); + }); + }); + it('should default future Claude 4.x Sonnet/Haiku models to 64K (future-proofing)', () => { const testCases = ['claude-sonnet-4-20250514', 'claude-sonnet-4-9', 'claude-haiku-4-8']; @@ -782,15 +826,24 @@ describe('getLLMConfig', () => { }); }); - it('should default future Claude 4.x Opus models to 32K (future-proofing)', () => { - const testCases = ['claude-opus-4-0', 'claude-opus-4-7']; - - testCases.forEach((model) => { + it('should default future Claude 4.x Opus models (future-proofing)', () => { + // opus-4-0 through opus-4-4 get 32K + const opus32kModels = ['claude-opus-4-0', 'claude-opus-4-1', 'claude-opus-4-4']; + opus32kModels.forEach((model) => { const result = getLLMConfig('test-key', { modelOptions: { model }, }); expect(result.llmConfig.maxTokens).toBe(32000); }); + + // opus-4-5+ get 64K + const opus64kModels = ['claude-opus-4-5', 'claude-opus-4-7', 'claude-opus-4-10']; + opus64kModels.forEach((model) => { + const result = getLLMConfig('test-key', { + modelOptions: { model }, + }); + expect(result.llmConfig.maxTokens).toBe(64000); + }); }); it('should handle explicit maxOutputTokens override for Claude 4.x models', () => { @@ -908,7 +961,7 @@ describe('getLLMConfig', () => { }); }); - it('should future-proof Claude 5.x Opus models with 32K default', () => { + it('should future-proof Claude 5.x Opus models with 64K default', () => { const testCases = [ 'claude-opus-5', 'claude-opus-5-0', @@ -920,28 +973,28 @@ describe('getLLMConfig', () => { const result = getLLMConfig('test-key', { modelOptions: { model }, }); - expect(result.llmConfig.maxTokens).toBe(32000); + expect(result.llmConfig.maxTokens).toBe(64000); }); }); it('should future-proof Claude 6-9.x models with correct defaults', () => { const testCases = [ - // Claude 6.x + // Claude 6.x - All get 64K since they're version 5+ { model: 'claude-sonnet-6', expected: 64000 }, { model: 'claude-haiku-6-0', expected: 64000 }, - { model: 'claude-opus-6-1', expected: 32000 }, + { model: 'claude-opus-6-1', expected: 64000 }, // opus 6+ gets 64K // Claude 7.x { model: 'claude-sonnet-7-20270101', expected: 64000 }, { model: 'claude-haiku-7.5', expected: 64000 }, - { model: 'claude-opus-7', expected: 32000 }, + { model: 'claude-opus-7', expected: 64000 }, // opus 7+ gets 64K // Claude 8.x { model: 'claude-sonnet-8', expected: 64000 }, { model: 'claude-haiku-8-2', expected: 64000 }, - { model: 'claude-opus-8-latest', expected: 32000 }, + { model: 'claude-opus-8-latest', expected: 64000 }, // opus 8+ gets 64K // Claude 9.x { model: 'claude-sonnet-9', expected: 64000 }, { model: 'claude-haiku-9', expected: 64000 }, - { model: 'claude-opus-9', expected: 32000 }, + { model: 'claude-opus-9', expected: 64000 }, // opus 9+ gets 64K ]; testCases.forEach(({ model, expected }) => { diff --git a/packages/api/src/utils/tokens.ts b/packages/api/src/utils/tokens.ts index 3842e7bf3e..5271d69f1d 100644 --- a/packages/api/src/utils/tokens.ts +++ b/packages/api/src/utils/tokens.ts @@ -133,8 +133,9 @@ const anthropicModels = { 'claude-3.5-sonnet-latest': 200000, 'claude-haiku-4-5': 200000, 'claude-sonnet-4': 1000000, - 'claude-opus-4': 200000, 'claude-4': 200000, + 'claude-opus-4': 200000, + 'claude-opus-4-5': 200000, }; const deepseekModels = { @@ -334,8 +335,9 @@ const anthropicMaxOutputs = { 'claude-3-sonnet': 4096, 'claude-3-opus': 4096, 'claude-haiku-4-5': 64000, - 'claude-opus-4': 32000, 'claude-sonnet-4': 64000, + 'claude-opus-4': 32000, + 'claude-opus-4-5': 64000, 'claude-3.5-sonnet': 8192, 'claude-3-5-sonnet': 8192, 'claude-3.7-sonnet': 128000, diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 5639e51d4b..5f8ab4d344 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1003,6 +1003,7 @@ const sharedAnthropicModels = [ 'claude-haiku-4-5-20251001', 'claude-opus-4-1', 'claude-opus-4-1-20250805', + 'claude-opus-4-5', 'claude-sonnet-4-20250514', 'claude-sonnet-4-0', 'claude-opus-4-20250514', diff --git a/packages/data-provider/src/schemas.spec.ts b/packages/data-provider/src/schemas.spec.ts new file mode 100644 index 0000000000..02a9fa2b01 --- /dev/null +++ b/packages/data-provider/src/schemas.spec.ts @@ -0,0 +1,341 @@ +import { anthropicSettings } from './schemas'; + +describe('anthropicSettings', () => { + describe('maxOutputTokens.reset()', () => { + const { reset } = anthropicSettings.maxOutputTokens; + + describe('Claude Sonnet models', () => { + it('should return 64K for claude-sonnet-4', () => { + expect(reset('claude-sonnet-4')).toBe(64000); + }); + + it('should return 64K for claude-sonnet-4-5', () => { + expect(reset('claude-sonnet-4-5')).toBe(64000); + }); + + it('should return 64K for claude-sonnet-5', () => { + expect(reset('claude-sonnet-5')).toBe(64000); + }); + + it('should return 64K for future versions like claude-sonnet-9', () => { + expect(reset('claude-sonnet-9')).toBe(64000); + }); + }); + + describe('Claude Haiku models', () => { + it('should return 64K for claude-haiku-4-5', () => { + expect(reset('claude-haiku-4-5')).toBe(64000); + }); + + it('should return 64K for claude-haiku-4', () => { + expect(reset('claude-haiku-4')).toBe(64000); + }); + + it('should return 64K for claude-haiku-5', () => { + expect(reset('claude-haiku-5')).toBe(64000); + }); + + it('should return 64K for future versions like claude-haiku-9', () => { + expect(reset('claude-haiku-9')).toBe(64000); + }); + }); + + describe('Claude Opus 4.0-4.4 models (32K limit)', () => { + it('should return 32K for claude-opus-4', () => { + expect(reset('claude-opus-4')).toBe(32000); + }); + + it('should return 32K for claude-opus-4-0', () => { + expect(reset('claude-opus-4-0')).toBe(32000); + }); + + it('should return 32K for claude-opus-4-1', () => { + expect(reset('claude-opus-4-1')).toBe(32000); + }); + + it('should return 32K for claude-opus-4-2', () => { + expect(reset('claude-opus-4-2')).toBe(32000); + }); + + it('should return 32K for claude-opus-4-3', () => { + expect(reset('claude-opus-4-3')).toBe(32000); + }); + + it('should return 32K for claude-opus-4-4', () => { + expect(reset('claude-opus-4-4')).toBe(32000); + }); + + it('should return 32K for claude-opus-4.0', () => { + expect(reset('claude-opus-4.0')).toBe(32000); + }); + + it('should return 32K for claude-opus-4.1', () => { + expect(reset('claude-opus-4.1')).toBe(32000); + }); + }); + + describe('Claude Opus 4.5+ models (64K limit - future-proof)', () => { + it('should return 64K for claude-opus-4-5', () => { + expect(reset('claude-opus-4-5')).toBe(64000); + }); + + it('should return 64K for claude-opus-4-6', () => { + expect(reset('claude-opus-4-6')).toBe(64000); + }); + + it('should return 64K for claude-opus-4-7', () => { + expect(reset('claude-opus-4-7')).toBe(64000); + }); + + it('should return 64K for claude-opus-4-8', () => { + expect(reset('claude-opus-4-8')).toBe(64000); + }); + + it('should return 64K for claude-opus-4-9', () => { + expect(reset('claude-opus-4-9')).toBe(64000); + }); + + it('should return 64K for claude-opus-4.5', () => { + expect(reset('claude-opus-4.5')).toBe(64000); + }); + + it('should return 64K for claude-opus-4.6', () => { + expect(reset('claude-opus-4.6')).toBe(64000); + }); + }); + + describe('Claude Opus 4.10+ models (double-digit minor versions)', () => { + it('should return 64K for claude-opus-4-10', () => { + expect(reset('claude-opus-4-10')).toBe(64000); + }); + + it('should return 64K for claude-opus-4-11', () => { + expect(reset('claude-opus-4-11')).toBe(64000); + }); + + it('should return 64K for claude-opus-4-15', () => { + expect(reset('claude-opus-4-15')).toBe(64000); + }); + + it('should return 64K for claude-opus-4-20', () => { + expect(reset('claude-opus-4-20')).toBe(64000); + }); + + it('should return 64K for claude-opus-4.10', () => { + expect(reset('claude-opus-4.10')).toBe(64000); + }); + }); + + describe('Claude Opus 5+ models (future major versions)', () => { + it('should return 64K for claude-opus-5', () => { + expect(reset('claude-opus-5')).toBe(64000); + }); + + it('should return 64K for claude-opus-6', () => { + expect(reset('claude-opus-6')).toBe(64000); + }); + + it('should return 64K for claude-opus-7', () => { + expect(reset('claude-opus-7')).toBe(64000); + }); + + it('should return 64K for claude-opus-9', () => { + expect(reset('claude-opus-9')).toBe(64000); + }); + + it('should return 64K for claude-opus-5-0', () => { + expect(reset('claude-opus-5-0')).toBe(64000); + }); + + it('should return 64K for claude-opus-5.0', () => { + expect(reset('claude-opus-5.0')).toBe(64000); + }); + }); + + describe('Model name variations with dates and suffixes', () => { + it('should return 64K for claude-opus-4-5-20250420', () => { + expect(reset('claude-opus-4-5-20250420')).toBe(64000); + }); + + it('should return 64K for claude-opus-4-6-20260101', () => { + expect(reset('claude-opus-4-6-20260101')).toBe(64000); + }); + + it('should return 32K for claude-opus-4-1-20250805', () => { + expect(reset('claude-opus-4-1-20250805')).toBe(32000); + }); + + it('should return 32K for claude-opus-4-0-20240229', () => { + expect(reset('claude-opus-4-0-20240229')).toBe(32000); + }); + }); + + describe('Legacy Claude models', () => { + it('should return 8192 for claude-3-opus', () => { + expect(reset('claude-3-opus')).toBe(8192); + }); + + it('should return 8192 for claude-3-5-sonnet', () => { + expect(reset('claude-3-5-sonnet')).toBe(8192); + }); + + it('should return 8192 for claude-3-5-haiku', () => { + expect(reset('claude-3-5-haiku')).toBe(8192); + }); + + it('should return 8192 for claude-3-7-sonnet', () => { + expect(reset('claude-3-7-sonnet')).toBe(8192); + }); + + it('should return 8192 for claude-2', () => { + expect(reset('claude-2')).toBe(8192); + }); + + it('should return 8192 for claude-2.1', () => { + expect(reset('claude-2.1')).toBe(8192); + }); + + it('should return 8192 for claude-instant', () => { + expect(reset('claude-instant')).toBe(8192); + }); + }); + + describe('Non-Claude models and edge cases', () => { + it('should return 8192 for unknown model', () => { + expect(reset('unknown-model')).toBe(8192); + }); + + it('should return 8192 for empty string', () => { + expect(reset('')).toBe(8192); + }); + + it('should return 8192 for gpt-4', () => { + expect(reset('gpt-4')).toBe(8192); + }); + + it('should return 8192 for gemini-pro', () => { + expect(reset('gemini-pro')).toBe(8192); + }); + }); + + describe('Regex pattern edge cases', () => { + it('should not match claude-opus-3', () => { + expect(reset('claude-opus-3')).toBe(8192); + }); + + it('should not match opus-4-5 without claude prefix', () => { + expect(reset('opus-4-5')).toBe(8192); + }); + + it('should NOT match claude.opus.4.5 (incorrect separator pattern)', () => { + // Model names use hyphens after "claude", not dots + expect(reset('claude.opus.4.5')).toBe(8192); + }); + + it('should match claude-opus45 (no separator after opus)', () => { + // The regex allows optional separators, so "45" can follow directly + // In practice, Anthropic uses separators, but regex is permissive + expect(reset('claude-opus45')).toBe(64000); + }); + }); + }); + + describe('maxOutputTokens.set()', () => { + const { set } = anthropicSettings.maxOutputTokens; + + describe('Claude Sonnet and Haiku 4+ models (64K cap)', () => { + it('should cap at 64K for claude-sonnet-4 when value exceeds', () => { + expect(set(100000, 'claude-sonnet-4')).toBe(64000); + }); + + it('should allow 50K for claude-sonnet-4', () => { + expect(set(50000, 'claude-sonnet-4')).toBe(50000); + }); + + it('should cap at 64K for claude-haiku-4-5 when value exceeds', () => { + expect(set(80000, 'claude-haiku-4-5')).toBe(64000); + }); + }); + + describe('Claude Opus 4.5+ models (64K cap)', () => { + it('should cap at 64K for claude-opus-4-5 when value exceeds', () => { + expect(set(100000, 'claude-opus-4-5')).toBe(64000); + }); + + it('should cap at model-specific 64K limit, not global 128K limit', () => { + // Values between 64K and 128K should be capped at 64K (model limit) + // This verifies the fix for the unreachable code issue + expect(set(70000, 'claude-opus-4-5')).toBe(64000); + expect(set(80000, 'claude-opus-4-5')).toBe(64000); + expect(set(100000, 'claude-opus-4-5')).toBe(64000); + expect(set(128000, 'claude-opus-4-5')).toBe(64000); + + // Values above 128K should also be capped at 64K (not 128K) + expect(set(150000, 'claude-opus-4-5')).toBe(64000); + }); + + it('should allow 50K for claude-opus-4-5', () => { + expect(set(50000, 'claude-opus-4-5')).toBe(50000); + }); + + it('should cap at 64K for claude-opus-4-6', () => { + expect(set(80000, 'claude-opus-4-6')).toBe(64000); + }); + + it('should cap at 64K for claude-opus-5', () => { + expect(set(100000, 'claude-opus-5')).toBe(64000); + }); + + it('should cap at 64K for claude-opus-4-10', () => { + expect(set(100000, 'claude-opus-4-10')).toBe(64000); + }); + }); + + describe('Claude Opus 4.0-4.4 models (32K cap)', () => { + it('should cap at 32K for claude-opus-4', () => { + expect(set(50000, 'claude-opus-4')).toBe(32000); + }); + + it('should allow 20K for claude-opus-4', () => { + expect(set(20000, 'claude-opus-4')).toBe(20000); + }); + + it('should cap at 32K for claude-opus-4-1', () => { + expect(set(50000, 'claude-opus-4-1')).toBe(32000); + }); + + it('should cap at 32K for claude-opus-4-4', () => { + expect(set(40000, 'claude-opus-4-4')).toBe(32000); + }); + }); + + describe('Global 128K cap for all models', () => { + it('should cap at model-specific limit first, then global', () => { + // claude-sonnet-4 has 64K limit, so caps at 64K not 128K + expect(set(150000, 'claude-sonnet-4')).toBe(64000); + }); + + it('should cap at 128K for claude-3 models', () => { + expect(set(150000, 'claude-3-opus')).toBe(128000); + }); + + it('should cap at 128K for unknown models', () => { + expect(set(200000, 'unknown-model')).toBe(128000); + }); + }); + + describe('Valid values within limits', () => { + it('should allow valid values for legacy models', () => { + expect(set(8000, 'claude-3-opus')).toBe(8000); + }); + + it('should allow 1 token minimum', () => { + expect(set(1, 'claude-opus-4-5')).toBe(1); + }); + + it('should allow 128K exactly', () => { + expect(set(128000, 'claude-3-opus')).toBe(128000); + }); + }); + }); +}); diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index afc07880da..50cae9f205 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -386,6 +386,10 @@ export const anthropicSettings = { return CLAUDE_4_64K_MAX_OUTPUT; } + if (/claude-opus[-.]?(?:[5-9]|4[-.]?([5-9]|\d{2,}))/.test(modelName)) { + return CLAUDE_4_64K_MAX_OUTPUT; + } + if (/claude-opus[-.]?[4-9]/.test(modelName)) { return CLAUDE_32K_MAX_OUTPUT; } @@ -397,7 +401,14 @@ export const anthropicSettings = { return CLAUDE_4_64K_MAX_OUTPUT; } - if (/claude-(?:opus|haiku)[-.]?[4-9]/.test(modelName) && value > CLAUDE_32K_MAX_OUTPUT) { + if (/claude-opus[-.]?(?:[5-9]|4[-.]?([5-9]|\d{2,}))/.test(modelName)) { + if (value > CLAUDE_4_64K_MAX_OUTPUT) { + return CLAUDE_4_64K_MAX_OUTPUT; + } + return value; + } + + if (/claude-opus[-.]?[4-9]/.test(modelName) && value > CLAUDE_32K_MAX_OUTPUT) { return CLAUDE_32K_MAX_OUTPUT; }