diff --git a/src/api/providers/__tests__/minimax.spec.ts b/src/api/providers/__tests__/minimax.spec.ts index d1e25358fa..06066c4ed7 100644 --- a/src/api/providers/__tests__/minimax.spec.ts +++ b/src/api/providers/__tests__/minimax.spec.ts @@ -131,6 +131,79 @@ describe("MiniMaxHandler", () => { expect(model.id).toBe(minimaxDefaultModelId) expect(model.info).toEqual(minimaxModels[minimaxDefaultModelId]) }) + + it("should map MiniMax-M2 to abab7-chat for China endpoint", async () => { + const handlerWithModel = new MiniMaxHandler({ + apiModelId: "MiniMax-M2", + minimaxApiKey: "test-minimax-api-key", + minimaxBaseUrl: "https://api.minimaxi.com/v1", + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const messageGenerator = handlerWithModel.createMessage("test", []) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "abab7-chat", // Mapped model ID for China endpoint + }), + undefined, + ) + }) + + it("should map MiniMax-M2-Stable to abab7-chat-hd for China endpoint", async () => { + const handlerWithModel = new MiniMaxHandler({ + apiModelId: "MiniMax-M2-Stable", + minimaxApiKey: "test-minimax-api-key", + minimaxBaseUrl: "https://api.minimaxi.com/v1", + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const messageGenerator = handlerWithModel.createMessage("test", []) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "abab7-chat-hd", // Mapped model ID for China endpoint + }), + undefined, + ) + }) + + it("should map model ID correctly in completePrompt for China endpoint", async () => { + const handlerWithModel = new MiniMaxHandler({ + apiModelId: "MiniMax-M2", + minimaxApiKey: "test-minimax-api-key", + minimaxBaseUrl: "https://api.minimaxi.com/v1", + }) + + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: "test response" } }] }) + await handlerWithModel.completePrompt("test prompt") + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "abab7-chat", // Mapped model ID for China endpoint + }), + ) + }) }) describe("Default behavior", () => { @@ -258,6 +331,35 @@ describe("MiniMaxHandler", () => { ) }) + it("should not apply model ID mapping for international endpoint", async () => { + const modelId: MinimaxModelId = "MiniMax-M2" + const handlerWithModel = new MiniMaxHandler({ + apiModelId: modelId, + minimaxApiKey: "test-minimax-api-key", + minimaxBaseUrl: "https://api.minimax.io/v1", + }) + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + async next() { + return { done: true } + }, + }), + } + }) + + const messageGenerator = handlerWithModel.createMessage("test", []) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: modelId, // Original model ID for international endpoint + }), + undefined, + ) + }) + it("should use temperature 1 by default", async () => { mockCreate.mockImplementationOnce(() => { return { diff --git a/src/api/providers/minimax.ts b/src/api/providers/minimax.ts index 8a8e8c14e5..8347e10898 100644 --- a/src/api/providers/minimax.ts +++ b/src/api/providers/minimax.ts @@ -1,19 +1,105 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + import { type MinimaxModelId, minimaxDefaultModelId, minimaxModels } from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" +import { getModelMaxOutputTokens } from "../../shared/api" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { handleOpenAIError } from "./utils/openai-error-handler" +import type { ApiHandlerCreateMessageMetadata } from "../index" import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" export class MiniMaxHandler extends BaseOpenAiCompatibleProvider { + private readonly isChinaEndpoint: boolean + constructor(options: ApiHandlerOptions) { + const baseURL = options.minimaxBaseUrl ?? "https://api.minimax.io/v1" + const isChinaEndpoint = baseURL.includes("minimaxi.com") + super({ ...options, providerName: "MiniMax", - baseURL: options.minimaxBaseUrl ?? "https://api.minimax.io/v1", + baseURL, apiKey: options.minimaxApiKey, defaultProviderModelId: minimaxDefaultModelId, providerModels: minimaxModels, defaultTemperature: 1.0, }) + + this.isChinaEndpoint = isChinaEndpoint + } + + // Override createStream to handle model ID mapping for China endpoint + protected override createStream( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + requestOptions?: OpenAI.RequestOptions, + ) { + const { id: modelId, info } = this.getModel() + + // Map model IDs for China endpoint - they use different model names + let apiModelId = modelId as string + if (this.isChinaEndpoint) { + const chinaModelMapping: Record = { + "MiniMax-M2": "abab7-chat", + "MiniMax-M2-Stable": "abab7-chat-hd", + } + apiModelId = chinaModelMapping[modelId] || modelId.toLowerCase() + } + + // Centralized cap: clamp to 20% of the context window (unless provider-specific exceptions apply) + const max_tokens = + getModelMaxOutputTokens({ + modelId, + model: info, + settings: this.options, + format: "openai", + }) ?? undefined + + const temperature = this.options.modelTemperature ?? 1.0 + + const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { + model: apiModelId, + max_tokens, + temperature, + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], + stream: true, + stream_options: { include_usage: true }, + } + + try { + return this.client.chat.completions.create(params, requestOptions) + } catch (error) { + throw handleOpenAIError(error, this.providerName) + } + } + + // Override completePrompt to handle model ID mapping for China endpoint + override async completePrompt(prompt: string): Promise { + const { id: modelId } = this.getModel() + + // Map model IDs for China endpoint + let apiModelId = modelId as string + if (this.isChinaEndpoint) { + const chinaModelMapping: Record = { + "MiniMax-M2": "abab7-chat", + "MiniMax-M2-Stable": "abab7-chat-hd", + } + apiModelId = chinaModelMapping[modelId] || modelId.toLowerCase() + } + + try { + const response = await this.client.chat.completions.create({ + model: apiModelId, + messages: [{ role: "user", content: prompt }], + }) + + return response.choices[0]?.message.content || "" + } catch (error) { + throw handleOpenAIError(error, this.providerName) + } } }