🧠 feat: Add Claude Opus 4.8 Support (#13380)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions

* feat: add Claude Opus 4.8 support

* fix: omit sampling params for Claude Opus 4.8

* fix: flatten Bedrock beta header merge

* fix: strip Bedrock sampling params for Opus 4.8
This commit is contained in:
Danny Avila 2026-05-28 13:50:39 -07:00 committed by GitHub
parent 0d981b08d8
commit 62dff69300
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 234 additions and 17 deletions

View file

@ -171,7 +171,7 @@ PROXY=
#============#
ANTHROPIC_API_KEY=user_provided
# ANTHROPIC_MODELS=claude-opus-4-7,claude-sonnet-4-6,claude-opus-4-6,claude-opus-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-8,claude-opus-4-7,claude-sonnet-4-6,claude-opus-4-6,claude-opus-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
@ -237,8 +237,8 @@ ANTHROPIC_API_KEY=user_provided
# BEDROCK_AWS_BEARER_TOKEN=yourBedrockApiKey
# 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-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
# BEDROCK_AWS_MODELS=anthropic.claude-opus-4-8,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-8,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

@ -1459,6 +1459,37 @@ describe('Claude Model Tests', () => {
});
});
it('should return correct context length for Claude Opus 4.8 (1M)', () => {
expect(getModelMaxTokens('claude-opus-4-8', EModelEndpoint.anthropic)).toBe(
maxTokensMap[EModelEndpoint.anthropic]['claude-opus-4-8'],
);
expect(getModelMaxTokens('claude-opus-4-8')).toBe(
maxTokensMap[EModelEndpoint.anthropic]['claude-opus-4-8'],
);
});
it('should return correct max output tokens for Claude Opus 4.8 (128K)', () => {
const { getModelMaxOutputTokens } = require('@librechat/api');
expect(getModelMaxOutputTokens('claude-opus-4-8', EModelEndpoint.anthropic)).toBe(
maxOutputTokensMap[EModelEndpoint.anthropic]['claude-opus-4-8'],
);
});
it('should match model names correctly for Claude Opus 4.8', () => {
const modelVariations = [
'claude-opus-4-8',
'claude-opus-4-8-20260528',
'claude-opus-4-8-latest',
'anthropic/claude-opus-4-8',
'claude-opus-4-8/anthropic',
'claude-opus-4-8-preview',
];
modelVariations.forEach((model) => {
expect(matchModelName(model, EModelEndpoint.anthropic)).toBe('claude-opus-4-8');
});
});
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

@ -223,7 +223,7 @@
"com_endpoint_agent": "Agent",
"com_endpoint_agent_placeholder": "Please select an Agent",
"com_endpoint_ai": "AI",
"com_endpoint_anthropic_effort": "Controls how much computational effort Claude applies. Lower effort saves tokens and reduces latency; higher effort produces more thorough responses. 'Max' enables the deepest reasoning (Opus 4.6 only).",
"com_endpoint_anthropic_effort": "Controls how much computational effort Claude applies. Lower effort saves tokens and reduces latency; higher effort produces more thorough responses. 'Max' enables the deepest reasoning on supported adaptive-thinking models.",
"com_endpoint_anthropic_maxoutputtokens": "Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. Note: models may stop before reaching this maximum.",
"com_endpoint_anthropic_prompt_cache": "Prompt caching allows reusing large context or instructions across API calls, reducing costs and latency",
"com_endpoint_anthropic_temp": "Ranges from 0 to 1. Use temp closer to 0 for analytical / multiple choice, and closer to 1 for creative and generative tasks. We recommend altering this or Top P but not both.",

View file

@ -401,6 +401,7 @@ endpoints:
# # Option 1: Simple array (legacy format - model name = deployment name)
# # Use this if you want the technical model IDs to show in the UI
# # models:
# # - "claude-opus-4-8"
# # - "claude-sonnet-4-6"
# # - "claude-3-7-sonnet-20250219"
# # - "claude-3-5-sonnet-v2@20241022"
@ -411,6 +412,8 @@ endpoints:
# # The deploymentName is the actual Vertex AI model ID used for API calls
# # You can use friendly names (avoid spaces for cleaner YAML) or technical IDs as keys
# models:
# claude-opus-4.8:
# deploymentName: claude-opus-4-8
# claude-opus-4.5:
# deploymentName: claude-opus-4-5@20251101
# claude-sonnet-4:

View file

