🤖 feat: Add Claude Sonnet 5 Support (#14042)

* ✨ feat: Add Claude Sonnet 5 Support

Wire up the claude-sonnet-5 model across token, pricing, and model-list
config:

- Context window (1M) and max output (128K) in @librechat/api token maps
- Standard pricing ($3/$15 per MTok) and cache rates in data-schemas tx
- 128K output-token carve-out in anthropicSettings (the family-wide 64K
  rule capped Sonnet 5 below its real limit); Bedrock/Vertex thinking and
  1M-context detection already cover sonnet major >= 5 generically
- Add to shared Anthropic, Bedrock, and Vertex default model lists, plus
  the .env.example examples
- Tests for context/output/pricing/matching across the affected packages

* ✅ test: Align Sonnet 5+ maxOutputTokens defaults with 128K spec

getLLMConfig defaults flow from anthropicSettings.maxOutputTokens.reset(),
which now returns 128K for Sonnet 5+. Update the future-proofing assertions
in llm.spec.ts (Sonnet 5.x and 6-9.x) that still expected the old family-wide
64K cap. Haiku stays 64K; Opus stays 128K.

* 🎚️ fix: Gate Sonnet 5 capability behaviors (sampling, thinking)

Adding claude-sonnet-5 to the default list exposed it without the Anthropic
capability gates, all confirmed against the live API:

- omitsSamplingParameters: Sonnet 5 returns 400 on non-default temperature/
  top_p/top_k ('deprecated for this model'); now dropped so selecting the
  model with saved sampling settings no longer fails.
- requiresExplicitThinkingDisabled: omitting 'thinking' runs adaptive ON by
  default on Sonnet 5, so disabling thinking now sends { type: 'disabled' }
  (verified: 200, no thinking block) instead of omitting the field.
- omitsThinkingByDefault: thinking.display defaults to omitted (empty thinking
  blocks); the display resolver now returns 'summarized' for Sonnet 5+ so the
  Thoughts UI keeps working (verified: 757-char summary returned).

Gates apply to both the direct Anthropic and Bedrock paths. Tests added in
bedrock.spec and llm.spec.

* 🩹 fix: Sonnet 5 Bedrock availability + thinking-off persistence

Round-2 Codex review (all verified against the live API / Anthropic docs):

- Sonnet 5 is NOT available on the legacy Bedrock InvokeModel/Converse surface
  (Anthropic docs: 'use Claude in Amazon Bedrock or Claude Platform on AWS'),
  which is what LibreChat's ChatBedrockConverse uses. Removed it from the
  default Bedrock model lists (config + .env.example). Opus 4.8/4.7/Fable 5
  stay — those ARE reachable via InvokeModel. Sonnet 5 remains on the direct
  Anthropic API and Vertex, where it works.
- Reverted the Bedrock-side explicit-disabled thinking handling added last
  round: with Sonnet 5 off Bedrock, no Bedrock model needs { type: 'disabled' },
  so that path (and its round-trip concern) no longer applies.
- Direct Anthropic path: a persisted { type: 'disabled' } thinking object now
  normalizes to a boolean flag in getLLMConfig, so a user's Sonnet 5
  'thinking off' setting stays off across the model_parameters round trip
  instead of flipping back to adaptive (a truthy object skipped the disabled
  branch).

* ↩️ fix: Restore Sonnet 5 on Bedrock (Converse) — verified live

Reverses the round-2 removal: Sonnet 5 IS available on AWS Bedrock. Tested
live via the Converse API:

- global.anthropic.claude-sonnet-5 returns a normal response
- bare anthropic.claude-sonnet-5 needs an inference profile — but that's
  identical to the already-shipping Opus 4.8 / Fable 5 / Sonnet 4.6 entries,
  which all fail bare on-demand the same way
- temperature=0.5 -> 400 'deprecated for this model'; thinking {type:disabled}
  suppresses reasoning — same as the direct API

The 'legacy' Bedrock docs page that claimed Sonnet 5 wasn't on the surface is
stale. Restored:
- anthropic.claude-sonnet-5 in bedrockModels + .env.example
- the Bedrock explicit-disabled thinking handling (requiresExplicitThinkingDisabled
  -> { type: 'disabled' })
- the Finding 4 round-trip fix in bedrockInputSchema (coerce a persisted
  disabled AMRF.thinking to thinking=false instead of !!thinking -> true), with
  an end-to-end schema->parser test proving 'thinking off' stays sticky.

Direct-path round-trip fix (getLLMConfig thinkingFlag) is unchanged.

* 💵 fix: Sonnet 5 intro pricing + sticky disabled thinking on Bedrock reload

Round-4 Codex review (both verified):

- Pricing: Anthropic lists Sonnet 5 at introductory $2/$10 per MTok (cache
  $2.50/$0.20) through 2026-08-31, reverting to $3/$15 ($3.75/$0.30) on
  Sep 1 (confirmed on platform.claude.com/pricing). The static tx multiplier
  table is used for real balance transactions, so the post-intro rates were
  overcharging ~50% during the launch window. Switched to the intro rates with
  a revert comment on both the token and cache entries.

- Bedrock disabled-thinking persistence: initializeBedrock feeds persisted
  model_parameters straight through bedrockInputParser (NOT bedrockInputSchema),
  where additionalModelRequestFields is a known key — so a prior
  thinking:{type:'disabled'} was ignored and rebuilt as adaptive on reload.
  bedrockInputParser now surfaces a persisted disabled AMRF.thinking as
  thinking=false so it re-emits {type:'disabled'}. Verified end-to-end against
  the real initializeBedrock call path.
This commit is contained in:
Danny Avila 2026-06-30 19:26:33 -04:00 • committed by GitHub
parent 6523a5add6
commit 9f8b6d92c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 377 additions and 21 deletions

View file

@ -279,7 +279,7 @@ PROXY=
#============#
ANTHROPIC_API_KEY=user_provided
# ANTHROPIC_MODELS=claude-fable-5,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_MODELS=claude-fable-5,claude-opus-4-8,claude-opus-4-7,claude-sonnet-5,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
@ -345,8 +345,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-fable-5,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-fable-5,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
# BEDROCK_AWS_MODELS=anthropic.claude-fable-5,anthropic.claude-opus-4-8,anthropic.claude-opus-4-7,anthropic.claude-sonnet-5,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-fable-5,us.anthropic.claude-opus-4-8,us.anthropic.claude-opus-4-7,us.anthropic.claude-sonnet-5,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

@ -1635,6 +1635,59 @@ describe('Claude Model Tests', () => {
expect(matchModelName(model, EModelEndpoint.anthropic)).toBe('claude-sonnet-4-6');
});
});
it('should return correct context length for Claude Sonnet 5 (1M)', () => {
expect(getModelMaxTokens('claude-sonnet-5', EModelEndpoint.anthropic)).toBe(
maxTokensMap[EModelEndpoint.anthropic]['claude-sonnet-5'],
);
expect(getModelMaxTokens('claude-sonnet-5')).toBe(
maxTokensMap[EModelEndpoint.anthropic]['claude-sonnet-5'],
);
});
it('should return correct max output tokens for Claude Sonnet 5 (128K)', () => {
const { getModelMaxOutputTokens } = require('@librechat/api');
expect(getModelMaxOutputTokens('claude-sonnet-5', EModelEndpoint.anthropic)).toBe(
maxOutputTokensMap[EModelEndpoint.anthropic]['claude-sonnet-5'],
);
expect(getModelMaxOutputTokens('claude-sonnet-5', EModelEndpoint.anthropic)).toBeGreaterThan(
getModelMaxOutputTokens('claude-sonnet-4-6', EModelEndpoint.anthropic),
);
});
it('should not match Claude Sonnet 5 against Claude Sonnet 4 keys', () => {
const modelVariations = [
'claude-sonnet-5',
'claude-sonnet-5-20260101',
'claude-sonnet-5-latest',
'anthropic/claude-sonnet-5',
'claude-sonnet-5/anthropic',
'anthropic.claude-sonnet-5',
];
modelVariations.forEach((model) => {
const modelKey = findMatchingPattern(model, maxTokensMap[EModelEndpoint.anthropic]);
expect(modelKey).toBe('claude-sonnet-5');
expect(getModelMaxTokens(model, EModelEndpoint.anthropic)).toBe(
maxTokensMap[EModelEndpoint.anthropic]['claude-sonnet-5'],
);
});
});
it('should match model names correctly for Claude Sonnet 5', () => {
const modelVariations = [
'claude-sonnet-5',
'claude-sonnet-5-20260101',
'claude-sonnet-5-latest',
'anthropic/claude-sonnet-5',
'claude-sonnet-5/anthropic',
'claude-sonnet-5-preview',
];
modelVariations.forEach((model) => {
expect(matchModelName(model, EModelEndpoint.anthropic)).toBe('claude-sonnet-5');
});
});
});
describe('Moonshot/Kimi Model Tests', () => {

View file

@ -8,6 +8,7 @@ import {
isMythosClassModel,
resolveThinkingDisplay,
supportsAdaptiveThinking,
requiresExplicitThinkingDisabled,
} from 'librechat-data-provider';
import { matchModelName } from '~/utils/tokens';
@ -99,6 +100,16 @@ function configureReasoning(
const currentMaxTokens = updatedOptions.max_tokens ?? updatedOptions.maxTokens;
const modelName = updatedOptions.model ?? '';
/**
* Sonnet 5 runs adaptive thinking by default when the `thinking` field is
* omitted, so honoring a user who turns thinking off requires sending an
* explicit disabled config rather than leaving the field unset.
*/
if (!extendedOptions.thinking && modelName && requiresExplicitThinkingDisabled(modelName)) {
updatedOptions.thinking = { type: 'disabled' } as AnthropicClientOptions['thinking'];
return updatedOptions;
}
if (extendedOptions.thinking && modelName && supportsAdaptiveThinking(modelName)) {
/**
* For Opus 4.7+, Anthropic omits thinking content from responses by

View file

@ -1215,6 +1215,64 @@ describe('getLLMConfig', () => {
expect(result.llmConfig).not.toHaveProperty('topK');
});
it('should request summarized thinking display for Sonnet 5 (opt back in)', () => {
const result = getLLMConfig('test-key', {
modelOptions: { model: 'claude-sonnet-5', thinking: true },
});
const thinking = result.llmConfig.thinking as unknown as {
type: string;
display?: string;
};
expect(thinking.type).toBe('adaptive');
expect(thinking.display).toBe('summarized');
});
it('should send explicit disabled thinking for Sonnet 5 when thinking is off', () => {
const result = getLLMConfig('test-key', {
modelOptions: { model: 'claude-sonnet-5', thinking: false },
});
expect((result.llmConfig.thinking as unknown as { type: string }).type).toBe('disabled');
});
it('should keep Sonnet 5 thinking off when a disabled config round-trips from persistence', () => {
const result = getLLMConfig('test-key', {
modelOptions: {
model: 'claude-sonnet-5',
// Persisted model_parameters round-trip the prior disabled object,
// not a boolean — it must stay disabled, not flip back to adaptive.
thinking: { type: 'disabled' } as unknown as boolean,
},
});
expect((result.llmConfig.thinking as unknown as { type: string }).type).toBe('disabled');
});
it('should omit sampling parameters for Sonnet 5', () => {
const result = getLLMConfig('test-key', {
modelOptions: {
model: 'claude-sonnet-5',
thinking: true,
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 send explicit disabled thinking for pre-5 Sonnet (omission is off)', () => {
const result = getLLMConfig('test-key', {
modelOptions: { model: 'claude-sonnet-4-6', thinking: false },
});
expect(result.llmConfig.thinking).toBeUndefined();
});
it('should NOT set thinking.display for pre-Opus-4.7 adaptive models', () => {
const pre47Models = ['claude-opus-4-6', 'claude-sonnet-4-6'];
@ -1408,7 +1466,7 @@ describe('getLLMConfig', () => {
});
});
it('should future-proof Claude 5.x Sonnet models with 64K default', () => {
it('should default Claude 5.x Sonnet models to 128K (matches Anthropic spec)', () => {
const testCases = [
'claude-sonnet-5',
'claude-sonnet-5-0',
@ -1420,7 +1478,7 @@ describe('getLLMConfig', () => {
const result = getLLMConfig('test-key', {
modelOptions: { model },
});
expect(result.llmConfig.maxTokens).toBe(64000);
expect(result.llmConfig.maxTokens).toBe(128000);
});
});
@ -1458,20 +1516,20 @@ describe('getLLMConfig', () => {
it('should future-proof Claude 6-9.x models with correct defaults', () => {
const testCases = [
// Claude 6.x - Sonnet/Haiku get 64K, Opus gets 128K
{ model: 'claude-sonnet-6', expected: 64000 },
// Claude 6.x - Sonnet/Opus get 128K, Haiku gets 64K
{ model: 'claude-sonnet-6', expected: 128000 },
{ model: 'claude-haiku-6-0', expected: 64000 },
{ model: 'claude-opus-6-1', expected: 128000 },
// Claude 7.x
{ model: 'claude-sonnet-7-20270101', expected: 64000 },
{ model: 'claude-sonnet-7-20270101', expected: 128000 },
{ model: 'claude-haiku-7.5', expected: 64000 },
{ model: 'claude-opus-7', expected: 128000 },
// Claude 8.x
{ model: 'claude-sonnet-8', expected: 64000 },
{ model: 'claude-sonnet-8', expected: 128000 },
{ model: 'claude-haiku-8-2', expected: 64000 },
{ model: 'claude-opus-8-latest', expected: 128000 },
// Claude 9.x
{ model: 'claude-sonnet-9', expected: 64000 },
{ model: 'claude-sonnet-9', expected: 128000 },
{ model: 'claude-haiku-9', expected: 64000 },
{ model: 'claude-opus-9', expected: 128000 },
];

View file

@ -145,8 +145,19 @@ function getLLMConfig(
? ((persistedThinking as { display: string }).display as ThinkingDisplay | string)
: undefined;
/**
* `thinking` may round-trip as the full Anthropic object rather than a
* boolean. Normalize to a flag so a persisted `{ type: 'disabled' }` (e.g. a
* Sonnet 5 "thinking off" config stored back into `model_parameters`) is
* treated as off — a truthy object would otherwise flip thinking back on.
*/
const thinkingFlag =
typeof persistedThinking === 'object' && persistedThinking != null
? (persistedThinking as { type?: string }).type !== 'disabled'
: (persistedThinking ?? anthropicSettings.thinking.default);
const systemOptions = {
thinking: options.modelOptions?.thinking ?? anthropicSettings.thinking.default,
thinking: thinkingFlag,
promptCache: options.modelOptions?.promptCache ?? anthropicSettings.promptCache.default,
promptCacheTtl:
options.modelOptions?.promptCacheTtl ?? anthropicSettings.promptCacheTtl.default,

View file

@ -156,6 +156,7 @@ const anthropicModels = {
'claude-sonnet-4': 200000,
'claude-sonnet-4-5': 200000,
'claude-sonnet-4-6': 1000000,
'claude-sonnet-5': 1000000,
'claude-opus-4-6': 1000000,
'claude-opus-4-7': 1000000,
'claude-opus-4-8': 1000000,
@ -408,6 +409,7 @@ const anthropicMaxOutputs = {
'claude-haiku-4-5': 64000,
'claude-sonnet-4': 64000,
'claude-sonnet-4-6': 64000,
'claude-sonnet-5': 128000,
'claude-opus-4': 32000,
'claude-opus-4-5': 64000,
'claude-opus-4-6': 128000,

View file

@ -4,6 +4,7 @@ import {
supportsAdaptiveThinking,
omitsSamplingParameters,
omitsThinkingByDefault,
requiresExplicitThinkingDisabled,
resolveThinkingDisplay,
bedrockOutputParser,
bedrockInputParser,
@ -288,10 +289,17 @@ describe('omitsThinkingByDefault', () => {
expect(omitsThinkingByDefault('claude-sonnet-4-6')).toBe(false);
});
test('returns false for claude-sonnet-4-7 (Sonnet is not affected by the Opus 4.7 default)', () => {
test('returns false for claude-sonnet-4-7 (pre-5 Sonnet keeps summarized default)', () => {
expect(omitsThinkingByDefault('claude-sonnet-4-7')).toBe(false);
});
test('returns true for Sonnet 5+ (omits thinking display by default)', () => {
expect(omitsThinkingByDefault('claude-sonnet-5')).toBe(true);
expect(omitsThinkingByDefault('claude-sonnet-5-20260101')).toBe(true);
expect(omitsThinkingByDefault('anthropic.claude-sonnet-5')).toBe(true);
expect(omitsThinkingByDefault('claude-sonnet-9')).toBe(true);
});
test('returns false for claude-haiku-4-5', () => {
expect(omitsThinkingByDefault('claude-haiku-4-5')).toBe(false);
});
@ -334,12 +342,27 @@ describe('omitsSamplingParameters', () => {
});
});
test('returns false for older Opus and non-Opus models', () => {
test('returns true for Sonnet 5+ models (non-default sampling params rejected)', () => {
const models = [
'claude-sonnet-5',
'claude-sonnet-5-20260101',
'anthropic.claude-sonnet-5',
'us.anthropic.claude-sonnet-5',
'claude-sonnet-9',
];
models.forEach((model) => {
expect(omitsSamplingParameters(model)).toBe(true);
});
});
test('returns false for older Opus and pre-5 Sonnet 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-6',
'claude-sonnet-4-7',
];
@ -349,6 +372,24 @@ describe('omitsSamplingParameters', () => {
});
});
describe('requiresExplicitThinkingDisabled', () => {
test('returns true for Sonnet 5+ (omitted thinking runs adaptive by default)', () => {
expect(requiresExplicitThinkingDisabled('claude-sonnet-5')).toBe(true);
expect(requiresExplicitThinkingDisabled('claude-sonnet-5-20260101')).toBe(true);
expect(requiresExplicitThinkingDisabled('anthropic.claude-sonnet-5')).toBe(true);
expect(requiresExplicitThinkingDisabled('claude-sonnet-9')).toBe(true);
});
test('returns false for pre-5 Sonnet, Opus, and Mythos-class models', () => {
// Opus 4.7+ omit -> off; Fable/Mythos reject an explicit disabled config (400)
expect(requiresExplicitThinkingDisabled('claude-sonnet-4-6')).toBe(false);
expect(requiresExplicitThinkingDisabled('claude-opus-4-8')).toBe(false);
expect(requiresExplicitThinkingDisabled('claude-fable-5')).toBe(false);
expect(requiresExplicitThinkingDisabled('claude-mythos-5')).toBe(false);
expect(requiresExplicitThinkingDisabled('gpt-4o')).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');
@ -539,6 +580,29 @@ describe('bedrockInputParser', () => {
expect(additionalFields.anthropic_beta).toEqual(BEDROCK_CLAUDE_4_BETAS);
});
test('should send explicit disabled thinking for Bedrock Sonnet 5 when thinking is off', () => {
const input = {
model: 'anthropic.claude-sonnet-5',
thinking: false,
};
const result = bedrockInputParser.parse(input) as Record<string, unknown>;
const additionalFields = result.additionalModelRequestFields as Record<string, unknown>;
expect(additionalFields.thinking).toEqual({ type: 'disabled' });
});
test('should keep Bedrock Sonnet 5 thinking off when only the persisted AMRF disabled config remains', () => {
// initializeBedrock feeds persisted model_parameters straight through this
// parser. A prior "thinking off" survives only as AMRF.thinking; it must
// not be rebuilt as adaptive on reload.
const input = {
model: 'anthropic.claude-sonnet-5',
additionalModelRequestFields: { thinking: { type: 'disabled' } },
};
const result = bedrockInputParser.parse(input) as Record<string, unknown>;
const additionalFields = result.additionalModelRequestFields as Record<string, unknown>;
expect(additionalFields.thinking).toEqual({ type: 'disabled' });
});
test('should respect custom thinking budget', () => {
const input = {
model: 'anthropic.claude-sonnet-4',
@ -804,6 +868,18 @@ describe('bedrockInputParser', () => {
expect(persisted.thinkingDisplay).toBe('summarized');
});
test('coerces a persisted disabled AMRF.thinking back to thinking=false (Sonnet 5)', () => {
/** A persisted Sonnet 5 "thinking off" carries AMRF.thinking =
* { type: 'disabled' }. The schema must coerce that to top-level
* thinking=false (not the truthy-object default of true), so the parser
* re-emits the disabled config instead of flipping back to adaptive. */
const persisted = bedrockInputSchema.parse({
model: 'anthropic.claude-sonnet-5',
additionalModelRequestFields: { thinking: { type: 'disabled' } },
}) as Record<string, unknown>;
expect(persisted.thinking).toBe(false);
});
test('ignores unknown display values during round-trip extraction', () => {
const persisted = bedrockInputSchema.parse({
model: 'anthropic.claude-opus-4-7',

View file

@ -10,7 +10,8 @@ const bedrockReasoningConfigValues = new Set<string>(Object.values(s.BedrockReas
type ThinkingConfig =
| { type: 'enabled'; budget_tokens: number }
| { type: 'adaptive'; display?: s.ThinkingDisplayWireValue };
| { type: 'adaptive'; display?: s.ThinkingDisplayWireValue }
| { type: 'disabled' };
/**
* Resolves the final `thinking.display` value for an adaptive-thinking request.
@ -142,6 +143,10 @@ export function omitsThinkingByDefault(model: string): boolean {
if (opus && (opus.major > 4 || (opus.major === 4 && opus.minor >= 7))) {
return true;
}
const sonnet = parseSonnetVersion(model);
if (sonnet != null && sonnet.major >= 5) {
return true;
}
if (s.isMythosClassModel(model)) {
return true;
}
@ -153,12 +158,33 @@ export function omitsSamplingParameters(model: string): boolean {
if (opus && (opus.major > 4 || (opus.major === 4 && opus.minor >= 7))) {
return true;
}
const sonnet = parseSonnetVersion(model);
if (sonnet != null && sonnet.major >= 5) {
return true;
}
if (s.isMythosClassModel(model)) {
return true;
}
return false;
}
/**
* Whether disabling thinking requires sending an explicit `{ type: 'disabled' }`
* config rather than simply omitting the `thinking` field.
*
* Sonnet 5 treats an omitted `thinking` field as adaptive thinking ON by
* default, so honoring a user who turns thinking off means sending the disabled
* config explicitly. Opus 4.7+ run without thinking when the field is omitted,
* and Fable/Mythos reject an explicit disabled config (400, thinking always
* on), so both are excluded.
*
* See https://platform.claude.com/docs/en/about-claude/models/migration-guide#migrating-to-claude-sonnet-5
*/
export function requiresExplicitThinkingDisabled(model: string): boolean {
const sonnet = parseSonnetVersion(model);
return sonnet != null && sonnet.major >= 5;
}
/** Checks if a model has a 1M context window (Sonnet 4.6+, Opus 4.6+, Opus 5+, Fable/Mythos) */
export function supportsContext1m(model: string): boolean {
const sonnet = parseSonnetVersion(model);
@ -265,7 +291,11 @@ export const bedrockInputSchema = s.tConversationSchema
if ((obj as AnthropicInput).additionalModelRequestFields?.thinking != null) {
const _obj = obj as AnthropicInput;
const thinking = _obj.additionalModelRequestFields.thinking;
obj.thinking = !!thinking;
const isDisabled =
typeof thinking === 'object' &&
thinking !== null &&
(thinking as { type?: string }).type === 'disabled';
obj.thinking = isDisabled ? false : !!thinking;
obj.thinkingBudget =
typeof thinking === 'object' && 'budget_tokens' in thinking
? thinking.budget_tokens
@ -357,6 +387,26 @@ export const bedrockInputParser = s.tConversationSchema
}
});
/**
* Persisted `model_parameters` can carry a prior "thinking off" only inside
* `additionalModelRequestFields.thinking = { type: 'disabled' }` (a known
* key that isn't spread into `additionalFields`). `initializeBedrock` feeds
* those params straight through this parser, so surface that as
* `thinking: false` — otherwise the disabled branch is skipped and the
* config rebuilds adaptive, flipping a user's Sonnet 5 setting back on.
*/
const persistedThinking = (
typedData.additionalModelRequestFields as { thinking?: unknown } | undefined
)?.thinking;
if (
additionalFields.thinking === undefined &&
typeof persistedThinking === 'object' &&
persistedThinking !== null &&
(persistedThinking as { type?: string }).type === 'disabled'
) {
additionalFields.thinking = false;
}
/** Configure thinking for Bedrock Anthropic models: 3.7 Sonnet, Claude 4+ (opus/sonnet/haiku), and Mythos-class (Fable/Mythos). */
if (
typeof typedData.model === 'string' &&
@ -376,9 +426,13 @@ export const bedrockInputParser = s.tConversationSchema
delete additionalFields.effort;
if (additionalFields.thinking === false) {
delete additionalFields.thinking;
delete additionalFields.thinkingBudget;
delete additionalFields.thinkingDisplay;
if (requiresExplicitThinkingDisabled(typedData.model as string)) {
additionalFields.thinking = { type: 'disabled' };
} else {
delete additionalFields.thinking;
}
} else {
/**
* Persisted agent `model_parameters` round-trip back through this

View file

@ -1996,6 +1996,7 @@ const sharedAnthropicModels = [
'claude-fable-5',
'claude-opus-4-8',
'claude-opus-4-7',
'claude-sonnet-5',
'claude-sonnet-4-6',
'claude-opus-4-6',
'claude-sonnet-4-5',
@ -2021,6 +2022,7 @@ export const bedrockModels = [
'anthropic.claude-fable-5',
'anthropic.claude-opus-4-8',
'anthropic.claude-opus-4-7',
'anthropic.claude-sonnet-5',
'anthropic.claude-sonnet-4-6',
'anthropic.claude-opus-4-6-v1',
'anthropic.claude-sonnet-4-5-20250929-v1:0',

View file

@ -10,7 +10,7 @@ describe('anthropicSettings', () => {
describe('maxOutputTokens.reset()', () => {
const { reset } = anthropicSettings.maxOutputTokens;
describe('Claude Sonnet models', () => {
describe('Claude Sonnet 4.x models (64K limit)', () => {
it('should return 64K for claude-sonnet-4', () => {
expect(reset('claude-sonnet-4')).toBe(64000);
});
@ -19,12 +19,22 @@ describe('anthropicSettings', () => {
expect(reset('claude-sonnet-4-5')).toBe(64000);
});
it('should return 64K for claude-sonnet-5', () => {
expect(reset('claude-sonnet-5')).toBe(64000);
it('should return 64K for claude-sonnet-4-6', () => {
expect(reset('claude-sonnet-4-6')).toBe(64000);
});
});
describe('Claude Sonnet 5+ models (128K limit)', () => {
it('should return 128K for claude-sonnet-5', () => {
expect(reset('claude-sonnet-5')).toBe(128000);
});
it('should return 64K for future versions like claude-sonnet-9', () => {
expect(reset('claude-sonnet-9')).toBe(64000);
it('should return 128K for future versions like claude-sonnet-9', () => {
expect(reset('claude-sonnet-9')).toBe(128000);
});
it('should return 128K for double-digit minor versions like claude-sonnet-5-10', () => {
expect(reset('claude-sonnet-5-10')).toBe(128000);
});
});
@ -265,6 +275,20 @@ describe('anthropicSettings', () => {
});
});
describe('Claude Sonnet 5+ models (128K cap)', () => {
it('should allow 100K for claude-sonnet-5', () => {
expect(set(100000, 'claude-sonnet-5')).toBe(100000);
});
it('should cap at 128K for claude-sonnet-5 when value exceeds', () => {
expect(set(150000, 'claude-sonnet-5')).toBe(128000);
});
it('should cap at 128K for future versions like claude-sonnet-9', () => {
expect(set(150000, 'claude-sonnet-9')).toBe(128000);
});
});
describe('Claude Opus 4.5+ models (64K cap)', () => {
it('should cap at 64K for claude-opus-4-5 when value exceeds', () => {
expect(set(100000, 'claude-opus-4-5')).toBe(64000);

View file

@ -547,6 +547,10 @@ export const anthropicSettings = {
return ANTHROPIC_MAX_OUTPUT;
}
if (/claude-sonnet[-.]?(?:[5-9]|\d{2,})/.test(modelName)) {
return ANTHROPIC_MAX_OUTPUT;
}
if (/claude-(?:sonnet|haiku)[-.]?[4-9]/.test(modelName)) {
return CLAUDE_4_64K_MAX_OUTPUT;
}
@ -576,6 +580,13 @@ export const anthropicSettings = {
return value;
}
if (/claude-sonnet[-.]?(?:[5-9]|\d{2,})/.test(modelName)) {
if (value > ANTHROPIC_MAX_OUTPUT) {
return ANTHROPIC_MAX_OUTPUT;
}
return value;
}
if (/claude-(?:sonnet|haiku)[-.]?[4-9]/.test(modelName) && value > CLAUDE_4_64K_MAX_OUTPUT) {
return CLAUDE_4_64K_MAX_OUTPUT;
}

View file

@ -18,6 +18,7 @@ import logger from '~/config/winston';
* These are the standard Anthropic model names as served by Vertex AI
*/
export const defaultVertexModels: string[] = [
'claude-sonnet-5',
'claude-sonnet-4-6',
'claude-3-7-sonnet-20250219',
'claude-3-5-sonnet-v2@20241022',

View file

@ -2473,6 +2473,54 @@ describe('Claude Model Tests', () => {
cacheTokenValues['claude-mythos-5'].read,
);
});
it('should return correct prompt and completion rates for Claude Sonnet 5', () => {
expect(getMultiplier({ model: 'claude-sonnet-5', tokenType: 'prompt' })).toBe(
tokenValues['claude-sonnet-5'].prompt,
);
expect(getMultiplier({ model: 'claude-sonnet-5', tokenType: 'completion' })).toBe(
tokenValues['claude-sonnet-5'].completion,
);
});
it('should pin Claude Sonnet 5 to introductory $2 / $10 per MTok (through 2026-08-31)', () => {
expect(tokenValues['claude-sonnet-5']).toEqual({ prompt: 2, completion: 10 });
});
it('should apply introductory cache rates ($2.50 / $0.20) for Claude Sonnet 5', () => {
expect(cacheTokenValues['claude-sonnet-5']).toEqual({ write: 2.5, read: 0.2 });
});
it('should handle Claude Sonnet 5 model name variations without matching Sonnet 4', () => {
const modelVariations = [
'claude-sonnet-5',
'claude-sonnet-5-20260101',
'claude-sonnet-5-latest',
'anthropic/claude-sonnet-5',
'claude-sonnet-5/anthropic',
'anthropic.claude-sonnet-5',
];
modelVariations.forEach((model) => {
const valueKey = getValueKey(model);
expect(valueKey).toBe('claude-sonnet-5');
expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(
tokenValues['claude-sonnet-5'].prompt,
);
expect(getMultiplier({ model, tokenType: 'completion' })).toBe(
tokenValues['claude-sonnet-5'].completion,
);
});
});
it('should return correct cache rates for Claude Sonnet 5', () => {
expect(getCacheMultiplier({ model: 'claude-sonnet-5', cacheType: 'write' })).toBe(
cacheTokenValues['claude-sonnet-5'].write,
);
expect(getCacheMultiplier({ model: 'claude-sonnet-5', cacheType: 'read' })).toBe(
cacheTokenValues['claude-sonnet-5'].read,
);
});
});
describe('Premium Token Pricing', () => {
@ -2500,6 +2548,7 @@ describe('Premium Token Pricing', () => {
'claude-fable-5',
'claude-mythos-5',
'claude-sonnet-4-6',
'claude-sonnet-5',
];
claudeModels.forEach((model) => {
expect(premiumTokenValues[model]).toBeUndefined();

View file

@ -172,6 +172,8 @@ export const tokenValues: Record<string, { prompt: number; completion: number }>
'claude-sonnet-4': { prompt: 3, completion: 15 },
'claude-sonnet-4-5': { prompt: 3, completion: 15 },
'claude-sonnet-4-6': { prompt: 3, completion: 15 },
// Sonnet 5 introductory pricing through 2026-08-31; revert to { prompt: 3, completion: 15 } after.
'claude-sonnet-5': { prompt: 2, completion: 10 },
'command-r': { prompt: 0.5, completion: 1.5 },
'command-r-plus': { prompt: 3, completion: 15 },
'command-text': { prompt: 1.5, completion: 2.0 },
@ -305,6 +307,8 @@ export const cacheTokenValues: Record<string, { write: number; read: number }> =
'claude-sonnet-4': { write: 3.75, read: 0.3 },
'claude-sonnet-4-5': { write: 3.75, read: 0.3 },
'claude-sonnet-4-6': { write: 3.75, read: 0.3 },
// Sonnet 5 introductory pricing through 2026-08-31; revert to { write: 3.75, read: 0.3 } after.
'claude-sonnet-5': { write: 2.5, read: 0.2 },
'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 },