mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-18 08:28:10 +01:00
🤖 feat: Claude Sonnet 4.6 support (#11829)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* 🤖 feat: Claude Sonnet 4.6 support
- Updated .env.example to include claude-sonnet-4-6 in the list of available models.
- Enhanced token value assignments in api/models/tx.js and packages/api/src/utils/tokens.ts to accommodate claude-sonnet-4-6.
- Added tests in packages/data-provider/specs/bedrock.spec.ts to verify support for claude-sonnet-4-6 in adaptive thinking and context-1m functionalities.
- Modified bedrock.ts to correctly parse and identify the version of claude-sonnet-4-6 for adaptive thinking checks.
- Included claude-sonnet-4-6 in sharedAnthropicModels and bedrockModels for consistent model availability.
* chore: additional Claude Sonnet 4.6 tests
- Added unit tests for Claude Sonnet 4.6 in `tokens.spec.js` to verify context length and max output tokens.
- Updated `helpers.ts` documentation to reflect adaptive thinking support for Sonnet 4.6.
- Enhanced `llm.spec.ts` with tests for context headers and adaptive thinking configurations for Claude Sonnet 4.6.
- Improved `bedrock.spec.ts` to ensure correct parsing and handling of Claude Sonnet 4.6 model variations with adaptive thinking.
This commit is contained in:
parent
e710a12bfb
commit
0697e8cd60
9 changed files with 218 additions and 16 deletions
|
|
@ -135,7 +135,7 @@ PROXY=
|
|||
#============#
|
||||
|
||||
ANTHROPIC_API_KEY=user_provided
|
||||
# ANTHROPIC_MODELS=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-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
|
||||
|
|
@ -170,8 +170,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-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-6-v1,global.anthropic.claude-opus-4-6-v1
|
||||
# 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
|
||||
|
||||
# See all Bedrock model IDs here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns
|
||||
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ const tokenValues = Object.assign(
|
|||
'claude-opus-4-5': { prompt: 5, completion: 25 },
|
||||
'claude-opus-4-6': { 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 },
|
||||
'command-r-plus': { prompt: 3, completion: 15 },
|
||||
'command-text': { prompt: 1.5, completion: 2.0 },
|
||||
|
|
@ -309,6 +310,7 @@ const cacheTokenValues = {
|
|||
'claude-3-haiku': { write: 0.3, read: 0.03 },
|
||||
'claude-haiku-4-5': { write: 1.25, read: 0.1 },
|
||||
'claude-sonnet-4': { write: 3.75, read: 0.3 },
|
||||
'claude-sonnet-4-6': { write: 3.75, read: 0.3 },
|
||||
'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 },
|
||||
|
|
@ -337,6 +339,7 @@ const cacheTokenValues = {
|
|||
*/
|
||||
const premiumTokenValues = {
|
||||
'claude-opus-4-6': { threshold: 200000, prompt: 10, completion: 37.5 },
|
||||
'claude-sonnet-4-6': { threshold: 200000, prompt: 6, completion: 22.5 },
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1162,6 +1162,56 @@ describe('Claude Model Tests', () => {
|
|||
expect(matchModelName(model, EModelEndpoint.anthropic)).toBe('claude-opus-4-6');
|
||||
});
|
||||
});
|
||||
|
||||
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'],
|
||||
);
|
||||
expect(getModelMaxTokens('claude-sonnet-4-6')).toBe(
|
||||
maxTokensMap[EModelEndpoint.anthropic]['claude-sonnet-4-6'],
|
||||
);
|
||||
});
|
||||
|
||||
it('should return correct max output tokens for Claude Sonnet 4.6 (64K)', () => {
|
||||
const { getModelMaxOutputTokens } = require('@librechat/api');
|
||||
expect(getModelMaxOutputTokens('claude-sonnet-4-6', EModelEndpoint.anthropic)).toBe(
|
||||
maxOutputTokensMap[EModelEndpoint.anthropic]['claude-sonnet-4-6'],
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Claude Sonnet 4.6 model name variations', () => {
|
||||
const modelVariations = [
|
||||
'claude-sonnet-4-6',
|
||||
'claude-sonnet-4-6-20260101',
|
||||
'claude-sonnet-4-6-latest',
|
||||
'anthropic/claude-sonnet-4-6',
|
||||
'claude-sonnet-4-6/anthropic',
|
||||
'claude-sonnet-4-6-preview',
|
||||
];
|
||||
|
||||
modelVariations.forEach((model) => {
|
||||
const modelKey = findMatchingPattern(model, maxTokensMap[EModelEndpoint.anthropic]);
|
||||
expect(modelKey).toBe('claude-sonnet-4-6');
|
||||
expect(getModelMaxTokens(model, EModelEndpoint.anthropic)).toBe(
|
||||
maxTokensMap[EModelEndpoint.anthropic]['claude-sonnet-4-6'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should match model names correctly for Claude Sonnet 4.6', () => {
|
||||
const modelVariations = [
|
||||
'claude-sonnet-4-6',
|
||||
'claude-sonnet-4-6-20260101',
|
||||
'claude-sonnet-4-6-latest',
|
||||
'anthropic/claude-sonnet-4-6',
|
||||
'claude-sonnet-4-6/anthropic',
|
||||
'claude-sonnet-4-6-preview',
|
||||
];
|
||||
|
||||
modelVariations.forEach((model) => {
|
||||
expect(matchModelName(model, EModelEndpoint.anthropic)).toBe('claude-sonnet-4-6');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Moonshot/Kimi Model Tests', () => {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ function getClaudeHeaders(
|
|||
|
||||
/**
|
||||
* Configures reasoning-related options for Claude models.
|
||||
* Models supporting adaptive thinking (Opus 4.6+, Sonnet 5+) use effort control instead of manual budget_tokens.
|
||||
* Models supporting adaptive thinking (Opus 4.6+, Sonnet 4.6+) use effort control instead of manual budget_tokens.
|
||||
*/
|
||||
function configureReasoning(
|
||||
anthropicInput: AnthropicClientOptions & { max_tokens?: number },
|
||||
|
|
|
|||
|
|
@ -121,6 +121,39 @@ describe('getLLMConfig', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should add "context-1m" beta header for claude-sonnet-4-6 model', () => {
|
||||
const modelOptions = {
|
||||
model: 'claude-sonnet-4-6',
|
||||
promptCache: true,
|
||||
};
|
||||
const result = getLLMConfig('test-key', { modelOptions });
|
||||
const clientOptions = result.llmConfig.clientOptions;
|
||||
expect(clientOptions?.defaultHeaders).toBeDefined();
|
||||
expect(clientOptions?.defaultHeaders).toHaveProperty('anthropic-beta');
|
||||
const defaultHeaders = clientOptions?.defaultHeaders as Record<string, string>;
|
||||
expect(defaultHeaders['anthropic-beta']).toBe('context-1m-2025-08-07');
|
||||
expect(result.llmConfig.promptCache).toBe(true);
|
||||
});
|
||||
|
||||
it('should add "context-1m" beta header for claude-sonnet-4-6 model formats', () => {
|
||||
const modelVariations = [
|
||||
'claude-sonnet-4-6',
|
||||
'claude-sonnet-4-6-20260101',
|
||||
'anthropic/claude-sonnet-4-6',
|
||||
];
|
||||
|
||||
modelVariations.forEach((model) => {
|
||||
const modelOptions = { model, promptCache: true };
|
||||
const result = getLLMConfig('test-key', { modelOptions });
|
||||
const clientOptions = result.llmConfig.clientOptions;
|
||||
expect(clientOptions?.defaultHeaders).toBeDefined();
|
||||
expect(clientOptions?.defaultHeaders).toHaveProperty('anthropic-beta');
|
||||
const defaultHeaders = clientOptions?.defaultHeaders as Record<string, string>;
|
||||
expect(defaultHeaders['anthropic-beta']).toBe('context-1m-2025-08-07');
|
||||
expect(result.llmConfig.promptCache).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass promptCache boolean for claude-opus-4-5 model (no beta header needed)', () => {
|
||||
const modelOptions = {
|
||||
model: 'claude-opus-4-5',
|
||||
|
|
@ -963,6 +996,51 @@ describe('getLLMConfig', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should use adaptive thinking for Sonnet 4.6 instead of enabled + budget_tokens', () => {
|
||||
const result = getLLMConfig('test-key', {
|
||||
modelOptions: {
|
||||
model: 'claude-sonnet-4-6',
|
||||
thinking: true,
|
||||
thinkingBudget: 10000,
|
||||
},
|
||||
});
|
||||
|
||||
expect((result.llmConfig.thinking as unknown as { type: string }).type).toBe('adaptive');
|
||||
expect(result.llmConfig.thinking).not.toHaveProperty('budget_tokens');
|
||||
expect(result.llmConfig.maxTokens).toBe(64000);
|
||||
});
|
||||
|
||||
it('should set effort via output_config for Sonnet 4.6', () => {
|
||||
const result = getLLMConfig('test-key', {
|
||||
modelOptions: {
|
||||
model: 'claude-sonnet-4-6',
|
||||
thinking: true,
|
||||
effort: AnthropicEffort.high,
|
||||
},
|
||||
});
|
||||
|
||||
expect((result.llmConfig.thinking as unknown as { type: string }).type).toBe('adaptive');
|
||||
expect(result.llmConfig.invocationKwargs).toHaveProperty('output_config');
|
||||
expect(result.llmConfig.invocationKwargs?.output_config).toEqual({
|
||||
effort: AnthropicEffort.high,
|
||||
});
|
||||
});
|
||||
|
||||
it('should exclude topP/topK for Sonnet 4.6 with adaptive thinking', () => {
|
||||
const result = getLLMConfig('test-key', {
|
||||
modelOptions: {
|
||||
model: 'claude-sonnet-4-6',
|
||||
thinking: true,
|
||||
topP: 0.9,
|
||||
topK: 40,
|
||||
},
|
||||
});
|
||||
|
||||
expect((result.llmConfig.thinking as unknown as { type: string }).type).toBe('adaptive');
|
||||
expect(result.llmConfig).not.toHaveProperty('topP');
|
||||
expect(result.llmConfig).not.toHaveProperty('topK');
|
||||
});
|
||||
|
||||
it('should NOT set adaptive thinking or effort for non-adaptive models', () => {
|
||||
const nonAdaptiveModels = [
|
||||
'claude-opus-4-5',
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ const anthropicModels = {
|
|||
'claude-3.5-sonnet-latest': 200000,
|
||||
'claude-haiku-4-5': 200000,
|
||||
'claude-sonnet-4': 1000000,
|
||||
'claude-sonnet-4-6': 1000000,
|
||||
'claude-4': 200000,
|
||||
'claude-opus-4': 200000,
|
||||
'claude-opus-4-5': 200000,
|
||||
|
|
@ -401,6 +402,7 @@ const anthropicMaxOutputs = {
|
|||
'claude-3-opus': 4096,
|
||||
'claude-haiku-4-5': 64000,
|
||||
'claude-sonnet-4': 64000,
|
||||
'claude-sonnet-4-6': 64000,
|
||||
'claude-opus-4': 32000,
|
||||
'claude-opus-4-5': 64000,
|
||||
'claude-opus-4-6': 128000,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,30 @@ describe('supportsAdaptiveThinking', () => {
|
|||
expect(supportsAdaptiveThinking('claude-opus-4-0')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true for claude-sonnet-4-6', () => {
|
||||
expect(supportsAdaptiveThinking('claude-sonnet-4-6')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for claude-sonnet-4.6', () => {
|
||||
expect(supportsAdaptiveThinking('claude-sonnet-4.6')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for claude-sonnet-4-7 (future)', () => {
|
||||
expect(supportsAdaptiveThinking('claude-sonnet-4-7')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for anthropic.claude-sonnet-4-6 (Bedrock)', () => {
|
||||
expect(supportsAdaptiveThinking('anthropic.claude-sonnet-4-6')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for us.anthropic.claude-sonnet-4-6 (cross-region Bedrock)', () => {
|
||||
expect(supportsAdaptiveThinking('us.anthropic.claude-sonnet-4-6')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for claude-4-6-sonnet (alternate naming)', () => {
|
||||
expect(supportsAdaptiveThinking('claude-4-6-sonnet')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for claude-sonnet-4-5', () => {
|
||||
expect(supportsAdaptiveThinking('claude-sonnet-4-5')).toBe(false);
|
||||
});
|
||||
|
|
@ -104,6 +128,14 @@ describe('supportsContext1m', () => {
|
|||
expect(supportsContext1m('claude-sonnet-4-5')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for claude-sonnet-4-6', () => {
|
||||
expect(supportsContext1m('claude-sonnet-4-6')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for anthropic.claude-sonnet-4-6 (Bedrock)', () => {
|
||||
expect(supportsContext1m('anthropic.claude-sonnet-4-6')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for claude-sonnet-5 (future)', () => {
|
||||
expect(supportsContext1m('claude-sonnet-5')).toBe(true);
|
||||
});
|
||||
|
|
@ -237,14 +269,42 @@ describe('bedrockInputParser', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('should match anthropic.claude-4-7-sonnet model with 1M context header', () => {
|
||||
test('should match anthropic.claude-sonnet-4-6 with adaptive thinking and 1M context header', () => {
|
||||
const input = {
|
||||
model: 'anthropic.claude-sonnet-4-6',
|
||||
};
|
||||
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.thinkingBudget).toBeUndefined();
|
||||
expect(additionalFields.anthropic_beta).toEqual([
|
||||
'output-128k-2025-02-19',
|
||||
'context-1m-2025-08-07',
|
||||
]);
|
||||
});
|
||||
|
||||
test('should match us.anthropic.claude-sonnet-4-6 with adaptive thinking and 1M context header', () => {
|
||||
const input = {
|
||||
model: 'us.anthropic.claude-sonnet-4-6',
|
||||
};
|
||||
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.thinkingBudget).toBeUndefined();
|
||||
expect(additionalFields.anthropic_beta).toEqual([
|
||||
'output-128k-2025-02-19',
|
||||
'context-1m-2025-08-07',
|
||||
]);
|
||||
});
|
||||
|
||||
test('should match anthropic.claude-4-7-sonnet model with adaptive thinking and 1M context header', () => {
|
||||
const input = {
|
||||
model: 'anthropic.claude-4-7-sonnet',
|
||||
};
|
||||
const result = bedrockInputParser.parse(input) as Record<string, unknown>;
|
||||
const additionalFields = result.additionalModelRequestFields as Record<string, unknown>;
|
||||
expect(additionalFields.thinking).toBe(true);
|
||||
expect(additionalFields.thinkingBudget).toBe(2000);
|
||||
expect(additionalFields.thinking).toEqual({ type: 'adaptive' });
|
||||
expect(additionalFields.thinkingBudget).toBeUndefined();
|
||||
expect(additionalFields.anthropic_beta).toEqual([
|
||||
'output-128k-2025-02-19',
|
||||
'context-1m-2025-08-07',
|
||||
|
|
|
|||
|
|
@ -35,27 +35,34 @@ function parseOpusVersion(model: string): { major: number; minor: number } | nul
|
|||
return null;
|
||||
}
|
||||
|
||||
/** Extracts sonnet major version from both naming formats */
|
||||
function parseSonnetVersion(model: string): number | null {
|
||||
const nameFirst = model.match(/claude-sonnet[-.]?(\d+)/);
|
||||
/** Extracts sonnet major/minor version from both naming formats.
|
||||
* Uses single-digit minor capture to avoid matching date suffixes (e.g., -20250514). */
|
||||
function parseSonnetVersion(model: string): { major: number; minor: number } | null {
|
||||
const nameFirst = model.match(/claude-sonnet[-.]?(\d+)(?:[-.](\d)(?!\d))?/);
|
||||
if (nameFirst) {
|
||||
return parseInt(nameFirst[1], 10);
|
||||
return {
|
||||
major: parseInt(nameFirst[1], 10),
|
||||
minor: nameFirst[2] != null ? parseInt(nameFirst[2], 10) : 0,
|
||||
};
|
||||
}
|
||||
const numFirst = model.match(/claude-(\d+)(?:[-.]?\d+)?-sonnet/);
|
||||
const numFirst = model.match(/claude-(\d+)(?:[-.](\d)(?!\d))?-sonnet/);
|
||||
if (numFirst) {
|
||||
return parseInt(numFirst[1], 10);
|
||||
return {
|
||||
major: parseInt(numFirst[1], 10),
|
||||
minor: numFirst[2] != null ? parseInt(numFirst[2], 10) : 0,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Checks if a model supports adaptive thinking (Opus 4.6+, Sonnet 5+) */
|
||||
/** Checks if a model supports adaptive thinking (Opus 4.6+, Sonnet 4.6+) */
|
||||
export function supportsAdaptiveThinking(model: string): boolean {
|
||||
const opus = parseOpusVersion(model);
|
||||
if (opus && (opus.major > 4 || (opus.major === 4 && opus.minor >= 6))) {
|
||||
return true;
|
||||
}
|
||||
const sonnet = parseSonnetVersion(model);
|
||||
if (sonnet != null && sonnet >= 5) {
|
||||
if (sonnet != null && (sonnet.major > 4 || (sonnet.major === 4 && sonnet.minor >= 6))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -64,7 +71,7 @@ export function supportsAdaptiveThinking(model: string): boolean {
|
|||
/** Checks if a model qualifies for the context-1m beta header (Sonnet 4+, Opus 4.6+, Opus 5+) */
|
||||
export function supportsContext1m(model: string): boolean {
|
||||
const sonnet = parseSonnetVersion(model);
|
||||
if (sonnet != null && sonnet >= 4) {
|
||||
if (sonnet != null && sonnet.major >= 4) {
|
||||
return true;
|
||||
}
|
||||
const opus = parseOpusVersion(model);
|
||||
|
|
|
|||
|
|
@ -1133,6 +1133,7 @@ const sharedOpenAIModels = [
|
|||
];
|
||||
|
||||
const sharedAnthropicModels = [
|
||||
'claude-sonnet-4-6',
|
||||
'claude-opus-4-6',
|
||||
'claude-sonnet-4-5',
|
||||
'claude-sonnet-4-5-20250929',
|
||||
|
|
@ -1154,6 +1155,7 @@ const sharedAnthropicModels = [
|
|||
];
|
||||
|
||||
export const bedrockModels = [
|
||||
'anthropic.claude-sonnet-4-6',
|
||||
'anthropic.claude-opus-4-6-v1',
|
||||
'anthropic.claude-sonnet-4-5-20250929-v1:0',
|
||||
'anthropic.claude-haiku-4-5-20251001-v1:0',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue