feat(minimax): move environment_details to system message for thinking models (#10284)

This commit is contained in:
Hannes Rudolph 2025-12-22 19:59:42 -07:00 committed by GitHub
parent bd78a63844
commit e7c1851a8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 491 additions and 4 deletions

View file

@ -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<string, ModelInfo>
export const minimaxDefaultModelInfo: ModelInfo = minimaxModels[minimaxDefaultModelId]

View file

@ -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,
}

View file

@ -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: "<environment_details>\nCurrent Time: 2024-01-01\n</environment_details>",
},
],
},
]
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<environment_details>\nCurrent Time: 2024-01-01\n</environment_details>",
)
})
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: "<environment_details>\nCurrent Time: 2024-01-01\n</environment_details>",
},
],
},
{
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: "<environment_details>Context</environment_details>",
},
],
},
]
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\n<environment_details>Context</environment_details>")
})
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: "<environment_details>Context</environment_details>",
},
],
},
]
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("<environment_details>Context</environment_details>")
})
})

View file

@ -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: [],
}
}