🪟 fix: Resolve Context Windows and Pricing for Newer Model Families (#14877)

* fix: resolve context windows for Qwen3.5+ and newer model families

Qwen3.5 model ids matched the `qwen3` key and inherited its 40,960 token
window instead of their native 262,144, so pruneMessages dropped whole
conversations down to the system message once tool output grew large.

Add the 3.5 through 3.8 generations, Meta Muse Spark, Muse Glimmer and
Llama 4, plus newer DeepSeek, GLM, Kimi, Grok, Mistral, Nova, Cohere and
MiniMax entries. Llama 4 Scout is capped at 1M rather than its 10M native
ceiling, since no host serves near that and a 10M value would stop
pruning from ever firing.

findMatchingPattern now matches a vendor-prefixed id on its model segment
first. No map key contains a slash, so a prefix could only ever
contribute a spurious longer match: moonshotai/kimi-k2 matched moonshot
(8 chars) over kimi-k2 (7) and reported half the real window.

* fix: price newer models and make the pricing test helper faithful

Models added to the context map billed either at defaultRate or at an
older generation's rate. Llama 4 and Muse Spark fell to $6/1M, Kimi K3
undercharged 5x through the kimi key, and Grok 4.5/4.6 overcharged
through grok-4. Add rates for 21 models.

tx.ts needs no matcher change of its own, since it receives
findMatchingPattern by injection from @librechat/api. The vendor-prefix
fix therefore already corrects moonshotai/kimi-k2, which was matching
moonshot at $2.00/$5.00 rather than kimi-k2 at $0.60/$2.50.

The data-schemas test helper implemented a different algorithm from the
function production injects, returning the first reverse-order match
instead of the longest, so the pricing suite was not describing real
billing behavior. Mirror the real implementation. Every existing test
still passes, so nothing was relying on the lossy version.

Rates for the Qwen 3.x plus/flash tiers, DeepSeek V3.1/V3.2, Kimi
K2.6/K2.7 and Nova 2 Pro are left as they are, since published figures
disagreed by more than the current fuzzy match is wrong.

* test: assert parity between the context and pricing maps

Neither map imports the other, so a model added to one and forgotten in
the other is silently wrong at runtime: it bills at defaultRate, or it
has no window to prune against. A comment in tx.spec.ts claimed such a
test lived in packages/api, but none existed.

Exact key parity is the wrong invariant. Both maps resolve by longest
substring match, so gpt-4-32k legitimately prices through the 32k bucket
without a row of its own. Assert instead that every key resolves in the
other map, with exemption lists covering the genuine gaps and a further
test that fails once an exemption goes stale, so the lists shrink rather
than rot.

Verified the assertions actually fail: injecting a context key with no
price, a price with no context window, and a rate for an exempted model
each fails the expected test and names the offending model.

* fix: correct GLM 5.3, Kimi K3 aliases, Grok tiering and prefixed overrides

Four gaps found in review.

glm-5.3 matched the glm-5 key and reported 204,800 tokens rather than the
1,048,576 it shares with 5.2. Add both the context and pricing rows.

The dot-prefixed Kimi K3 ids were added to the context map without
pricing counterparts, so moonshot.kimi-k3 resolved to moonshot.kimi and
moonshotai.kimi-k3 to moonshot, billing completion at 2.5 and 5.0 rather
than 15.0.

Grok 4.5/4.6 carry a 500K window, which makes xAI's doubled rate above a
200K prompt reachable, but neither key existed in premiumTokenValues so
long prompts stayed on the base rate. Add the premium entries.

Matching the model segment of a vendor-prefixed id was too eager.
EndpointTokenConfig is an arbitrary record and one built from OpenRouter
is keyed by org/model, so an alias like org/model-latest could resolve to
a bare model entry and pick the wrong configured context and rates. Keep
a matched key that carries the vendor, and retry on the segment only when
the whole id resolved to a prefix-only match.

* refactor: type the model matcher as keys-only and drop the caller casts

findMatchingPattern reads keys and never inspects a value, but its
parameter was typed as a union of the two maps it happens to be called
with, so every injection site cast into it. pricing.spec.ts already
carried that cast and parity.spec.ts copied it.

Name the intent instead. Both adapters now pass their map through
unchanged, and a future change that starts reading values will fail to
compile rather than slip past a cast.
This commit is contained in:
Marco Beretta 2026-08-16 22:41:02 +02:00 • committed by GitHub
parent 5fc05ac037
commit 5d88b8453e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 479 additions and 19 deletions

View file

@ -8,8 +8,7 @@ import { buildTokenConfigMap } from './pricing';
/** Adapters: TxDeps types are looser than the utils signatures (string endpoint, undefined miss) */
const { getValueKey, getMultiplier, getCacheMultiplier } = createTxMethods(mongoose, {
matchModelName: (model, endpoint) => matchModelName(model, endpoint as EModelEndpoint),
findMatchingPattern: (model, values) =>
findMatchingPattern(model, values as Record<string, number>) ?? undefined,
findMatchingPattern: (model, values) => findMatchingPattern(model, values) ?? undefined,
});
const deps = { getValueKey, getMultiplier, getCacheMultiplier };

View file

@ -0,0 +1,89 @@
import { EModelEndpoint } from 'librechat-data-provider';
import { createTxMethods, tokenValues, defaultRate } from '@librechat/data-schemas';
import { maxTokensMap, getModelMaxTokens, matchModelName, findMatchingPattern } from './tokens';
/**
* Parity between the context-window map here and the pricing map in
* `@librechat/data-schemas`. The two are maintained by hand and neither imports
* the other, so a model added to one and forgotten in the other is silently
* wrong at runtime: it either bills at `defaultRate` or has no window to prune
* against. These tests fail on the half-added model rather than on the invoice.
*
* Exact key parity is not the invariant. Both maps resolve by longest-substring
* match, so `gpt-4-32k` legitimately prices via the `32k` bucket without its own
* row. What must hold is that every key *resolves* in the other map.
*/
/** Context-map keys with no pricing key. Shrink this list, do not grow it. */
const UNPRICED = new Set([
/** Degenerate prefix key: no pricing key is a substring of it. */
'mistral-',
/** AWS has not published a Nova 2 Pro rate. */
'nova-2-pro',
]);
/** Pricing keys with no context window. Shrink this list, do not grow it. */
const NO_CONTEXT = new Set([
/** Legacy OpenAI pricing buckets, not model ids. */
'8k',
'32k',
'4k',
'16k',
/** Priced, but no published window has been added to the context map. */
'devstral',
'voxtral',
'holo2',
]);
const { getValueKey } = createTxMethods({} as typeof import('mongoose'), {
matchModelName: (model: string, endpoint?: string) =>
matchModelName(model, endpoint as EModelEndpoint),
findMatchingPattern: (model: string, values: Record<string, number | Record<string, number>>) =>
findMatchingPattern(model, values) ?? undefined,
});
const contextKeys = Object.keys(maxTokensMap[EModelEndpoint.openAI]);
const priceKeys = Object.keys(tokenValues);
describe('context map and pricing map parity', () => {
it('prices every model that has a context window', () => {
const unpriced = contextKeys.filter((model) => !getValueKey(model) && !UNPRICED.has(model));
expect(unpriced).toEqual([]);
});
it('gives every priced model a context window', () => {
const windowless = priceKeys.filter(
(model) =>
getModelMaxTokens(model, EModelEndpoint.openAI) === undefined && !NO_CONTEXT.has(model),
);
expect(windowless).toEqual([]);
});
it('never lets a context-map model silently fall to the default rate', () => {
for (const model of contextKeys) {
if (UNPRICED.has(model)) {
continue;
}
const valueKey = getValueKey(model);
expect(valueKey).toBeDefined();
expect(tokenValues[valueKey as string]).toBeDefined();
}
});
it('keeps both exemption lists free of stale entries', () => {
const fixed = {
unpriced: [...UNPRICED].filter((model) => getValueKey(model)),
windowless: [...NO_CONTEXT].filter(
(model) => getModelMaxTokens(model, EModelEndpoint.openAI) !== undefined,
),
};
expect(fixed).toEqual({ unpriced: [], windowless: [] });
});
it('bills a representative new model at its own rate, not the default', () => {
for (const model of ['muse-spark-1.1', 'kimi-k3', 'glm-5.2', 'minimax-m3']) {
expect(getModelMaxTokens(model, EModelEndpoint.openAI)).toBeGreaterThan(0);
expect(tokenValues[getValueKey(model) as string].prompt).not.toBe(defaultRate);
}
});
});

View file

@ -60,3 +60,107 @@ describe('Gemini 3.7 Flash', () => {
expect(getModelMaxTokens('gemini-3', EModelEndpoint.google)).toBe(1000000);
});
});
describe('Qwen3.5 and later generations', () => {
it('resolves 262K for the vLLM model id that regressed to the qwen3 window', () => {
expect(getModelMaxTokens('Qwen/Qwen3.5-397B-A17B-FP8', EModelEndpoint.openAI)).toBe(262144);
});
it('resolves 262K natively for every 3.5 through 3.8 generation', () => {
for (const model of ['qwen3.5', 'qwen3.6', 'qwen3.7', 'qwen3.8']) {
expect(getModelMaxTokens(model, EModelEndpoint.openAI)).toBe(262144);
expect(getModelMaxTokens(`Qwen/${model}-27B`, EModelEndpoint.openAI)).toBe(262144);
}
});
it('prefers the hosted plus/flash tiers over the generation window', () => {
for (const model of ['qwen3.5-plus', 'qwen3.6-flash', 'qwen3.7-plus']) {
expect(getModelMaxTokens(model, EModelEndpoint.openAI)).toBe(1000000);
}
});
it('leaves the qwen3 generation and its parameter sizes untouched', () => {
expect(getModelMaxTokens('qwen3', EModelEndpoint.openAI)).toBe(40960);
expect(getModelMaxTokens('qwen3-30b-a3b', EModelEndpoint.openAI)).toBe(40960);
expect(getModelMaxTokens('qwen3-235b-a22b', EModelEndpoint.openAI)).toBe(40960);
expect(getModelMaxTokens('qwen2.5-72b', EModelEndpoint.openAI)).toBe(32000);
});
});
describe('Meta Muse and Llama 4', () => {
it('resolves the 1M Muse Spark window across id spellings', () => {
for (const model of ['muse-spark-1.1', 'meta/muse-spark-1.2', 'musespark']) {
expect(getModelMaxTokens(model, EModelEndpoint.openAI)).toBe(1000000);
}
});
it('resolves the 131K Muse Glimmer window', () => {
expect(getModelMaxTokens('meta-models/Muse-Glimmer-30B', EModelEndpoint.openAI)).toBe(131072);
});
it('resolves Llama 4 instead of falling through to no match', () => {
for (const model of [
'meta.llama4-scout-17b-instruct-v1:0',
'meta-llama/Llama-4-Maverick-17B-128E-Instruct',
]) {
expect(getModelMaxTokens(model, EModelEndpoint.bedrock)).toBe(1048576);
}
});
it('keeps Llama 3.x on its own window', () => {
expect(getModelMaxTokens('llama-3.3-70b', EModelEndpoint.openAI)).toBe(127500);
});
});
describe('newer windows for existing families', () => {
it.each([
['grok-4.6', 500000],
['x-ai/grok-4-5', 500000],
['deepseek-v3.2', 163840],
['deepseek-v4-flash', 1048576],
['glm-4.7', 204800],
['z-ai/glm-5.2', 1048576],
['glm-5.3', 1048576],
['moonshotai/kimi-k3', 1048576],
['kimi-k2.7-code', 262144],
['minimax-m3', 1048576],
['mistral-medium-3-5', 262144],
['nova-2-lite', 995000],
])('resolves %s to %i tokens', (model, expected) => {
expect(getModelMaxTokens(model, EModelEndpoint.openAI)).toBe(expected);
});
it('keeps command-a-plus off the wider command-a window', () => {
expect(getModelMaxTokens('command-a', EModelEndpoint.openAI)).toBe(255500);
expect(getModelMaxTokens('command-a-plus-05-2026', EModelEndpoint.openAI)).toBe(127500);
});
});
describe('vendor-prefixed model ids', () => {
it('matches the model segment rather than the vendor prefix', () => {
expect(getModelMaxTokens('moonshotai/kimi-k2', EModelEndpoint.openAI)).toBe(262144);
expect(getModelMaxTokens('moonshot/kimi-k2', EModelEndpoint.openAI)).toBe(262144);
expect(getModelMaxTokens('moonshotai/kimi-k3', EModelEndpoint.openAI)).toBe(1048576);
});
it('still resolves prefixed ids whose model segment alone has no match', () => {
expect(getModelMaxTokens('moonshot/v1-8k', EModelEndpoint.openAI)).toBe(
getModelMaxTokens('moonshot', EModelEndpoint.openAI),
);
});
it('keeps a vendor-keyed override entry over a bare one', () => {
const config: EndpointTokenConfig = {
'openrouter/foo': { prompt: 1, completion: 2, context: 900000, output: 1 },
foo: { prompt: 1, completion: 2, context: 1000, output: 1 },
};
expect(getModelMaxTokens('openrouter/foo', EModelEndpoint.custom, config)).toBe(900000);
expect(getModelMaxTokens('openrouter/foo-latest', EModelEndpoint.custom, config)).toBe(900000);
expect(getModelMaxTokens('other/foo-latest', EModelEndpoint.custom, config)).toBe(1000);
});
it('leaves dot-separated bedrock ids alone', () => {
expect(getModelMaxTokens('moonshot.kimi-k2-thinking', EModelEndpoint.bedrock)).toBe(262144);
expect(getModelMaxTokens('us.anthropic.claude-3-5-sonnet-20241022-v2:0')).toBe(200000);
});
});

