diff --git a/api/models/tx.js b/api/models/tx.js index 6ff105a458..97a4f89dbc 100644 --- a/api/models/tx.js +++ b/api/models/tx.js @@ -1,10 +1,40 @@ const { matchModelName, findMatchingPattern } = require('@librechat/api'); const defaultRate = 6; +/** + * Token Pricing Configuration + * + * IMPORTANT: Key Ordering for Pattern Matching + * ============================================ + * The `findMatchingPattern` function iterates through object keys in REVERSE order + * (last-defined keys are checked first) and uses `modelName.includes(key)` for matching. + * + * This means: + * 1. BASE PATTERNS must be defined FIRST (e.g., "kimi", "moonshot") + * 2. SPECIFIC PATTERNS must be defined AFTER their base patterns (e.g., "kimi-k2", "kimi-k2.5") + * + * Example ordering for Kimi models: + * kimi: { prompt: 0.6, completion: 2.5 }, // Base pattern - checked last + * 'kimi-k2': { prompt: 0.6, completion: 2.5 }, // More specific - checked before "kimi" + * 'kimi-k2.5': { prompt: 0.6, completion: 3.0 }, // Most specific - checked first + * + * Why this matters: + * - Model name "kimi-k2.5" contains both "kimi" and "kimi-k2" as substrings + * - If "kimi" were checked first, it would incorrectly match and return wrong pricing + * - By defining specific patterns AFTER base patterns, they're checked first in reverse iteration + * + * This applies to BOTH `tokenValues` and `cacheTokenValues` objects. + * + * When adding new model families: + * 1. Define the base/generic pattern first + * 2. Define increasingly specific patterns after + * 3. Ensure no pattern is a substring of another that should match differently + */ + /** * AWS Bedrock pricing * source: https://aws.amazon.com/bedrock/pricing/ - * */ + */ const bedrockValues = { // Basic llama2 patterns (base defaults to smallest variant) llama2: { prompt: 0.75, completion: 1.0 }, @@ -80,6 +110,11 @@ const bedrockValues = { 'nova-pro': { prompt: 0.8, completion: 3.2 }, 'nova-premier': { prompt: 2.5, completion: 12.5 }, 'deepseek.r1': { prompt: 1.35, completion: 5.4 }, + // Moonshot/Kimi models on Bedrock + '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 }, }; /** @@ -189,7 +224,31 @@ const tokenValues = Object.assign( 'pixtral-large': { prompt: 2.0, completion: 6.0 }, 'mistral-large': { prompt: 2.0, completion: 6.0 }, 'mixtral-8x22b': { prompt: 0.65, completion: 0.65 }, - kimi: { prompt: 0.14, completion: 2.49 }, // Base pattern (using kimi-k2 pricing) + // Moonshot/Kimi models (base patterns first, specific patterns last for correct matching) + kimi: { prompt: 0.6, completion: 2.5 }, // Base pattern + moonshot: { prompt: 2.0, completion: 5.0 }, // Base pattern (using 128k pricing) + 'kimi-latest': { prompt: 0.2, completion: 2.0 }, // Uses 8k/32k/128k pricing dynamically + 'kimi-k2': { prompt: 0.6, completion: 2.5 }, + 'kimi-k2.5': { prompt: 0.6, completion: 3.0 }, + 'kimi-k2-turbo': { prompt: 1.15, completion: 8.0 }, + 'kimi-k2-turbo-preview': { prompt: 1.15, completion: 8.0 }, + 'kimi-k2-0905': { prompt: 0.6, completion: 2.5 }, + 'kimi-k2-0905-preview': { prompt: 0.6, completion: 2.5 }, + 'kimi-k2-0711': { prompt: 0.6, completion: 2.5 }, + '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 }, + '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 }, + 'moonshot-v1-8k-vision': { prompt: 0.2, completion: 2.0 }, + 'moonshot-v1-8k-vision-preview': { prompt: 0.2, completion: 2.0 }, + 'moonshot-v1-32k': { prompt: 1.0, completion: 3.0 }, + 'moonshot-v1-32k-vision': { prompt: 1.0, completion: 3.0 }, + 'moonshot-v1-32k-vision-preview': { prompt: 1.0, completion: 3.0 }, + 'moonshot-v1-128k': { prompt: 2.0, completion: 5.0 }, + 'moonshot-v1-128k-vision': { prompt: 2.0, completion: 5.0 }, + 'moonshot-v1-128k-vision-preview': { prompt: 2.0, completion: 5.0 }, // GPT-OSS models (specific sizes) 'gpt-oss:20b': { prompt: 0.05, completion: 0.2 }, 'gpt-oss-20b': { prompt: 0.05, completion: 0.2 }, @@ -255,6 +314,18 @@ const cacheTokenValues = { deepseek: { write: 0.28, read: 0.028 }, 'deepseek-chat': { write: 0.28, read: 0.028 }, 'deepseek-reasoner': { write: 0.28, read: 0.028 }, + // Moonshot/Kimi models - cache hit: $0.15/1M (k2) or $0.10/1M (k2.5), cache miss: $0.60/1M + kimi: { write: 0.6, read: 0.15 }, + 'kimi-k2': { write: 0.6, read: 0.15 }, + 'kimi-k2.5': { write: 0.6, read: 0.1 }, + 'kimi-k2-turbo': { write: 1.15, read: 0.15 }, + 'kimi-k2-turbo-preview': { write: 1.15, read: 0.15 }, + 'kimi-k2-0905': { write: 0.6, read: 0.15 }, + 'kimi-k2-0905-preview': { write: 0.6, read: 0.15 }, + 'kimi-k2-0711': { write: 0.6, read: 0.15 }, + 'kimi-k2-0711-preview': { write: 0.6, read: 0.15 }, + 'kimi-k2-thinking': { write: 0.6, read: 0.15 }, + 'kimi-k2-thinking-turbo': { write: 1.15, read: 0.15 }, }; /** diff --git a/api/models/tx.spec.js b/api/models/tx.spec.js index f70a6af47c..5058446104 100644 --- a/api/models/tx.spec.js +++ b/api/models/tx.spec.js @@ -881,6 +881,193 @@ describe('Deepseek Model Tests', () => { }); }); +describe('Moonshot/Kimi Model Tests - Pricing', () => { + describe('Kimi Models', () => { + it('should return correct pricing for kimi base pattern', () => { + expect(getMultiplier({ model: 'kimi', tokenType: 'prompt' })).toBe( + tokenValues['kimi'].prompt, + ); + expect(getMultiplier({ model: 'kimi', tokenType: 'completion' })).toBe( + tokenValues['kimi'].completion, + ); + }); + + it('should return correct pricing for kimi-k2.5', () => { + expect(getMultiplier({ model: 'kimi-k2.5', tokenType: 'prompt' })).toBe( + tokenValues['kimi-k2.5'].prompt, + ); + expect(getMultiplier({ model: 'kimi-k2.5', tokenType: 'completion' })).toBe( + tokenValues['kimi-k2.5'].completion, + ); + }); + + it('should return correct pricing for kimi-k2 series', () => { + expect(getMultiplier({ model: 'kimi-k2', tokenType: 'prompt' })).toBe( + tokenValues['kimi-k2'].prompt, + ); + expect(getMultiplier({ model: 'kimi-k2', tokenType: 'completion' })).toBe( + tokenValues['kimi-k2'].completion, + ); + }); + + it('should return correct pricing for kimi-k2-turbo (higher pricing)', () => { + expect(getMultiplier({ model: 'kimi-k2-turbo', tokenType: 'prompt' })).toBe( + tokenValues['kimi-k2-turbo'].prompt, + ); + expect(getMultiplier({ model: 'kimi-k2-turbo', tokenType: 'completion' })).toBe( + tokenValues['kimi-k2-turbo'].completion, + ); + }); + + it('should return correct pricing for kimi-k2-thinking models', () => { + expect(getMultiplier({ model: 'kimi-k2-thinking', tokenType: 'prompt' })).toBe( + tokenValues['kimi-k2-thinking'].prompt, + ); + expect(getMultiplier({ model: 'kimi-k2-thinking', tokenType: 'completion' })).toBe( + tokenValues['kimi-k2-thinking'].completion, + ); + expect(getMultiplier({ model: 'kimi-k2-thinking-turbo', tokenType: 'prompt' })).toBe( + tokenValues['kimi-k2-thinking-turbo'].prompt, + ); + expect(getMultiplier({ model: 'kimi-k2-thinking-turbo', tokenType: 'completion' })).toBe( + tokenValues['kimi-k2-thinking-turbo'].completion, + ); + }); + + it('should handle Kimi model variations with provider prefixes', () => { + const modelVariations = ['openrouter/kimi-k2', 'openrouter/kimi-k2.5', 'openrouter/kimi']; + + modelVariations.forEach((model) => { + const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' }); + const completionMultiplier = getMultiplier({ model, tokenType: 'completion' }); + expect(promptMultiplier).toBe(tokenValues['kimi'].prompt); + expect([tokenValues['kimi'].completion, tokenValues['kimi-k2.5'].completion]).toContain( + completionMultiplier, + ); + }); + }); + }); + + describe('Moonshot Models', () => { + it('should return correct pricing for moonshot base pattern (128k pricing)', () => { + expect(getMultiplier({ model: 'moonshot', tokenType: 'prompt' })).toBe( + tokenValues['moonshot'].prompt, + ); + expect(getMultiplier({ model: 'moonshot', tokenType: 'completion' })).toBe( + tokenValues['moonshot'].completion, + ); + }); + + it('should return correct pricing for moonshot-v1-8k', () => { + expect(getMultiplier({ model: 'moonshot-v1-8k', tokenType: 'prompt' })).toBe( + tokenValues['moonshot-v1-8k'].prompt, + ); + expect(getMultiplier({ model: 'moonshot-v1-8k', tokenType: 'completion' })).toBe( + tokenValues['moonshot-v1-8k'].completion, + ); + }); + + it('should return correct pricing for moonshot-v1-32k', () => { + expect(getMultiplier({ model: 'moonshot-v1-32k', tokenType: 'prompt' })).toBe( + tokenValues['moonshot-v1-32k'].prompt, + ); + expect(getMultiplier({ model: 'moonshot-v1-32k', tokenType: 'completion' })).toBe( + tokenValues['moonshot-v1-32k'].completion, + ); + }); + + it('should return correct pricing for moonshot-v1-128k', () => { + expect(getMultiplier({ model: 'moonshot-v1-128k', tokenType: 'prompt' })).toBe( + tokenValues['moonshot-v1-128k'].prompt, + ); + expect(getMultiplier({ model: 'moonshot-v1-128k', tokenType: 'completion' })).toBe( + tokenValues['moonshot-v1-128k'].completion, + ); + }); + + it('should return correct pricing for moonshot-v1 vision models', () => { + expect(getMultiplier({ model: 'moonshot-v1-8k-vision', tokenType: 'prompt' })).toBe( + tokenValues['moonshot-v1-8k-vision'].prompt, + ); + expect(getMultiplier({ model: 'moonshot-v1-8k-vision', tokenType: 'completion' })).toBe( + tokenValues['moonshot-v1-8k-vision'].completion, + ); + expect(getMultiplier({ model: 'moonshot-v1-32k-vision', tokenType: 'prompt' })).toBe( + tokenValues['moonshot-v1-32k-vision'].prompt, + ); + expect(getMultiplier({ model: 'moonshot-v1-32k-vision', tokenType: 'completion' })).toBe( + tokenValues['moonshot-v1-32k-vision'].completion, + ); + expect(getMultiplier({ model: 'moonshot-v1-128k-vision', tokenType: 'prompt' })).toBe( + tokenValues['moonshot-v1-128k-vision'].prompt, + ); + expect(getMultiplier({ model: 'moonshot-v1-128k-vision', tokenType: 'completion' })).toBe( + tokenValues['moonshot-v1-128k-vision'].completion, + ); + }); + }); + + describe('Kimi Cache Multipliers', () => { + it('should return correct cache multipliers for kimi-k2 models', () => { + expect(getCacheMultiplier({ model: 'kimi', cacheType: 'write' })).toBe( + cacheTokenValues['kimi'].write, + ); + expect(getCacheMultiplier({ model: 'kimi', cacheType: 'read' })).toBe( + cacheTokenValues['kimi'].read, + ); + }); + + it('should return correct cache multipliers for kimi-k2.5 (lower read price)', () => { + expect(getCacheMultiplier({ model: 'kimi-k2.5', cacheType: 'write' })).toBe( + cacheTokenValues['kimi-k2.5'].write, + ); + expect(getCacheMultiplier({ model: 'kimi-k2.5', cacheType: 'read' })).toBe( + cacheTokenValues['kimi-k2.5'].read, + ); + }); + + it('should return correct cache multipliers for kimi-k2-turbo', () => { + expect(getCacheMultiplier({ model: 'kimi-k2-turbo', cacheType: 'write' })).toBe( + cacheTokenValues['kimi-k2-turbo'].write, + ); + expect(getCacheMultiplier({ model: 'kimi-k2-turbo', cacheType: 'read' })).toBe( + cacheTokenValues['kimi-k2-turbo'].read, + ); + }); + + it('should handle Kimi cache multipliers with model variations', () => { + const modelVariations = ['openrouter/kimi-k2', 'openrouter/kimi']; + + modelVariations.forEach((model) => { + const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }); + const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }); + expect(writeMultiplier).toBe(cacheTokenValues['kimi'].write); + expect(readMultiplier).toBe(cacheTokenValues['kimi'].read); + }); + }); + }); + + describe('Bedrock Moonshot Models', () => { + it('should return correct pricing for Bedrock moonshot models', () => { + expect(getMultiplier({ model: 'moonshot.kimi', tokenType: 'prompt' })).toBe( + tokenValues['moonshot.kimi'].prompt, + ); + expect(getMultiplier({ model: 'moonshot.kimi', tokenType: 'completion' })).toBe( + tokenValues['moonshot.kimi'].completion, + ); + expect(getMultiplier({ model: 'moonshot.kimi-k2', tokenType: 'prompt' })).toBe( + tokenValues['moonshot.kimi-k2'].prompt, + ); + expect(getMultiplier({ model: 'moonshot.kimi-k2.5', tokenType: 'prompt' })).toBe( + tokenValues['moonshot.kimi-k2.5'].prompt, + ); + expect(getMultiplier({ model: 'moonshot.kimi-k2.5', tokenType: 'completion' })).toBe( + tokenValues['moonshot.kimi-k2.5'].completion, + ); + }); + }); +}); + describe('Qwen3 Model Tests', () => { describe('Qwen3 Base Models', () => { it('should return correct pricing for qwen3 base pattern', () => { diff --git a/api/server/services/Endpoints/index.js b/api/server/services/Endpoints/index.js index 034162702d..3cabfe1c58 100644 --- a/api/server/services/Endpoints/index.js +++ b/api/server/services/Endpoints/index.js @@ -12,7 +12,7 @@ const initGoogle = require('~/server/services/Endpoints/google/initialize'); * @returns {boolean} - True if the provider is a known custom provider, false otherwise */ function isKnownCustomProvider(provider) { - return [Providers.XAI, Providers.DEEPSEEK, Providers.OPENROUTER].includes( + return [Providers.XAI, Providers.DEEPSEEK, Providers.OPENROUTER, Providers.MOONSHOT].includes( provider?.toLowerCase() || '', ); } @@ -20,6 +20,7 @@ function isKnownCustomProvider(provider) { const providerConfigMap = { [Providers.XAI]: initCustom, [Providers.DEEPSEEK]: initCustom, + [Providers.MOONSHOT]: initCustom, [Providers.OPENROUTER]: initCustom, [EModelEndpoint.openAI]: initOpenAI, [EModelEndpoint.google]: initGoogle, diff --git a/api/utils/tokens.spec.js b/api/utils/tokens.spec.js index 3336a0f82d..c57ed2b6d4 100644 --- a/api/utils/tokens.spec.js +++ b/api/utils/tokens.spec.js @@ -1064,44 +1064,149 @@ describe('Claude Model Tests', () => { }); }); -describe('Kimi Model Tests', () => { +describe('Moonshot/Kimi Model Tests', () => { describe('getModelMaxTokens', () => { - test('should return correct tokens for Kimi models', () => { - expect(getModelMaxTokens('kimi')).toBe(131000); - expect(getModelMaxTokens('kimi-k2')).toBe(131000); - expect(getModelMaxTokens('kimi-vl')).toBe(131000); + test('should return correct tokens for kimi-k2.5 (multi-modal)', () => { + expect(getModelMaxTokens('kimi-k2.5')).toBe(maxTokensMap[EModelEndpoint.openAI]['kimi-k2.5']); + expect(getModelMaxTokens('kimi-k2.5-latest')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2.5'], + ); }); - test('should return correct tokens for Kimi models with provider prefix', () => { - expect(getModelMaxTokens('moonshotai/kimi-k2')).toBe(131000); - expect(getModelMaxTokens('moonshotai/kimi')).toBe(131000); - expect(getModelMaxTokens('moonshotai/kimi-vl')).toBe(131000); + test('should return correct tokens for kimi-k2 series models', () => { + expect(getModelMaxTokens('kimi')).toBe(maxTokensMap[EModelEndpoint.openAI]['kimi']); + expect(getModelMaxTokens('kimi-k2')).toBe(maxTokensMap[EModelEndpoint.openAI]['kimi-k2']); + expect(getModelMaxTokens('kimi-k2-turbo')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-turbo'], + ); + expect(getModelMaxTokens('kimi-k2-turbo-preview')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-turbo-preview'], + ); + expect(getModelMaxTokens('kimi-k2-0905')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-0905'], + ); + expect(getModelMaxTokens('kimi-k2-0905-preview')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-0905-preview'], + ); + expect(getModelMaxTokens('kimi-k2-thinking')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-thinking'], + ); + expect(getModelMaxTokens('kimi-k2-thinking-turbo')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-thinking-turbo'], + ); }); - test('should handle partial matches for Kimi models', () => { - expect(getModelMaxTokens('kimi-k2-latest')).toBe(131000); - expect(getModelMaxTokens('kimi-vl-preview')).toBe(131000); - expect(getModelMaxTokens('kimi-2024')).toBe(131000); + test('should return correct tokens for kimi-k2-0711 (smaller context)', () => { + expect(getModelMaxTokens('kimi-k2-0711')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-0711'], + ); + expect(getModelMaxTokens('kimi-k2-0711-preview')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-0711-preview'], + ); + }); + + test('should return correct tokens for kimi-latest', () => { + expect(getModelMaxTokens('kimi-latest')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-latest'], + ); + }); + + test('should return correct tokens for moonshot-v1 series models', () => { + expect(getModelMaxTokens('moonshot')).toBe(maxTokensMap[EModelEndpoint.openAI]['moonshot']); + expect(getModelMaxTokens('moonshot-v1')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1'], + ); + expect(getModelMaxTokens('moonshot-v1-auto')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-auto'], + ); + expect(getModelMaxTokens('moonshot-v1-8k')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-8k'], + ); + expect(getModelMaxTokens('moonshot-v1-8k-vision')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-8k-vision'], + ); + expect(getModelMaxTokens('moonshot-v1-8k-vision-preview')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-8k-vision-preview'], + ); + expect(getModelMaxTokens('moonshot-v1-32k')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-32k'], + ); + expect(getModelMaxTokens('moonshot-v1-32k-vision')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-32k-vision'], + ); + expect(getModelMaxTokens('moonshot-v1-32k-vision-preview')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-32k-vision-preview'], + ); + expect(getModelMaxTokens('moonshot-v1-128k')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-128k'], + ); + expect(getModelMaxTokens('moonshot-v1-128k-vision')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-128k-vision'], + ); + expect(getModelMaxTokens('moonshot-v1-128k-vision-preview')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-128k-vision-preview'], + ); + }); + + test('should return correct tokens for Bedrock moonshot models', () => { + expect(getModelMaxTokens('moonshot.kimi', EModelEndpoint.bedrock)).toBe( + maxTokensMap[EModelEndpoint.bedrock]['moonshot.kimi'], + ); + expect(getModelMaxTokens('moonshot.kimi-k2', EModelEndpoint.bedrock)).toBe( + maxTokensMap[EModelEndpoint.bedrock]['moonshot.kimi-k2'], + ); + expect(getModelMaxTokens('moonshot.kimi-k2.5', EModelEndpoint.bedrock)).toBe( + maxTokensMap[EModelEndpoint.bedrock]['moonshot.kimi-k2.5'], + ); + expect(getModelMaxTokens('moonshot.kimi-k2-thinking', EModelEndpoint.bedrock)).toBe( + maxTokensMap[EModelEndpoint.bedrock]['moonshot.kimi-k2-thinking'], + ); + expect(getModelMaxTokens('moonshot.kimi-k2-0711', EModelEndpoint.bedrock)).toBe( + maxTokensMap[EModelEndpoint.bedrock]['moonshot.kimi-k2-0711'], + ); + }); + + test('should handle Moonshot/Kimi models with provider prefixes', () => { + expect(getModelMaxTokens('openrouter/kimi-k2')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2'], + ); + expect(getModelMaxTokens('openrouter/kimi-k2.5')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2.5'], + ); + expect(getModelMaxTokens('openrouter/kimi-k2-turbo')).toBe( + maxTokensMap[EModelEndpoint.openAI]['kimi-k2-turbo'], + ); + expect(getModelMaxTokens('openrouter/moonshot-v1-128k')).toBe( + maxTokensMap[EModelEndpoint.openAI]['moonshot-v1-128k'], + ); }); }); describe('matchModelName', () => { test('should match exact Kimi model names', () => { expect(matchModelName('kimi')).toBe('kimi'); - expect(matchModelName('kimi-k2')).toBe('kimi'); - expect(matchModelName('kimi-vl')).toBe('kimi'); + expect(matchModelName('kimi-k2')).toBe('kimi-k2'); + expect(matchModelName('kimi-k2.5')).toBe('kimi-k2.5'); + expect(matchModelName('kimi-k2-turbo')).toBe('kimi-k2-turbo'); + expect(matchModelName('kimi-k2-0711')).toBe('kimi-k2-0711'); + }); + + test('should match moonshot model names', () => { + expect(matchModelName('moonshot')).toBe('moonshot'); + expect(matchModelName('moonshot-v1-8k')).toBe('moonshot-v1-8k'); + expect(matchModelName('moonshot-v1-32k')).toBe('moonshot-v1-32k'); + expect(matchModelName('moonshot-v1-128k')).toBe('moonshot-v1-128k'); }); test('should match Kimi model variations with provider prefix', () => { - expect(matchModelName('moonshotai/kimi')).toBe('kimi'); - expect(matchModelName('moonshotai/kimi-k2')).toBe('kimi'); - expect(matchModelName('moonshotai/kimi-vl')).toBe('kimi'); + expect(matchModelName('openrouter/kimi')).toBe('kimi'); + expect(matchModelName('openrouter/kimi-k2')).toBe('kimi-k2'); + expect(matchModelName('openrouter/kimi-k2.5')).toBe('kimi-k2.5'); }); test('should match Kimi model variations with suffixes', () => { - expect(matchModelName('kimi-k2-latest')).toBe('kimi'); - expect(matchModelName('kimi-vl-preview')).toBe('kimi'); - expect(matchModelName('kimi-2024')).toBe('kimi'); + expect(matchModelName('kimi-k2-latest')).toBe('kimi-k2'); + expect(matchModelName('kimi-k2.5-preview')).toBe('kimi-k2.5'); }); }); }); diff --git a/client/src/hooks/Endpoint/UnknownIcon.tsx b/client/src/hooks/Endpoint/UnknownIcon.tsx index 20619e0b7d..be7531a34a 100644 --- a/client/src/hooks/Endpoint/UnknownIcon.tsx +++ b/client/src/hooks/Endpoint/UnknownIcon.tsx @@ -1,6 +1,6 @@ import { memo } from 'react'; -import { CustomMinimalIcon, XAIcon } from '@librechat/client'; import { EModelEndpoint, KnownEndpoints } from 'librechat-data-provider'; +import { CustomMinimalIcon, XAIcon, MoonshotIcon } from '@librechat/client'; import { IconContext } from '~/common'; import { cn } from '~/utils'; @@ -30,9 +30,6 @@ const knownEndpointClasses = { [KnownEndpoints.cohere]: { [IconContext.landing]: 'p-2', }, - [KnownEndpoints.xai]: { - [IconContext.landing]: 'p-2', - }, }; const getKnownClass = ({ @@ -73,15 +70,11 @@ function UnknownIcon({ const currentEndpoint = endpoint.toLowerCase(); if (currentEndpoint === KnownEndpoints.xai) { - return ( - - ); + return ; + } + + if (currentEndpoint === KnownEndpoints.moonshot) { + return ; } if (iconURL) { diff --git a/packages/api/src/agents/run.ts b/packages/api/src/agents/run.ts index 8544d01300..189ef59469 100644 --- a/packages/api/src/agents/run.ts +++ b/packages/api/src/agents/run.ts @@ -132,6 +132,7 @@ export function overrideDeferLoadingForDiscoveredTools( const customProviders = new Set([ Providers.XAI, Providers.DEEPSEEK, + Providers.MOONSHOT, Providers.OPENROUTER, KnownEndpoints.ollama, ]); diff --git a/packages/api/src/endpoints/config.ts b/packages/api/src/endpoints/config.ts index 041f8ca73d..97246fa336 100644 --- a/packages/api/src/endpoints/config.ts +++ b/packages/api/src/endpoints/config.ts @@ -21,7 +21,7 @@ export type InitializeFn = (params: BaseInitializeParams) => Promise = { [Providers.XAI]: initializeCustom, [Providers.DEEPSEEK]: initializeCustom, + [Providers.MOONSHOT]: initializeCustom, [Providers.OPENROUTER]: initializeCustom, [EModelEndpoint.openAI]: initializeOpenAI, [EModelEndpoint.google]: initializeGoogle, diff --git a/packages/api/src/utils/tokens.ts b/packages/api/src/utils/tokens.ts index 750a2c9244..37dbb7c8da 100644 --- a/packages/api/src/utils/tokens.ts +++ b/packages/api/src/utils/tokens.ts @@ -2,6 +2,34 @@ import z from 'zod'; import { EModelEndpoint } from 'librechat-data-provider'; import type { EndpointTokenConfig, TokenConfig } from '~/types'; +/** + * Model Token Configuration Maps + * + * IMPORTANT: Key Ordering for Pattern Matching + * ============================================ + * The `findMatchingPattern` function iterates through object keys in REVERSE order + * (last-defined keys are checked first) and uses `modelName.includes(key)` for matching. + * + * This means: + * 1. BASE PATTERNS must be defined FIRST (e.g., "kimi", "moonshot") + * 2. SPECIFIC PATTERNS must be defined AFTER their base patterns (e.g., "kimi-k2", "kimi-k2.5") + * + * Example ordering for Kimi models: + * kimi: 262144, // Base pattern - checked last + * 'kimi-k2': 262144, // More specific - checked before "kimi" + * 'kimi-k2.5': 262144, // Most specific - checked first + * + * Why this matters: + * - Model name "kimi-k2.5" contains both "kimi" and "kimi-k2" as substrings + * - If "kimi" were checked first, it would incorrectly match "kimi-k2.5" + * - By defining specific patterns AFTER base patterns, they're checked first in reverse iteration + * + * When adding new model families: + * 1. Define the base/generic pattern first + * 2. Define increasingly specific patterns after + * 3. Ensure no pattern is a substring of another that should match differently + */ + const openAIModels = { 'o4-mini': 200000, 'o3-mini': 195000, // -5000 from max @@ -134,6 +162,42 @@ const deepseekModels = { 'deepseek.r1': 128000, }; +const moonshotModels = { + // Base patterns (check last due to reverse iteration) + kimi: 262144, + moonshot: 131072, + // kimi-k2 series (specific patterns) + 'kimi-latest': 128000, + 'kimi-k2': 262144, + 'kimi-k2.5': 262144, + 'kimi-k2-turbo': 262144, + 'kimi-k2-turbo-preview': 262144, + 'kimi-k2-0905': 262144, + 'kimi-k2-0905-preview': 262144, + 'kimi-k2-0711': 131072, + 'kimi-k2-0711-preview': 131072, + 'kimi-k2-thinking': 262144, + 'kimi-k2-thinking-turbo': 262144, + // moonshot-v1 series (specific patterns) + 'moonshot-v1': 131072, + 'moonshot-v1-auto': 131072, + 'moonshot-v1-8k': 8192, + 'moonshot-v1-8k-vision': 8192, + 'moonshot-v1-8k-vision-preview': 8192, + 'moonshot-v1-32k': 32768, + 'moonshot-v1-32k-vision': 32768, + 'moonshot-v1-32k-vision-preview': 32768, + 'moonshot-v1-128k': 131072, + 'moonshot-v1-128k-vision': 131072, + 'moonshot-v1-128k-vision-preview': 131072, + // Bedrock moonshot models + 'moonshot.kimi': 262144, + 'moonshot.kimi-k2': 262144, + 'moonshot.kimi-k2.5': 262144, + 'moonshot.kimi-k2-thinking': 262144, + 'moonshot.kimi-k2-0711': 131072, +}; + const metaModels = { // Basic patterns llama3: 8000, @@ -248,6 +312,7 @@ const bedrockModels = { ...mistralModels, ...cohereModels, ...deepseekModels, + ...moonshotModels, ...metaModels, ...ai21Models, ...amazonModels, @@ -279,8 +344,6 @@ const aggregateModels = { ...bedrockModels, ...xAIModels, ...qwenModels, - // misc. - kimi: 131000, // GPT-OSS 'gpt-oss': 131000, 'gpt-oss:20b': 131000, diff --git a/packages/client/src/svgs/MoonshotIcon.tsx b/packages/client/src/svgs/MoonshotIcon.tsx new file mode 100644 index 0000000000..599de9f058 --- /dev/null +++ b/packages/client/src/svgs/MoonshotIcon.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +export default function MoonshotIcon({ className = '' }: { className?: string }) { + return ( + + ); +} diff --git a/packages/client/src/svgs/index.ts b/packages/client/src/svgs/index.ts index d3f8c6e45b..1d67d44552 100644 --- a/packages/client/src/svgs/index.ts +++ b/packages/client/src/svgs/index.ts @@ -73,3 +73,4 @@ export { default as SheetPaths } from './SheetPaths'; export { default as TextPaths } from './TextPaths'; export { default as VideoPaths } from './VideoPaths'; export { default as SharePointIcon } from './SharePointIcon'; +export { default as MoonshotIcon } from './MoonshotIcon'; diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 53a518b06d..03073e32f8 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1046,6 +1046,7 @@ export enum KnownEndpoints { cohere = 'cohere', fireworks = 'fireworks', deepseek = 'deepseek', + moonshot = 'moonshot', groq = 'groq', helicone = 'helicone', huggingface = 'huggingface', @@ -1090,6 +1091,7 @@ export const alternateName = { [EModelEndpoint.bedrock]: 'AWS Bedrock', [KnownEndpoints.ollama]: 'Ollama', [KnownEndpoints.deepseek]: 'DeepSeek', + [KnownEndpoints.moonshot]: 'Moonshot', [KnownEndpoints.xai]: 'xAI', [KnownEndpoints.vercel]: 'Vercel', [KnownEndpoints.helicone]: 'Helicone', diff --git a/packages/data-provider/src/parsers.ts b/packages/data-provider/src/parsers.ts index 85b22e7068..f7add8bc1d 100644 --- a/packages/data-provider/src/parsers.ts +++ b/packages/data-provider/src/parsers.ts @@ -227,6 +227,10 @@ export const getResponseSender = (endpointOption: Partial): s return 'Mistral'; } else if (model && model.includes('deepseek')) { return 'Deepseek'; + } else if (model && model.includes('kimi')) { + return 'Kimi'; + } else if (model && model.includes('moonshot')) { + return 'Moonshot'; } else if (model && model.includes('gpt-')) { const gptVersion = extractGPTVersion(model); return gptVersion || 'GPT'; @@ -264,6 +268,10 @@ export const getResponseSender = (endpointOption: Partial): s return 'Mistral'; } else if (model && model.includes('deepseek')) { return 'Deepseek'; + } else if (model && model.includes('kimi')) { + return 'Kimi'; + } else if (model && model.includes('moonshot')) { + return 'Moonshot'; } else if (model && model.includes('gpt-')) { const gptVersion = extractGPTVersion(model); return gptVersion || 'GPT'; diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index d4c4740267..25a3b59481 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -38,6 +38,7 @@ export enum Providers { MISTRALAI = 'mistralai', MISTRAL = 'mistral', DEEPSEEK = 'deepseek', + MOONSHOT = 'moonshot', OPENROUTER = 'openrouter', XAI = 'xai', } @@ -56,6 +57,7 @@ export const documentSupportedProviders = new Set([ Providers.MISTRALAI, Providers.MISTRAL, Providers.DEEPSEEK, + Providers.MOONSHOT, Providers.OPENROUTER, Providers.XAI, ]); @@ -67,6 +69,7 @@ const openAILikeProviders = new Set([ Providers.MISTRALAI, Providers.MISTRAL, Providers.DEEPSEEK, + Providers.MOONSHOT, Providers.OPENROUTER, Providers.XAI, ]);