diff --git a/packages/types/src/providers/minimax.ts b/packages/types/src/providers/minimax.ts index aac397bf2ce..7152946f7f1 100644 --- a/packages/types/src/providers/minimax.ts +++ b/packages/types/src/providers/minimax.ts @@ -15,6 +15,8 @@ export const minimaxModels = { supportsPromptCache: true, supportsNativeTools: true, defaultToolProtocol: "native", + includedTools: ["search_and_replace"], + excludedTools: ["apply_diff"], preserveReasoning: true, inputPrice: 0.3, outputPrice: 1.2, @@ -30,6 +32,8 @@ export const minimaxModels = { supportsPromptCache: true, supportsNativeTools: true, defaultToolProtocol: "native", + includedTools: ["search_and_replace"], + excludedTools: ["apply_diff"], preserveReasoning: true, inputPrice: 0.3, outputPrice: 1.2, @@ -38,6 +42,23 @@ export const minimaxModels = { description: "MiniMax M2 Stable (High Concurrency, Commercial Use), a model born for Agents and code, featuring Top-tier Coding Capabilities, Powerful Agentic Performance, and Ultimate Cost-Effectiveness & Speed.", }, + "MiniMax-M2.1": { + maxTokens: 16_384, + contextWindow: 192_000, + supportsImages: false, + supportsPromptCache: true, + supportsNativeTools: true, + defaultToolProtocol: "native", + includedTools: ["search_and_replace"], + excludedTools: ["apply_diff"], + preserveReasoning: true, + inputPrice: 0.3, + outputPrice: 1.2, + cacheWritesPrice: 0.375, + cacheReadsPrice: 0.03, + description: + "MiniMax M2.1 builds on M2 with improved overall performance for agentic coding tasks and significantly faster response times.", + }, } as const satisfies Record export const minimaxDefaultModelInfo: ModelInfo = minimaxModels[minimaxDefaultModelId] diff --git a/src/api/providers/minimax.ts b/src/api/providers/minimax.ts index 12d7934546e..a7cea478ed0 100644 --- a/src/api/providers/minimax.ts +++ b/src/api/providers/minimax.ts @@ -9,6 +9,7 @@ import type { ApiHandlerOptions } from "../../shared/api" import { ApiStream } from "../transform/stream" import { getModelParams } from "../transform/model-params" +import { mergeEnvironmentDetailsForMiniMax } from "../transform/minimax-format" import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" @@ -87,15 +88,26 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand // MiniMax M2 models support prompt caching const supportsPromptCache = info.supportsPromptCache ?? false + // Merge environment_details from messages that follow tool_result blocks + // into the tool_result content. This preserves reasoning continuity for + // thinking models by preventing user messages from interrupting the + // reasoning context after tool use (similar to r1-format's mergeToolResultText). + const processedMessages = mergeEnvironmentDetailsForMiniMax(messages) + + // Build the system blocks array + const systemBlocks: Anthropic.Messages.TextBlockParam[] = [ + supportsPromptCache + ? { text: systemPrompt, type: "text", cache_control: cacheControl } + : { text: systemPrompt, type: "text" }, + ] + // Prepare request parameters const requestParams: Anthropic.Messages.MessageCreateParams = { model: modelId, max_tokens: maxTokens ?? 16_384, temperature: temperature ?? 1.0, - system: supportsPromptCache - ? [{ text: systemPrompt, type: "text", cache_control: cacheControl }] - : [{ text: systemPrompt, type: "text" }], - messages: supportsPromptCache ? this.addCacheControl(messages, cacheControl) : messages, + system: systemBlocks, + messages: supportsPromptCache ? this.addCacheControl(processedMessages, cacheControl) : processedMessages, stream: true, } diff --git a/src/api/transform/__tests__/minimax-format.spec.ts b/src/api/transform/__tests__/minimax-format.spec.ts new file mode 100644 index 00000000000..271dfb51052 --- /dev/null +++ b/src/api/transform/__tests__/minimax-format.spec.ts @@ -0,0 +1,336 @@ +// npx vitest run api/transform/__tests__/minimax-format.spec.ts + +import { Anthropic } from "@anthropic-ai/sdk" + +import { mergeEnvironmentDetailsForMiniMax } from "../minimax-format" + +describe("mergeEnvironmentDetailsForMiniMax", () => { + it("should pass through simple text messages unchanged", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello", + }, + { + role: "assistant", + content: "Hi there!", + }, + ] + + const result = mergeEnvironmentDetailsForMiniMax(messages) + + expect(result).toHaveLength(2) + expect(result).toEqual(messages) + }) + + it("should pass through user messages with only tool_result blocks unchanged", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-123", + content: "Tool result content", + }, + ], + }, + ] + + const result = mergeEnvironmentDetailsForMiniMax(messages) + + expect(result).toHaveLength(1) + expect(result).toEqual(messages) + }) + + it("should pass through user messages with only text blocks unchanged", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "text", + text: "Some user message", + }, + ], + }, + ] + + const result = mergeEnvironmentDetailsForMiniMax(messages) + + expect(result).toHaveLength(1) + expect(result).toEqual(messages) + }) + + it("should merge text content into last tool_result when both tool_result AND text blocks exist", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-123", + content: "Tool result content", + }, + { + type: "text", + text: "\nCurrent Time: 2024-01-01\n", + }, + ], + }, + ] + + const result = mergeEnvironmentDetailsForMiniMax(messages) + + // The message should have only tool_result with merged content + expect(result).toHaveLength(1) + expect(result[0].role).toBe("user") + const content = result[0].content as Anthropic.Messages.ToolResultBlockParam[] + expect(content).toHaveLength(1) + expect(content[0].type).toBe("tool_result") + expect(content[0].tool_use_id).toBe("tool-123") + expect(content[0].content).toBe( + "Tool result content\n\n\nCurrent Time: 2024-01-01\n", + ) + }) + + it("should merge multiple text blocks into last tool_result", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-123", + content: "Tool result 1", + }, + { + type: "text", + text: "First text block", + }, + { + type: "tool_result", + tool_use_id: "tool-456", + content: "Tool result 2", + }, + { + type: "text", + text: "Second text block", + }, + ], + }, + ] + + const result = mergeEnvironmentDetailsForMiniMax(messages) + + // The message should have only tool_result blocks, with text merged into the last one + expect(result).toHaveLength(1) + const content = result[0].content as Anthropic.Messages.ToolResultBlockParam[] + expect(content).toHaveLength(2) + expect(content[0].type).toBe("tool_result") + expect(content[0].content).toBe("Tool result 1") // First one unchanged + expect(content[1].type).toBe("tool_result") + expect(content[1].content).toBe("Tool result 2\n\nFirst text block\n\nSecond text block") // Second has merged text + }) + + it("should NOT merge text when images are present (cannot move images to tool_result)", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-123", + content: "Tool result content", + }, + { + type: "text", + text: "Some text", + }, + { + type: "image", + source: { + type: "base64", + media_type: "image/png", + data: "base64data", + }, + }, + ], + }, + ] + + const result = mergeEnvironmentDetailsForMiniMax(messages) + + // Message should be unchanged since images are present + expect(result).toHaveLength(1) + expect(result).toEqual(messages) + }) + + it("should pass through assistant messages unchanged", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "text", + text: "I will help you with that.", + }, + { + type: "tool_use", + id: "tool-123", + name: "read_file", + input: { path: "test.ts" }, + }, + ], + }, + ] + + const result = mergeEnvironmentDetailsForMiniMax(messages) + + expect(result).toHaveLength(1) + expect(result).toEqual(messages) + }) + + it("should handle mixed conversation with merging only for eligible messages", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Create a file", + }, + { + role: "assistant", + content: [ + { + type: "text", + text: "I'll create the file.", + }, + { + type: "tool_use", + id: "tool-123", + name: "write_file", + input: { path: "test.ts", content: "// test" }, + }, + ], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-123", + content: "File created successfully", + }, + { + type: "text", + text: "\nCurrent Time: 2024-01-01\n", + }, + ], + }, + { + role: "assistant", + content: "The file has been created.", + }, + ] + + const result = mergeEnvironmentDetailsForMiniMax(messages) + + // Should have all 4 messages + expect(result).toHaveLength(4) + + // First user message unchanged (simple string) + expect(result[0]).toEqual(messages[0]) + + // Assistant message unchanged + expect(result[1]).toEqual(messages[1]) + + // Third message should have tool_result with merged environment_details + const thirdMessage = result[2].content as Anthropic.Messages.ToolResultBlockParam[] + expect(thirdMessage).toHaveLength(1) + expect(thirdMessage[0].type).toBe("tool_result") + expect(thirdMessage[0].content).toContain("File created successfully") + expect(thirdMessage[0].content).toContain("environment_details") + + // Fourth message unchanged + expect(result[3]).toEqual(messages[3]) + }) + + it("should handle string content in user messages", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Just a string message", + }, + ] + + const result = mergeEnvironmentDetailsForMiniMax(messages) + + expect(result).toHaveLength(1) + expect(result).toEqual(messages) + }) + + it("should handle empty messages array", () => { + const messages: Anthropic.Messages.MessageParam[] = [] + + const result = mergeEnvironmentDetailsForMiniMax(messages) + + expect(result).toHaveLength(0) + }) + + it("should handle tool_result with array content", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-123", + content: [ + { type: "text", text: "Part 1" }, + { type: "text", text: "Part 2" }, + ], + }, + { + type: "text", + text: "Context", + }, + ], + }, + ] + + const result = mergeEnvironmentDetailsForMiniMax(messages) + + expect(result).toHaveLength(1) + const content = result[0].content as Anthropic.Messages.ToolResultBlockParam[] + expect(content).toHaveLength(1) + expect(content[0].type).toBe("tool_result") + // Array content should be concatenated and then merged with text + expect(content[0].content).toBe("Part 1\nPart 2\n\nContext") + }) + + it("should handle tool_result with empty content", () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-123", + content: "", + }, + { + type: "text", + text: "Context", + }, + ], + }, + ] + + const result = mergeEnvironmentDetailsForMiniMax(messages) + + expect(result).toHaveLength(1) + const content = result[0].content as Anthropic.Messages.ToolResultBlockParam[] + expect(content).toHaveLength(1) + expect(content[0].type).toBe("tool_result") + expect(content[0].content).toBe("Context") + }) +}) diff --git a/src/api/transform/minimax-format.ts b/src/api/transform/minimax-format.ts new file mode 100644 index 00000000000..32a32a4437e --- /dev/null +++ b/src/api/transform/minimax-format.ts @@ -0,0 +1,118 @@ +import { Anthropic } from "@anthropic-ai/sdk" + +type ContentBlock = Anthropic.Messages.ContentBlockParam + +/** + * Merges text content (like environment_details) that follows tool_result blocks + * into the last tool_result's content. This preserves reasoning continuity for + * thinking models by avoiding separate user messages after tool results. + * + * Key behavior: + * - User messages with ONLY tool_result blocks: keep as-is + * - User messages with ONLY text/image: keep as-is + * - User messages with tool_result blocks AND text blocks: merge the text blocks + * into the last tool_result's content + * + * @param messages Array of Anthropic messages + * @returns Modified messages with text merged into tool_result content + */ +export function mergeEnvironmentDetailsForMiniMax( + messages: Anthropic.Messages.MessageParam[], +): Anthropic.Messages.MessageParam[] { + const result: Anthropic.Messages.MessageParam[] = [] + + for (const message of messages) { + if (message.role === "user") { + if (typeof message.content === "string") { + // Simple string content - keep as-is + result.push(message) + } else if (Array.isArray(message.content)) { + // Check if this message has both tool_result blocks and text blocks + const toolResultBlocks: Anthropic.Messages.ToolResultBlockParam[] = [] + const textBlocks: Anthropic.Messages.TextBlockParam[] = [] + const imageBlocks: Anthropic.Messages.ImageBlockParam[] = [] + + for (const block of message.content) { + if (block.type === "tool_result") { + toolResultBlocks.push(block) + } else if (block.type === "text") { + textBlocks.push(block) + } else if (block.type === "image") { + imageBlocks.push(block) + } + } + + // If we have tool_result blocks AND text blocks (like environment_details), + // merge the text into the last tool_result's content + const hasToolResults = toolResultBlocks.length > 0 + const hasTextBlocks = textBlocks.length > 0 + const hasImageBlocks = imageBlocks.length > 0 + + if (hasToolResults && hasTextBlocks && !hasImageBlocks) { + // Merge text content into the last tool_result + const textContent = textBlocks.map((b) => b.text).join("\n\n") + const modifiedToolResults = [...toolResultBlocks] + const lastToolResult = modifiedToolResults[modifiedToolResults.length - 1] + + // Get existing content as string + let existingContent: string + if (typeof lastToolResult.content === "string") { + existingContent = lastToolResult.content + } else if (Array.isArray(lastToolResult.content)) { + existingContent = + lastToolResult.content + ?.map((c) => { + if (c.type === "text") return c.text + if (c.type === "image") return "(image)" + return "" + }) + .join("\n") ?? "" + } else { + existingContent = "" + } + + // Merge text into the last tool_result + modifiedToolResults[modifiedToolResults.length - 1] = { + ...lastToolResult, + content: existingContent ? `${existingContent}\n\n${textContent}` : textContent, + } + + result.push({ + ...message, + content: modifiedToolResults as ContentBlock[], + }) + } else { + // Keep the message as-is if: + // - Only tool_result blocks (no text to merge) + // - Only text/image blocks (no tool results) + // - Has images (can't merge into tool_result) + result.push(message) + } + } else { + // Unknown format - keep as-is + result.push(message) + } + } else { + // Assistant messages - keep as-is + result.push(message) + } + } + + return result +} + +/** + * @deprecated Use mergeEnvironmentDetailsForMiniMax instead. This function extracted + * environment_details to the system prompt, but the new approach merges them into + * tool_result content like r1-format does with mergeToolResultText. + */ +export function extractEnvironmentDetailsForMiniMax(messages: Anthropic.Messages.MessageParam[]): { + messages: Anthropic.Messages.MessageParam[] + extractedSystemContent: string[] +} { + // For backwards compatibility, just return the merged messages with empty extracted content + return { + messages: mergeEnvironmentDetailsForMiniMax(messages), + extractedSystemContent: [], + } +}