diff --git a/packages/api/src/endpoints/pricing.spec.ts b/packages/api/src/endpoints/pricing.spec.ts index 388b71a509..fdff15f84e 100644 --- a/packages/api/src/endpoints/pricing.spec.ts +++ b/packages/api/src/endpoints/pricing.spec.ts @@ -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) ?? undefined, + findMatchingPattern: (model, values) => findMatchingPattern(model, values) ?? undefined, }); const deps = { getValueKey, getMultiplier, getCacheMultiplier }; diff --git a/packages/api/src/utils/parity.spec.ts b/packages/api/src/utils/parity.spec.ts new file mode 100644 index 0000000000..4de23c33de --- /dev/null +++ b/packages/api/src/utils/parity.spec.ts @@ -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>) => + 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); + } + }); +}); diff --git a/packages/api/src/utils/tokens.spec.ts b/packages/api/src/utils/tokens.spec.ts index a6e4cb0e78..7890af6c7c 100644 --- a/packages/api/src/utils/tokens.spec.ts +++ b/packages/api/src/utils/tokens.spec.ts @@ -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); + }); +}); diff --git a/packages/api/src/utils/tokens.ts b/packages/api/src/utils/tokens.ts index 1b1dafa7cb..8656c643b1 100644 --- a/packages/api/src/utils/tokens.ts +++ b/packages/api/src/utils/tokens.ts @@ -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> = { [EModelEndpoint.custom]: { ...modelMaxOutputs, ...deepseekMaxOutputs }, }; -/** Finds the longest matching key in the tokens map via substring match. */ -export function findMatchingPattern( - modelName: string, - tokensMap: Record | 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>; + +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. * diff --git a/packages/data-schemas/src/methods/test-helpers.ts b/packages/data-schemas/src/methods/test-helpers.ts index bd64e0268a..ebf507c2cb 100644 --- a/packages/data-schemas/src/methods/test-helpers.ts +++ b/packages/data-schemas/src/methods/test-helpers.ts @@ -3,25 +3,51 @@ * These are used only by test files in data-schemas. */ +function findLongestKey( + lowerModelName: string, + tokensMap: Record>, +): 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 | 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; } /** diff --git a/packages/data-schemas/src/methods/tx.spec.ts b/packages/data-schemas/src/methods/tx.spec.ts index 1a0e8e9483..42688881ef 100644 --- a/packages/data-schemas/src/methods/tx.spec.ts +++ b/packages/data-schemas/src/methods/tx.spec.ts @@ -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. diff --git a/packages/data-schemas/src/methods/tx.ts b/packages/data-schemas/src/methods/tx.ts index ae83c2df27..c474ee5da2 100644 --- a/packages/data-schemas/src/methods/tx.ts +++ b/packages/data-schemas/src/methods/tx.ts @@ -74,6 +74,14 @@ const bedrockValues: Record = { '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 = { '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 '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 '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 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 '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 '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 '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 }, }; /**