@ -888,7 +888,7 @@ describe('getLLMConfig', () => {
expect(opus64kResult.llmConfig.maxTokens).toBe(64000);
// opus-4-6+ get 128K
const opus128kModels = ['claude-opus-4-7', 'claude-opus-4-10'];
const opus128kModels = ['claude-opus-4-7', 'claude-opus-4-8', 'claude-opus-4-10'];
opus128kModels.forEach((model) => {
const result = getLLMConfig('test-key', {
modelOptions: { model },
@ -1066,6 +1066,22 @@ describe('getLLMConfig', () => {
expect(thinking.display).toBe('summarized');
});
it('should omit sampling parameters for Opus 4.8', () => {
const result = getLLMConfig('test-key', {
modelOptions: {
model: 'claude-opus-4-8',
thinking: false,
temperature: 0.7,
topP: 0.9,
topK: 40,
},
});
expect(result.llmConfig).not.toHaveProperty('temperature');
expect(result.llmConfig).not.toHaveProperty('topP');
expect(result.llmConfig).not.toHaveProperty('topK');
});
it('should NOT set thinking.display for pre-Opus-4.7 adaptive models', () => {
const pre47Models = ['claude-opus-4-6', 'claude-sonnet-4-6'];

View file

@ -3,6 +3,7 @@ import { logger } from '@librechat/data-schemas';
import { AnthropicClientOptions } from '@librechat/agents';
import {
anthropicSettings,
omitsSamplingParameters,
removeNullishValues,
ThinkingDisplay,
AuthKeys,
@ -177,9 +178,12 @@ function getLLMConfig(
);
}
const resolvedModel = requestOptions.model ?? mergedOptions.model;
const shouldOmitSamplingParameters = omitsSamplingParameters(resolvedModel);
requestOptions = configureReasoning(requestOptions, systemOptions);
if (supportsAdaptiveThinking(mergedOptions.model)) {
if (supportsAdaptiveThinking(resolvedModel)) {
if (
systemOptions.effort &&
(systemOptions.effort as string) !== '' &&
@ -204,8 +208,8 @@ function getLLMConfig(
const hasActiveThinking = requestOptions.thinking != null;
const isThinkingModel =
/claude-3[-.]7/.test(mergedOptions.model) || supportsAdaptiveThinking(mergedOptions.model);
if (!isThinkingModel || !hasActiveThinking) {
/claude-3[-.]7/.test(resolvedModel) || supportsAdaptiveThinking(resolvedModel);
if (!shouldOmitSamplingParameters && (!isThinkingModel || !hasActiveThinking)) {
requestOptions.topP = mergedOptions.topP;
requestOptions.topK = mergedOptions.topK;
}
@ -293,6 +297,12 @@ function getLLMConfig(
});
}
if (shouldOmitSamplingParameters) {
delete requestOptions.temperature;
delete requestOptions.topP;
delete requestOptions.topK;
}
const tools = [];
if (enableWebSearch) {

View file

@ -146,6 +146,7 @@ const anthropicModels = {
'claude-sonnet-4-6': 1000000,
'claude-opus-4-6': 1000000,
'claude-opus-4-7': 1000000,
'claude-opus-4-8': 1000000,
};
const deepseekModels = {
@ -394,6 +395,7 @@ const anthropicMaxOutputs = {
'claude-opus-4-5': 64000,
'claude-opus-4-6': 128000,
'claude-opus-4-7': 128000,
'claude-opus-4-8': 128000,
'claude-3.5-sonnet': 8192,
'claude-3-5-sonnet': 8192,
'claude-3.7-sonnet': 128000,

View file

@ -2,6 +2,7 @@ import { ThinkingDisplay } from '../src/schemas';
import {
BEDROCK_OUTPUT_128K_BETA,
supportsAdaptiveThinking,
omitsSamplingParameters,
omitsThinkingByDefault,
resolveThinkingDisplay,
bedrockOutputParser,
@ -222,7 +223,7 @@ describe('omitsThinkingByDefault', () => {
expect(omitsThinkingByDefault('us.anthropic.claude-opus-4-7')).toBe(true);
});
test('returns true for claude-opus-4-8 (future Opus 4.x)', () => {
test('returns true for claude-opus-4-8', () => {
expect(omitsThinkingByDefault('claude-opus-4-8')).toBe(true);
});
@ -238,6 +239,11 @@ describe('omitsThinkingByDefault', () => {
expect(omitsThinkingByDefault('claude-opus-4-6')).toBe(false);
});
test('returns false for base Opus 4 snapshot IDs', () => {
expect(omitsThinkingByDefault('claude-opus-4-20250514')).toBe(false);
expect(omitsThinkingByDefault('anthropic.claude-opus-4-20250514-v1:0')).toBe(false);
});
test('returns false for claude-opus-4-5', () => {
expect(omitsThinkingByDefault('claude-opus-4-5')).toBe(false);
});
@ -264,6 +270,36 @@ describe('omitsThinkingByDefault', () => {
});
});
describe('omitsSamplingParameters', () => {
test('returns true for Opus 4.7+ models', () => {
const models = [
'claude-opus-4-7',
'claude-opus-4-8',
'anthropic.claude-opus-4-8',
'us.anthropic.claude-opus-4-8',
'claude-opus-5',
];
models.forEach((model) => {
expect(omitsSamplingParameters(model)).toBe(true);
});
});
test('returns false for older Opus and non-Opus models', () => {
const models = [
'claude-opus-4-20250514',
'anthropic.claude-opus-4-20250514-v1:0',
'claude-opus-4-1-20250805',
'claude-opus-4-6',
'claude-sonnet-4-7',
];
models.forEach((model) => {
expect(omitsSamplingParameters(model)).toBe(false);
});
});
});
describe('resolveThinkingDisplay', () => {
test('returns "summarized" for Opus 4.7 when explicit is auto/null/undefined', () => {
expect(resolveThinkingDisplay('claude-opus-4-7', ThinkingDisplay.auto)).toBe('summarized');
@ -541,6 +577,47 @@ describe('bedrockInputParser', () => {
expect(additionalFields.effort).toBeUndefined();
});
test('should strip sampling parameters for Opus 4.8 Bedrock models', () => {
const input = {
model: 'anthropic.claude-opus-4-8',
temperature: 0.7,
topP: 0.9,
topK: 40,
top_p: 0.8,
additionalModelRequestFields: {
custom_flag: true,
temperature: 0.5,
topP: 0.95,
top_k: 20,
},
};
const result = bedrockInputParser.parse(input) as Record<string, unknown>;
const additionalFields = result.additionalModelRequestFields as Record<string, unknown>;
expect(result.temperature).toBeUndefined();
expect(result.topP).toBeUndefined();
expect(additionalFields.temperature).toBeUndefined();
expect(additionalFields.topP).toBeUndefined();
expect(additionalFields.top_p).toBeUndefined();
expect(additionalFields.top_k).toBeUndefined();
expect(additionalFields.custom_flag).toBe(true);
expect(additionalFields.thinking).toEqual({ type: 'adaptive', display: 'summarized' });
expect(additionalFields.anthropic_beta).toEqual(BEDROCK_CLAUDE_4_BETAS);
});
test('should preserve sampling parameters for Opus 4.6 Bedrock models', () => {
const input = {
model: 'anthropic.claude-opus-4-6-v1',
temperature: 0.7,
topP: 0.9,
topK: 40,
};
const result = bedrockInputParser.parse(input) as Record<string, unknown>;
const additionalFields = result.additionalModelRequestFields as Record<string, unknown>;
expect(result.temperature).toBe(0.7);
expect(result.topP).toBe(0.9);
expect(additionalFields.top_k).toBe(40);
});
test('should set thinking.display to "summarized" so Opus 4.7 returns reasoning blocks', () => {
const input = {
model: 'anthropic.claude-opus-4-7',

View file

@ -68,14 +68,14 @@ type AnthropicInput = BedrockConverseInput & {
/** Extracts opus major/minor version from both naming formats */
function parseOpusVersion(model: string): { major: number; minor: number } | null {
const nameFirst = model.match(/claude-opus[-.]?(\d+)(?:[-.](\d+))?/);
const nameFirst = model.match(/claude-opus[-.]?(\d+)(?:[-.](\d{1,2})(?!\d))?/);
if (nameFirst) {
return {
major: parseInt(nameFirst[1], 10),
minor: nameFirst[2] != null ? parseInt(nameFirst[2], 10) : 0,
};
}
const numFirst = model.match(/claude-(\d+)(?:[-.](\d+))?-opus/);
const numFirst = model.match(/claude-(\d+)(?:[-.](\d{1,2})(?!\d))?-opus/);
if (numFirst) {
return {
major: parseInt(numFirst[1], 10),
@ -136,6 +136,14 @@ export function omitsThinkingByDefault(model: string): boolean {
return false;
}
export function omitsSamplingParameters(model: string): boolean {
const opus = parseOpusVersion(model);
if (opus && (opus.major > 4 || (opus.major === 4 && opus.minor >= 7))) {
return true;
}
return false;
}
/** Checks if a model has a 1M context window (Sonnet 4.6+, Opus 4.6+, Opus 5+) */
export function supportsContext1m(model: string): boolean {
const sonnet = parseSonnetVersion(model);
@ -177,11 +185,12 @@ function getBedrockAnthropicBetaHeaders(model: string): string[] {
}
function mergeBedrockAnthropicBetaHeaders(existing: unknown, generated: string[]): string[] {
const existingValues: unknown[] = Array.isArray(existing)
? existing
: typeof existing === 'string'
? [existing]
: [];
let existingValues: unknown[] = [];
if (Array.isArray(existing)) {
existingValues = existing;
} else if (typeof existing === 'string') {
existingValues = [existing];
}
const betaHeaders = new Set<string>();
@ -308,6 +317,8 @@ export const bedrockInputParser = s.tConversationSchema
const additionalFields: Record<string, unknown> = {};
const typedData = data as Record<string, unknown>;
const shouldOmitSamplingParameters =
typeof typedData.model === 'string' && omitsSamplingParameters(typedData.model);
Object.entries(typedData).forEach(([key, value]) => {
if (!knownKeys.includes(key)) {
@ -436,6 +447,24 @@ export const bedrockInputParser = s.tConversationSchema
delete amrf.reasoning_config;
delete amrf.reasoning_effort;
}
if (shouldOmitSamplingParameters) {
delete amrf.temperature;
delete amrf.topP;
delete amrf.top_p;
delete amrf.topK;
delete amrf.top_k;
}
}
if (shouldOmitSamplingParameters) {
delete typedData.temperature;
delete typedData.topP;
delete additionalFields.temperature;
delete additionalFields.topP;
delete additionalFields.top_p;
delete additionalFields.topK;
delete additionalFields.top_k;
}
/** Default promptCache for claude and nova models, if not defined */

View file

@ -1542,6 +1542,7 @@ const sharedOpenAIModels = [
];
const sharedAnthropicModels = [
'claude-opus-4-8',
'claude-opus-4-7',
'claude-sonnet-4-6',
'claude-opus-4-6',
@ -1565,6 +1566,7 @@ const sharedAnthropicModels = [
];
export const bedrockModels = [
'anthropic.claude-opus-4-8',
'anthropic.claude-opus-4-7',
'anthropic.claude-sonnet-4-6',
'anthropic.claude-opus-4-6-v1',

View file

@ -2337,6 +2337,46 @@ describe('Claude Model Tests', () => {
cacheTokenValues['claude-opus-4-7'].read,
);
});
it('should return correct prompt and completion rates for Claude Opus 4.8', () => {
expect(getMultiplier({ model: 'claude-opus-4-8', tokenType: 'prompt' })).toBe(
tokenValues['claude-opus-4-8'].prompt,
);
expect(getMultiplier({ model: 'claude-opus-4-8', tokenType: 'completion' })).toBe(
tokenValues['claude-opus-4-8'].completion,
);
});
it('should handle Claude Opus 4.8 model name variations', () => {
const modelVariations = [
'claude-opus-4-8',
'claude-opus-4-8-20260528',
'claude-opus-4-8-latest',
'anthropic/claude-opus-4-8',
'claude-opus-4-8/anthropic',
'claude-opus-4-8-preview',
];
modelVariations.forEach((model) => {
const valueKey = getValueKey(model);
expect(valueKey).toBe('claude-opus-4-8');
expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(
tokenValues['claude-opus-4-8'].prompt,
);
expect(getMultiplier({ model, tokenType: 'completion' })).toBe(
tokenValues['claude-opus-4-8'].completion,
);
});
});
it('should return correct cache rates for Claude Opus 4.8', () => {
expect(getCacheMultiplier({ model: 'claude-opus-4-8', cacheType: 'write' })).toBe(
cacheTokenValues['claude-opus-4-8'].write,
);
expect(getCacheMultiplier({ model: 'claude-opus-4-8', cacheType: 'read' })).toBe(
cacheTokenValues['claude-opus-4-8'].read,
);
});
});
describe('Premium Token Pricing', () => {
@ -2357,7 +2397,12 @@ describe('Premium Token Pricing', () => {
});
it('should not apply premium pricing to Claude 1M GA models', () => {
const claudeModels = ['claude-opus-4-6', 'claude-opus-4-7', 'claude-sonnet-4-6'];
const claudeModels = [
'claude-opus-4-6',
'claude-opus-4-7',
'claude-opus-4-8',
'claude-sonnet-4-6',
];
claudeModels.forEach((model) => {
expect(premiumTokenValues[model]).toBeUndefined();
expect(getPremiumRate(model, 'prompt', wellAboveThreshold)).toBeNull();

View file

@ -158,6 +158,7 @@ export const tokenValues: Record<string, { prompt: number; completion: number }>
'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-opus-4-8': { prompt: 5, completion: 25 },
'claude-sonnet-4': { prompt: 3, completion: 15 },
'claude-sonnet-4-5': { prompt: 3, completion: 15 },
'claude-sonnet-4-6': { prompt: 3, completion: 15 },
@ -294,6 +295,7 @@ export const cacheTokenValues: Record<string, { write: number; read: number }> =
'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 },
'claude-opus-4-8': { 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 },