diff --git a/.env.example b/.env.example index 4fe3ac4886..7779276bcc 100644 --- a/.env.example +++ b/.env.example @@ -140,7 +140,7 @@ PROXY= #============# ANTHROPIC_API_KEY=user_provided -# ANTHROPIC_MODELS=claude-sonnet-4-6,claude-opus-4-6,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_MODELS=claude-opus-4-7,claude-sonnet-4-6,claude-opus-4-6,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= # Set to true to use Anthropic models through Google Vertex AI instead of direct API @@ -175,8 +175,8 @@ ANTHROPIC_API_KEY=user_provided # BEDROCK_AWS_SESSION_TOKEN=someSessionToken # Note: This example list is not meant to be exhaustive. If omitted, all known, supported model IDs will be included for you. -# BEDROCK_AWS_MODELS=anthropic.claude-sonnet-4-6,anthropic.claude-opus-4-6-v1,anthropic.claude-3-5-sonnet-20240620-v1:0,meta.llama3-1-8b-instruct-v1:0 -# Cross-region inference model IDs: us.anthropic.claude-sonnet-4-6,us.anthropic.claude-opus-4-6-v1,global.anthropic.claude-opus-4-6-v1 +# BEDROCK_AWS_MODELS=anthropic.claude-opus-4-7,anthropic.claude-sonnet-4-6,anthropic.claude-opus-4-6-v1,anthropic.claude-3-5-sonnet-20240620-v1:0,meta.llama3-1-8b-instruct-v1:0 +# Cross-region inference model IDs: us.anthropic.claude-opus-4-7,us.anthropic.claude-sonnet-4-6,us.anthropic.claude-opus-4-6-v1,global.anthropic.claude-opus-4-6-v1 # See all Bedrock model IDs here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns diff --git a/api/utils/tokens.spec.js b/api/utils/tokens.spec.js index dfa6762ee5..79073ba83c 100644 --- a/api/utils/tokens.spec.js +++ b/api/utils/tokens.spec.js @@ -1377,6 +1377,37 @@ describe('Claude Model Tests', () => { }); }); + it('should return correct context length for Claude Opus 4.7 (1M)', () => { + expect(getModelMaxTokens('claude-opus-4-7', EModelEndpoint.anthropic)).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-opus-4-7'], + ); + expect(getModelMaxTokens('claude-opus-4-7')).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-opus-4-7'], + ); + }); + + it('should return correct max output tokens for Claude Opus 4.7 (128K)', () => { + const { getModelMaxOutputTokens } = require('@librechat/api'); + expect(getModelMaxOutputTokens('claude-opus-4-7', EModelEndpoint.anthropic)).toBe( + maxOutputTokensMap[EModelEndpoint.anthropic]['claude-opus-4-7'], + ); + }); + + it('should match model names correctly for Claude Opus 4.7', () => { + const modelVariations = [ + 'claude-opus-4-7', + 'claude-opus-4-7-20260401', + 'claude-opus-4-7-latest', + 'anthropic/claude-opus-4-7', + 'claude-opus-4-7/anthropic', + 'claude-opus-4-7-preview', + ]; + + modelVariations.forEach((model) => { + expect(matchModelName(model, EModelEndpoint.anthropic)).toBe('claude-opus-4-7'); + }); + }); + it('should return correct context length for Claude Sonnet 4.6 (1M)', () => { expect(getModelMaxTokens('claude-sonnet-4-6', EModelEndpoint.anthropic)).toBe( maxTokensMap[EModelEndpoint.anthropic]['claude-sonnet-4-6'], diff --git a/packages/api/src/endpoints/anthropic/llm.spec.ts b/packages/api/src/endpoints/anthropic/llm.spec.ts index b945eacb34..e4b893d487 100644 --- a/packages/api/src/endpoints/anthropic/llm.spec.ts +++ b/packages/api/src/endpoints/anthropic/llm.spec.ts @@ -1026,6 +1026,21 @@ describe('getLLMConfig', () => { }); }); + it('should set xhigh effort via output_config for Opus 4.7', () => { + const result = getLLMConfig('test-key', { + modelOptions: { + model: 'claude-opus-4-7', + thinking: true, + effort: 'xhigh' as AnthropicEffort, + }, + }); + + expect((result.llmConfig.thinking as unknown as { type: string }).type).toBe('adaptive'); + expect(result.llmConfig.invocationKwargs?.output_config).toEqual({ + effort: 'xhigh', + }); + }); + it('should exclude topP/topK for Sonnet 4.6 with adaptive thinking', () => { const result = getLLMConfig('test-key', { modelOptions: { diff --git a/packages/api/src/utils/tokens.ts b/packages/api/src/utils/tokens.ts index 14215698a6..cd7f5f9ac5 100644 --- a/packages/api/src/utils/tokens.ts +++ b/packages/api/src/utils/tokens.ts @@ -141,6 +141,7 @@ const anthropicModels = { 'claude-sonnet-4': 1000000, 'claude-sonnet-4-6': 1000000, 'claude-opus-4-6': 1000000, + 'claude-opus-4-7': 1000000, }; const deepseekModels = { @@ -386,6 +387,7 @@ const anthropicMaxOutputs = { 'claude-opus-4': 32000, 'claude-opus-4-5': 64000, 'claude-opus-4-6': 128000, + 'claude-opus-4-7': 128000, 'claude-3.5-sonnet': 8192, 'claude-3-5-sonnet': 8192, 'claude-3.7-sonnet': 128000, diff --git a/packages/data-provider/specs/bedrock.spec.ts b/packages/data-provider/specs/bedrock.spec.ts index 1398b17b25..014fe0dec0 100644 --- a/packages/data-provider/specs/bedrock.spec.ts +++ b/packages/data-provider/specs/bedrock.spec.ts @@ -144,6 +144,10 @@ describe('supportsContext1m', () => { expect(supportsContext1m('claude-opus-4-6')).toBe(true); }); + test('should return true for claude-opus-4-7', () => { + expect(supportsContext1m('claude-opus-4-7')).toBe(true); + }); + test('should return true for claude-opus-5 (future)', () => { expect(supportsContext1m('claude-opus-5')).toBe(true); }); @@ -437,6 +441,18 @@ describe('bedrockInputParser', () => { expect(additionalFields.effort).toBeUndefined(); }); + test('should pass xhigh effort via output_config for adaptive models (Opus 4.7)', () => { + const input = { + model: 'anthropic.claude-opus-4-7', + effort: 'xhigh', + }; + const result = bedrockInputParser.parse(input) as Record; + const additionalFields = result.additionalModelRequestFields as Record; + expect(additionalFields.thinking).toEqual({ type: 'adaptive' }); + expect(additionalFields.output_config).toEqual({ effort: 'xhigh' }); + expect(additionalFields.effort).toBeUndefined(); + }); + test('should not include output_config when effort is unset (empty string)', () => { const input = { model: 'anthropic.claude-opus-4-6-v1', diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 73b1a86cb2..c0f614decd 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1238,6 +1238,7 @@ const sharedOpenAIModels = [ ]; const sharedAnthropicModels = [ + 'claude-opus-4-7', 'claude-sonnet-4-6', 'claude-opus-4-6', 'claude-sonnet-4-5', @@ -1260,6 +1261,7 @@ const sharedAnthropicModels = [ ]; export const bedrockModels = [ + 'anthropic.claude-opus-4-7', 'anthropic.claude-sonnet-4-6', 'anthropic.claude-opus-4-6-v1', 'anthropic.claude-sonnet-4-5-20250929-v1:0', diff --git a/packages/data-provider/src/parameterSettings.ts b/packages/data-provider/src/parameterSettings.ts index d0cfdf210f..ab98417ac4 100644 --- a/packages/data-provider/src/parameterSettings.ts +++ b/packages/data-provider/src/parameterSettings.ts @@ -462,6 +462,7 @@ const anthropic: Record = { [AnthropicEffort.low]: 'com_ui_low', [AnthropicEffort.medium]: 'com_ui_medium', [AnthropicEffort.high]: 'com_ui_high', + [AnthropicEffort.xhigh]: 'com_ui_xhigh', [AnthropicEffort.max]: 'com_ui_max', }, optionType: 'model', diff --git a/packages/data-provider/src/schemas.spec.ts b/packages/data-provider/src/schemas.spec.ts index db7d370606..34cd72a346 100644 --- a/packages/data-provider/src/schemas.spec.ts +++ b/packages/data-provider/src/schemas.spec.ts @@ -1,4 +1,4 @@ -import { anthropicSettings } from './schemas'; +import { AnthropicEffort, anthropicSettings, eAnthropicEffortSchema } from './schemas'; describe('anthropicSettings', () => { describe('maxOutputTokens.reset()', () => { @@ -353,3 +353,25 @@ describe('anthropicSettings', () => { }); }); }); + +describe('AnthropicEffort', () => { + it('exposes xhigh between high and max in the enum', () => { + expect(AnthropicEffort.xhigh).toBe('xhigh'); + const keys = Object.keys(AnthropicEffort); + expect(keys.indexOf('xhigh')).toBeGreaterThan(keys.indexOf('high')); + expect(keys.indexOf('xhigh')).toBeLessThan(keys.indexOf('max')); + }); + + it('includes xhigh in anthropicSettings.effort.options', () => { + expect(anthropicSettings.effort.options).toContain(AnthropicEffort.xhigh); + }); + + it('accepts xhigh through the zod schema', () => { + expect(eAnthropicEffortSchema.parse('xhigh')).toBe('xhigh'); + expect(eAnthropicEffortSchema.parse(AnthropicEffort.xhigh)).toBe('xhigh'); + }); + + it('rejects unknown effort values', () => { + expect(() => eAnthropicEffortSchema.parse('ultra')).toThrow(); + }); +}); diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 084f74af86..45c0653f92 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -182,6 +182,7 @@ export enum AnthropicEffort { low = 'low', medium = 'medium', high = 'high', + xhigh = 'xhigh', max = 'max', } @@ -491,6 +492,7 @@ export const anthropicSettings = { AnthropicEffort.low, AnthropicEffort.medium, AnthropicEffort.high, + AnthropicEffort.xhigh, AnthropicEffort.max, ], }, diff --git a/packages/data-schemas/src/methods/tx.spec.ts b/packages/data-schemas/src/methods/tx.spec.ts index d1e12e5a55..a2d1199afc 100644 --- a/packages/data-schemas/src/methods/tx.spec.ts +++ b/packages/data-schemas/src/methods/tx.spec.ts @@ -2279,6 +2279,46 @@ describe('Claude Model Tests', () => { ); }); }); + + it('should return correct prompt and completion rates for Claude Opus 4.7', () => { + expect(getMultiplier({ model: 'claude-opus-4-7', tokenType: 'prompt' })).toBe( + tokenValues['claude-opus-4-7'].prompt, + ); + expect(getMultiplier({ model: 'claude-opus-4-7', tokenType: 'completion' })).toBe( + tokenValues['claude-opus-4-7'].completion, + ); + }); + + it('should handle Claude Opus 4.7 model name variations', () => { + const modelVariations = [ + 'claude-opus-4-7', + 'claude-opus-4-7-20260401', + 'claude-opus-4-7-latest', + 'anthropic/claude-opus-4-7', + 'claude-opus-4-7/anthropic', + 'claude-opus-4-7-preview', + ]; + + modelVariations.forEach((model) => { + const valueKey = getValueKey(model); + expect(valueKey).toBe('claude-opus-4-7'); + expect(getMultiplier({ model, tokenType: 'prompt' })).toBe( + tokenValues['claude-opus-4-7'].prompt, + ); + expect(getMultiplier({ model, tokenType: 'completion' })).toBe( + tokenValues['claude-opus-4-7'].completion, + ); + }); + }); + + it('should return correct cache rates for Claude Opus 4.7', () => { + expect(getCacheMultiplier({ model: 'claude-opus-4-7', cacheType: 'write' })).toBe( + cacheTokenValues['claude-opus-4-7'].write, + ); + expect(getCacheMultiplier({ model: 'claude-opus-4-7', cacheType: 'read' })).toBe( + cacheTokenValues['claude-opus-4-7'].read, + ); + }); }); describe('Premium Token Pricing', () => { @@ -2298,6 +2338,16 @@ describe('Premium Token Pricing', () => { expect(premiumEntry.completion).toBeGreaterThan(tokenValues[premiumModel].completion); }); + it('should have premium pricing defined for claude-opus-4-7', () => { + const entry = premiumTokenValues['claude-opus-4-7']; + expect(entry).toBeDefined(); + expect(entry.threshold).toBe(200000); + expect(entry.prompt).toBe(10); + expect(entry.completion).toBe(37.5); + expect(entry.prompt).toBeGreaterThan(tokenValues['claude-opus-4-7'].prompt); + expect(entry.completion).toBeGreaterThan(tokenValues['claude-opus-4-7'].completion); + }); + it('should return null from getPremiumRate when inputTokenCount is below threshold', () => { expect(getPremiumRate(premiumModel, 'prompt', belowThreshold)).toBeNull(); expect(getPremiumRate(premiumModel, 'completion', belowThreshold)).toBeNull(); diff --git a/packages/data-schemas/src/methods/tx.ts b/packages/data-schemas/src/methods/tx.ts index a048874457..26bf13b0b6 100644 --- a/packages/data-schemas/src/methods/tx.ts +++ b/packages/data-schemas/src/methods/tx.ts @@ -157,6 +157,7 @@ export const tokenValues: Record 'claude-opus-4': { prompt: 15, completion: 75 }, 'claude-opus-4-5': { prompt: 5, completion: 25 }, 'claude-opus-4-6': { prompt: 5, completion: 25 }, + 'claude-opus-4-7': { prompt: 5, completion: 25 }, 'claude-sonnet-4': { prompt: 3, completion: 15 }, 'claude-sonnet-4-6': { prompt: 3, completion: 15 }, 'command-r': { prompt: 0.5, completion: 1.5 }, @@ -289,6 +290,7 @@ export const cacheTokenValues: Record = 'claude-opus-4': { write: 18.75, read: 1.5 }, 'claude-opus-4-5': { write: 6.25, read: 0.5 }, 'claude-opus-4-6': { write: 6.25, read: 0.5 }, + 'claude-opus-4-7': { write: 6.25, read: 0.5 }, 'gpt-4o': { write: 2.5, read: 1.25 }, 'gpt-4o-mini': { write: 0.15, read: 0.075 }, 'gpt-4.1': { write: 2, read: 0.5 }, @@ -335,6 +337,7 @@ export const premiumTokenValues: Record< { threshold: number; prompt: number; completion: number } > = { 'claude-opus-4-6': { threshold: 200000, prompt: 10, completion: 37.5 }, + 'claude-opus-4-7': { threshold: 200000, prompt: 10, completion: 37.5 }, 'claude-sonnet-4-6': { threshold: 200000, prompt: 6, completion: 22.5 }, 'gemini-3.1': { threshold: 200000, prompt: 4, completion: 18 }, };