🦉 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:
Danny Avila 2026-04-16 14:51:00 -04:00 committed by GitHub
parent 49f228de78
commit e2e3284713
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 148 additions and 4 deletions

View file

@ -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

View file

@ -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'],

View file

@ -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: {

View file

@ -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,

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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();
});
});

View file

@ -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,
],
},

View file

@ -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();

View file

@ -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 },
};