Răsfoiți Sursa

feat: implement Minimax as Anthropic-compatible provider (#9455)

Daniel 1 lună în urmă
părinte
comite
905181810e

+ 1 - 1
packages/types/src/provider-settings.ts

@@ -601,7 +601,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
  */
  */
 
 
 // Providers that use Anthropic-style API protocol.
 // Providers that use Anthropic-style API protocol.
-export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "claude-code", "bedrock"]
+export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "claude-code", "bedrock", "minimax"]
 
 
 export const getApiProtocol = (provider: ProviderName | undefined, modelId?: string): "anthropic" | "openai" => {
 export const getApiProtocol = (provider: ProviderName | undefined, modelId?: string): "anthropic" | "openai" => {
 	if (provider && ANTHROPIC_STYLE_PROVIDERS.includes(provider)) {
 	if (provider && ANTHROPIC_STYLE_PROVIDERS.includes(provider)) {

+ 7 - 2
packages/types/src/providers/minimax.ts

@@ -13,11 +13,12 @@ export const minimaxModels = {
 		contextWindow: 192_000,
 		contextWindow: 192_000,
 		supportsImages: false,
 		supportsImages: false,
 		supportsPromptCache: true,
 		supportsPromptCache: true,
+		supportsNativeTools: true,
+		preserveReasoning: true,
 		inputPrice: 0.3,
 		inputPrice: 0.3,
 		outputPrice: 1.2,
 		outputPrice: 1.2,
 		cacheWritesPrice: 0.375,
 		cacheWritesPrice: 0.375,
 		cacheReadsPrice: 0.03,
 		cacheReadsPrice: 0.03,
-		preserveReasoning: true,
 		description:
 		description:
 			"MiniMax M2, a model born for Agents and code, featuring Top-tier Coding Capabilities, Powerful Agentic Performance, and Ultimate Cost-Effectiveness & Speed.",
 			"MiniMax M2, a model born for Agents and code, featuring Top-tier Coding Capabilities, Powerful Agentic Performance, and Ultimate Cost-Effectiveness & Speed.",
 	},
 	},
@@ -26,14 +27,18 @@ export const minimaxModels = {
 		contextWindow: 192_000,
 		contextWindow: 192_000,
 		supportsImages: false,
 		supportsImages: false,
 		supportsPromptCache: true,
 		supportsPromptCache: true,
+		supportsNativeTools: true,
+		preserveReasoning: true,
 		inputPrice: 0.3,
 		inputPrice: 0.3,
 		outputPrice: 1.2,
 		outputPrice: 1.2,
 		cacheWritesPrice: 0.375,
 		cacheWritesPrice: 0.375,
 		cacheReadsPrice: 0.03,
 		cacheReadsPrice: 0.03,
-		preserveReasoning: true,
 		description:
 		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 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.",
 	},
 	},
 } as const satisfies Record<string, ModelInfo>
 } as const satisfies Record<string, ModelInfo>
 
 
+export const minimaxDefaultModelInfo: ModelInfo = minimaxModels[minimaxDefaultModelId]
+
+export const MINIMAX_DEFAULT_MAX_TOKENS = 16_384
 export const MINIMAX_DEFAULT_TEMPERATURE = 1.0
 export const MINIMAX_DEFAULT_TEMPERATURE = 1.0

+ 151 - 79
src/api/providers/__tests__/minimax.spec.ts

@@ -8,27 +8,35 @@ vitest.mock("vscode", () => ({
 	},
 	},
 }))
 }))
 
 
-import OpenAI from "openai"
 import { Anthropic } from "@anthropic-ai/sdk"
 import { Anthropic } from "@anthropic-ai/sdk"
 
 
 import { type MinimaxModelId, minimaxDefaultModelId, minimaxModels } from "@roo-code/types"
 import { type MinimaxModelId, minimaxDefaultModelId, minimaxModels } from "@roo-code/types"
 
 
 import { MiniMaxHandler } from "../minimax"
 import { MiniMaxHandler } from "../minimax"
 
 
