fix: add model ID mapping for MiniMax China endpoint (minimaxi.com)

- Map MiniMax-M2 to abab7-chat for China endpoint
- Map MiniMax-M2-Stable to abab7-chat-hd for China endpoint
- Override createStream and completePrompt methods to handle model mapping
- Add comprehensive tests for both endpoints

Fixes #9100
This commit is contained in:
Roo Code 2025-11-07 15:57:31 +00:00
parent 2abdad6d55
commit b3d41d5709
2 changed files with 189 additions and 1 deletions

View file

@ -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 {

View file

@ -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<MinimaxModelId> {
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<string, string> = {
"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<string> {
const { id: modelId } = this.getModel()
// Map model IDs for China endpoint
let apiModelId = modelId as string
if (this.isChinaEndpoint) {
const chinaModelMapping: Record<string, string> = {
"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)
}
}
}