View file

@ -87,6 +87,11 @@ const mistralModels = {
'mistral-nemo': 131000,
'pixtral-large': 131000,
codestral: 256000,
'mistral-medium-3': 131000,
'mistral-medium-3.5': 262144,
'mistral-medium-3-5': 262144,
'ministral-8b-2512': 262144,
'ministral-14b-2512': 262144,
};
const cohereModels = {
@ -97,6 +102,8 @@ const cohereModels = {
'command-text': 4086, // -10 from max
'command-r': 127500, // -500 from max
'command-r-plus': 127500, // -500 from max
'command-a': 255500, // -500 from max
'command-a-plus': 127500, // -500 from max
};
const googleModels = {
@ -220,6 +227,12 @@ const deepseekModels = {
'deepseek.r1': 128000,
'deepseek-r1': 128000,
'deepseek-reasoner': 128000,
'deepseek-v3.1': 163840,
'deepseek-v3.2': 163840,
'deepseek-v3.2-speciale': 131072,
'deepseek-v4': 1048576,
'deepseek-v4-flash': 1048576,
'deepseek-v4-pro': 1048576,
};
const moonshotModels = {
@ -249,6 +262,9 @@ const moonshotModels = {
'kimi-k2-thinking': 262144,
'kimi-k2-thinking-turbo': 262144,
'kimi-k2.5': 262144,
'kimi-k2.6': 262144,
'kimi-k2.7': 262144,
'kimi-k3': 1048576,
// Bedrock moonshot models
'moonshot.kimi-k2-0711': 131072,
'moonshot.kimi': 262144,
@ -257,6 +273,8 @@ const moonshotModels = {
'moonshotai.kimi': 262144,
'moonshot.kimi-k2.5': 262144,
'moonshotai.kimi-k2.5': 262144,
'moonshot.kimi-k3': 1048576,
'moonshotai.kimi-k3': 1048576,
};
const metaModels = {
@ -302,13 +320,28 @@ const metaModels = {
'llama-3.2-3b': 127500,
'llama-3.2-11b': 127500,
'llama-3.2-90b': 127500,
// Llama 3.3 (newest)
// Llama 3.3
'llama3.3': 127500,
'llama3-3': 127500,
'llama-3.3': 127500,
'llama3.3:70b': 127500,
'llama3-3-70b': 127500,
'llama-3.3-70b': 127500,
/* Llama 4. Scout's native ceiling is 10M, but no host serves anywhere near it,
so capping at 1M keeps pruning useful instead of never firing. */
llama4: 1048576,
'llama-4': 1048576,
'llama4-scout': 1048576,
'llama-4-scout': 1048576,
'llama4-maverick': 1048576,
'llama-4-maverick': 1048576,
// Muse (newest)
musespark: 1000000,
'muse-spark': 1000000,
'muse-spark-1.1': 1000000,
'muse-spark-1.2': 1000000,
'muse-glimmer': 131072,
'muse-glimmer-30b': 131072,
};
const qwenModels = {
@ -335,6 +368,21 @@ const qwenModels = {
'qwen3-coder-plus': 128000,
'qwen3-coder-flash': 128000,
'qwen3-next-80b-a3b': 262144,
'qwen3-coder-next': 262144,
/* Qwen3.5 onward: every open-weight size ships a 262K native window, while the
hosted plus/flash tiers serve 1M. */
'qwen3.5': 262144,
'qwen3.5-plus': 1000000,
'qwen3.5-flash': 1000000,
'qwen3.6': 262144,
'qwen3.6-plus': 1000000,
'qwen3.6-flash': 1000000,
'qwen3.7': 262144,
'qwen3.7-plus': 1000000,
'qwen3.7-flash': 1000000,
'qwen3.8': 262144,
'qwen3.8-plus': 1000000,
'qwen3.8-flash': 1000000,
};
const ai21Models = {
@ -354,6 +402,8 @@ const amazonModels = {
'nova-lite': 295000, // -5000 from max
'nova-pro': 295000, // -5000 from max
'nova-premier': 995000, // -5000 from max
'nova-2-lite': 995000, // -5000 from max
'nova-2-pro': 995000, // -5000 from max
};
const openAIBedrockModels = {
@ -391,6 +441,10 @@ const xAIModels = {
'grok-4': 256000, // 256K context
'grok-4-fast': 2000000, // 2M context
'grok-4-1-fast': 2000000, // 2M context (covers reasoning & non-reasoning variants)
'grok-4.5': 500000,
'grok-4-5': 500000,
'grok-4.6': 500000,
'grok-4-6': 500000,
};
const aggregateModels = {
@ -402,6 +456,17 @@ const aggregateModels = {
'glm-4.5-air': 131000,
'glm-4.5v': 66000,
'glm-4.6': 200000,
'glm-4.7': 204800,
'glm-4.7-flash': 202752,
'glm-5': 204800,
'glm-5.1': 204800,
'glm-5.2': 1048576,
'glm-5.3': 1048576,
// MiniMax
'minimax-m1': 1000000,
'minimax-m2': 204800,
'minimax-m2.7': 204800,
'minimax-m3': 1048576,
// GPT-OSS
'gpt-oss': 131000,
'gpt-oss:20b': 131000,
@ -500,13 +565,15 @@ export const maxOutputTokensMap: Record<string, Record<string, number>> = {
[EModelEndpoint.custom]: { ...modelMaxOutputs, ...deepseekMaxOutputs },
};
/** Finds the longest matching key in the tokens map via substring match. */
export function findMatchingPattern(
modelName: string,
tokensMap: Record<string, number> | EndpointTokenConfig,
): string | null {
/**
* Any model-keyed map. Matching reads keys only and never inspects a value, so
* the value type is deliberately open rather than a union that every caller has
* to cast into.
*/
type ModelKeyedMap = Readonly<Record<string, unknown>>;
function findLongestKey(lowerModelName: string, tokensMap: ModelKeyedMap): string | null {
const keys = Object.keys(tokensMap);
const lowerModelName = modelName.toLowerCase();
let bestMatch: string | null = null;
let bestLength = 0;
for (let i = keys.length - 1; i >= 0; i--) {
@ -524,6 +591,31 @@ export function findMatchingPattern(
return bestMatch;
}
/**
* Finds the longest matching key in the tokens map via substring match.
*
* A vendor prefix can outrank the id it precedes: `moonshotai/kimi-k2` matched
* `moonshot` (8) over `kimi-k2` (7) and reported half the real window. When the
* whole id resolves to a prefix-only match, retry against the model segment.
*
* A key that carries the vendor itself is already the most specific answer and
* is kept. `EndpointTokenConfig` is an arbitrary record, and one built from
* OpenRouter is keyed by `org/model`, so those must still beat a bare `model`.
*/
export function findMatchingPattern(modelName: string, tokensMap: ModelKeyedMap): string | null {
const lowerModelName = modelName.toLowerCase();
const slashIndex = lowerModelName.lastIndexOf('/');
if (slashIndex === -1) {
return findLongestKey(lowerModelName, tokensMap);
}
const fullMatch = findLongestKey(lowerModelName, tokensMap);
if (fullMatch != null && fullMatch.includes('/')) {
return fullMatch;
}
return findLongestKey(lowerModelName.slice(slashIndex + 1), tokensMap) ?? fullMatch;
}
/**
* Retrieves a token value for a given model name from a tokens map.
*

View file

@ -3,25 +3,51 @@
* These are used only by test files in data-schemas.
*/
function findLongestKey(
lowerModelName: string,
tokensMap: Record<string, number | Record<string, number>>,
): string | undefined {
const keys = Object.keys(tokensMap);
let bestMatch: string | undefined;
let bestLength = 0;
for (let i = keys.length - 1; i >= 0; i--) {
const key = keys[i];
const lowerKey = key.toLowerCase();
if (lowerKey.length > bestLength && lowerModelName.includes(lowerKey)) {
if (lowerKey.length === lowerModelName.length) {
return key;
}
bestMatch = key;
bestLength = lowerKey.length;
}
}
return bestMatch;
}
/**
* Finds the first matching pattern in a tokens/values map by reverse-iterating
* and checking if the model name (lowercased) includes the key.
* Finds the longest matching key in a tokens/values map by substring match,
* preferring the model segment of a vendor-prefixed id.
*
* Inlined from @librechat/api findMatchingPattern
* Mirrors @librechat/api findMatchingPattern. Keep the two in step: this is the
* function production injects into `createTxMethods`, so a divergence here means
* the pricing tests stop describing real billing behavior.
*/
export function findMatchingPattern(
modelName: string,
tokensMap: Record<string, number | Record<string, number>>,
): string | undefined {
const keys = Object.keys(tokensMap);
const lowerModelName = modelName.toLowerCase();
for (let i = keys.length - 1; i >= 0; i--) {
const modelKey = keys[i];
if (lowerModelName.includes(modelKey)) {
return modelKey;
}
const slashIndex = lowerModelName.lastIndexOf('/');
if (slashIndex === -1) {
return findLongestKey(lowerModelName, tokensMap);
}
return undefined;
const fullMatch = findLongestKey(lowerModelName, tokensMap);
if (fullMatch != null && fullMatch.includes('/')) {
return fullMatch;
}
return findLongestKey(lowerModelName.slice(slashIndex + 1), tokensMap) ?? fullMatch;
}
/**

View file

@ -2915,5 +2915,109 @@ describe('Long-Context Premium Cache Pricing', () => {
});
});
describe('Newer model pricing', () => {
const newlyPriced = [
'llama4-scout',
'llama-4-maverick',
'muse-spark-1.1',
'musespark',
'muse-glimmer-30b',
'deepseek-v4-flash',
'deepseek-v4-pro',
'kimi-k3',
'glm-4.7',
'glm-4.7-flash',
'glm-5.1',
'glm-5.2',
'grok-4.5',
'grok-4.6',
'ministral-8b-2512',
'ministral-14b-2512',
'nova-2-lite',
'command-a',
'minimax-m2.7',
'minimax-m3',
'qwen3-coder-next',
];
it.each(newlyPriced)('bills %s at its own rate rather than the default', (model) => {
const valueKey = getValueKey(model);
expect(valueKey).toBe(model);
expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(tokenValues[model].prompt);
expect(getMultiplier({ model, tokenType: 'completion' })).toBe(tokenValues[model].completion);
expect(getMultiplier({ model, tokenType: 'prompt' })).not.toBe(defaultRate);
});
it('keeps the preceding generation on its own rate', () => {
for (const model of [
'grok-4',
'grok-4-fast',
'ministral-8b',
'qwen3-coder',
'command-r-plus',
]) {
expect(getValueKey(model)).toBe(model);
expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(tokenValues[model].prompt);
}
});
it('resolves dated and vendor-prefixed variants to the same rate', () => {
expect(getMultiplier({ model: 'moonshotai/kimi-k3', tokenType: 'completion' })).toBe(
tokenValues['kimi-k3'].completion,
);
expect(getMultiplier({ model: 'x-ai/grok-4-6', tokenType: 'prompt' })).toBe(
tokenValues['grok-4-6'].prompt,
);
expect(getMultiplier({ model: 'command-a-plus-05-2026', tokenType: 'prompt' })).toBe(
tokenValues['command-a'].prompt,
);
});
});
describe('dot-prefixed Kimi K3 aliases', () => {
it('bills both bedrock forms at the K3 rate rather than an older Kimi row', () => {
for (const model of ['moonshot.kimi-k3', 'moonshotai.kimi-k3']) {
expect(getMultiplier({ model, tokenType: 'completion' })).toBe(
tokenValues['kimi-k3'].completion,
);
expect(getMultiplier({ model, tokenType: 'completion' })).not.toBe(
tokenValues['moonshot.kimi'].completion,
);
expect(getMultiplier({ model, tokenType: 'completion' })).not.toBe(
tokenValues['moonshot'].completion,
);
}
});
});
describe('Grok long-context premium tier', () => {
it('doubles the rate once a prompt passes the 200K threshold', () => {
for (const model of ['grok-4.5', 'grok-4.6']) {
const { threshold } = premiumTokenValues[model];
expect(getMultiplier({ model, tokenType: 'prompt', inputTokenCount: threshold })).toBe(
tokenValues[model].prompt,
);
expect(getMultiplier({ model, tokenType: 'prompt', inputTokenCount: threshold + 1 })).toBe(
premiumTokenValues[model].prompt,
);
expect(
getMultiplier({ model, tokenType: 'completion', inputTokenCount: threshold + 1 }),
).toBe(premiumTokenValues[model].completion);
}
});
});
describe('vendor-prefixed pricing keys', () => {
it('prices the model segment rather than the vendor prefix', () => {
expect(getValueKey('moonshotai/kimi-k2')).toBe('kimi-k2');
expect(getMultiplier({ model: 'moonshotai/kimi-k2', tokenType: 'prompt' })).toBe(
tokenValues['kimi-k2'].prompt,
);
expect(getMultiplier({ model: 'moonshotai/kimi-k2', tokenType: 'prompt' })).not.toBe(
tokenValues['moonshot'].prompt,
);
});
});
// Cross-package sync validation tests (tokens.ts ↔ tx.ts) moved to
// packages/api tests since they require maxTokensMap from @librechat/api.

View file

@ -74,6 +74,14 @@ const bedrockValues: Record<string, { prompt: number; completion: number }> = {
'llama-3.2-90b': { prompt: 0.72, completion: 0.72 },
'llama-3.3': { prompt: 2.65, completion: 3.5 },
'llama-3.3-70b': { prompt: 2.65, completion: 3.5 },
/* Llama 4. Per-host rates vary widely (0.08 to 0.22 prompt); these follow the
published Bedrock/OpenRouter figures. */
llama4: { prompt: 0.1, completion: 0.3 },
'llama-4': { prompt: 0.1, completion: 0.3 },
'llama4-scout': { prompt: 0.1, completion: 0.3 },
'llama-4-scout': { prompt: 0.1, completion: 0.3 },
'llama4-maverick': { prompt: 0.2, completion: 0.7 },
'llama-4-maverick': { prompt: 0.2, completion: 0.7 },
'mistral-7b': { prompt: 0.15, completion: 0.2 },
'mistral-small': { prompt: 0.15, completion: 0.2 },
'mixtral-8x7b': { prompt: 0.45, completion: 0.7 },
@ -91,11 +99,16 @@ const bedrockValues: Record<string, { prompt: number; completion: number }> = {
'nova-lite': { prompt: 0.06, completion: 0.24 },
'nova-pro': { prompt: 0.8, completion: 3.2 },
'nova-premier': { prompt: 2.5, completion: 12.5 },
'nova-2-lite': { prompt: 0.3, completion: 2.5 },
'deepseek.r1': { prompt: 1.35, completion: 5.4 },
'moonshot.kimi': { prompt: 0.6, completion: 2.5 },
'moonshot.kimi-k2': { prompt: 0.6, completion: 2.5 },
'moonshot.kimi-k2.5': { prompt: 0.6, completion: 3.0 },
'moonshot.kimi-k2-thinking': { prompt: 0.6, completion: 2.5 },
/* Both dot-prefixed forms need their own row: longest-substring matching
otherwise reaches `moonshot.kimi` or `moonshot` before `kimi-k3`. */
'moonshot.kimi-k3': { prompt: 3.0, completion: 15.0 },
'moonshotai.kimi-k3': { prompt: 3.0, completion: 15.0 },
};
/**
@ -181,10 +194,13 @@ export const tokenValues: Record<string, { prompt: number; completion: number }>
'command-r': { prompt: 0.5, completion: 1.5 },
'command-r-plus': { prompt: 3, completion: 15 },
'command-text': { prompt: 1.5, completion: 2.0 },
'command-a': { prompt: 2.5, completion: 10.0 },
'deepseek-chat': { prompt: 0.28, completion: 0.42 },
'deepseek-reasoner': { prompt: 0.28, completion: 0.42 },
'deepseek-r1': { prompt: 0.4, completion: 2.0 },
'deepseek-v3': { prompt: 0.2, completion: 0.8 },
'deepseek-v4-flash': { prompt: 0.14, completion: 0.28 },
'deepseek-v4-pro': { prompt: 0.435, completion: 0.87 },
'gemma-2': { prompt: 0.01, completion: 0.03 },
'gemma-3': { prompt: 0.02, completion: 0.04 },
'gemma-3-27b': { prompt: 0.09, completion: 0.16 },
@ -225,6 +241,12 @@ export const tokenValues: Record<string, { prompt: number; completion: number }>
'grok-4': { prompt: 3.0, completion: 15.0 },
'grok-4-fast': { prompt: 0.2, completion: 0.5 },
'grok-4-1-fast': { prompt: 0.2, completion: 0.5 },
/* Standard tier. The doubled rate above a 200K prompt lives in
`premiumTokenValues`, since the 500K window makes it reachable. */
'grok-4.5': { prompt: 2.0, completion: 6.0 },
'grok-4-5': { prompt: 2.0, completion: 6.0 },
'grok-4.6': { prompt: 2.0, completion: 6.0 },
'grok-4-6': { prompt: 2.0, completion: 6.0 },
'grok-code-fast': { prompt: 0.2, completion: 1.5 },
codestral: { prompt: 0.3, completion: 0.9 },
devstral: { prompt: 0.4, completion: 2.0 },
@ -233,6 +255,8 @@ export const tokenValues: Record<string, { prompt: number; completion: number }>
holo2: { prompt: 0.3, completion: 0.7 },
'ministral-3b': { prompt: 0.04, completion: 0.04 },
'ministral-8b': { prompt: 0.1, completion: 0.1 },
'ministral-8b-2512': { prompt: 0.15, completion: 0.15 },
'ministral-14b-2512': { prompt: 0.2, completion: 0.2 },
'mistral-nemo': { prompt: 0.15, completion: 0.15 },
'mistral-saba': { prompt: 0.2, completion: 0.6 },
'pixtral-large': { prompt: 2.0, completion: 6.0 },
@ -251,6 +275,7 @@ export const tokenValues: Record<string, { prompt: number; completion: number }>
'kimi-k2-0711-preview': { prompt: 0.6, completion: 2.5 },
'kimi-k2-thinking': { prompt: 0.6, completion: 2.5 },
'kimi-k2-thinking-turbo': { prompt: 1.15, completion: 8.0 },
'kimi-k3': { prompt: 3.0, completion: 15.0 },
'moonshot-v1': { prompt: 2.0, completion: 5.0 },
'moonshot-v1-auto': { prompt: 2.0, completion: 5.0 },
'moonshot-v1-8k': { prompt: 0.2, completion: 2.0 },
@ -273,6 +298,16 @@ export const tokenValues: Record<string, { prompt: number; completion: number }>
'glm-4.5-air': { prompt: 0.14, completion: 0.86 },
'glm-4.5v': { prompt: 0.6, completion: 1.8 },
'glm-4.6': { prompt: 0.5, completion: 1.75 },
'glm-4.7': { prompt: 0.6, completion: 2.2 },
'glm-4.7-flash': { prompt: 0.06, completion: 0.4 },
'glm-5': { prompt: 1.4, completion: 4.4 },
'glm-5.1': { prompt: 1.4, completion: 4.4 },
'glm-5.2': { prompt: 1.4, completion: 4.4 },
'glm-5.3': { prompt: 1.4, completion: 4.4 },
'minimax-m1': { prompt: 0.55, completion: 2.2 },
'minimax-m2': { prompt: 0.3, completion: 1.2 },
'minimax-m2.7': { prompt: 0.3, completion: 1.2 },
'minimax-m3': { prompt: 0.3, completion: 1.2 },
qwen: { prompt: 0.08, completion: 0.33 },
'qwen2.5': { prompt: 0.08, completion: 0.33 },
'qwen-turbo': { prompt: 0.05, completion: 0.2 },
@ -295,6 +330,13 @@ export const tokenValues: Record<string, { prompt: number; completion: number }>
'qwen3-coder-plus': { prompt: 1, completion: 5 },
'qwen3-coder-flash': { prompt: 0.3, completion: 1.5 },
'qwen3-next-80b-a3b': { prompt: 0.1, completion: 0.8 },
'qwen3-coder-next': { prompt: 0.12, completion: 0.8 },
musespark: { prompt: 1.25, completion: 4.25 },
'muse-spark': { prompt: 1.25, completion: 4.25 },
'muse-spark-1.1': { prompt: 1.25, completion: 4.25 },
'muse-spark-1.2': { prompt: 1.25, completion: 4.25 },
'muse-glimmer': { prompt: 0.3, completion: 1.2 },
'muse-glimmer-30b': { prompt: 0.3, completion: 1.2 },
},
bedrockValues,
);
@ -396,6 +438,10 @@ export const premiumTokenValues: Record<
'gpt-5.6': { threshold: 272000, prompt: 10, completion: 45 },
'gpt-5.6-terra': { threshold: 272000, prompt: 5, completion: 22.5 },
'gpt-5.6-luna': { threshold: 272000, prompt: 2, completion: 9 },
'grok-4.5': { threshold: 200000, prompt: 4, completion: 12 },
'grok-4-5': { threshold: 200000, prompt: 4, completion: 12 },
'grok-4.6': { threshold: 200000, prompt: 4, completion: 12 },
'grok-4-6': { threshold: 200000, prompt: 4, completion: 12 },
};
/**