-vitest.mock("openai", () => {
-	const createMock = vitest.fn()
+vitest.mock("@anthropic-ai/sdk", () => {
+	const mockCreate = vitest.fn()
+	const mockCountTokens = vitest.fn()
 	return {
 	return {
-		default: vitest.fn(() => ({ chat: { completions: { create: createMock } } })),
+		Anthropic: vitest.fn(() => ({
+			messages: {
+				create: mockCreate,
+				countTokens: mockCountTokens,
+			},
+		})),
 	}
 	}
 })
 })
 
 
 describe("MiniMaxHandler", () => {
 describe("MiniMaxHandler", () => {
 	let handler: MiniMaxHandler
 	let handler: MiniMaxHandler
 	let mockCreate: any
 	let mockCreate: any
+	let mockCountTokens: any
 
 
 	beforeEach(() => {
 	beforeEach(() => {
 		vitest.clearAllMocks()
 		vitest.clearAllMocks()
-		mockCreate = (OpenAI as unknown as any)().chat.completions.create
+		const anthropicInstance = (Anthropic as unknown as any)()
+		mockCreate = anthropicInstance.messages.create
+		mockCountTokens = anthropicInstance.messages.countTokens
 	})
 	})
 
 
 	describe("International MiniMax (default)", () => {
 	describe("International MiniMax (default)", () => {
@@ -41,9 +49,21 @@ describe("MiniMaxHandler", () => {
 
 
 		it("should use the correct international MiniMax base URL by default", () => {
 		it("should use the correct international MiniMax base URL by default", () => {
 			new MiniMaxHandler({ minimaxApiKey: "test-minimax-api-key" })
 			new MiniMaxHandler({ minimaxApiKey: "test-minimax-api-key" })
-			expect(OpenAI).toHaveBeenCalledWith(
+			expect(Anthropic).toHaveBeenCalledWith(
 				expect.objectContaining({
 				expect.objectContaining({
-					baseURL: "https://api.minimax.io/v1",
+					baseURL: "https://api.minimax.io/anthropic",
+				}),
+			)
+		})
+
+		it("should convert /v1 endpoint to /anthropic endpoint", () => {
+			new MiniMaxHandler({
+				minimaxApiKey: "test-minimax-api-key",
+				minimaxBaseUrl: "https://api.minimax.io/v1",
+			})
+			expect(Anthropic).toHaveBeenCalledWith(
+				expect.objectContaining({
+					baseURL: "https://api.minimax.io/anthropic",
 				}),
 				}),
 			)
 			)
 		})
 		})
@@ -51,7 +71,7 @@ describe("MiniMaxHandler", () => {
 		it("should use the provided API key", () => {
 		it("should use the provided API key", () => {
 			const minimaxApiKey = "test-minimax-api-key"
 			const minimaxApiKey = "test-minimax-api-key"
 			new MiniMaxHandler({ minimaxApiKey })
 			new MiniMaxHandler({ minimaxApiKey })
-			expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: minimaxApiKey }))
+			expect(Anthropic).toHaveBeenCalledWith(expect.objectContaining({ apiKey: minimaxApiKey }))
 		})
 		})
 
 
 		it("should return default model when no model is specified", () => {
 		it("should return default model when no model is specified", () => {
@@ -117,13 +137,25 @@ describe("MiniMaxHandler", () => {
 				minimaxApiKey: "test-minimax-api-key",
 				minimaxApiKey: "test-minimax-api-key",
 				minimaxBaseUrl: "https://api.minimaxi.com/v1",
 				minimaxBaseUrl: "https://api.minimaxi.com/v1",
 			})
 			})
-			expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.minimaxi.com/v1" }))
+			expect(Anthropic).toHaveBeenCalledWith(
+				expect.objectContaining({ baseURL: "https://api.minimaxi.com/anthropic" }),
+			)
+		})
+
+		it("should convert China /v1 endpoint to /anthropic endpoint", () => {
+			new MiniMaxHandler({
+				minimaxApiKey: "test-minimax-api-key",
+				minimaxBaseUrl: "https://api.minimaxi.com/v1",
+			})
+			expect(Anthropic).toHaveBeenCalledWith(
+				expect.objectContaining({ baseURL: "https://api.minimaxi.com/anthropic" }),
+			)
 		})
 		})
 
 
 		it("should use the provided API key for China", () => {
 		it("should use the provided API key for China", () => {
 			const minimaxApiKey = "test-minimax-api-key"
 			const minimaxApiKey = "test-minimax-api-key"
 			new MiniMaxHandler({ minimaxApiKey, minimaxBaseUrl: "https://api.minimaxi.com/v1" })
 			new MiniMaxHandler({ minimaxApiKey, minimaxBaseUrl: "https://api.minimaxi.com/v1" })
-			expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: minimaxApiKey }))
+			expect(Anthropic).toHaveBeenCalledWith(expect.objectContaining({ apiKey: minimaxApiKey }))
 		})
 		})
 
 
 		it("should return default model when no model is specified", () => {
 		it("should return default model when no model is specified", () => {
@@ -136,9 +168,9 @@ describe("MiniMaxHandler", () => {
 	describe("Default behavior", () => {
 	describe("Default behavior", () => {
 		it("should default to international base URL when none is specified", () => {
 		it("should default to international base URL when none is specified", () => {
 			const handlerDefault = new MiniMaxHandler({ minimaxApiKey: "test-minimax-api-key" })
 			const handlerDefault = new MiniMaxHandler({ minimaxApiKey: "test-minimax-api-key" })
-			expect(OpenAI).toHaveBeenCalledWith(
+			expect(Anthropic).toHaveBeenCalledWith(
 				expect.objectContaining({
 				expect.objectContaining({
-					baseURL: "https://api.minimax.io/v1",
+					baseURL: "https://api.minimax.io/anthropic",
 				}),
 				}),
 			)
 			)
 
 
@@ -161,7 +193,9 @@ describe("MiniMaxHandler", () => {
 
 
 		it("completePrompt method should return text from MiniMax API", async () => {
 		it("completePrompt method should return text from MiniMax API", async () => {
 			const expectedResponse = "This is a test response from MiniMax"
 			const expectedResponse = "This is a test response from MiniMax"
-			mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] })
+			mockCreate.mockResolvedValueOnce({
+				content: [{ type: "text", text: expectedResponse }],
+			})
 			const result = await handler.completePrompt("test prompt")
 			const result = await handler.completePrompt("test prompt")
 			expect(result).toBe(expectedResponse)
 			expect(result).toBe(expectedResponse)
 		})
 		})
@@ -175,18 +209,20 @@ describe("MiniMaxHandler", () => {
 		it("createMessage should yield text content from stream", async () => {
 		it("createMessage should yield text content from stream", async () => {
 			const testContent = "This is test content from MiniMax stream"
 			const testContent = "This is test content from MiniMax stream"
 
 
-			mockCreate.mockImplementationOnce(() => {
-				return {
-					[Symbol.asyncIterator]: () => ({
-						next: vitest
-							.fn()
-							.mockResolvedValueOnce({
-								done: false,
-								value: { choices: [{ delta: { content: testContent } }] },
-							})
-							.mockResolvedValueOnce({ done: true }),
-					}),
-				}
+			mockCreate.mockResolvedValueOnce({
+				[Symbol.asyncIterator]: () => ({
+					next: vitest
+						.fn()
+						.mockResolvedValueOnce({
+							done: false,
+							value: {
+								type: "content_block_start",
+								index: 0,
+								content_block: { type: "text", text: testContent },
+							},
+						})
+						.mockResolvedValueOnce({ done: true }),
+				}),
 			})
 			})
 
 
 			const stream = handler.createMessage("system prompt", [])
 			const stream = handler.createMessage("system prompt", [])
@@ -197,21 +233,24 @@ describe("MiniMaxHandler", () => {
 		})
 		})
 
 
 		it("createMessage should yield usage data from stream", async () => {
 		it("createMessage should yield usage data from stream", async () => {
-			mockCreate.mockImplementationOnce(() => {
-				return {
-					[Symbol.asyncIterator]: () => ({
-						next: vitest
-							.fn()
-							.mockResolvedValueOnce({
-								done: false,
-								value: {
-									choices: [{ delta: {} }],
-									usage: { prompt_tokens: 10, completion_tokens: 20 },
+			mockCreate.mockResolvedValueOnce({
+				[Symbol.asyncIterator]: () => ({
+					next: vitest
+						.fn()
+						.mockResolvedValueOnce({
+							done: false,
+							value: {
+								type: "message_start",
+								message: {
+									usage: {
+										input_tokens: 10,
+										output_tokens: 20,
+									},
 								},
 								},
-							})
-							.mockResolvedValueOnce({ done: true }),
-					}),
-				}
+							},
+						})
+						.mockResolvedValueOnce({ done: true }),
+				}),
 			})
 			})
 
 
 			const stream = handler.createMessage("system prompt", [])
 			const stream = handler.createMessage("system prompt", [])
@@ -229,14 +268,12 @@ describe("MiniMaxHandler", () => {
 				minimaxApiKey: "test-minimax-api-key",
 				minimaxApiKey: "test-minimax-api-key",
 			})
 			})
 
 
-			mockCreate.mockImplementationOnce(() => {
-				return {
-					[Symbol.asyncIterator]: () => ({
-						async next() {
-							return { done: true }
-						},
-					}),
-				}
+			mockCreate.mockResolvedValueOnce({
+				[Symbol.asyncIterator]: () => ({
+					async next() {
+						return { done: true }
+					},
+				}),
 			})
 			})
 
 
 			const systemPrompt = "Test system prompt for MiniMax"
 			const systemPrompt = "Test system prompt for MiniMax"
@@ -250,23 +287,20 @@ describe("MiniMaxHandler", () => {
 					model: modelId,
 					model: modelId,
 					max_tokens: Math.min(modelInfo.maxTokens, Math.ceil(modelInfo.contextWindow * 0.2)),
 					max_tokens: Math.min(modelInfo.maxTokens, Math.ceil(modelInfo.contextWindow * 0.2)),
 					temperature: 1,
 					temperature: 1,
-					messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
+					system: expect.any(Array),
+					messages: expect.any(Array),
 					stream: true,
 					stream: true,
-					stream_options: { include_usage: true },
 				}),
 				}),
-				undefined,
 			)
 			)
 		})
 		})
 
 
 		it("should use temperature 1 by default", async () => {
 		it("should use temperature 1 by default", async () => {
-			mockCreate.mockImplementationOnce(() => {
-				return {
-					[Symbol.asyncIterator]: () => ({
-						async next() {
-							return { done: true }
-						},
-					}),
-				}
+			mockCreate.mockResolvedValueOnce({
+				[Symbol.asyncIterator]: () => ({
+					async next() {
+						return { done: true }
+					},
+				}),
 			})
 			})
 
 
 			const messageGenerator = handler.createMessage("test", [])
 			const messageGenerator = handler.createMessage("test", [])
@@ -276,36 +310,74 @@ describe("MiniMaxHandler", () => {
 				expect.objectContaining({
 				expect.objectContaining({
 					temperature: 1,
 					temperature: 1,
 				}),
 				}),
-				undefined,
 			)
 			)
 		})
 		})
 
 
-		it("should handle streaming chunks with null choices array", async () => {
-			const testContent = "Content after null choices"
-
-			mockCreate.mockImplementationOnce(() => {
-				return {
-					[Symbol.asyncIterator]: () => ({
-						next: vitest
-							.fn()
-							.mockResolvedValueOnce({
-								done: false,
-								value: { choices: null },
-							})
-							.mockResolvedValueOnce({
-								done: false,
-								value: { choices: [{ delta: { content: testContent } }] },
-							})
-							.mockResolvedValueOnce({ done: true }),
-					}),
-				}
+		it("should handle thinking blocks in stream", async () => {
+			const thinkingContent = "Let me think about this..."
+
+			mockCreate.mockResolvedValueOnce({
+				[Symbol.asyncIterator]: () => ({
+					next: vitest
+						.fn()
+						.mockResolvedValueOnce({
+							done: false,
+							value: {
+								type: "content_block_start",
+								index: 0,
+								content_block: { type: "thinking", thinking: thinkingContent },
+							},
+						})
+						.mockResolvedValueOnce({ done: true }),
+				}),
 			})
 			})
 
 
 			const stream = handler.createMessage("system prompt", [])
 			const stream = handler.createMessage("system prompt", [])
 			const firstChunk = await stream.next()
 			const firstChunk = await stream.next()
 
 
 			expect(firstChunk.done).toBe(false)
 			expect(firstChunk.done).toBe(false)
-			expect(firstChunk.value).toEqual({ type: "text", text: testContent })
+			expect(firstChunk.value).toEqual({ type: "reasoning", text: thinkingContent })
+		})
+
+		it("should handle tool calls in stream", async () => {
+			mockCreate.mockResolvedValueOnce({
+				[Symbol.asyncIterator]: () => ({
+					next: vitest
+						.fn()
+						.mockResolvedValueOnce({
+							done: false,
+							value: {
+								type: "content_block_start",
+								index: 0,
+								content_block: {
+									type: "tool_use",
+									id: "tool-123",
+									name: "get_weather",
+									input: { city: "London" },
+								},
+							},
+						})
+						.mockResolvedValueOnce({
+							done: false,
+							value: {
+								type: "content_block_stop",
+								index: 0,
+							},
+						})
+						.mockResolvedValueOnce({ done: true }),
+				}),
+			})
+
+			const stream = handler.createMessage("system prompt", [])
+			const firstChunk = await stream.next()
+
+			expect(firstChunk.done).toBe(false)
+			expect(firstChunk.value).toEqual({
+				type: "tool_call",
+				id: "tool-123",
+				name: "get_weather",
+				arguments: JSON.stringify({ city: "London" }),
+			})
 		})
 		})
 	})
 	})
 
 

+ 332 - 8
src/api/providers/minimax.ts

@@ -1,19 +1,343 @@
+import { Anthropic } from "@anthropic-ai/sdk"
+import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
+import { CacheControlEphemeral } from "@anthropic-ai/sdk/resources"
+import OpenAI from "openai"
+
 import { type MinimaxModelId, minimaxDefaultModelId, minimaxModels } from "@roo-code/types"
 import { type MinimaxModelId, minimaxDefaultModelId, minimaxModels } from "@roo-code/types"
 
 
 import type { ApiHandlerOptions } from "../../shared/api"
 import type { ApiHandlerOptions } from "../../shared/api"
 
 
-import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
+import { ApiStream } from "../transform/stream"
+import { getModelParams } from "../transform/model-params"
+
+import { BaseProvider } from "./base-provider"
+import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
+import { calculateApiCostAnthropic } from "../../shared/cost"
+import { convertOpenAIToolsToAnthropic } from "../../core/prompts/tools/native-tools/converters"
+
+/**
+ * Converts OpenAI tool_choice to Anthropic ToolChoice format
+ */
+function convertOpenAIToolChoice(
+	toolChoice: OpenAI.Chat.ChatCompletionCreateParams["tool_choice"],
+): Anthropic.Messages.MessageCreateParams["tool_choice"] | undefined {
+	if (!toolChoice) {
+		return undefined
+	}
+
+	if (typeof toolChoice === "string") {
+		switch (toolChoice) {
+			case "none":
+				return undefined // Anthropic doesn't have "none", just omit tools
+			case "auto":
+				return { type: "auto" }
+			case "required":
+				return { type: "any" }
+			default:
+				return { type: "auto" }
+		}
+	}
+
+	// Handle object form { type: "function", function: { name: string } }
+	if (typeof toolChoice === "object" && "function" in toolChoice) {
+		return {
+			type: "tool",
+			name: toolChoice.function.name,
+		}
+	}
+
+	return { type: "auto" }
+}
+
+export class MiniMaxHandler extends BaseProvider implements SingleCompletionHandler {
+	private options: ApiHandlerOptions
+	private client: Anthropic
 
 
-export class MiniMaxHandler extends BaseOpenAiCompatibleProvider<MinimaxModelId> {
 	constructor(options: ApiHandlerOptions) {
 	constructor(options: ApiHandlerOptions) {
-		super({
-			...options,
-			providerName: "MiniMax",
-			baseURL: options.minimaxBaseUrl ?? "https://api.minimax.io/v1",
+		super()
+		this.options = options
+
+		// Use Anthropic-compatible endpoint
+		// Default to international endpoint: https://api.minimax.io/anthropic
+		// China endpoint: https://api.minimaxi.com/anthropic
+		let baseURL = options.minimaxBaseUrl || "https://api.minimax.io/anthropic"
+
+		// If user provided a /v1 endpoint, convert to /anthropic
+		if (baseURL.endsWith("/v1")) {
+			baseURL = baseURL.replace(/\/v1$/, "/anthropic")
+		} else if (!baseURL.endsWith("/anthropic")) {
+			baseURL = `${baseURL.replace(/\/$/, "")}/anthropic`
+		}
+
+		this.client = new Anthropic({
+			baseURL,
 			apiKey: options.minimaxApiKey,
 			apiKey: options.minimaxApiKey,
-			defaultProviderModelId: minimaxDefaultModelId,
-			providerModels: minimaxModels,
+		})
+	}
+
+	async *createMessage(
+		systemPrompt: string,
+		messages: Anthropic.Messages.MessageParam[],
+		metadata?: ApiHandlerCreateMessageMetadata,
+	): ApiStream {
+		let stream: AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>
+		const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
+		const { id: modelId, info, maxTokens, temperature } = this.getModel()
+
+		// MiniMax M2 models support prompt caching
+		const supportsPromptCache = info.supportsPromptCache ?? false
+
+		// 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,
+			stream: true,
+		}
+
+		// Add tool support if provided - convert OpenAI format to Anthropic format
+		// Only include native tools when toolProtocol is not 'xml'
+		if (metadata?.tools && metadata.tools.length > 0 && metadata?.toolProtocol !== "xml") {
+			requestParams.tools = convertOpenAIToolsToAnthropic(metadata.tools)
+
+			// Only add tool_choice if tools are present
+			if (metadata?.tool_choice) {
+				const convertedChoice = convertOpenAIToolChoice(metadata.tool_choice)
+				if (convertedChoice) {
+					requestParams.tool_choice = convertedChoice
+				}
+			}
+		}
+
+		stream = await this.client.messages.create(requestParams)
+
+		let inputTokens = 0
+		let outputTokens = 0
+		let cacheWriteTokens = 0
+		let cacheReadTokens = 0
+
+		// Track tool calls being accumulated via streaming
+		const toolCallAccumulator = new Map<number, { id: string; name: string; input: string }>()
+
+		for await (const chunk of stream) {
+			switch (chunk.type) {
+				case "message_start": {
+					// Tells us cache reads/writes/input/output.
+					const {
+						input_tokens = 0,
+						output_tokens = 0,
+						cache_creation_input_tokens,
+						cache_read_input_tokens,
+					} = chunk.message.usage
+
+					yield {
+						type: "usage",
+						inputTokens: input_tokens,
+						outputTokens: output_tokens,
+						cacheWriteTokens: cache_creation_input_tokens || undefined,
+						cacheReadTokens: cache_read_input_tokens || undefined,
+					}
+
+					inputTokens += input_tokens
+					outputTokens += output_tokens
+					cacheWriteTokens += cache_creation_input_tokens || 0
+					cacheReadTokens += cache_read_input_tokens || 0
+
+					break
+				}
+				case "message_delta":
+					// Tells us stop_reason, stop_sequence, and output tokens
+					yield {
+						type: "usage",
+						inputTokens: 0,
+						outputTokens: chunk.usage.output_tokens || 0,
+					}
+
+					break
+				case "message_stop":
+					// No usage data, just an indicator that the message is done.
+					break
+				case "content_block_start":
+					switch (chunk.content_block.type) {
+						case "thinking":
+							// Yield thinking/reasoning content
+							if (chunk.index > 0) {
+								yield { type: "reasoning", text: "\n" }
+							}
+
+							yield { type: "reasoning", text: chunk.content_block.thinking }
+							break
+						case "text":
+							// We may receive multiple text blocks
+							if (chunk.index > 0) {
+								yield { type: "text", text: "\n" }
+							}
+
+							yield { type: "text", text: chunk.content_block.text }
+							break
+						case "tool_use": {
+							// Tool use block started - store initial data
+							// If input is empty ({}), start with empty string as deltas will build it
+							// Otherwise, stringify the initial input as a base for potential deltas
+							const initialInput = chunk.content_block.input || {}
+							const hasInitialContent = Object.keys(initialInput).length > 0
+							toolCallAccumulator.set(chunk.index, {
+								id: chunk.content_block.id,
+								name: chunk.content_block.name,
+								input: hasInitialContent ? JSON.stringify(initialInput) : "",
+							})
+							break
+						}
+					}
+					break
+				case "content_block_delta":
+					switch (chunk.delta.type) {
+						case "thinking_delta":
+							yield { type: "reasoning", text: chunk.delta.thinking }
+							break
+						case "text_delta":
+							yield { type: "text", text: chunk.delta.text }
+							break
+						case "input_json_delta": {
+							// Accumulate tool input JSON as it streams
+							const existingToolCall = toolCallAccumulator.get(chunk.index)
+							if (existingToolCall) {
+								existingToolCall.input += chunk.delta.partial_json
+							}
+							break
+						}
+					}
+
+					break
+				case "content_block_stop": {
+					// Block is complete - yield tool call if this was a tool_use block
+					const completedToolCall = toolCallAccumulator.get(chunk.index)
+					if (completedToolCall) {
+						yield {
+							type: "tool_call",
+							id: completedToolCall.id,
+							name: completedToolCall.name,
+							arguments: completedToolCall.input,
+						}
+						// Remove from accumulator after yielding
+						toolCallAccumulator.delete(chunk.index)
+					}
+					break
+				}
+			}
+		}
+
+		// Calculate and yield final cost
+		if (inputTokens > 0 || outputTokens > 0 || cacheWriteTokens > 0 || cacheReadTokens > 0) {
+			const { totalCost } = calculateApiCostAnthropic(
+				this.getModel().info,
+				inputTokens,
+				outputTokens,
+				cacheWriteTokens,
+				cacheReadTokens,
+			)
+
+			yield {
+				type: "usage",
+				inputTokens: 0,
+				outputTokens: 0,
+				totalCost,
+			}
+		}
+	}
+
+	/**
+	 * Add cache control to the last two user messages for prompt caching
+	 */
+	private addCacheControl(
+		messages: Anthropic.Messages.MessageParam[],
+		cacheControl: CacheControlEphemeral,
+	): Anthropic.Messages.MessageParam[] {
+		const userMsgIndices = messages.reduce(
+			(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
+			[] as number[],
+		)
+
+		const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
+		const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
+
+		return messages.map((message, index) => {
+			if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
+				return {
+					...message,
+					content:
+						typeof message.content === "string"
+							? [{ type: "text", text: message.content, cache_control: cacheControl }]
+							: message.content.map((content, contentIndex) =>
+									contentIndex === message.content.length - 1
+										? { ...content, cache_control: cacheControl }
+										: content,
+								),
+				}
+			}
+			return message
+		})
+	}
+
+	getModel() {
+		const modelId = this.options.apiModelId
+		const id = modelId && modelId in minimaxModels ? (modelId as MinimaxModelId) : minimaxDefaultModelId
+		const info = minimaxModels[id]
+
+		const params = getModelParams({
+			format: "anthropic",
+			modelId: id,
+			model: info,
+			settings: this.options,
 			defaultTemperature: 1.0,
 			defaultTemperature: 1.0,
 		})
 		})
+
+		return {
+			id,
+			info,
+			...params,
+		}
+	}
+
+	async completePrompt(prompt: string) {
+		const { id: model, temperature } = this.getModel()
+
+		const message = await this.client.messages.create({
+			model,
+			max_tokens: 16_384,
+			temperature: temperature ?? 1.0,
+			messages: [{ role: "user", content: prompt }],
+			stream: false,
+		})
+
+		const content = message.content.find(({ type }) => type === "text")
+		return content?.type === "text" ? content.text : ""
+	}
+
+	/**
+	 * Counts tokens for the given content using Anthropic's token counting
+	 * Falls back to base provider's tiktoken estimation if counting fails
+	 */
+	override async countTokens(content: Array<Anthropic.Messages.ContentBlockParam>): Promise<number> {
+		try {
+			const { id: model } = this.getModel()
+
+			const response = await this.client.messages.countTokens({
+				model,
+				messages: [{ role: "user", content: content }],
+			})
+
+			return response.input_tokens
+		} catch (error) {
+			// Log error but fallback to tiktoken estimation
+			console.warn("MiniMax token counting failed, using fallback", error)
+
+			// Use the base provider's implementation as fallback
+			return super.countTokens(content)
+		}
 	}
 	}
 }
 }