From 2aea5f4a3ac77e10e7c1efc447c918d1b1b7ed79 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 9 Jun 2026 16:22:39 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=96=20feat:=20Add=20Claude=20Fable=205?= =?UTF-8?q?=20Support=20(#13628)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ๐Ÿ“– feat: Add Claude Fable 5 Support Claude Fable 5 (`claude-fable-5`) is Anthropic's most capable widely released model (GA 2026-06-09). Its naming drops the opus/sonnet/haiku tier, so LibreChat's name-parsing helpers miss it; this teaches them the Mythos-class family (Fable / Mythos) and registers the model. - Add `parseMythosClassVersion` and route Fable/Mythos through `supportsAdaptiveThinking`, `omitsThinkingByDefault`, `omitsSamplingParameters`, and `supportsContext1m` - Extend the Bedrock detection regexes (beta headers + adaptive-thinking branch) and `checkPromptCacheSupport` to match `claude-(fable|mythos)` - Return 128K max output for Fable/Mythos in `maxOutputTokens.reset`/`set` - Register `claude-fable-5` in shared Anthropic + Bedrock model lists, 1M context / 128K output token maps, and $10/$50 pricing with 12.5/1 cache rates (`claude-mythos-5` added to token + pricing maps only, since it is limited-availability) - Update `.env.example` and the Vertex `librechat.example.yaml` examples - Add parallel tests across tokens, Anthropic llm config, the Bedrock parser, and tx pricing * ๐Ÿงน refactor: Centralize Mythos-class detection; address review feedback - Add `isMythosClassModel` + `MYTHOS_CLASS_FAMILIES` in schemas.ts as the single source of truth for the Fable/Mythos family; route every gate (adaptive thinking, omit-thinking, omit-sampling, 1M context, prompt cache, 128K max-output reset/set) through it. A future sibling class is now a one-line edit. - [Codex P2] Exclude Mythos-class from getBedrockAnthropicBetaHeaders: Fable/ Mythos ship 128K output + fine-grained tool streaming by default, and the legacy output-128k-2025-02-19 beta is 3.7-Sonnet-only on Bedrock and risks request rejection. They still get adaptive thinking + effort. - [Copilot] Add Mythos 5 test parity (name variations, cache rates, pinned $10/$50) in tx.spec; add Mythos context/max-output/name-match in tokens.spec; fix the stale claude-3-7-sonnet-only comment in bedrock.ts. - Add isMythosClassModel unit tests covering all declared families. * ๐Ÿ“ docs: Clarify Mythos-class Bedrock requirements; correct beta-omit rationale Verified live against Bedrock (acct 951834775723, us-west-2): - anthropic.claude-fable-5 IS a real Bedrock catalog model, INFERENCE_PROFILE-only exactly like the existing anthropic.claude-opus-4-7/4-8 and claude-sonnet-4-6 default entries (refutes the "invalid model id" review claim). - Mythos-class also requires opting into Anthropic data sharing (Bedrock Data Retention API) before invocation. Changes: - .env.example: note that Mythos-class (Fable/Mythos) is inference-profile-only on Bedrock and needs the data-sharing opt-in. - bedrock.ts: reword the beta-omit comment to the verified rationale โ€” output-128k / fine-grained-tool-streaming are built-in/no-op for the 4.7+ generation, so omitting them is lossless (dropped the unverified "Bedrock may reject" wording). * ๐Ÿ”„ refactor: Reorganize imports in schemas.ts and tx.spec.ts - Moved `TFeedback` and `Tools` imports to the top of `schemas.ts` for better readability. - Adjusted import order in `tx.spec.ts` to maintain consistency and improve clarity. --- .env.example | 10 +- api/utils/tokens.spec.js | 62 +++++++++++++ librechat.example.yaml | 3 + .../api/src/endpoints/anthropic/helpers.ts | 4 +- .../api/src/endpoints/anthropic/llm.spec.ts | 36 ++++++++ packages/api/src/utils/tokens.ts | 4 + packages/data-provider/specs/bedrock.spec.ts | 79 +++++++++++++++- packages/data-provider/src/bedrock.ts | 29 +++++- packages/data-provider/src/config.ts | 2 + packages/data-provider/src/schemas.ts | 36 +++++++- packages/data-schemas/src/methods/tx.spec.ts | 92 ++++++++++++++++++- packages/data-schemas/src/methods/tx.ts | 4 + 12 files changed, 349 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index b58207657b..468a20968f 100644 --- a/.env.example +++ b/.env.example @@ -209,7 +209,7 @@ PROXY= #============# ANTHROPIC_API_KEY=user_provided -# 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_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_REVERSE_PROXY= # Set to true to use Anthropic models through Google Vertex AI instead of direct API @@ -275,8 +275,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-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 +# 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 # See all Bedrock model IDs here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns @@ -287,6 +287,10 @@ ANTHROPIC_API_KEY=user_provided # The following models are not support due to not supporting conversation history: # ai21.j2-ultra-v1, cohere.command-text-v14, cohere.command-light-text-v14 +# Claude Mythos-class models (anthropic.claude-fable-5, anthropic.claude-mythos-5) are inference-profile +# only on Bedrock โ€” use a profile ID (e.g. us.anthropic.claude-fable-5) โ€” and require opting into Anthropic +# data sharing via the Bedrock Data Retention API/console before they can be invoked. + #============# # Google # #============# diff --git a/api/utils/tokens.spec.js b/api/utils/tokens.spec.js index e2c4ac9ba4..82c5a8b31f 100644 --- a/api/utils/tokens.spec.js +++ b/api/utils/tokens.spec.js @@ -1521,6 +1521,68 @@ describe('Claude Model Tests', () => { }); }); + it('should return correct context length for Claude Fable 5 (1M)', () => { + expect(getModelMaxTokens('claude-fable-5', EModelEndpoint.anthropic)).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-fable-5'], + ); + expect(getModelMaxTokens('claude-fable-5')).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-fable-5'], + ); + }); + + it('should return correct max output tokens for Claude Fable 5 (128K)', () => { + const { getModelMaxOutputTokens } = require('@librechat/api'); + expect(getModelMaxOutputTokens('claude-fable-5', EModelEndpoint.anthropic)).toBe( + maxOutputTokensMap[EModelEndpoint.anthropic]['claude-fable-5'], + ); + }); + + it('should match model names correctly for Claude Fable 5', () => { + const modelVariations = [ + 'claude-fable-5', + 'claude-fable-5-20260609', + 'claude-fable-5-latest', + 'anthropic/claude-fable-5', + 'claude-fable-5/anthropic', + 'anthropic.claude-fable-5', + ]; + + modelVariations.forEach((model) => { + expect(matchModelName(model, EModelEndpoint.anthropic)).toBe('claude-fable-5'); + }); + }); + + it('should return correct context length for Claude Mythos 5 (1M)', () => { + expect(getModelMaxTokens('claude-mythos-5', EModelEndpoint.anthropic)).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-mythos-5'], + ); + expect(getModelMaxTokens('claude-mythos-5')).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-mythos-5'], + ); + }); + + it('should return correct max output tokens for Claude Mythos 5 (128K)', () => { + const { getModelMaxOutputTokens } = require('@librechat/api'); + expect(getModelMaxOutputTokens('claude-mythos-5', EModelEndpoint.anthropic)).toBe( + maxOutputTokensMap[EModelEndpoint.anthropic]['claude-mythos-5'], + ); + }); + + it('should match model names correctly for Claude Mythos 5', () => { + const modelVariations = [ + 'claude-mythos-5', + 'claude-mythos-5-20260609', + 'claude-mythos-5-latest', + 'anthropic/claude-mythos-5', + 'claude-mythos-5/anthropic', + 'anthropic.claude-mythos-5', + ]; + + modelVariations.forEach((model) => { + expect(matchModelName(model, EModelEndpoint.anthropic)).toBe('claude-mythos-5'); + }); + }); + 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'], diff --git a/librechat.example.yaml b/librechat.example.yaml index 198cf304e4..bd0385bd16 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -420,6 +420,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-fable-5" # # - "claude-opus-4-8" # # - "claude-sonnet-4-6" # # - "claude-3-7-sonnet-20250219" @@ -431,6 +432,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-fable-5: + # deploymentName: claude-fable-5 # claude-opus-4.8: # deploymentName: claude-opus-4-8 # claude-opus-4.5: diff --git a/packages/api/src/endpoints/anthropic/helpers.ts b/packages/api/src/endpoints/anthropic/helpers.ts index 612d947a22..33431b30e1 100644 --- a/packages/api/src/endpoints/anthropic/helpers.ts +++ b/packages/api/src/endpoints/anthropic/helpers.ts @@ -5,6 +5,7 @@ import { ThinkingDisplay, AnthropicEffort, anthropicSettings, + isMythosClassModel, resolveThinkingDisplay, supportsAdaptiveThinking, } from 'librechat-data-provider'; @@ -49,7 +50,8 @@ function checkPromptCacheSupport(modelName: string): boolean { /claude-3-(?:sonnet|haiku|opus)?/.test(modelMatch) || /claude-(?:sonnet|opus|haiku)-[4-9]/.test(modelMatch) || /claude-[4-9]-(?:sonnet|opus|haiku)?/.test(modelMatch) || - /claude-4(?:-(?:sonnet|opus|haiku))?/.test(modelMatch) + /claude-4(?:-(?:sonnet|opus|haiku))?/.test(modelMatch) || + isMythosClassModel(modelMatch) ); } diff --git a/packages/api/src/endpoints/anthropic/llm.spec.ts b/packages/api/src/endpoints/anthropic/llm.spec.ts index 41fc898cff..844d4ef2c0 100644 --- a/packages/api/src/endpoints/anthropic/llm.spec.ts +++ b/packages/api/src/endpoints/anthropic/llm.spec.ts @@ -1082,6 +1082,42 @@ describe('getLLMConfig', () => { expect(result.llmConfig).not.toHaveProperty('topK'); }); + it('should set adaptive thinking with summarized display for Fable 5', () => { + const result = getLLMConfig('test-key', { + modelOptions: { model: 'claude-fable-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 default Fable 5 max output tokens to 128K', () => { + const result = getLLMConfig('test-key', { + modelOptions: { model: 'claude-fable-5', thinking: true }, + }); + expect(result.llmConfig.maxTokens).toBe(128000); + }); + + it('should omit sampling parameters for Fable 5', () => { + const result = getLLMConfig('test-key', { + modelOptions: { + model: 'claude-fable-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 set thinking.display for pre-Opus-4.7 adaptive models', () => { const pre47Models = ['claude-opus-4-6', 'claude-sonnet-4-6']; diff --git a/packages/api/src/utils/tokens.ts b/packages/api/src/utils/tokens.ts index 4101f7b8c0..7a1bc0b1c7 100644 --- a/packages/api/src/utils/tokens.ts +++ b/packages/api/src/utils/tokens.ts @@ -156,6 +156,8 @@ const anthropicModels = { 'claude-opus-4-6': 1000000, 'claude-opus-4-7': 1000000, 'claude-opus-4-8': 1000000, + 'claude-fable-5': 1000000, + 'claude-mythos-5': 1000000, }; const deepseekModels = { @@ -405,6 +407,8 @@ const anthropicMaxOutputs = { 'claude-opus-4-6': 128000, 'claude-opus-4-7': 128000, 'claude-opus-4-8': 128000, + 'claude-fable-5': 128000, + 'claude-mythos-5': 128000, 'claude-3.5-sonnet': 8192, 'claude-3-5-sonnet': 8192, 'claude-3.7-sonnet': 128000, diff --git a/packages/data-provider/specs/bedrock.spec.ts b/packages/data-provider/specs/bedrock.spec.ts index d54e5c504e..f8dfd7f22a 100644 --- a/packages/data-provider/specs/bedrock.spec.ts +++ b/packages/data-provider/specs/bedrock.spec.ts @@ -1,4 +1,4 @@ -import { ThinkingDisplay } from '../src/schemas'; +import { ThinkingDisplay, isMythosClassModel, MYTHOS_CLASS_FAMILIES } from '../src/schemas'; import { BEDROCK_OUTPUT_128K_BETA, supportsAdaptiveThinking, @@ -14,6 +14,23 @@ import { const BEDROCK_CLAUDE_4_BETAS = [BEDROCK_OUTPUT_128K_BETA, BEDROCK_FINE_GRAINED_TOOL_STREAMING_BETA]; +describe('isMythosClassModel (single source of truth for Fable/Mythos)', () => { + test('matches every declared family across naming variants', () => { + MYTHOS_CLASS_FAMILIES.forEach((family) => { + expect(isMythosClassModel(`claude-${family}-5`)).toBe(true); + expect(isMythosClassModel(`anthropic.claude-${family}-5`)).toBe(true); + expect(isMythosClassModel(`us.anthropic.claude-${family}-5`)).toBe(true); + expect(isMythosClassModel(`claude-${family}-5-20260609`)).toBe(true); + }); + }); + + test('does not match opus/sonnet/haiku or unrelated models', () => { + ['claude-opus-4-8', 'claude-sonnet-4-6', 'claude-haiku-4-5', 'gpt-4o', ''].forEach((model) => { + expect(isMythosClassModel(model)).toBe(false); + }); + }); +}); + describe('supportsAdaptiveThinking', () => { test('should return true for claude-opus-4-6', () => { expect(supportsAdaptiveThinking('claude-opus-4-6')).toBe(true); @@ -43,6 +60,13 @@ describe('supportsAdaptiveThinking', () => { expect(supportsAdaptiveThinking('claude-sonnet-6')).toBe(true); }); + test('should return true for Mythos-class models (Fable / Mythos)', () => { + expect(supportsAdaptiveThinking('claude-fable-5')).toBe(true); + expect(supportsAdaptiveThinking('claude-mythos-5')).toBe(true); + expect(supportsAdaptiveThinking('anthropic.claude-fable-5')).toBe(true); + expect(supportsAdaptiveThinking('us.anthropic.claude-fable-5')).toBe(true); + }); + test('should return false for claude-opus-4-5', () => { expect(supportsAdaptiveThinking('claude-opus-4-5')).toBe(false); }); @@ -161,6 +185,12 @@ describe('supportsContext1m', () => { expect(supportsContext1m('claude-opus-5')).toBe(true); }); + test('should return true for Mythos-class models (Fable / Mythos)', () => { + expect(supportsContext1m('claude-fable-5')).toBe(true); + expect(supportsContext1m('claude-mythos-5')).toBe(true); + expect(supportsContext1m('anthropic.claude-fable-5')).toBe(true); + }); + test('should return false for claude-opus-4-5', () => { expect(supportsContext1m('claude-opus-4-5')).toBe(false); }); @@ -235,6 +265,12 @@ describe('omitsThinkingByDefault', () => { expect(omitsThinkingByDefault('claude-opus-9')).toBe(true); }); + test('returns true for Mythos-class models (Fable / Mythos)', () => { + expect(omitsThinkingByDefault('claude-fable-5')).toBe(true); + expect(omitsThinkingByDefault('claude-mythos-5')).toBe(true); + expect(omitsThinkingByDefault('anthropic.claude-fable-5')).toBe(true); + }); + test('returns false for claude-opus-4-6 (adaptive but pre-4.7)', () => { expect(omitsThinkingByDefault('claude-opus-4-6')).toBe(false); }); @@ -285,6 +321,19 @@ describe('omitsSamplingParameters', () => { }); }); + test('returns true for Mythos-class models (Fable / Mythos)', () => { + const models = [ + 'claude-fable-5', + 'claude-mythos-5', + 'anthropic.claude-fable-5', + 'us.anthropic.claude-fable-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', @@ -618,6 +667,34 @@ describe('bedrockInputParser', () => { expect(additionalFields.top_k).toBe(40); }); + test('should set adaptive thinking and strip sampling params for Fable 5 Bedrock models', () => { + const input = { + model: 'anthropic.claude-fable-5', + effort: 'high', + temperature: 0.7, + topP: 0.9, + topK: 40, + top_p: 0.8, + additionalModelRequestFields: { + custom_flag: true, + temperature: 0.5, + top_k: 20, + }, + }; + const result = bedrockInputParser.parse(input) as Record; + const additionalFields = result.additionalModelRequestFields as Record; + expect(result.temperature).toBeUndefined(); + expect(result.topP).toBeUndefined(); + expect(additionalFields.temperature).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.output_config).toEqual({ effort: 'high' }); + /** Mythos-class models do not receive the legacy output-128k / fine-grained-tool-streaming betas. */ + expect(additionalFields.anthropic_beta).toBeUndefined(); + }); + test('should set thinking.display to "summarized" so Opus 4.7 returns reasoning blocks', () => { const input = { model: 'anthropic.claude-opus-4-7', diff --git a/packages/data-provider/src/bedrock.ts b/packages/data-provider/src/bedrock.ts index 372c0d002b..84ec3708dc 100644 --- a/packages/data-provider/src/bedrock.ts +++ b/packages/data-provider/src/bedrock.ts @@ -105,7 +105,13 @@ function parseSonnetVersion(model: string): { major: number; minor: number } | n return null; } -/** Checks if a model supports adaptive thinking (Opus 4.6+, Sonnet 4.6+) */ +/** + * Mythos-class detection (Claude Fable / Mythos) lives in `schemas.ts` as + * `isMythosClassModel` โ€” the single source of truth for the family names. + * The helpers below OR it in alongside the `opus`/`sonnet` version parsers. + */ + +/** Checks if a model supports adaptive thinking (Opus 4.6+, Sonnet 4.6+, Fable/Mythos) */ export function supportsAdaptiveThinking(model: string): boolean { const opus = parseOpusVersion(model); if (opus && (opus.major > 4 || (opus.major === 4 && opus.minor >= 6))) { @@ -115,6 +121,9 @@ export function supportsAdaptiveThinking(model: string): boolean { if (sonnet != null && (sonnet.major > 4 || (sonnet.major === 4 && sonnet.minor >= 6))) { return true; } + if (s.isMythosClassModel(model)) { + return true; + } return false; } @@ -133,6 +142,9 @@ export function omitsThinkingByDefault(model: string): boolean { if (opus && (opus.major > 4 || (opus.major === 4 && opus.minor >= 7))) { return true; } + if (s.isMythosClassModel(model)) { + return true; + } return false; } @@ -141,10 +153,13 @@ export function omitsSamplingParameters(model: string): boolean { if (opus && (opus.major > 4 || (opus.major === 4 && opus.minor >= 7))) { return true; } + if (s.isMythosClassModel(model)) { + return true; + } return false; } -/** Checks if a model has a 1M context window (Sonnet 4.6+, Opus 4.6+, Opus 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); if (sonnet != null && (sonnet.major > 4 || (sonnet.major === 4 && sonnet.minor >= 6))) { @@ -154,6 +169,9 @@ export function supportsContext1m(model: string): boolean { if (opus && (opus.major > 4 || (opus.major === 4 && opus.minor >= 6))) { return true; } + if (s.isMythosClassModel(model)) { + return true; + } return false; } @@ -167,6 +185,8 @@ export function supportsContext1m(model: string): boolean { function getBedrockAnthropicBetaHeaders(model: string): string[] { const betaHeaders: string[] = []; + /** Mythos-class (Fable/Mythos) is intentionally not matched: these betas are built-in/no-op for the + * 4.7+ generation (Fable has native 128K output), so omitting them on Bedrock is lossless. */ const isClaude4PlusModel = /anthropic\.claude-(?:[4-9](?:\.\d+)?(?:-\d+)?-(?:sonnet|opus|haiku)|(?:sonnet|opus|haiku)-[4-9])/.test( model, @@ -334,13 +354,14 @@ export const bedrockInputParser = s.tConversationSchema } }); - /** Default thinking and thinkingBudget for 'anthropic.claude-3-7-sonnet' models, if not defined */ + /** Configure thinking for Bedrock Anthropic models: 3.7 Sonnet, Claude 4+ (opus/sonnet/haiku), and Mythos-class (Fable/Mythos). */ if ( typeof typedData.model === 'string' && (typedData.model.includes('anthropic.claude-3-7-sonnet') || /anthropic\.claude-(?:[4-9](?:\.\d+)?(?:-\d+)?-(?:sonnet|opus|haiku)|(?:sonnet|opus|haiku)-[4-9])/.test( typedData.model, - )) + ) || + s.isMythosClassModel(typedData.model)) ) { const isAdaptive = supportsAdaptiveThinking(typedData.model as string); diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 34fa03c642..7426d20631 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1615,6 +1615,7 @@ const sharedOpenAIModels = [ ]; const sharedAnthropicModels = [ + 'claude-fable-5', 'claude-opus-4-8', 'claude-opus-4-7', 'claude-sonnet-4-6', @@ -1639,6 +1640,7 @@ const sharedAnthropicModels = [ ]; export const bedrockModels = [ + 'anthropic.claude-fable-5', 'anthropic.claude-opus-4-8', 'anthropic.claude-opus-4-7', 'anthropic.claude-sonnet-4-6', diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 43598d1ff5..4503d2af05 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { Tools } from './types/assistants'; import type { TMessageContentParts, FunctionTool, FunctionToolCall } from './types/assistants'; -import { TFeedback, feedbackSchema } from './feedback'; import type { SearchResultData } from './types/web'; import type { TFile } from './types/files'; +import { TFeedback, feedbackSchema } from './feedback'; +import { Tools } from './types/assistants'; export const isUUID = z.string().uuid(); @@ -459,6 +459,27 @@ const CLAUDE_4_64K_MAX_OUTPUT = 64000 as const; const CLAUDE_32K_MAX_OUTPUT = 32000 as const; const DEFAULT_MAX_OUTPUT = 8192 as const; const LEGACY_ANTHROPIC_MAX_OUTPUT = 4096 as const; + +/** + * Claude "Mythos-class" model families โ€” new top-level classes (peers of + * `opus`/`sonnet`/`haiku`) that ship with the post-Opus-4.7 modern profile: + * adaptive thinking always on, raw thinking omitted by default (summarized + * opt-in), sampling parameters rejected, and a 1M context window. The tier + * word is the class name itself, so the `opus`/`sonnet` version parsers don't + * cover them. + * + * Single source of truth: add a future sibling class name here and every + * Mythos-class gate (adaptive thinking, sampling omission, prompt caching, 1M + * context, 128K output) picks it up. + */ +export const MYTHOS_CLASS_FAMILIES = ['fable', 'mythos'] as const; +const MYTHOS_CLASS_PATTERN = new RegExp(`claude-(?:${MYTHOS_CLASS_FAMILIES.join('|')})[-.]?\\d`); + +/** Whether the model is a Claude Mythos-class model (e.g. `claude-fable-5`). */ +export function isMythosClassModel(model: string): boolean { + return MYTHOS_CLASS_PATTERN.test(model); +} + export const anthropicSettings = { model: { default: 'claude-3-5-sonnet-latest' as const, @@ -487,6 +508,10 @@ export const anthropicSettings = { step: 1 as const, default: DEFAULT_MAX_OUTPUT, reset: (modelName: string) => { + if (isMythosClassModel(modelName)) { + return ANTHROPIC_MAX_OUTPUT; + } + if (/claude-opus[-.]?(?:4[-.]?(?:[6-9]|\d{2,})|[5-9]|\d{2,})/.test(modelName)) { return ANTHROPIC_MAX_OUTPUT; } @@ -506,6 +531,13 @@ export const anthropicSettings = { return DEFAULT_MAX_OUTPUT; }, set: (value: number, modelName: string) => { + if (isMythosClassModel(modelName)) { + if (value > ANTHROPIC_MAX_OUTPUT) { + return ANTHROPIC_MAX_OUTPUT; + } + return value; + } + if (/claude-opus[-.]?(?:4[-.]?(?:[6-9]|\d{2,})|[5-9]|\d{2,})/.test(modelName)) { if (value > ANTHROPIC_MAX_OUTPUT) { return ANTHROPIC_MAX_OUTPUT; diff --git a/packages/data-schemas/src/methods/tx.spec.ts b/packages/data-schemas/src/methods/tx.spec.ts index 95882c9f80..2ef86e8dfd 100644 --- a/packages/data-schemas/src/methods/tx.spec.ts +++ b/packages/data-schemas/src/methods/tx.spec.ts @@ -1,5 +1,4 @@ /** Note: No hard-coded values should be used in this file. */ -import { matchModelName, findMatchingPattern } from './test-helpers'; import { EModelEndpoint } from 'librechat-data-provider'; import { createTxMethods, @@ -8,6 +7,7 @@ import { premiumTokenValues, defaultRate, } from './tx'; +import { matchModelName, findMatchingPattern } from './test-helpers'; const { getValueKey, getMultiplier, getPremiumRate, getCacheMultiplier } = createTxMethods( {} as typeof import('mongoose'), @@ -2377,6 +2377,94 @@ describe('Claude Model Tests', () => { cacheTokenValues['claude-opus-4-8'].read, ); }); + + it('should return correct prompt and completion rates for Claude Fable 5', () => { + expect(getMultiplier({ model: 'claude-fable-5', tokenType: 'prompt' })).toBe( + tokenValues['claude-fable-5'].prompt, + ); + expect(getMultiplier({ model: 'claude-fable-5', tokenType: 'completion' })).toBe( + tokenValues['claude-fable-5'].completion, + ); + }); + + it('should pin Claude Fable 5 pricing to $10 / $50 per MTok', () => { + expect(tokenValues['claude-fable-5']).toEqual({ prompt: 10, completion: 50 }); + }); + + it('should handle Claude Fable 5 model name variations', () => { + const modelVariations = [ + 'claude-fable-5', + 'claude-fable-5-20260609', + 'claude-fable-5-latest', + 'anthropic/claude-fable-5', + 'claude-fable-5/anthropic', + 'anthropic.claude-fable-5', + ]; + + modelVariations.forEach((model) => { + const valueKey = getValueKey(model); + expect(valueKey).toBe('claude-fable-5'); + expect(getMultiplier({ model, tokenType: 'prompt' })).toBe( + tokenValues['claude-fable-5'].prompt, + ); + expect(getMultiplier({ model, tokenType: 'completion' })).toBe( + tokenValues['claude-fable-5'].completion, + ); + }); + }); + + it('should return correct cache rates for Claude Fable 5', () => { + expect(getCacheMultiplier({ model: 'claude-fable-5', cacheType: 'write' })).toBe( + cacheTokenValues['claude-fable-5'].write, + ); + expect(getCacheMultiplier({ model: 'claude-fable-5', cacheType: 'read' })).toBe( + cacheTokenValues['claude-fable-5'].read, + ); + }); + + it('should return correct prompt and completion rates for Claude Mythos 5', () => { + expect(getMultiplier({ model: 'claude-mythos-5', tokenType: 'prompt' })).toBe( + tokenValues['claude-mythos-5'].prompt, + ); + expect(getMultiplier({ model: 'claude-mythos-5', tokenType: 'completion' })).toBe( + tokenValues['claude-mythos-5'].completion, + ); + }); + + it('should pin Claude Mythos 5 pricing to $10 / $50 per MTok', () => { + expect(tokenValues['claude-mythos-5']).toEqual({ prompt: 10, completion: 50 }); + }); + + it('should handle Claude Mythos 5 model name variations', () => { + const modelVariations = [ + 'claude-mythos-5', + 'claude-mythos-5-20260609', + 'claude-mythos-5-latest', + 'anthropic/claude-mythos-5', + 'claude-mythos-5/anthropic', + 'anthropic.claude-mythos-5', + ]; + + modelVariations.forEach((model) => { + const valueKey = getValueKey(model); + expect(valueKey).toBe('claude-mythos-5'); + expect(getMultiplier({ model, tokenType: 'prompt' })).toBe( + tokenValues['claude-mythos-5'].prompt, + ); + expect(getMultiplier({ model, tokenType: 'completion' })).toBe( + tokenValues['claude-mythos-5'].completion, + ); + }); + }); + + it('should return correct cache rates for Claude Mythos 5', () => { + expect(getCacheMultiplier({ model: 'claude-mythos-5', cacheType: 'write' })).toBe( + cacheTokenValues['claude-mythos-5'].write, + ); + expect(getCacheMultiplier({ model: 'claude-mythos-5', cacheType: 'read' })).toBe( + cacheTokenValues['claude-mythos-5'].read, + ); + }); }); describe('Premium Token Pricing', () => { @@ -2401,6 +2489,8 @@ describe('Premium Token Pricing', () => { 'claude-opus-4-6', 'claude-opus-4-7', 'claude-opus-4-8', + 'claude-fable-5', + 'claude-mythos-5', 'claude-sonnet-4-6', ]; claudeModels.forEach((model) => { diff --git a/packages/data-schemas/src/methods/tx.ts b/packages/data-schemas/src/methods/tx.ts index 2a226d34f0..827af52580 100644 --- a/packages/data-schemas/src/methods/tx.ts +++ b/packages/data-schemas/src/methods/tx.ts @@ -159,6 +159,8 @@ export const tokenValues: Record '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-fable-5': { prompt: 10, completion: 50 }, + 'claude-mythos-5': { prompt: 10, completion: 50 }, 'claude-sonnet-4': { prompt: 3, completion: 15 }, 'claude-sonnet-4-5': { prompt: 3, completion: 15 }, 'claude-sonnet-4-6': { prompt: 3, completion: 15 }, @@ -296,6 +298,8 @@ export const cacheTokenValues: Record = '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 }, + 'claude-fable-5': { write: 12.5, read: 1 }, + 'claude-mythos-5': { write: 12.5, read: 1 }, '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 },