From f34052c6bb8d39b34101169394af8139c5f2cb48 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 4 Feb 2026 10:53:57 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=99=20feat:=20Moonshot=20Provider=20Su?= =?UTF-8?q?pport=20(#11621)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: Add Moonshot Provider Support - Updated the `isKnownCustomProvider` function to include `Providers.MOONSHOT` in the list of recognized custom providers. - Enhanced the `providerConfigMap` to initialize `MOONSHOT` with the custom initialization function. - Introduced `MoonshotIcon` component for visual representation in the UI, integrated into the `UnknownIcon` component. - Updated various files across the API and client to support the new `MOONSHOT` provider, including configuration and response handling. This update expands the capabilities of the application by integrating support for the Moonshot provider, enhancing both backend and frontend functionalities. * ✨ feat: Add Moonshot/Kimi Model Pricing and Tests - Introduced new pricing configurations for Moonshot and Kimi models in `tx.js`, including various model variations and their respective prompt and completion values. - Expanded unit tests in `tx.spec.js` and `tokens.spec.js` to validate pricing and token limits for the newly added Moonshot/Kimi models, ensuring accurate calculations and handling of model variations. - Updated utility functions to support the new model structures and ensure compatibility with existing functionalities. This update enhances the pricing model capabilities and improves test coverage for the Moonshot/Kimi integration. * ✨ feat: Enhance Token Pricing Documentation and Configuration - Added comprehensive documentation for token pricing configuration in `tx.js` and `tokens.ts`, emphasizing the importance of key ordering for pattern matching. - Clarified the process for defining base and specific patterns to ensure accurate pricing retrieval based on model names. - Improved code comments to guide future additions of model families, enhancing maintainability and understanding of the pricing structure. This update improves the clarity and usability of the token pricing configuration, facilitating better integration and future enhancements. * chore: import order * chore: linting --- api/models/tx.js | 75 ++++++++- api/models/tx.spec.js | 187 ++++++++++++++++++++++ api/server/services/Endpoints/index.js | 3 +- api/utils/tokens.spec.js | 147 ++++++++++++++--- client/src/hooks/Endpoint/UnknownIcon.tsx | 19 +-- packages/api/src/agents/run.ts | 1 + packages/api/src/endpoints/config.ts | 3 +- packages/api/src/utils/tokens.ts | 67 +++++++- packages/client/src/svgs/MoonshotIcon.tsx | 16 ++ packages/client/src/svgs/index.ts | 1 + packages/data-provider/src/config.ts | 2 + packages/data-provider/src/parsers.ts | 8 + packages/data-provider/src/schemas.ts | 3 + 13 files changed, 492 insertions(+), 40 deletions(-) create mode 100644 packages/client/src/svgs/MoonshotIcon.tsx 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, ]);