diff --git a/.env.example b/.env.example index bd39f653bd..bd28732068 100644 --- a/.env.example +++ b/.env.example @@ -88,7 +88,7 @@ PROXY= #============# ANTHROPIC_API_KEY=user_provided -# ANTHROPIC_MODELS=claude-3-7-sonnet-latest,claude-3-7-sonnet-20250219,claude-3-5-haiku-20241022,claude-3-5-sonnet-20241022,claude-3-5-sonnet-latest,claude-3-5-sonnet-20240620,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307,claude-2.1,claude-2,claude-1.2,claude-1,claude-1-100k,claude-instant-1,claude-instant-1-100k +# ANTHROPIC_MODELS=claude-opus-4-20250514,claude-sonnet-4-20250514,claude-3-7-sonnet-20250219,claude-3-5-sonnet-20241022,claude-3-5-haiku-20241022,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307 # ANTHROPIC_REVERSE_PROXY= #============# diff --git a/api/app/clients/AnthropicClient.js b/api/app/clients/AnthropicClient.js index 91939975c4..2b832f2e0c 100644 --- a/api/app/clients/AnthropicClient.js +++ b/api/app/clients/AnthropicClient.js @@ -70,7 +70,7 @@ class AnthropicClient extends BaseClient { this.message_delta; /** Whether the model is part of the Claude 3 Family * @type {boolean} */ - this.isClaude3; + this.isClaudeLatest; /** Whether to use Messages API or Completions API * @type {boolean} */ this.useMessages; @@ -116,7 +116,8 @@ class AnthropicClient extends BaseClient { ); const modelMatch = matchModelName(this.modelOptions.model, EModelEndpoint.anthropic); - this.isClaude3 = modelMatch.includes('claude-3'); + this.isClaudeLatest = + /claude-[3-9]/.test(modelMatch) || /claude-(?:sonnet|opus|haiku)-[4-9]/.test(modelMatch); this.isLegacyOutput = !( /claude-3[-.]5-sonnet/.test(modelMatch) || /claude-3[-.]7/.test(modelMatch) ); @@ -130,7 +131,7 @@ class AnthropicClient extends BaseClient { this.modelOptions.maxOutputTokens = legacy.maxOutputTokens.default; } - this.useMessages = this.isClaude3 || !!this.options.attachments; + this.useMessages = this.isClaudeLatest || !!this.options.attachments; this.defaultVisionModel = this.options.visionModel ?? 'claude-3-sonnet-20240229'; this.options.attachments?.then((attachments) => this.checkVisionRequest(attachments)); @@ -654,7 +655,10 @@ class AnthropicClient extends BaseClient { ); }; - if (this.modelOptions.model.includes('claude-3')) { + if ( + /claude-[3-9]/.test(this.modelOptions.model) || + /claude-(?:sonnet|opus|haiku)-[4-9]/.test(this.modelOptions.model) + ) { await buildMessagesPayload(); processTokens(); return { diff --git a/api/app/clients/specs/AnthropicClient.test.js b/api/app/clients/specs/AnthropicClient.test.js index 223f3038c0..5f331e8d3c 100644 --- a/api/app/clients/specs/AnthropicClient.test.js +++ b/api/app/clients/specs/AnthropicClient.test.js @@ -15,7 +15,7 @@ describe('AnthropicClient', () => { { role: 'user', isCreatedByUser: true, - text: 'What\'s up', + text: "What's up", messageId: '3', parentMessageId: '2', }, @@ -170,7 +170,7 @@ describe('AnthropicClient', () => { client.options.modelLabel = 'Claude-2'; const result = await client.buildMessages(messages, parentMessageId); const { prompt } = result; - expect(prompt).toContain('Human\'s name: John'); + expect(prompt).toContain("Human's name: John"); expect(prompt).toContain('You are Claude-2'); }); }); @@ -244,6 +244,64 @@ describe('AnthropicClient', () => { ); }); + describe('Claude 4 model headers', () => { + it('should add "prompt-caching" beta header for claude-sonnet-4 model', () => { + const client = new AnthropicClient('test-api-key'); + const modelOptions = { + model: 'claude-sonnet-4-20250514', + }; + client.setOptions({ modelOptions, promptCache: true }); + const anthropicClient = client.getClient(modelOptions); + expect(anthropicClient._options.defaultHeaders).toBeDefined(); + expect(anthropicClient._options.defaultHeaders).toHaveProperty('anthropic-beta'); + expect(anthropicClient._options.defaultHeaders['anthropic-beta']).toBe( + 'prompt-caching-2024-07-31', + ); + }); + + it('should add "prompt-caching" beta header for claude-opus-4 model', () => { + const client = new AnthropicClient('test-api-key'); + const modelOptions = { + model: 'claude-opus-4-20250514', + }; + client.setOptions({ modelOptions, promptCache: true }); + const anthropicClient = client.getClient(modelOptions); + expect(anthropicClient._options.defaultHeaders).toBeDefined(); + expect(anthropicClient._options.defaultHeaders).toHaveProperty('anthropic-beta'); + expect(anthropicClient._options.defaultHeaders['anthropic-beta']).toBe( + 'prompt-caching-2024-07-31', + ); + }); + + it('should add "prompt-caching" beta header for claude-4-sonnet model', () => { + const client = new AnthropicClient('test-api-key'); + const modelOptions = { + model: 'claude-4-sonnet-20250514', + }; + client.setOptions({ modelOptions, promptCache: true }); + const anthropicClient = client.getClient(modelOptions); + expect(anthropicClient._options.defaultHeaders).toBeDefined(); + expect(anthropicClient._options.defaultHeaders).toHaveProperty('anthropic-beta'); + expect(anthropicClient._options.defaultHeaders['anthropic-beta']).toBe( + 'prompt-caching-2024-07-31', + ); + }); + + it('should add "prompt-caching" beta header for claude-4-opus model', () => { + const client = new AnthropicClient('test-api-key'); + const modelOptions = { + model: 'claude-4-opus-20250514', + }; + client.setOptions({ modelOptions, promptCache: true }); + const anthropicClient = client.getClient(modelOptions); + expect(anthropicClient._options.defaultHeaders).toBeDefined(); + expect(anthropicClient._options.defaultHeaders).toHaveProperty('anthropic-beta'); + expect(anthropicClient._options.defaultHeaders['anthropic-beta']).toBe( + 'prompt-caching-2024-07-31', + ); + }); + }); + it('should not add beta header for claude-3-5-sonnet-latest model', () => { const client = new AnthropicClient('test-api-key'); const modelOptions = { @@ -729,4 +787,223 @@ describe('AnthropicClient', () => { expect(capturedOptions).toHaveProperty('topK', 10); expect(capturedOptions).toHaveProperty('topP', 0.9); }); + + describe('isClaudeLatest', () => { + it('should set isClaudeLatest to true for claude-3 models', () => { + const client = new AnthropicClient('test-api-key'); + client.setOptions({ + modelOptions: { + model: 'claude-3-sonnet-20240229', + }, + }); + expect(client.isClaudeLatest).toBe(true); + }); + + it('should set isClaudeLatest to true for claude-3.5 models', () => { + const client = new AnthropicClient('test-api-key'); + client.setOptions({ + modelOptions: { + model: 'claude-3.5-sonnet-20240229', + }, + }); + expect(client.isClaudeLatest).toBe(true); + }); + + it('should set isClaudeLatest to true for claude-sonnet-4 models', () => { + const client = new AnthropicClient('test-api-key'); + client.setOptions({ + modelOptions: { + model: 'claude-sonnet-4-20240229', + }, + }); + expect(client.isClaudeLatest).toBe(true); + }); + + it('should set isClaudeLatest to true for claude-opus-4 models', () => { + const client = new AnthropicClient('test-api-key'); + client.setOptions({ + modelOptions: { + model: 'claude-opus-4-20240229', + }, + }); + expect(client.isClaudeLatest).toBe(true); + }); + + it('should set isClaudeLatest to true for claude-3.5-haiku models', () => { + const client = new AnthropicClient('test-api-key'); + client.setOptions({ + modelOptions: { + model: 'claude-3.5-haiku-20240229', + }, + }); + expect(client.isClaudeLatest).toBe(true); + }); + + it('should set isClaudeLatest to false for claude-2 models', () => { + const client = new AnthropicClient('test-api-key'); + client.setOptions({ + modelOptions: { + model: 'claude-2', + }, + }); + expect(client.isClaudeLatest).toBe(false); + }); + + it('should set isClaudeLatest to false for claude-instant models', () => { + const client = new AnthropicClient('test-api-key'); + client.setOptions({ + modelOptions: { + model: 'claude-instant', + }, + }); + expect(client.isClaudeLatest).toBe(false); + }); + + it('should set isClaudeLatest to false for claude-sonnet-3 models', () => { + const client = new AnthropicClient('test-api-key'); + client.setOptions({ + modelOptions: { + model: 'claude-sonnet-3-20240229', + }, + }); + expect(client.isClaudeLatest).toBe(false); + }); + + it('should set isClaudeLatest to false for claude-opus-3 models', () => { + const client = new AnthropicClient('test-api-key'); + client.setOptions({ + modelOptions: { + model: 'claude-opus-3-20240229', + }, + }); + expect(client.isClaudeLatest).toBe(false); + }); + + it('should set isClaudeLatest to false for claude-haiku-3 models', () => { + const client = new AnthropicClient('test-api-key'); + client.setOptions({ + modelOptions: { + model: 'claude-haiku-3-20240229', + }, + }); + expect(client.isClaudeLatest).toBe(false); + }); + }); + + describe('configureReasoning', () => { + it('should enable thinking for claude-opus-4 and claude-sonnet-4 models', async () => { + const client = new AnthropicClient('test-api-key'); + // Create a mock async generator function + async function* mockAsyncGenerator() { + yield { type: 'message_start', message: { usage: {} } }; + yield { delta: { text: 'Test response' } }; + yield { type: 'message_delta', usage: {} }; + } + + // Mock createResponse to return the async generator + jest.spyOn(client, 'createResponse').mockImplementation(() => { + return mockAsyncGenerator(); + }); + + // Test claude-opus-4 + client.setOptions({ + modelOptions: { + model: 'claude-opus-4-20250514', + }, + thinking: true, + thinkingBudget: 2000, + }); + + let capturedOptions = null; + jest.spyOn(client, 'getClient').mockImplementation((options) => { + capturedOptions = options; + return {}; + }); + + const payload = [{ role: 'user', content: 'Test message' }]; + await client.sendCompletion(payload, {}); + + expect(capturedOptions).toHaveProperty('thinking'); + expect(capturedOptions.thinking).toEqual({ + type: 'enabled', + budget_tokens: 2000, + }); + + // Test claude-sonnet-4 + client.setOptions({ + modelOptions: { + model: 'claude-sonnet-4-20250514', + }, + thinking: true, + thinkingBudget: 2000, + }); + + await client.sendCompletion(payload, {}); + + expect(capturedOptions).toHaveProperty('thinking'); + expect(capturedOptions.thinking).toEqual({ + type: 'enabled', + budget_tokens: 2000, + }); + }); + }); +}); + +describe('Claude Model Tests', () => { + it('should handle Claude 3 and 4 series models correctly', () => { + const client = new AnthropicClient('test-key'); + // Claude 3 series models + const claude3Models = [ + 'claude-3-opus-20240229', + 'claude-3-sonnet-20240229', + 'claude-3-haiku-20240307', + 'claude-3-5-sonnet-20240620', + 'claude-3-5-haiku-20240620', + 'claude-3.5-sonnet-20240620', + 'claude-3.5-haiku-20240620', + 'claude-3.7-sonnet-20240620', + 'claude-3.7-haiku-20240620', + 'anthropic/claude-3-opus-20240229', + 'claude-3-opus-20240229/anthropic', + ]; + + // Claude 4 series models + const claude4Models = [ + 'claude-sonnet-4-20250514', + 'claude-opus-4-20250514', + 'claude-4-sonnet-20250514', + 'claude-4-opus-20250514', + 'anthropic/claude-sonnet-4-20250514', + 'claude-sonnet-4-20250514/anthropic', + ]; + + // Test Claude 3 series + claude3Models.forEach((model) => { + client.setOptions({ modelOptions: { model } }); + expect( + /claude-[3-9]/.test(client.modelOptions.model) || + /claude-(?:sonnet|opus|haiku)-[4-9]/.test(client.modelOptions.model), + ).toBe(true); + }); + + // Test Claude 4 series + claude4Models.forEach((model) => { + client.setOptions({ modelOptions: { model } }); + expect( + /claude-[3-9]/.test(client.modelOptions.model) || + /claude-(?:sonnet|opus|haiku)-[4-9]/.test(client.modelOptions.model), + ).toBe(true); + }); + + // Test non-Claude 3/4 models + const nonClaudeModels = ['claude-2', 'claude-instant', 'gpt-4', 'gpt-3.5-turbo']; + + nonClaudeModels.forEach((model) => { + client.setOptions({ modelOptions: { model } }); + expect( + /claude-[3-9]/.test(client.modelOptions.model) || + /claude-(?:sonnet|opus|haiku)-[4-9]/.test(client.modelOptions.model), + ).toBe(false); + }); + }); }); diff --git a/api/models/tx.js b/api/models/tx.js index df88390b17..ddd098b80f 100644 --- a/api/models/tx.js +++ b/api/models/tx.js @@ -100,6 +100,8 @@ const tokenValues = Object.assign( 'claude-3-5-haiku': { prompt: 0.8, completion: 4 }, 'claude-3.5-haiku': { prompt: 0.8, completion: 4 }, 'claude-3-haiku': { prompt: 0.25, completion: 1.25 }, + 'claude-sonnet-4': { prompt: 3, completion: 15 }, + 'claude-opus-4': { prompt: 15, completion: 75 }, 'claude-2.1': { prompt: 8, completion: 24 }, 'claude-2': { prompt: 8, completion: 24 }, 'claude-instant': { prompt: 0.8, completion: 2.4 }, @@ -162,6 +164,8 @@ 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-sonnet-4': { write: 3.75, read: 0.3 }, + 'claude-opus-4': { write: 18.75, read: 1.5 }, }; /** diff --git a/api/models/tx.spec.js b/api/models/tx.spec.js index 97a730232d..1c886c1994 100644 --- a/api/models/tx.spec.js +++ b/api/models/tx.spec.js @@ -664,3 +664,97 @@ describe('Grok Model Tests - Pricing', () => { }); }); }); + +describe('Claude Model Tests', () => { + it('should return correct prompt and completion rates for Claude 4 models', () => { + expect(getMultiplier({ model: 'claude-sonnet-4', tokenType: 'prompt' })).toBe( + tokenValues['claude-sonnet-4'].prompt, + ); + expect(getMultiplier({ model: 'claude-sonnet-4', tokenType: 'completion' })).toBe( + tokenValues['claude-sonnet-4'].completion, + ); + expect(getMultiplier({ model: 'claude-opus-4', tokenType: 'prompt' })).toBe( + tokenValues['claude-opus-4'].prompt, + ); + expect(getMultiplier({ model: 'claude-opus-4', tokenType: 'completion' })).toBe( + tokenValues['claude-opus-4'].completion, + ); + }); + + it('should handle Claude 4 model name variations with different prefixes and suffixes', () => { + const modelVariations = [ + 'claude-sonnet-4', + 'claude-sonnet-4-20240229', + 'claude-sonnet-4-latest', + 'anthropic/claude-sonnet-4', + 'claude-sonnet-4/anthropic', + 'claude-sonnet-4-preview', + 'claude-sonnet-4-20240229-preview', + 'claude-opus-4', + 'claude-opus-4-20240229', + 'claude-opus-4-latest', + 'anthropic/claude-opus-4', + 'claude-opus-4/anthropic', + 'claude-opus-4-preview', + 'claude-opus-4-20240229-preview', + ]; + + modelVariations.forEach((model) => { + const valueKey = getValueKey(model); + const isSonnet = model.includes('sonnet'); + const expectedKey = isSonnet ? 'claude-sonnet-4' : 'claude-opus-4'; + + expect(valueKey).toBe(expectedKey); + expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(tokenValues[expectedKey].prompt); + expect(getMultiplier({ model, tokenType: 'completion' })).toBe( + tokenValues[expectedKey].completion, + ); + }); + }); + + it('should return correct cache rates for Claude 4 models', () => { + expect(getCacheMultiplier({ model: 'claude-sonnet-4', cacheType: 'write' })).toBe( + cacheTokenValues['claude-sonnet-4'].write, + ); + expect(getCacheMultiplier({ model: 'claude-sonnet-4', cacheType: 'read' })).toBe( + cacheTokenValues['claude-sonnet-4'].read, + ); + expect(getCacheMultiplier({ model: 'claude-opus-4', cacheType: 'write' })).toBe( + cacheTokenValues['claude-opus-4'].write, + ); + expect(getCacheMultiplier({ model: 'claude-opus-4', cacheType: 'read' })).toBe( + cacheTokenValues['claude-opus-4'].read, + ); + }); + + it('should handle Claude 4 model cache rates with different prefixes and suffixes', () => { + const modelVariations = [ + 'claude-sonnet-4', + 'claude-sonnet-4-20240229', + 'claude-sonnet-4-latest', + 'anthropic/claude-sonnet-4', + 'claude-sonnet-4/anthropic', + 'claude-sonnet-4-preview', + 'claude-sonnet-4-20240229-preview', + 'claude-opus-4', + 'claude-opus-4-20240229', + 'claude-opus-4-latest', + 'anthropic/claude-opus-4', + 'claude-opus-4/anthropic', + 'claude-opus-4-preview', + 'claude-opus-4-20240229-preview', + ]; + + modelVariations.forEach((model) => { + const isSonnet = model.includes('sonnet'); + const expectedKey = isSonnet ? 'claude-sonnet-4' : 'claude-opus-4'; + + expect(getCacheMultiplier({ model, cacheType: 'write' })).toBe( + cacheTokenValues[expectedKey].write, + ); + expect(getCacheMultiplier({ model, cacheType: 'read' })).toBe( + cacheTokenValues[expectedKey].read, + ); + }); + }); +}); diff --git a/api/server/cleanup.js b/api/server/cleanup.js index 6d5b77196a..93ae8d805e 100644 --- a/api/server/cleanup.js +++ b/api/server/cleanup.js @@ -16,17 +16,17 @@ const FinalizationRegistry = global.FinalizationRegistry || null; */ const clientRegistry = FinalizationRegistry ? new FinalizationRegistry((heldValue) => { - try { - // This will run when the client is garbage collected - if (heldValue && heldValue.userId) { - logger.debug(`[FinalizationRegistry] Cleaning up client for user ${heldValue.userId}`); - } else { - logger.debug('[FinalizationRegistry] Cleaning up client'); + try { + // This will run when the client is garbage collected + if (heldValue && heldValue.userId) { + logger.debug(`[FinalizationRegistry] Cleaning up client for user ${heldValue.userId}`); + } else { + logger.debug('[FinalizationRegistry] Cleaning up client'); + } + } catch (e) { + // Ignore errors } - } catch (e) { - // Ignore errors - } - }) + }) : null; /** @@ -134,8 +134,8 @@ function disposeClient(client) { if (client.message_delta) { client.message_delta = null; } - if (client.isClaude3 !== undefined) { - client.isClaude3 = null; + if (client.isClaudeLatest !== undefined) { + client.isClaudeLatest = null; } if (client.useMessages !== undefined) { client.useMessages = null; diff --git a/api/server/services/Endpoints/anthropic/helpers.js b/api/server/services/Endpoints/anthropic/helpers.js index 04e4efc61c..60040ed984 100644 --- a/api/server/services/Endpoints/anthropic/helpers.js +++ b/api/server/services/Endpoints/anthropic/helpers.js @@ -15,20 +15,14 @@ function checkPromptCacheSupport(modelName) { return false; } - if ( - modelMatch === 'claude-3-7-sonnet' || - modelMatch === 'claude-3-5-sonnet' || - modelMatch === 'claude-3-5-haiku' || - modelMatch === 'claude-3-haiku' || - modelMatch === 'claude-3-opus' || - modelMatch === 'claude-3.7-sonnet' || - modelMatch === 'claude-3.5-sonnet' || - modelMatch === 'claude-3.5-haiku' - ) { - return true; - } - - return false; + return ( + /claude-3[-.]7/.test(modelMatch) || + /claude-3[-.]5-(?:sonnet|haiku)/.test(modelMatch) || + /claude-3-(?:sonnet|haiku|opus)?/.test(modelMatch) || + /claude-(?:sonnet|opus|haiku)-[4-9]/.test(modelMatch) || + /claude-[4-9]-(?:sonnet|opus|haiku)?/.test(modelMatch) || + /claude-4(?:-(?:sonnet|opus|haiku))?/.test(modelMatch) + ); } /** @@ -51,6 +45,14 @@ function getClaudeHeaders(model, supportsCacheControl) { 'anthropic-beta': 'token-efficient-tools-2025-02-19,output-128k-2025-02-19,prompt-caching-2024-07-31', }; + } else if ( + /claude-(?:sonnet|opus|haiku)-[4-9]/.test(model) || + /claude-[4-9]-(?:sonnet|opus|haiku)?/.test(model) || + /claude-4(?:-(?:sonnet|opus|haiku))?/.test(model) + ) { + return { + 'anthropic-beta': 'prompt-caching-2024-07-31', + }; } else { return { 'anthropic-beta': 'prompt-caching-2024-07-31', @@ -72,7 +74,8 @@ function configureReasoning(anthropicInput, extendedOptions = {}) { if ( extendedOptions.thinking && updatedOptions?.model && - /claude-3[-.]7/.test(updatedOptions.model) + (/claude-3[-.]7/.test(updatedOptions.model) || + /claude-(?:sonnet|opus|haiku)-[4-9]/.test(updatedOptions.model)) ) { updatedOptions.thinking = { type: 'enabled', diff --git a/api/utils/tokens.js b/api/utils/tokens.js index 7ff59acfdd..21608fddc6 100644 --- a/api/utils/tokens.js +++ b/api/utils/tokens.js @@ -105,6 +105,9 @@ const anthropicModels = { 'claude-3.7-sonnet': 200000, 'claude-3-5-sonnet-latest': 200000, 'claude-3.5-sonnet-latest': 200000, + 'claude-sonnet-4': 200000, + 'claude-opus-4': 200000, + 'claude-4': 200000, }; const deepseekModels = { @@ -246,6 +249,8 @@ const anthropicMaxOutputs = { 'claude-3-haiku': 4096, 'claude-3-sonnet': 4096, 'claude-3-opus': 4096, + 'claude-opus-4': 32000, + 'claude-sonnet-4': 64000, 'claude-3.5-sonnet': 8192, 'claude-3-5-sonnet': 8192, 'claude-3.7-sonnet': 128000, diff --git a/api/utils/tokens.spec.js b/api/utils/tokens.spec.js index 57a9f72e89..4a34746e8b 100644 --- a/api/utils/tokens.spec.js +++ b/api/utils/tokens.spec.js @@ -649,3 +649,58 @@ describe('Grok Model Tests - Tokens', () => { }); }); }); + +describe('Claude Model Tests', () => { + it('should return correct context length for Claude 4 models', () => { + expect(getModelMaxTokens('claude-sonnet-4')).toBe(200000); + expect(getModelMaxTokens('claude-opus-4')).toBe(200000); + }); + + it('should handle Claude 4 model name variations with different prefixes and suffixes', () => { + const modelVariations = [ + 'claude-sonnet-4', + 'claude-sonnet-4-20240229', + 'claude-sonnet-4-latest', + 'anthropic/claude-sonnet-4', + 'claude-sonnet-4/anthropic', + 'claude-sonnet-4-preview', + 'claude-sonnet-4-20240229-preview', + 'claude-opus-4', + 'claude-opus-4-20240229', + 'claude-opus-4-latest', + 'anthropic/claude-opus-4', + 'claude-opus-4/anthropic', + 'claude-opus-4-preview', + 'claude-opus-4-20240229-preview', + ]; + + modelVariations.forEach((model) => { + expect(getModelMaxTokens(model)).toBe(200000); + }); + }); + + it('should match model names correctly for Claude 4 models', () => { + const modelVariations = [ + 'claude-sonnet-4', + 'claude-sonnet-4-20240229', + 'claude-sonnet-4-latest', + 'anthropic/claude-sonnet-4', + 'claude-sonnet-4/anthropic', + 'claude-sonnet-4-preview', + 'claude-sonnet-4-20240229-preview', + 'claude-opus-4', + 'claude-opus-4-20240229', + 'claude-opus-4-latest', + 'anthropic/claude-opus-4', + 'claude-opus-4/anthropic', + 'claude-opus-4-preview', + 'claude-opus-4-20240229-preview', + ]; + + modelVariations.forEach((model) => { + const isSonnet = model.includes('sonnet'); + const expectedModel = isSonnet ? 'claude-sonnet-4' : 'claude-opus-4'; + expect(matchModelName(model, EModelEndpoint.anthropic)).toBe(expectedModel); + }); + }); +}); diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index e40a662fdc..1d727470ff 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -735,6 +735,10 @@ const sharedOpenAIModels = [ ]; const sharedAnthropicModels = [ + 'claude-sonnet-4-20250514', + 'claude-sonnet-4-latest', + 'claude-opus-4-20250514', + 'claude-opus-4-latest', 'claude-3-7-sonnet-latest', 'claude-3-7-sonnet-20250219', 'claude-3-5-haiku-20241022', @@ -904,6 +908,9 @@ export const visionModels = [ 'llama-3.2-90b-vision', 'llama-3-2-90b-vision', 'llama-4', + 'claude-opus-4', + 'claude-sonnet-4', + 'claude-haiku-4', ]; export enum VisionModes { generative = 'generative',