📖 feat: Add Claude Fable 5 Support (#13628)

* 📖 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.
This commit is contained in:
Danny Avila 2026-06-09 16:22:39 -04:00 committed by GitHub
parent 8fc2314208
commit 2aea5f4a3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 349 additions and 12 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<string, unknown>;
const additionalFields = result.additionalModelRequestFields as Record<string, unknown>;
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',

View file

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

View file

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

View file

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

View file

@ -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) => {

View file

@ -159,6 +159,8 @@ export const tokenValues: Record<string, { prompt: number; completion: number }>
'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<string, { write: number; read: number }> =
'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 },