From 581f974f3da69a13827e232a2603810892cf6113 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Sun, 12 Apr 2026 16:39:13 +0800 Subject: [PATCH 1/3] feat: add MiniMax provider support - Add MiniMaxOpenAICompatibleProvider extending DefaultOpenAICompatibleProvider - Use OpenAI-compatible interface with https://api.minimax.io/v1 as default base URL - Enforce temperature in (0.0, 1.0] range; remove unsupported response_format - Register provider detection in determineProvider() factory - Add token limits for MiniMax-M2.7 (200K input, 64K output) - Add unit tests for all provider behaviors --- .../src/core/openaiContentGenerator/index.ts | 10 ++ .../openaiContentGenerator/provider/index.ts | 1 + .../provider/minimax.test.ts | 152 ++++++++++++++++++ .../provider/minimax.ts | 83 ++++++++++ packages/core/src/core/tokenLimits.ts | 2 + 5 files changed, 248 insertions(+) create mode 100644 packages/core/src/core/openaiContentGenerator/provider/minimax.test.ts create mode 100644 packages/core/src/core/openaiContentGenerator/provider/minimax.ts diff --git a/packages/core/src/core/openaiContentGenerator/index.ts b/packages/core/src/core/openaiContentGenerator/index.ts index fee32a04904..216604ea6e2 100644 --- a/packages/core/src/core/openaiContentGenerator/index.ts +++ b/packages/core/src/core/openaiContentGenerator/index.ts @@ -13,6 +13,7 @@ import { OpenAIContentGenerator } from './openaiContentGenerator.js'; import { DashScopeOpenAICompatibleProvider, DeepSeekOpenAICompatibleProvider, + MiniMaxOpenAICompatibleProvider, ModelScopeOpenAICompatibleProvider, OpenRouterOpenAICompatibleProvider, type OpenAICompatibleProvider, @@ -26,6 +27,7 @@ export { type OpenAICompatibleProvider, DashScopeOpenAICompatibleProvider, DeepSeekOpenAICompatibleProvider, + MiniMaxOpenAICompatibleProvider, OpenRouterOpenAICompatibleProvider, } from './provider/index.js'; @@ -87,6 +89,14 @@ export function determineProvider( ); } + // Check for MiniMax provider + if (MiniMaxOpenAICompatibleProvider.isMiniMaxProvider(config)) { + return new MiniMaxOpenAICompatibleProvider( + contentGeneratorConfig, + cliConfig, + ); + } + // Default provider for standard OpenAI-compatible APIs return new DefaultOpenAICompatibleProvider(contentGeneratorConfig, cliConfig); } diff --git a/packages/core/src/core/openaiContentGenerator/provider/index.ts b/packages/core/src/core/openaiContentGenerator/provider/index.ts index cb33834dd6b..907c1e9f53c 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/index.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/index.ts @@ -2,6 +2,7 @@ export { ModelScopeOpenAICompatibleProvider } from './modelscope.js'; export { DashScopeOpenAICompatibleProvider } from './dashscope.js'; export { DeepSeekOpenAICompatibleProvider } from './deepseek.js'; export { OpenRouterOpenAICompatibleProvider } from './openrouter.js'; +export { MiniMaxOpenAICompatibleProvider } from './minimax.js'; export { DefaultOpenAICompatibleProvider } from './default.js'; export type { OpenAICompatibleProvider, diff --git a/packages/core/src/core/openaiContentGenerator/provider/minimax.test.ts b/packages/core/src/core/openaiContentGenerator/provider/minimax.test.ts new file mode 100644 index 00000000000..d7ada5de100 --- /dev/null +++ b/packages/core/src/core/openaiContentGenerator/provider/minimax.test.ts @@ -0,0 +1,152 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type OpenAI from 'openai'; +import { MiniMaxOpenAICompatibleProvider } from './minimax.js'; +import type { ContentGeneratorConfig } from '../../contentGenerator.js'; +import type { Config } from '../../../config/config.js'; + +// Mock OpenAI client to avoid real network calls +vi.mock('openai', () => ({ + default: vi.fn().mockImplementation((config) => ({ + config, + })), +})); + +describe('MiniMaxOpenAICompatibleProvider', () => { + let provider: MiniMaxOpenAICompatibleProvider; + let mockContentGeneratorConfig: ContentGeneratorConfig; + let mockCliConfig: Config; + + beforeEach(() => { + vi.clearAllMocks(); + + mockContentGeneratorConfig = { + apiKey: 'test-minimax-api-key', + baseUrl: 'https://api.minimax.io/v1', + model: 'MiniMax-M2.7', + } as ContentGeneratorConfig; + + mockCliConfig = { + getCliVersion: vi.fn().mockReturnValue('1.0.0'), + getProxy: vi.fn().mockReturnValue(undefined), + } as unknown as Config; + + provider = new MiniMaxOpenAICompatibleProvider( + mockContentGeneratorConfig, + mockCliConfig, + ); + }); + + describe('isMiniMaxProvider', () => { + it('returns true when baseUrl contains api.minimax.io', () => { + const result = MiniMaxOpenAICompatibleProvider.isMiniMaxProvider( + mockContentGeneratorConfig, + ); + expect(result).toBe(true); + }); + + it('returns true when baseUrl contains api.minimaxi.com', () => { + const config = { + ...mockContentGeneratorConfig, + baseUrl: 'https://api.minimaxi.com/v1', + } as ContentGeneratorConfig; + expect(MiniMaxOpenAICompatibleProvider.isMiniMaxProvider(config)).toBe( + true, + ); + }); + + it('returns false for non-MiniMax baseUrl', () => { + const config = { + ...mockContentGeneratorConfig, + baseUrl: 'https://api.openai.com/v1', + } as ContentGeneratorConfig; + expect(MiniMaxOpenAICompatibleProvider.isMiniMaxProvider(config)).toBe( + false, + ); + }); + + it('returns false when baseUrl is undefined', () => { + const config = { + ...mockContentGeneratorConfig, + baseUrl: undefined, + } as ContentGeneratorConfig; + expect(MiniMaxOpenAICompatibleProvider.isMiniMaxProvider(config)).toBe( + false, + ); + }); + }); + + describe('getDefaultGenerationConfig', () => { + it('returns temperature 1.0 as default', () => { + expect(provider.getDefaultGenerationConfig()).toEqual({ + temperature: 1.0, + }); + }); + }); + + describe('buildRequest', () => { + const userPromptId = 'prompt-123'; + + it('removes response_format from request', () => { + const request = { + model: 'MiniMax-M2.7', + messages: [{ role: 'user' as const, content: 'Hello' }], + response_format: { type: 'json_object' as const }, + } as OpenAI.Chat.ChatCompletionCreateParams; + + const result = provider.buildRequest(request, userPromptId); + expect(result).not.toHaveProperty('response_format'); + }); + + it('sets temperature to 1.0 when temperature is 0', () => { + const request: OpenAI.Chat.ChatCompletionCreateParams = { + model: 'MiniMax-M2.7', + messages: [{ role: 'user', content: 'Hello' }], + temperature: 0, + }; + + const result = provider.buildRequest(request, userPromptId); + expect(result.temperature).toBe(1.0); + }); + + it('sets temperature to 1.0 when temperature is undefined', () => { + const request: OpenAI.Chat.ChatCompletionCreateParams = { + model: 'MiniMax-M2.7', + messages: [{ role: 'user', content: 'Hello' }], + }; + + const result = provider.buildRequest(request, userPromptId); + expect(result.temperature).toBe(1.0); + }); + + it('preserves valid temperature values', () => { + const request: OpenAI.Chat.ChatCompletionCreateParams = { + model: 'MiniMax-M2.7', + messages: [{ role: 'user', content: 'Hello' }], + temperature: 0.7, + }; + + const result = provider.buildRequest(request, userPromptId); + expect(result.temperature).toBe(0.7); + }); + + it('preserves messages unchanged', () => { + const request: OpenAI.Chat.ChatCompletionCreateParams = { + model: 'MiniMax-M2.7', + messages: [ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: 'Hi there' }, + ], + }; + + const result = provider.buildRequest(request, userPromptId); + expect(result.messages).toHaveLength(2); + expect(result.messages?.[0].content).toBe('Hello'); + }); + }); +}); diff --git a/packages/core/src/core/openaiContentGenerator/provider/minimax.ts b/packages/core/src/core/openaiContentGenerator/provider/minimax.ts new file mode 100644 index 00000000000..8e728c31f4e --- /dev/null +++ b/packages/core/src/core/openaiContentGenerator/provider/minimax.ts @@ -0,0 +1,83 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import type OpenAI from 'openai'; +import type { GenerateContentConfig } from '@google/genai'; +import type { Config } from '../../../config/config.js'; +import type { ContentGeneratorConfig } from '../../contentGenerator.js'; +import { DefaultOpenAICompatibleProvider } from './default.js'; + +const MINIMAX_DEFAULT_BASE_URL = 'https://api.minimax.io/v1'; + +/** + * Provider for MiniMax API (OpenAI-compatible interface). + * + * MiniMax-specific constraints: + * - temperature must be in the range (0.0, 1.0]; 0 is not allowed, default is 1.0 + * - response_format is not supported and must be removed from requests + */ +export class MiniMaxOpenAICompatibleProvider extends DefaultOpenAICompatibleProvider { + constructor( + contentGeneratorConfig: ContentGeneratorConfig, + cliConfig: Config, + ) { + super(contentGeneratorConfig, cliConfig); + // Use MiniMax default base URL if not explicitly configured + if (!this.contentGeneratorConfig.baseUrl) { + this.contentGeneratorConfig = { + ...this.contentGeneratorConfig, + baseUrl: MINIMAX_DEFAULT_BASE_URL, + }; + } + } + + /** + * Checks if the configuration targets the MiniMax API. + */ + static isMiniMaxProvider(config: ContentGeneratorConfig): boolean { + const baseUrl = config.baseUrl ?? ''; + return ( + baseUrl.includes('api.minimax.io') || baseUrl.includes('api.minimaxi.com') + ); + } + + /** + * MiniMax default generation config. + * Temperature defaults to 1.0 because MiniMax does not accept temperature = 0. + */ + override getDefaultGenerationConfig(): GenerateContentConfig { + return { + temperature: 1.0, + }; + } + + /** + * Build a MiniMax-compatible request by: + * 1. Ensuring temperature is within the allowed range (0.0, 1.0] + * 2. Removing unsupported `response_format` parameter + */ + override buildRequest( + request: OpenAI.Chat.ChatCompletionCreateParams, + userPromptId: string, + ): OpenAI.Chat.ChatCompletionCreateParams { + const baseRequest = super.buildRequest(request, userPromptId); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: any = { ...baseRequest }; + + // MiniMax does not support temperature = 0; default to 1.0 + if (result.temperature === 0 || result.temperature === undefined) { + result.temperature = 1.0; + } + + // MiniMax does not support response_format + if ('response_format' in result) { + delete result.response_format; + } + + return result as OpenAI.Chat.ChatCompletionCreateParams; + } +} diff --git a/packages/core/src/core/tokenLimits.ts b/packages/core/src/core/tokenLimits.ts index 8b5cce6a346..75cdc62e47b 100644 --- a/packages/core/src/core/tokenLimits.ts +++ b/packages/core/src/core/tokenLimits.ts @@ -136,6 +136,7 @@ const PATTERNS: Array<[RegExp, TokenCount]> = [ // ------------------- // MiniMax // ------------------- + [/^minimax-m2\.7/i, LIMITS['200k']], // MiniMax-M2.7: 200K [/^minimax-m2\.5/i, LIMITS['192k']], // MiniMax-M2.5: 196,608 [/^minimax-/i, LIMITS['200k']], // MiniMax fallback: 200K @@ -185,6 +186,7 @@ const OUTPUT_PATTERNS: Array<[RegExp, TokenCount]> = [ [/^glm-4\.7/, LIMITS['16k']], // MiniMax + [/^minimax-m2\.7/i, LIMITS['64k']], // MiniMax-M2.7: 64K output [/^minimax-m2\.5/i, LIMITS['64k']], // Kimi From bc6d6a6b15720af17f30a2908a51b05d128bb9b9 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Sat, 18 Apr 2026 05:05:03 +0800 Subject: [PATCH 2/3] fix(minimax): address review feedback and add docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix URL detection to use hostname comparison (prevents substring false positives flagged by CodeQL) - Move default base URL to buildClient() override — avoids mutating contentGeneratorConfig in the constructor (matches sibling pattern) - Replace any cast + delete with typed destructuring for response_format - Guard temperature against null (number | null | undefined per SDK types) - Clarify token-limit comment: regex covers M2.7-highspeed too - Add MiniMax section to model-providers.md with config examples for both MiniMax-M2.7 and MiniMax-M2.7-highspeed --- docs/users/configuration/model-providers.md | 51 ++++++++++++ .../core/openaiContentGenerator/constants.ts | 1 + .../provider/minimax.test.ts | 57 ++++++++++++- .../provider/minimax.ts | 82 +++++++++++++------ packages/core/src/core/tokenLimits.ts | 4 +- 5 files changed, 164 insertions(+), 31 deletions(-) diff --git a/docs/users/configuration/model-providers.md b/docs/users/configuration/model-providers.md index 83a66e8de49..20bbd585a1c 100644 --- a/docs/users/configuration/model-providers.md +++ b/docs/users/configuration/model-providers.md @@ -195,6 +195,57 @@ This auth type supports not only OpenAI's official API but also any OpenAI-compa } ``` +### MiniMax (`openai`) + +MiniMax provides OpenAI-compatible chat models. Configure using the `openai` auth type with MiniMax's base URL. MiniMax requires temperature to be in the range `(0.0, 1.0]` — the provider enforces this automatically. + +| Model ID | Description | +| ------------------------ | -------------------------------------------------------- | +| `MiniMax-M2.7` | Peak performance, ultimate value — handles complex tasks | +| `MiniMax-M2.7-highspeed` | Same performance, faster and more agile | + +```json +{ + "env": { + "MINIMAX_API_KEY": "your-minimax-api-key" + }, + "modelProviders": { + "openai": [ + { + "id": "MiniMax-M2.7", + "name": "MiniMax-M2.7", + "envKey": "MINIMAX_API_KEY", + "baseUrl": "https://api.minimax.io/v1", + "generationConfig": { + "contextWindowSize": 200000, + "samplingParams": { + "temperature": 1.0, + "max_tokens": 65536 + } + } + }, + { + "id": "MiniMax-M2.7-highspeed", + "name": "MiniMax-M2.7-highspeed", + "envKey": "MINIMAX_API_KEY", + "baseUrl": "https://api.minimax.io/v1", + "generationConfig": { + "contextWindowSize": 200000, + "samplingParams": { + "temperature": 1.0, + "max_tokens": 65536 + } + } + } + ] + } +} +``` + +> [!note] +> +> **Domestic mirror**: Users in mainland China can replace `api.minimax.io` with `api.minimaxi.com` (note the extra `i`). + ### Local Self-Hosted Models (via OpenAI-compatible API) Most local inference servers (vLLM, Ollama, LM Studio, etc.) provide an OpenAI-compatible API endpoint. Configure them using the `openai` auth type with a local `baseUrl`: diff --git a/packages/core/src/core/openaiContentGenerator/constants.ts b/packages/core/src/core/openaiContentGenerator/constants.ts index c213d64327e..06b629bd94d 100644 --- a/packages/core/src/core/openaiContentGenerator/constants.ts +++ b/packages/core/src/core/openaiContentGenerator/constants.ts @@ -6,3 +6,4 @@ export const DEFAULT_DASHSCOPE_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1'; export const DEFAULT_DEEPSEEK_BASE_URL = 'https://api.deepseek.com/v1'; export const DEFAULT_OPEN_ROUTER_BASE_URL = 'https://openrouter.ai/api/v1'; +export const DEFAULT_MINIMAX_BASE_URL = 'https://api.minimax.io/v1'; diff --git a/packages/core/src/core/openaiContentGenerator/provider/minimax.test.ts b/packages/core/src/core/openaiContentGenerator/provider/minimax.test.ts index d7ada5de100..fd3755d4546 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/minimax.test.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/minimax.test.ts @@ -43,14 +43,14 @@ describe('MiniMaxOpenAICompatibleProvider', () => { }); describe('isMiniMaxProvider', () => { - it('returns true when baseUrl contains api.minimax.io', () => { + it('returns true when baseUrl hostname is api.minimax.io', () => { const result = MiniMaxOpenAICompatibleProvider.isMiniMaxProvider( mockContentGeneratorConfig, ); expect(result).toBe(true); }); - it('returns true when baseUrl contains api.minimaxi.com', () => { + it('returns true when baseUrl hostname is api.minimaxi.com', () => { const config = { ...mockContentGeneratorConfig, baseUrl: 'https://api.minimaxi.com/v1', @@ -79,6 +79,26 @@ describe('MiniMaxOpenAICompatibleProvider', () => { false, ); }); + + it('returns false for a URL that contains api.minimax.io as a path component', () => { + const config = { + ...mockContentGeneratorConfig, + baseUrl: 'https://proxy.example.com/api.minimax.io/v1', + } as ContentGeneratorConfig; + expect(MiniMaxOpenAICompatibleProvider.isMiniMaxProvider(config)).toBe( + false, + ); + }); + + it('returns false for a URL with api.minimax.io embedded in another hostname', () => { + const config = { + ...mockContentGeneratorConfig, + baseUrl: 'https://evil.api.minimax.io.malicious.com/v1', + } as ContentGeneratorConfig; + expect(MiniMaxOpenAICompatibleProvider.isMiniMaxProvider(config)).toBe( + false, + ); + }); }); describe('getDefaultGenerationConfig', () => { @@ -124,6 +144,17 @@ describe('MiniMaxOpenAICompatibleProvider', () => { expect(result.temperature).toBe(1.0); }); + it('sets temperature to 1.0 when temperature is null', () => { + const request = { + model: 'MiniMax-M2.7', + messages: [{ role: 'user' as const, content: 'Hello' }], + temperature: null, + } as unknown as OpenAI.Chat.ChatCompletionCreateParams; + + const result = provider.buildRequest(request, userPromptId); + expect(result.temperature).toBe(1.0); + }); + it('preserves valid temperature values', () => { const request: OpenAI.Chat.ChatCompletionCreateParams = { model: 'MiniMax-M2.7', @@ -149,4 +180,26 @@ describe('MiniMaxOpenAICompatibleProvider', () => { expect(result.messages?.[0].content).toBe('Hello'); }); }); + + describe('buildClient', () => { + it('uses DEFAULT_MINIMAX_BASE_URL when no baseUrl configured', () => { + const configWithoutUrl = { + apiKey: 'test-key', + model: 'MiniMax-M2.7', + } as ContentGeneratorConfig; + + const providerWithoutUrl = new MiniMaxOpenAICompatibleProvider( + configWithoutUrl, + mockCliConfig, + ); + + const client = providerWithoutUrl.buildClient(); + expect(client).toBeDefined(); + }); + + it('uses configured baseUrl when provided', () => { + const client = provider.buildClient(); + expect(client).toBeDefined(); + }); + }); }); diff --git a/packages/core/src/core/openaiContentGenerator/provider/minimax.ts b/packages/core/src/core/openaiContentGenerator/provider/minimax.ts index 8e728c31f4e..6773956e659 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/minimax.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/minimax.ts @@ -4,19 +4,27 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type OpenAI from 'openai'; +import OpenAI from 'openai'; import type { GenerateContentConfig } from '@google/genai'; import type { Config } from '../../../config/config.js'; import type { ContentGeneratorConfig } from '../../contentGenerator.js'; import { DefaultOpenAICompatibleProvider } from './default.js'; +import { + DEFAULT_MINIMAX_BASE_URL, + DEFAULT_TIMEOUT, + DEFAULT_MAX_RETRIES, +} from '../constants.js'; +import { buildRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js'; -const MINIMAX_DEFAULT_BASE_URL = 'https://api.minimax.io/v1'; +/** Hostnames that identify the MiniMax API (global and domestic mirror). */ +const MINIMAX_HOSTNAMES = new Set(['api.minimax.io', 'api.minimaxi.com']); /** * Provider for MiniMax API (OpenAI-compatible interface). * * MiniMax-specific constraints: - * - temperature must be in the range (0.0, 1.0]; 0 is not allowed, default is 1.0 + * - temperature must be in the range (0.0, 1.0]; 0 and null are not allowed, + * defaults to 1.0 * - response_format is not supported and must be removed from requests */ export class MiniMaxOpenAICompatibleProvider extends DefaultOpenAICompatibleProvider { @@ -25,23 +33,47 @@ export class MiniMaxOpenAICompatibleProvider extends DefaultOpenAICompatibleProv cliConfig: Config, ) { super(contentGeneratorConfig, cliConfig); - // Use MiniMax default base URL if not explicitly configured - if (!this.contentGeneratorConfig.baseUrl) { - this.contentGeneratorConfig = { - ...this.contentGeneratorConfig, - baseUrl: MINIMAX_DEFAULT_BASE_URL, - }; - } } /** * Checks if the configuration targets the MiniMax API. + * Uses hostname comparison to avoid substring-match false positives. */ static isMiniMaxProvider(config: ContentGeneratorConfig): boolean { - const baseUrl = config.baseUrl ?? ''; - return ( - baseUrl.includes('api.minimax.io') || baseUrl.includes('api.minimaxi.com') + const baseUrl = config.baseUrl; + if (!baseUrl) return false; + try { + const { hostname } = new URL(baseUrl); + return MINIMAX_HOSTNAMES.has(hostname.toLowerCase()); + } catch { + return false; + } + } + + /** + * Override buildClient to apply the MiniMax default base URL when none is + * configured, matching the pattern used by sibling providers (DashScope, etc.). + */ + override buildClient(): OpenAI { + const { + apiKey, + baseUrl = DEFAULT_MINIMAX_BASE_URL, + timeout = DEFAULT_TIMEOUT, + maxRetries = DEFAULT_MAX_RETRIES, + } = this.contentGeneratorConfig; + const defaultHeaders = this.buildHeaders(); + const runtimeOptions = buildRuntimeFetchOptions( + 'openai', + this.cliConfig.getProxy(), ); + return new OpenAI({ + apiKey, + baseURL: baseUrl, + timeout, + maxRetries, + defaultHeaders, + ...(runtimeOptions || {}), + }); } /** @@ -56,8 +88,8 @@ export class MiniMaxOpenAICompatibleProvider extends DefaultOpenAICompatibleProv /** * Build a MiniMax-compatible request by: - * 1. Ensuring temperature is within the allowed range (0.0, 1.0] - * 2. Removing unsupported `response_format` parameter + * 1. Removing the unsupported `response_format` parameter + * 2. Ensuring temperature is within the allowed range (0.0, 1.0] */ override buildRequest( request: OpenAI.Chat.ChatCompletionCreateParams, @@ -65,19 +97,15 @@ export class MiniMaxOpenAICompatibleProvider extends DefaultOpenAICompatibleProv ): OpenAI.Chat.ChatCompletionCreateParams { const baseRequest = super.buildRequest(request, userPromptId); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result: any = { ...baseRequest }; - - // MiniMax does not support temperature = 0; default to 1.0 - if (result.temperature === 0 || result.temperature === undefined) { - result.temperature = 1.0; - } + // Remove unsupported response_format via typed destructuring (no `any` cast) + const { response_format: _rf, ...rest } = baseRequest; - // MiniMax does not support response_format - if ('response_format' in result) { - delete result.response_format; - } + // MiniMax does not accept temperature = 0 or null; default to 1.0 + const temperature = + rest.temperature == null || rest.temperature === 0 + ? 1.0 + : rest.temperature; - return result as OpenAI.Chat.ChatCompletionCreateParams; + return { ...rest, temperature }; } } diff --git a/packages/core/src/core/tokenLimits.ts b/packages/core/src/core/tokenLimits.ts index 75cdc62e47b..f6d496affe5 100644 --- a/packages/core/src/core/tokenLimits.ts +++ b/packages/core/src/core/tokenLimits.ts @@ -136,7 +136,7 @@ const PATTERNS: Array<[RegExp, TokenCount]> = [ // ------------------- // MiniMax // ------------------- - [/^minimax-m2\.7/i, LIMITS['200k']], // MiniMax-M2.7: 200K + [/^minimax-m2\.7/i, LIMITS['200k']], // MiniMax-M2.7 / M2.7-highspeed: 200K [/^minimax-m2\.5/i, LIMITS['192k']], // MiniMax-M2.5: 196,608 [/^minimax-/i, LIMITS['200k']], // MiniMax fallback: 200K @@ -186,7 +186,7 @@ const OUTPUT_PATTERNS: Array<[RegExp, TokenCount]> = [ [/^glm-4\.7/, LIMITS['16k']], // MiniMax - [/^minimax-m2\.7/i, LIMITS['64k']], // MiniMax-M2.7: 64K output + [/^minimax-m2\.7/i, LIMITS['64k']], // MiniMax-M2.7 / M2.7-highspeed: 64K output [/^minimax-m2\.5/i, LIMITS['64k']], // Kimi From 9417be7f45baeb26cfbbe35e2bbd262efe1cfa06 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Sat, 18 Apr 2026 13:38:51 +0800 Subject: [PATCH 3/3] fix(minimax): clamp temperature > 1.0 and dedupe redundant token-limit pattern - buildRequest now clamps temperature > 1.0 down to 1.0 (was previously passed through to API and would have failed). Adds debug logs via the project DebugLogger when an explicit user value is rewritten so the adjustment is observable. - Removed redundant /^minimax-m2.7/ entry in tokenLimits.ts; the existing /^minimax-/ fallback already maps M2.7 / M2.7-highspeed to 200K. Addresses review suggestions on #3165. --- .../provider/minimax.test.ts | 22 ++++++++++++ .../provider/minimax.ts | 36 +++++++++++++++---- packages/core/src/core/tokenLimits.ts | 3 +- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/packages/core/src/core/openaiContentGenerator/provider/minimax.test.ts b/packages/core/src/core/openaiContentGenerator/provider/minimax.test.ts index fd3755d4546..9ab13e787dd 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/minimax.test.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/minimax.test.ts @@ -166,6 +166,28 @@ describe('MiniMaxOpenAICompatibleProvider', () => { expect(result.temperature).toBe(0.7); }); + it('clamps temperature above 1.0 down to 1.0', () => { + const request: OpenAI.Chat.ChatCompletionCreateParams = { + model: 'MiniMax-M2.7', + messages: [{ role: 'user', content: 'Hello' }], + temperature: 1.5, + }; + + const result = provider.buildRequest(request, userPromptId); + expect(result.temperature).toBe(1.0); + }); + + it('preserves temperature exactly at 1.0', () => { + const request: OpenAI.Chat.ChatCompletionCreateParams = { + model: 'MiniMax-M2.7', + messages: [{ role: 'user', content: 'Hello' }], + temperature: 1.0, + }; + + const result = provider.buildRequest(request, userPromptId); + expect(result.temperature).toBe(1.0); + }); + it('preserves messages unchanged', () => { const request: OpenAI.Chat.ChatCompletionCreateParams = { model: 'MiniMax-M2.7', diff --git a/packages/core/src/core/openaiContentGenerator/provider/minimax.ts b/packages/core/src/core/openaiContentGenerator/provider/minimax.ts index 6773956e659..47890a8c964 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/minimax.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/minimax.ts @@ -15,16 +15,20 @@ import { DEFAULT_MAX_RETRIES, } from '../constants.js'; import { buildRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js'; +import { createDebugLogger } from '../../../utils/debugLogger.js'; /** Hostnames that identify the MiniMax API (global and domestic mirror). */ const MINIMAX_HOSTNAMES = new Set(['api.minimax.io', 'api.minimaxi.com']); +const debugLogger = createDebugLogger('MINIMAX'); + /** * Provider for MiniMax API (OpenAI-compatible interface). * * MiniMax-specific constraints: - * - temperature must be in the range (0.0, 1.0]; 0 and null are not allowed, - * defaults to 1.0 + * - temperature must be in the range (0.0, 1.0]; values of 0, null, or above + * 1.0 are rewritten/clamped to 1.0 (with a debug log when an explicit value + * was changed) * - response_format is not supported and must be removed from requests */ export class MiniMaxOpenAICompatibleProvider extends DefaultOpenAICompatibleProvider { @@ -90,6 +94,10 @@ export class MiniMaxOpenAICompatibleProvider extends DefaultOpenAICompatibleProv * Build a MiniMax-compatible request by: * 1. Removing the unsupported `response_format` parameter * 2. Ensuring temperature is within the allowed range (0.0, 1.0] + * - `0`/`null`/`undefined` are rewritten to 1.0 + * - values above 1.0 are clamped down to 1.0 + * Both rewrites are logged at debug level so users can see when their + * explicit value was adjusted. */ override buildRequest( request: OpenAI.Chat.ChatCompletionCreateParams, @@ -100,11 +108,25 @@ export class MiniMaxOpenAICompatibleProvider extends DefaultOpenAICompatibleProv // Remove unsupported response_format via typed destructuring (no `any` cast) const { response_format: _rf, ...rest } = baseRequest; - // MiniMax does not accept temperature = 0 or null; default to 1.0 - const temperature = - rest.temperature == null || rest.temperature === 0 - ? 1.0 - : rest.temperature; + // MiniMax accepts temperature only in (0.0, 1.0]; rewrite invalid values. + const original = rest.temperature; + let temperature: number; + if (original == null || original === 0) { + temperature = 1.0; + if (original === 0) { + // Only log when the user explicitly set 0; null/undefined is a default fill. + debugLogger.debug( + `temperature=0 is not supported; using 1.0 instead (request ${userPromptId})`, + ); + } + } else if (original > 1.0) { + temperature = 1.0; + debugLogger.debug( + `temperature=${original} exceeds the 1.0 max; clamping to 1.0 (request ${userPromptId})`, + ); + } else { + temperature = original; + } return { ...rest, temperature }; } diff --git a/packages/core/src/core/tokenLimits.ts b/packages/core/src/core/tokenLimits.ts index f6d496affe5..d2a2f633531 100644 --- a/packages/core/src/core/tokenLimits.ts +++ b/packages/core/src/core/tokenLimits.ts @@ -136,9 +136,8 @@ const PATTERNS: Array<[RegExp, TokenCount]> = [ // ------------------- // MiniMax // ------------------- - [/^minimax-m2\.7/i, LIMITS['200k']], // MiniMax-M2.7 / M2.7-highspeed: 200K [/^minimax-m2\.5/i, LIMITS['192k']], // MiniMax-M2.5: 196,608 - [/^minimax-/i, LIMITS['200k']], // MiniMax fallback: 200K + [/^minimax-/i, LIMITS['200k']], // MiniMax fallback (M2.7 / M2.7-highspeed / others): 200K // ------------------- // Moonshot / Kimi