mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-05-13 07:46:47 +00:00
🦉 feat: Claude Opus 4.7 Model Support (#12698)
* 🦉 feat: Claude Opus 4.7 Model Support
- Add `claude-opus-4-7` to shared Anthropic models and `anthropic.claude-opus-4-7` to Bedrock models
- Register 1M context window and 128K max output in anthropic token maps
- Add token pricing ($5/$25), cache rates (6.25/0.5), and premium tier ($10/$37.50 above 200K) in tx.ts
- Update `.env.example` with Opus 4.7 IDs in `ANTHROPIC_MODELS` and `BEDROCK_AWS_MODELS` examples
- Add parallel Opus 4.7 test cases for token/cache/premium rates, context length, max output, name-variation matching, and 1M-context qualification
* feat: Add `xhigh` Effort Level for Opus 4.7
- Add `xhigh` variant to `AnthropicEffort` enum between `high` and `max`
- Expose `xhigh` in `anthropicSettings.effort.options` and the UI slider `enumMappings`
- Reuse existing `com_ui_xhigh` translation key
* test: Cover `xhigh` Effort and Exact Opus 4.7 Premium Rates
- Assert `xhigh` position (between high and max), inclusion in
`anthropicSettings.effort.options`, zod acceptance, and rejection of
unknown values in schemas.spec.ts
- Verify bedrockInputParser emits `output_config: { effort: 'xhigh' }`
for adaptive `anthropic.claude-opus-4-7`
- Verify getLLMConfig sets adaptive thinking and `output_config.effort =
'xhigh'` for `claude-opus-4-7`
- Pin Opus 4.7 premium pricing to exact threshold/prompt/completion
values (200000 / 10 / 37.5) so silent rate drift fails the test
This commit is contained in:
parent
49f228de78
commit
e2e3284713
11 changed files with 148 additions and 4 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
const additionalFields = result.additionalModelRequestFields as Record<string, unknown>;
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -462,6 +462,7 @@ const anthropic: Record<string, SettingDefinition> = {
|
|||
[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',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ export const tokenValues: Record<string, { prompt: number; completion: number }>
|
|||
'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<string, { write: number; read: number }> =
|
|||
'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 },
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue