` tag content, which must be preserved completely
+ - In the Interleaved Thinking compatible format, by enabling the additional parameter (`reasoning_split=True`), the model’s thinking content is provided separately via the `reasoning_details` field, which must also be preserved completely
+
+## [](https://platform.minimax.io/docs/api-reference/text-openai-api\#supported-models) Supported Models
+
+When using the OpenAI SDK, the following MiniMax models are supported:
+
+| Model Name | Context Window | Description |
+| --- | --- | --- |
+| MiniMax-M3 | 1,000,000 | **Latest M-series language model for agentic reasoning, tool use, coding, and long-context tasks** |
+| MiniMax-M2.7 | 204,800 | **Beginning the journey of recursive self-improvement** (output speed approximately 60 tps) |
+| MiniMax-M2.7-highspeed | 204,800 | **M2.7 Highspeed: Same performance, faster and more agile (output speed approximately 100 tps)** |
+| MiniMax-M2.5 | 204,800 | **Peak Performance. Ultimate Value. Master the Complex (output speed approximately 60 tps)** |
+| MiniMax-M2.5-highspeed | 204,800 | **M2.5 highspeed: Same performance, faster and more agile (output speed approximately 100 tps)** |
+| MiniMax-M2.1 | 204,800 | **Powerful Multi-Language Programming Capabilities with Comprehensively Enhanced Programming Experience (output speed approximately 60 tps)** |
+| MiniMax-M2.1-highspeed | 204,800 | **Faster and More Agile (output speed approximately 100 tps)** |
+| MiniMax-M2 | 204,800 | **Agentic capabilities, Advanced reasoning** |
+
+For details on how tps (Tokens Per Second) is calculated, please refer to [FAQ > About APIs](https://platform.minimax.io/docs/faq/about-apis#q-how-is-tps-tokens-per-second-calculated-for-text-models).
+
+For more model information, please refer to the standard MiniMax API
+documentation.
+
+## [](https://platform.minimax.io/docs/api-reference/text-openai-api\#multimodal-input) Multimodal Input
+
+OpenAI-compatible Chat Completions support text, image, and video input for `MiniMax-M3`.Use `image_url` content parts for images and `video_url` content parts for videos. The `detail` field accepts `low`, `default`, or `high` and defaults to `default`; `max_long_side_pixel` can be used to control the longest side. Images support JPEG, PNG, GIF, and WEBP. Videos support MP4, AVI, MOV, and MKV; `fps` defaults to 1 and accepts values from 0.2 to 5. URL or base64 videos can be up to 50 MB, images can be up to 10 MB, and the request body can be up to 64 MB. For larger videos, upload through the Files API and pass `mm_file://{file_id}`; Files API videos can be up to 512 MB.Image token usage depends on image size and content. Use this as a rough single-image heuristic; check response `usage` or token counting where available for exact usage:
+
+| `detail` | Rough single-image token usage |
+| --- | --- |
+| `low` | Usually a few hundred tokens, up to ~600 |
+| `default` | Often ~1k-3k tokens, up to ~5k |
+| `high` | Often several thousand tokens, up to ~15k+ |
+
+Python
+
+```
+response = client.chat.completions.create(
+ model="MiniMax-M3",
+ messages=[\
+ {\
+ "role": "user",\
+ "content": [\
+ {"type": "text", "text": "Summarize what is happening here."},\
+ {\
+ "type": "image_url",\
+ "image_url": {\
+ "url": "https://example.com/image.png",\
+ "detail": "default",\
+ },\
+ },\
+ {\
+ "type": "video_url",\
+ "video_url": {\
+ "url": "mm_file://file_id",\
+ "detail": "default",\
+ },\
+ },\
+ ],\
+ }\
+ ],
+)
+```
+
+## [](https://platform.minimax.io/docs/api-reference/text-openai-api\#minimax-m3-request-parameters) MiniMax-M3 Request Parameters
+
+`MiniMax-M3` supports these additional Chat Completions parameters through the OpenAI-compatible API:
+
+| Parameter | Description |
+| --- | --- |
+| `thinking` | Controls MiniMax-M3 thinking. `type` can be `disabled` or `adaptive`; when omitted, thinking is on by default. For M2.x models, thinking cannot be disabled. |
+| `stream_options.include_usage` | When streaming, set to `true` to include token usage in the stream. |
+| `max_tokens` | Legacy generation length limit. |
+| `max_completion_tokens` | Generation length limit; use this field for new integrations. |
+| `temperature` | Sampling temperature. Range `[0, 2]`, default `1`. |
+| `top_p` | Nucleus sampling. Range `[0, 1]`. Default `0.95` for `MiniMax-M3` and `0.9` for M2.x models. |
+| `tools` | Function tool definitions. |
+| `reasoning_split` | Output-format switch. When enabled, separates thinking content into `reasoning_content` and `reasoning_details`. |
+| `service_tier` | Request admission tier. Supported values are `standard` and `priority`; if omitted, requests use `standard`. The `priority` [price](https://platform.minimax.io/docs/guides/pricing-paygo) is 1.5 times the `standard` price and ensures priority admission so the request is processed ahead of other requests, leading to faster responses and fewer failures. |
+
+### [](https://platform.minimax.io/docs/api-reference/text-openai-api\#thinking-control) Thinking Control
+
+For `MiniMax-M3`, the `thinking` parameter controls whether the model can emit thinking content.
+
+- If `thinking` is omitted, thinking is on by default and the response includes thinking content.
+- Set `thinking: {"type": "adaptive"}` to explicitly keep thinking on. For MiniMax-M3, `adaptive` is equivalent to thinking on.
+- Set `thinking: {"type": "disabled"}` to skip thinking and answer directly.
+- For M2.x models, thinking cannot be disabled; `thinking: {"type": "disabled"}` is accepted but thinking remains on.
+
+`reasoning_split` does not enable or disable thinking. It only controls how thinking content is returned: when `true`, thinking is exposed through `reasoning_content` and `reasoning_details`; when `false`, native Chat Completions responses keep thinking inside the `content` field with `...` tags.
+
+Python
+
+```
+response = client.chat.completions.create(
+ model="MiniMax-M3",
+ messages=[{"role": "user", "content": "Hi, how are you?"}],
+ extra_body={
+ "thinking": {"type": "adaptive"},
+ },
+)
+```
+
+## [](https://platform.minimax.io/docs/api-reference/text-openai-api\#examples) Examples
+
+### [](https://platform.minimax.io/docs/api-reference/text-openai-api\#streaming-response) Streaming Response
+
+Python
+
+```
+from openai import OpenAI
+
+client = OpenAI()
+
+print("Starting stream response...\n")
+print("=" * 60)
+print("Thinking Process:")
+print("=" * 60)
+
+stream = client.chat.completions.create(
+ model="MiniMax-M3",
+ messages=[\
+ {"role": "system", "content": "You are a helpful assistant."},\
+ {"role": "user", "content": "Hi, how are you?"},\
+ ],
+ # Set reasoning_split=True to separate thinking content into reasoning_details field
+ extra_body={"reasoning_split": True},
+ stream=True,
+)
+
+reasoning_buffer = ""
+text_buffer = ""
+
+for chunk in stream:
+ if (
+ hasattr(chunk.choices[0].delta, "reasoning_details")
+ and chunk.choices[0].delta.reasoning_details
+ ):
+ for detail in chunk.choices[0].delta.reasoning_details:
+ if "text" in detail:
+ reasoning_text = detail["text"]
+ new_reasoning = reasoning_text[len(reasoning_buffer) :]
+ if new_reasoning:
+ print(new_reasoning, end="", flush=True)
+ reasoning_buffer = reasoning_text
+
+ if chunk.choices[0].delta.content:
+ content_text = chunk.choices[0].delta.content
+ new_text = content_text[len(text_buffer) :] if text_buffer else content_text
+ if new_text:
+ print(new_text, end="", flush=True)
+ text_buffer = content_text
+
+print("\n" + "=" * 60)
+print("Response Content:")
+print("=" * 60)
+print(f"{text_buffer}\n")
+```
+
+### [](https://platform.minimax.io/docs/api-reference/text-openai-api\#tool-use-&-interleaved-thinking) Tool Use & Interleaved Thinking
+
+Learn how to use M3 Tool Use and Interleaved Thinking capabilities with OpenAI SDK, please refer to the following documentation.
+
+## Tool Use & Interleaved Thinking
+
+Learn how to leverage MiniMax-M3 tool calling and interleaved thinking capabilities to enhance performance in complex tasks.
+
+Click here
+
+## [](https://platform.minimax.io/docs/api-reference/text-openai-api\#important-notes) Important Notes
+
+1. The `temperature` parameter range is \[0, 2\], recommended value: 1.0, values outside this range will return an error
+2. Some OpenAI parameters (such as `presence_penalty`, `frequency_penalty`, `logit_bias`, etc.) will be ignored
+3. Image and video inputs are supported by `MiniMax-M3` through OpenAI-compatible message content parts; audio input is not currently supported
+4. The `n` parameter only supports value 1
+5. The deprecated `function_call` is not supported, please use the `tools` parameter
+
+[Anthropic SDK (Recommended)](https://platform.minimax.io/docs/api-reference/text-anthropic-api) [AI SDK](https://platform.minimax.io/docs/api-reference/text-ai-sdk)
+
+⌘I
\ No newline at end of file
diff --git a/llmsdk_docs/minimax_m3/docs/token-plan-overview.md b/llmsdk_docs/minimax_m3/docs/token-plan-overview.md
new file mode 100644
index 00000000..48609372
--- /dev/null
+++ b/llmsdk_docs/minimax_m3/docs/token-plan-overview.md
@@ -0,0 +1,120 @@
+> ## Documentation Index
+> Fetch the complete documentation index at: https://platform.minimax.io/docs/llms.txt
+> Use this file to discover all available pages before exploring further.
+
+# Token Plan Overview
+
+> Token Plan subscription and usage overview
+
+
+
+## Welcome to the Token Plan!
+
+MiniMax is one of the few AI labs that develops frontier models across the full spectrum of modalities: language, speech, video, music, and image. The [Token Plan](https://platform.minimax.io/subscribe/token-plan) extends upon our former Coding Plan by providing included Token Plan usage beyond language models, allowing more creative agents and applications to be built and used.
+
+## Core Advantages
+
+
+
+ One subscription covers eligible MiniMax resources through a shared usage bar.
+
+
+
+ Plans are designed for long-context agent, coding, and multimodal workflows.
+
+
+
+ A flat subscription fee includes broad resource coverage while keeping usage predictable.
+
+
+
+## Subscription Key
+
+Each user has a dedicated **Subscription Key** for each Team they belong to. This key can exist before the Team has purchased Token Plan seats or Credits. If no resources are available to the user, the key has no usable paid resources. Once a Token Plan seat is assigned, or Credits access is available, the same Subscription Key can use those resources.
+
+For subscription and Credits rules, see [Token Plan pricing](/docs/guides/pricing-token-plan). For team usage, see [Token Plan for Teams](/docs/guides/pricing-token-plan-team).
+
+## Usage Quota
+
+The [Token Plan](https://platform.minimax.io/subscribe/token-plan) usage quota is shown as a usage bar in the console. For API endpoints that have pay-as-you-go pricing, usage deducts from the included Token Plan quota according to the corresponding endpoint pricing.
+
+| | **Plus** | **Max** | **Ultra** |
+| :---------------- | :-------------------------------- | :------------------------------------------- | :------------------------------------------ |
+| **Price** | **\$20 /month** | **\$50 /month** | **\$120 /month** |
+| **Best for** | Personal projects and prototyping | Daily coding with agents and multimodal work | Heavy Agent workflows and extended sessions |
+| **Quota windows** | 5-hour rolling and weekly windows | 5-hour rolling and weekly windows | 5-hour rolling and weekly windows |
+| **Agent usage** | 3-4 agents | 4-5 agents | 6-7 agents |
+
+Available model coverage includes the full MiniMax lineup (M3 / M2.7 / image / speech / music). A small number of special models (MiniMax H3, voice design, rapid voice cloning, etc.) are not currently supported. Credits usage is unrestricted.
+
+
+ To call supported multimodal resources with your Subscription Key, see the [MiniMax CLI guide](https://platform.minimax.io/docs/token-plan/minimax-cli).
+
+
+## Getting Started
+
+
+
+ Visit the [Token Plan](https://platform.minimax.io/subscribe/token-plan) subscription page to buy an individual subscription or Credits in your Default Team, or join a Team where a Token Plan seat or shared Credits are available.
+
+
+
+ Navigate to [Account / Token Plan](https://platform.minimax.io/user-center/payment/token-plan) to view your available resources and get your **Subscription Key**.
+
+
+
+
+ **Important Notes**
+
+ * The Subscription Key is used for Token Plan subscriptions and purchased Credits.
+ * The Subscription Key is not interchangeable with pay-as-you-go API Keys.
+ * A Subscription Key can exist before any paid resource is available. It becomes usable when the user has a Token Plan seat or Credits access.
+ * Please protect your API Key to prevent any loss of resources.
+
+
+## Use in AI Agents and Coding Tools
+
+Pick your tool and follow the integration guide:
+
+
+
+
+
+
+
+
+
+
+
+
+
+For other tools, see [Other Tools](/docs/token-plan/other-tools).
+
+## After Reaching the Usage Limit
+
+When you reach the 5-hour rolling quota or weekly quota, you have the following options:
+
+1. **Use purchased Credits**:
+ If purchased Credits are available, usage within Token Plan resource coverage can be automatically covered by purchased Credits.
+2. **Upgrade or get another assignment**:
+ Upgrade your subscription, or ask the Team Owner or Admin to assign a higher available plan.
+3. **Switch to Pay-As-You-Go**:
+ If you wish to continue without rate limits, you can replace your Subscription Key with your [pay-as-you-go API Key](https://platform.minimax.io/user-center/basic-information/interface-key). This will switch the tool to a pay-as-you-go model based on actual token usage, which will consume your API account balance.
+4. **Wait for the quota window to reset**:
+ The included Token Plan quota uses 5-hour rolling and weekly windows. Unused subscription quota does not carry over to the next billing cycle.
+
+## Next Steps
+
+
+
+ Run your first MiniMax API call in 5 minutes.
+
+
+
+ Common questions on quotas, billing, switching, refunds.
+
+
+
+ Current discount campaigns.
+
+
diff --git a/llmsdk_docs/minimax_m3/docs/tool-use-interleaved-thinking.md b/llmsdk_docs/minimax_m3/docs/tool-use-interleaved-thinking.md
new file mode 100644
index 00000000..ae5ca558
--- /dev/null
+++ b/llmsdk_docs/minimax_m3/docs/tool-use-interleaved-thinking.md
@@ -0,0 +1,609 @@
+> ## Documentation Index
+> Fetch the complete documentation index at: https://platform.minimax.io/docs/llms.txt
+> Use this file to discover all available pages before exploring further.
+
+# Tool Use & Interleaved Thinking
+
+> MiniMax-M3 is an Agentic Model with exceptional Tool Use capabilities.
+
+M3 natively supports Interleaved Thinking, enabling it to reason between each round of tool interactions. Before every Tool Use, the model reflects on the current environment and the tool outputs to decide its next action.
+
+
+
+This ability allows M3 to excel at long-horizon and complex tasks, achieving state-of-the-art (SOTA) results on benchmarks such as SWE, BrowseCamp, and xBench, which test both coding and agentic reasoning performance.
+
+In the following examples, we’ll illustrate best practices for Tool Use and Interleaved Thinking with M3. The key principle is to return the model’s full response each time—especially the internal reasoning fields (e.g., thinking or reasoning\_details).
+
+## Parameters
+
+### Request Parameters
+
+* `tools`: Defines the list of callable functions, including function names, descriptions, and parameter schemas
+
+### Response Parameters
+
+Key fields in Tool Use responses:
+
+* `thinking/reasoning_details`: The model's thinking/reasoning process
+* `text/content`: The text content output by the model
+* `tool_calls`: Contains information about functions the model has decided to invoke
+* `function.name`: The name of the function being called
+* `function.arguments`: Function call parameters (JSON string format)
+* `id`: Unique identifier for the tool call
+
+## Important Note
+
+In multi-turn function call conversations, the complete model response (i.e., the assistant message) must be append to the conversation history to maintain the continuity of the reasoning chain.
+
+**OpenAI SDK:**
+
+* Append the full `response_message` object (including the `tool_calls` field) to the message history
+ * When using MiniMax-M3, the `content` field contains `` tags which will be automatically preserved
+ * In the Interleaved Thinking Compatible Format, by using the additional parameter (`reasoning_split=True`), the model's thinking content is separated into the `reasoning_details` field. This content also needs to be added to historical messages.
+
+**Anthropic SDK:**
+
+* Append the full `response.content` list to the message history (includes all content blocks: thinking/text/tool\_use)
+
+See examples below for implementation details.
+
+## Examples
+
+### Anthropic SDK
+
+#### Configure Environment Variables
+
+For international users, use `https://api.minimax.io/anthropic`; for users in China, use `https://api.minimaxi.com/anthropic`
+
+```bash theme={null}
+export ANTHROPIC_BASE_URL=https://api.minimax.io/anthropic
+export ANTHROPIC_API_KEY=${YOUR_API_KEY}
+```
+
+#### Example
+
+```python theme={null}
+import anthropic
+import json
+
+# Initialize client
+client = anthropic.Anthropic()
+
+# Define tool: weather query
+tools = [
+ {
+ "name": "get_weather",
+ "description": "Get weather of a location, the user should supply a location first.",
+ "input_schema": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string",
+ "description": "The city and state, e.g. San Francisco, US",
+ }
+ },
+ "required": ["location"]
+ }
+ }
+]
+
+def send_messages(messages):
+ params = {
+ "model": "MiniMax-M3",
+ "max_tokens": 4096,
+ "messages": messages,
+ "tools": tools,
+ }
+
+ response = client.messages.create(**params)
+ return response
+
+def process_response(response):
+ thinking_blocks = []
+ text_blocks = []
+ tool_use_blocks = []
+
+ # Iterate through all content blocks
+ for block in response.content:
+ if block.type == "thinking":
+ thinking_blocks.append(block)
+ print(f"💭 Thinking>\n{block.thinking}\n")
+ elif block.type == "text":
+ text_blocks.append(block)
+ print(f"💬 Model>\t{block.text}")
+ elif block.type == "tool_use":
+ tool_use_blocks.append(block)
+ print(f"🔧 Tool>\t{block.name}({json.dumps(block.input, ensure_ascii=False)})")
+
+ return thinking_blocks, text_blocks, tool_use_blocks
+
+# 1. User query
+messages = [{"role": "user", "content": "How's the weather in San Francisco?"}]
+print(f"\n👤 User>\t {messages[0]['content']}")
+
+# 2. Model returns first response (may include tool calls)
+response = send_messages(messages)
+thinking_blocks, text_blocks, tool_use_blocks = process_response(response)
+
+# 3. If tool calls exist, execute tools and continue conversation
+if tool_use_blocks:
+ # ⚠️ Critical: Append the assistant's complete response to message history
+ # response.content contains a list of all blocks: [thinking block, text block, tool_use block]
+ # Must be fully preserved, otherwise subsequent conversation will lose context
+ messages.append({
+ "role": "assistant",
+ "content": response.content
+ })
+
+ # Execute tool and return result (simulating weather API call)
+ print(f"\n🔨 Executing tool: {tool_use_blocks[0].name}")
+ tool_result = "24℃, sunny"
+ print(f"📊 Tool result: {tool_result}")
+
+ # Add tool execution result
+ messages.append({
+ "role": "user",
+ "content": [
+ {
+ "type": "tool_result",
+ "tool_use_id": tool_use_blocks[0].id,
+ "content": tool_result
+ }
+ ]
+ })
+
+ # 4. Get final response
+ final_response = send_messages(messages)
+ process_response(final_response)
+```
+
+**Output:**
+
+```nushell theme={null}
+👤 User> How's the weather in San Francisco?
+💭 Thinking>
+Okay, so the user is asking about the weather in San Francisco. This is a straightforward request that requires me to get current weather information for a specific location.
+
+Looking at my available tools, I see I have a `get_weather` function that can provide weather information for a location. This is exactly what I need to answer the user's question. The function requires a "location" parameter, which should be a string containing the city and potentially the state/country.
+
+In this case, the user has clearly specified San Francisco as the location. San Francisco is a major city in US, so I don't need to include the country code - just "San Francisco" should be sufficient for the weather service to identify the correct location.
+
+The required parameters for the get_weather tool are:
+- location: "San Francisco"
+
+I should call this tool to retrieve the current weather information for San Francisco before I can provide a meaningful answer to the user. Once I get the weather data back from the tool, I'll be able to share details like temperature, conditions (sunny, cloudy, rainy, etc.), and possibly other relevant information like humidity or wind speed.
+
+So I'll make a tool call to get_weather with the location parameter set to "San Francisco". After I receive the response from this tool call, I'll be able to provide the user with the current weather information they requested.
+
+🔧 Tool> get_weather({"location": "San Francisco"})
+
+🔨 Executing tool: get_weather
+📊 Tool result: 24℃, sunny
+💭 Thinking>
+I've just called the get_weather tool to check the current conditions in San Francisco as the user requested. Let me analyze what information I received back.
+
+The tool returned a simple response: "24℃, sunny". This is quite straightforward - it tells me the current temperature is 24 degrees Celsius and the weather conditions are sunny. This is exactly the basic weather information the user was asking about.
+
+The temperature is given in Celsius (24℃), which is appropriate since US uses the metric system. I could convert this to Fahrenheit for users who might be more familiar with that scale, but since the user didn't specify a preference, I'll stick with the metric measurement as provided by the tool.
+
+For the weather condition, I received "sunny" which indicates clear skies and good visibility. This is useful information that tells the user they can expect good weather if they're planning to be outside.
+
+I don't have additional details like humidity, wind speed, or UV index from the tool response. If the user wants more detailed information, they could ask a follow-up question, and I might need to provide general advice about sunny weather conditions or suggest checking a more detailed weather service.
+
+Now I need to formulate a clear, concise response to the user that directly answers their question about the weather in San Francisco. I'll keep it simple and factual, stating the temperature and conditions clearly. I should also add a friendly closing to invite further questions if needed.
+
+The most straightforward way to present this information is to state the temperature first, followed by the conditions, and then add a friendly note inviting the user to ask for more information if they want it.
+
+💬 Model> The current weather in San Francisco is 24℃ and sunny.
+```
+
+**Response Body**
+
+```json theme={null}
+{
+ "id": "05566b15ee32962663694a2772193ac7",
+ "type": "message",
+ "role": "assistant",
+ "model": "MiniMax-M3",
+ "content": [
+ {
+ "thinking": "Let me think about this request. The user is asking about the weather in San Francisco. This is a straightforward request that requires current weather information.\n\nTo provide accurate weather information, I need to use the appropriate tool. Looking at the tools available to me, I see there's a \"get_weather\" tool that seems perfect for this task. This tool requires a location parameter, which should include both the city and state/region.\n\nThe user has specified \"San Francisco\" as the location, but they haven't included the state. For the US, it's common practice to include the state when specifying a city, especially for well-known cities like San Francisco that exist in multiple states (though there's really only one San Francisco that's famous).\n\nAccording to the tool description, I need to provide the location in the format \"San Francisco, US\" - with the city, comma, and the country code for the United States. This follows the standard format specified in the tool's parameter description: \"The city and state, e.g. San Francisco, US\".\n\nSo I need to call the get_weather tool with the location parameter set to \"San Francisco, US\". This will retrieve the current weather information for San Francisco, which I can then share with the user.\n\nI'll format my response using the required XML tags for tool calls, providing the tool name \"get_weather\" and the arguments as a JSON object with the location parameter set to \"San Francisco, US\".",
+ "signature": "cfa12f9d651953943c7a33278051b61f586e2eae016258ad6b824836778406bd",
+ "type": "thinking"
+ },
+ {
+ "type": "tool_use",
+ "id": "call_function_3679004591_1",
+ "name": "get_weather",
+ "input": {
+ "location": "San Francisco, US"
+ }
+ }
+ ],
+ "usage": {
+ "input_tokens": 222,
+ "output_tokens": 321
+ },
+ "stop_reason": "tool_use",
+ "base_resp": {
+ "status_code": 0,
+ "status_msg": ""
+ }
+}
+```
+
+### OpenAI SDK
+
+#### Configure Environment Variables
+
+For international users, use `https://api.minimax.io/v1`; for users in China, use `https://api.minimaxi.com/v1`
+
+```bash theme={null}
+export OPENAI_BASE_URL=https://api.minimax.io/v1
+export OPENAI_API_KEY=${YOUR_API_KEY}
+```
+
+#### Interleaved Thinking Compatible Format
+
+When calling MiniMax-M3 via the OpenAI SDK, you can pass the extra parameter `reasoning_split=True` to get a more developer-friendly output format.
+
+
+ Important Note: To ensure that Interleaved Thinking functions properly and the model’s chain of thought remains uninterrupted, the entire `response_message` — including the `reasoning_details` field — must be preserved in the message history and passed back to the model in the next round of interaction.This is essential for achieving the model’s best performance.
+
+
+Be sure to review how your API request and response handling function (e.g., `send_messages`) is implemented, as well as how you append the historical messages with `messages.append(response_message)`.
+
+```python theme={null}
+import json
+
+from openai import OpenAI
+
+client = OpenAI()
+
+# Define tool: weather query
+tools = [
+ {
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "description": "Get weather of a location, the user should supply a location first.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string",
+ "description": "The city and state, e.g. San Francisco, US",
+ }
+ },
+ "required": ["location"],
+ },
+ },
+ },
+]
+
+
+def send_messages(messages):
+ """Send messages and return response"""
+ response = client.chat.completions.create(
+ model="MiniMax-M3",
+ messages=messages,
+ tools=tools,
+ # Set reasoning_split=True to separate thinking content into reasoning_details field
+ extra_body={"reasoning_split": True},
+ )
+ return response.choices[0].message
+
+
+# 1. User query
+messages = [{"role": "user", "content": "How's the weather in San Francisco?"}]
+print(f"👤 User>\t {messages[0]['content']}")
+
+# 2. Model returns tool call
+response_message = send_messages(messages)
+
+if response_message.tool_calls:
+ tool_call = response_message.tool_calls[0]
+ function_args = json.loads(tool_call.function.arguments)
+ print(f"💭 Thinking>\t {response_message.reasoning_details[0]['text']}")
+ print(f"💬 Model>\t {response_message.content}")
+ print(f"🔧 Tool>\t {tool_call.function.name}({function_args['location']})")
+
+ # 3. Execute tool and return result
+ messages.append(response_message)
+ messages.append(
+ {
+ "role": "tool",
+ "tool_call_id": tool_call.id,
+ "content": "24℃, sunny", # In real applications, call actual weather API here
+ }
+ )
+
+ # 4. Get final response
+ final_message = send_messages(messages)
+ print(
+ f"💭 Thinking>\t {final_message.model_dump()['reasoning_details'][0]['text']}"
+ )
+ print(f"💬 Model>\t {final_message.content}")
+else:
+ print(f"💬 Model>\t {response_message.content}")
+```
+
+**Output:**
+
+```
+👤 User> How's the weather in San Francisco?
+💭 Thinking> Alright, the user is asking about the weather in San Francisco. This is a straightforward question that requires real-time information about current weather conditions.
+
+Looking at the available tools, I see I have access to a "get_weather" tool that's specifically designed for this purpose. The tool requires a "location" parameter, which should be in the format of city and state, like "San Francisco, CA".
+
+The user has clearly specified they want weather information for "San Francisco" in their question. However, they didn't include the state (California), which is recommended for the tool parameter. While "San Francisco" alone might be sufficient since it's a well-known city, for accuracy and to follow the parameter format, I should include the state as well.
+
+Since I need to use the tool to get the current weather information, I'll need to call the "get_weather" tool with "San Francisco, CA" as the location parameter. This will provide the user with the most accurate and up-to-date weather information for their query.
+
+I'll format my response using the required tool_calls XML tags and include the tool name and arguments in the specified JSON format.
+💬 Model>
+
+🔧 Tool> get_weather(San Francisco, US)
+💭 Thinking> Okay, I've received the user's question about the weather in San Francisco, and I've used the get_weather tool to retrieve the current conditions.
+
+The tool has returned a simple response: "24℃, sunny". This gives me two pieces of information - the temperature is 24 degrees Celsius, and the weather condition is sunny. That's quite straightforward and matches what I would expect for San Francisco on a nice day.
+
+Now I need to present this information to the user in a clear, concise way. Since the response from the tool was quite brief, I'll keep my answer similarly concise. I'll directly state the temperature and weather condition that the tool provided.
+
+I should make sure to mention that this information is current, so the user understands they're getting up-to-date conditions. I don't need to provide additional details like humidity, wind speed, or forecast since the user only asked about the current weather.
+
+The temperature is given in Celsius (24℃), which is the standard metric unit, so I'll leave it as is rather than converting to Fahrenheit, though I could mention the conversion if the user seems to be more familiar with Fahrenheit.
+
+Since this is a simple informational query, I don't need to ask follow-up questions or suggest activities based on the weather. I'll just provide the requested information clearly and directly.
+
+My response will be a single sentence stating the current temperature and weather conditions in San Francisco, which directly answers the user's question.
+💬 Model> The weather in San Francisco is currently sunny with a temperature of 24℃.
+```
+
+**Response Body**
+
+```json theme={null}
+{
+ "id": "05566b8d51ded3a3016d6cc100685cad",
+ "choices": [
+ {
+ "finish_reason": "tool_calls",
+ "index": 0,
+ "message": {
+ "content": "\n",
+ "role": "assistant",
+ "name": "MiniMax AI",
+ "tool_calls": [
+ {
+ "id": "call_function_2831178524_1",
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "arguments": "{\"location\": \"San Francisco, US\"}"
+ },
+ "index": 0
+ }
+ ],
+ "audio_content": "",
+ "reasoning_details": [
+ {
+ "type": "reasoning.text",
+ "id": "reasoning-text-1",
+ "format": "MiniMax-response-v1",
+ "index": 0,
+ "text": "Let me think about this request. The user is asking about the weather in San Francisco. This is a straightforward request where they want to know current weather conditions in a specific location.\n\nLooking at the tools available to me, I have access to a \"get_weather\" tool that can retrieve weather information for a location. The tool requires a location parameter in the format of \"city, state\" or \"city, country\". In this case, the user has specified \"San Francisco\" which is a city in the United States.\n\nTo properly use the tool, I need to format the location parameter correctly. The tool description mentions examples like \"San Francisco, US\" which follows the format of city, country code. However, since the user just mentioned \"San Francisco\" without specifying the state, and San Francisco is a well-known city that is specifically in California, I could use \"San Francisco, CA\" as the parameter value instead.\n\nActually, \"San Francisco, US\" would also work since the user is asking about the famous San Francisco in the United States, and there aren't other well-known cities with the same name that would cause confusion. The US country code is explicit and clear.\n\nBoth \"San Francisco, CA\" and \"San Francisco, US\" would be valid inputs for the tool. I'll go with \"San Francisco, US\" since it follows the exact format shown in the tool description example and is unambiguous.\n\nSo I'll need to call the get_weather tool with the location parameter set to \"San Francisco, US\". This will retrieve the current weather information for San Francisco, which I can then present to the user."
+ }
+ ]
+ }
+ }
+ ],
+ "created": 1762080909,
+ "model": "MiniMax-M3",
+ "object": "chat.completion",
+ "usage": {
+ "total_tokens": 560,
+ "total_characters": 0,
+ "prompt_tokens": 203,
+ "completion_tokens": 357
+ },
+ "input_sensitive": false,
+ "output_sensitive": false,
+ "input_sensitive_type": 0,
+ "output_sensitive_type": 0,
+ "output_sensitive_int": 0,
+ "base_resp": {
+ "status_code": 0,
+ "status_msg": ""
+ }
+}
+```
+
+#### OpenAI Native Format
+
+Since the OpenAI ChatCompletion API native format does not natively support thinking return and pass-back, the model's thinking is injected into the `content` field in the form of `reasoning_content`. Developers can manually parse it for display purposes. However, we strongly recommend developers use the Interleaved Thinking compatible format.
+
+What `extra_body={"reasoning_split": False}` does:
+
+* Embeds thinking in content: The model's reasoning is wrapped in `` tags within the `content` field
+* Requires manual parsing: You need to parse `` tags if you want to display reasoning separately
+
+
+ Important Reminder: If you choose to use the native format, please note that in the message history, do not modify the `content` field. You must preserve the model's thinking content completely, i.e., `reasoning_content`. This is essential to ensure Interleaved Thinking works effectively and achieves optimal model performance!
+
+
+```python theme={null}
+from openai import OpenAI
+import json
+
+# Initialize client
+client = OpenAI(
+ api_key="",
+ base_url="https://api.minimax.io/v1",
+)
+
+# Define tool: weather query
+tools = [
+ {
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "description": "Get weather of a location, the user should supply a location first.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string",
+ "description": "The city and state, e.g. San Francisco, US",
+ }
+ },
+ "required": ["location"]
+ },
+ }
+ },
+]
+
+def send_messages(messages):
+ """Send messages and return response"""
+ response = client.chat.completions.create(
+ model="MiniMax-M3",
+ messages=messages,
+ tools=tools,
+ # Set reasoning_split=False to keep thinking content in tags within content field
+ extra_body={"reasoning_split": False},
+ )
+ return response.choices[0].message
+
+# 1. User query
+messages = [{"role": "user", "content": "How's the weather in San Francisco?"}]
+print(f"👤 User>\t {messages[0]['content']}")
+
+# 2. Model returns tool call
+response_message = send_messages(messages)
+
+if response_message.tool_calls:
+ tool_call = response_message.tool_calls[0]
+ function_args = json.loads(tool_call.function.arguments)
+ print(f"💬 Model>\t {response_message.content}")
+ print(f"🔧 Tool>\t {tool_call.function.name}({function_args['location']})")
+
+ # 3. Execute tool and return result
+ messages.append(response_message)
+ messages.append({
+ "role": "tool",
+ "tool_call_id": tool_call.id,
+ "content": "24℃, sunny" # In production, call actual weather API here
+ })
+
+ # 4. Get final response
+ final_message = send_messages(messages)
+ print(f"💬 Model>\t {final_message.content}")
+else:
+ print(f"💬 Model>\t {response_message.content}")
+```
+
+**Output:**
+
+```nushell theme={null}
+👤 User> How's the weather in San Francisco?
+💬 Model>
+Alright, the user is asking about the weather in San Francisco. This is a straightforward request that I can handle using the tools provided to me.
+
+I see that I have access to a tool called "get_weather" which can provide weather information for a location. Looking at the parameters, it requires a "location" parameter which should be a string in the format of "city and state, e.g. San Francisco, US".
+
+In this case, the user has already specified the location as "San Francisco", which is a major city in California, US. I need to format this properly for the tool call. Following the example format in the tool description, I should format it as "San Francisco, US".
+
+The user didn't specify any other parameters or requirements, so a simple weather query should be sufficient. I don't need to ask for clarification since they've provided a clear location.
+
+Let me prepare the tool call to get the weather information for San Francisco. I'll use the "get_weather" tool with the location parameter set to "San Francisco, US". This should return the current weather conditions for San Francisco, which is what the user is asking about.
+
+Once I get the weather information back from the tool, I'll be able to provide the user with details about the current weather in San Francisco, such as temperature, conditions (sunny, cloudy, rainy, etc.), and possibly other relevant information like humidity or wind speed if that data is available.
+
+So I'll proceed with making the tool call to get_weather with the location parameter.
+
+
+🔧 Tool> get_weather(San Francisco, US)
+💬 Model>
+Let me analyze what's happening in this conversation. The user asked about the weather in San Francisco, and I needed to provide them with this information.
+
+Looking at the tools available to me, I have access to a "get_weather" tool that can retrieve weather information for a specific location. I used this tool and called it with the argument "location": "San Francisco, US" as specified in the tool's parameters.
+
+The tool has now returned a response with the weather information for San Francisco. The response is quite concise - it simply states "24℃, sunny". This gives me two pieces of information:
+1. The temperature is 24 degrees Celsius
+2. The weather condition is sunny
+
+This is exactly what the user wanted to know - how's the weather in San Francisco. The information is clear and straightforward.
+
+Now I need to format this information in a clear, natural way for the user. Since the tool returned the temperature in Celsius, I'll use that unit rather than converting to Fahrenheit (though 24°C is about 75°F if the user happens to think in those terms).
+
+I should keep my response concise since the weather information itself is simple. I don't need to add any caveats or additional explanations since the weather report is straightforward. I won't include any details about wind, humidity, or other meteorological data since the tool didn't provide that information.
+
+So my response will simply state the current temperature and that it's sunny in San Francisco, which directly answers the user's question.
+
+
+The weather in San Francisco is currently sunny with a temperature of 24℃.
+```
+
+**Response Body**
+
+```JSON theme={null}
+{
+ "id": "055b7928a143b2d21ad6b2bab2c8f8b2",
+ "choices": [{
+ "finish_reason": "tool_calls",
+ "index": 0,
+ "message": {
+ "content": "\nAlright, the user is asking about the weather in San Francisco. This is a straightforward request that I can handle using the tools provided to me.\n\nI see that I have access to a tool called \"get_weather\" which can provide weather information for a location. Looking at the parameters, it requires a \"location\" parameter which should be a string in the format of \"city and state, e.g. San Francisco, US\".\n\nIn this case, the user has already specified the location as \"San Francisco\", which is a major city in California, US. I need to format this properly for the tool call. Following the example format in the tool description, I should format it as \"San Francisco, US\".\n\nThe user didn't specify any other parameters or requirements, so a simple weather query should be sufficient. I don't need to ask for clarification since they've provided a clear location.\n\nLet me prepare the tool call to get the weather information for San Francisco. I'll use the \"get_weather\" tool with the location parameter set to \"San Francisco, US\". This should return the current weather conditions for San Francisco, which is what the user is asking about.\n\nOnce I get the weather information back from the tool, I'll be able to provide the user with details about the current weather in San Francisco, such as temperature, conditions (sunny, cloudy, rainy, etc.), and possibly other relevant information like humidity or wind speed if that data is available.\n\nSo I'll proceed with making the tool call to get_weather with the location parameter.\n\n\n\n",
+ "role": "assistant",
+ "name": "MiniMax AI",
+ "tool_calls": [{
+ "id": "call_function_1202729600_1",
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "arguments": "{\"location\": \"San Francisco, US\"}"
+ },
+ "index": 0
+ }],
+ "audio_content": ""
+ }
+ }],
+ "created": 1762412072,
+ "model": "MiniMax-M3",
+ "object": "chat.completion",
+ "usage": {
+ "total_tokens": 560,
+ "total_characters": 0,
+ "prompt_tokens": 222,
+ "completion_tokens": 338
+ },
+ "input_sensitive": false,
+ "output_sensitive": false,
+ "input_sensitive_type": 0,
+ "output_sensitive_type": 0,
+ "output_sensitive_int": 0,
+ "base_resp": {
+ "status_code": 0,
+ "status_msg": ""
+ }
+}
+```
+
+## Recommended Reading
+
+
+
+ MiniMax-M3 excels at code understanding, dialogue, and reasoning.
+
+
+
+ Supports text generation via compatible Anthropic API and OpenAI API.
+
+
+
+ Use Anthropic SDK with MiniMax models
+
+
+
+ Use OpenAI SDK with MiniMax models
+
+
diff --git a/llmsdk_docs/minimax_m3/quickstart.python.md b/llmsdk_docs/minimax_m3/quickstart.python.md
new file mode 100644
index 00000000..783db6b1
--- /dev/null
+++ b/llmsdk_docs/minimax_m3/quickstart.python.md
@@ -0,0 +1,32 @@
+# MiniMax M3 Python Quick Start
+
+MiniMax documents an OpenAI Responses API-compatible endpoint at `https://api.minimax.io/v1/responses`.
+
+```python
+from openai import AsyncOpenAI
+
+client = AsyncOpenAI(
+ api_key="",
+ base_url="https://api.minimax.io/v1",
+)
+
+stream = await client.responses.create(
+ model="MiniMax-M3",
+ input="Explain the purpose of a hash function.",
+ reasoning={"effort": "minimal"},
+ stream=True,
+)
+
+async for event in stream:
+ print(event)
+```
+
+Use a Token Plan **Subscription Key** for Token Plan quota/Credits, or a separate API Key for pay-as-you-go billing. The keys are not interchangeable.
+
+For multi-turn tool use, preserve and replay the full assistant response, including reasoning output and function-call items, before adding matching function-call output items.
+
+Sources:
+
+- https://platform.minimax.io/docs/api-reference/responses-create
+- https://platform.minimax.io/docs/guides/text-m3-function-call
+- https://platform.minimax.io/docs/token-plan/intro
diff --git a/llmsdk_docs/minimax_m3/quickstart.typescript.md b/llmsdk_docs/minimax_m3/quickstart.typescript.md
new file mode 100644
index 00000000..87290deb
--- /dev/null
+++ b/llmsdk_docs/minimax_m3/quickstart.typescript.md
@@ -0,0 +1,33 @@
+# MiniMax M3 TypeScript Quick Start
+
+MiniMax documents an OpenAI Responses API-compatible endpoint at `https://api.minimax.io/v1/responses`.
+
+```typescript
+import OpenAI from "openai";
+
+const client = new OpenAI({
+ apiKey: "",
+ baseURL: "https://api.minimax.io/v1",
+});
+
+const stream = await client.responses.create({
+ model: "MiniMax-M3",
+ input: "Explain the purpose of a hash function.",
+ reasoning: { effort: "minimal" },
+ stream: true,
+});
+
+for await (const event of stream) {
+ console.log(event);
+}
+```
+
+Use a Token Plan **Subscription Key** for Token Plan quota/Credits, or a separate API Key for pay-as-you-go billing. The keys are not interchangeable.
+
+For multi-turn tool use, preserve and replay the full assistant response, including reasoning output and function-call items, before adding matching function-call output items.
+
+Sources:
+
+- https://platform.minimax.io/docs/api-reference/responses-create
+- https://platform.minimax.io/docs/guides/text-m3-function-call
+- https://platform.minimax.io/docs/token-plan/intro
diff --git a/src_py/agenthub/auto_client.py b/src_py/agenthub/auto_client.py
index 38f942a8..f9418efd 100644
--- a/src_py/agenthub/auto_client.py
+++ b/src_py/agenthub/auto_client.py
@@ -46,7 +46,11 @@ def _create_client_for_model(
self, model: str, api_key: str | None = None, base_url: str | None = None, client_type: str | None = None
) -> LLMClient:
"""Create the appropriate client for the given model."""
- client_type = (client_type or os.getenv("CLIENT_TYPE", model)).lower()
+ client_type = (client_type or os.getenv("CLIENT_TYPE") or model).lower()
+ if client_type == "minimax-m3":
+ from .minimax_m3 import MiniMaxM3Client
+
+ return MiniMaxM3Client(model=model, api_key=api_key, base_url=base_url)
# gemini-3.6 must be matched before the broader gemini-3 prefix below
if any(
prefix in client_type for prefix in ("gemini-3.6", "gemini-3.5-flash-lite")
@@ -105,7 +109,10 @@ def _create_client_for_model(
else:
raise ValueError(
f"{client_type} is not supported. "
- "Supported client types: gemini-3.6, gemini-3, claude-5, claude-4-8, claude-4-7, claude-4-6, gpt-5.5, gpt-5.4, glm-5.2, glm-5.1, kimi-k3, kimi-k2.6, kimi-k2.5, deepseek-v4, openai-embedding, openai."
+ "Supported client types: minimax-m3, gemini-3.6, gemini-3, "
+ "claude-5, claude-4-8, claude-4-7, claude-4-6, gpt-5.5, gpt-5.4, glm-5.2, glm-5.1, kimi-k3, "
+ "kimi-k2.6, kimi-k2.5, "
+ "deepseek-v4, openai-embedding, openai."
)
def transform_uni_config_to_model_config(self, config: UniConfig) -> Any:
diff --git a/src_py/agenthub/minimax_m3/__init__.py b/src_py/agenthub/minimax_m3/__init__.py
new file mode 100644
index 00000000..7dce4d52
--- /dev/null
+++ b/src_py/agenthub/minimax_m3/__init__.py
@@ -0,0 +1,18 @@
+# Copyright 2025 Prism Shadow. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .client import MiniMaxM3Client
+
+
+__all__ = ["MiniMaxM3Client"]
diff --git a/src_py/agenthub/minimax_m3/client.py b/src_py/agenthub/minimax_m3/client.py
new file mode 100644
index 00000000..737464a0
--- /dev/null
+++ b/src_py/agenthub/minimax_m3/client.py
@@ -0,0 +1,457 @@
+# Copyright 2025 Prism Shadow. and/or its affiliates
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+from typing import Any, AsyncIterator
+
+from openai import AsyncOpenAI
+
+from ..base_client import LLMClient
+from ..errors import UnsupportedParameterError, parse_tool_call_arguments
+from ..types import (
+ EventType,
+ FinishReason,
+ PartialContentItem,
+ PromptCaching,
+ ThinkingLevel,
+ ToolChoice,
+ UniConfig,
+ UniEvent,
+ UniMessage,
+ UsageMetadata,
+)
+
+
+_DEFAULT_BASE_URL = "https://api.minimax.io/v1"
+_WIRE_ITEM_FIDELITY_KEY = "wire_item"
+
+
+def _field(value: Any, name: str, default: Any = None) -> Any:
+ """Read a field from either an SDK model or a raw capture dictionary."""
+ if isinstance(value, dict):
+ return value.get(name, default)
+ return getattr(value, name, default)
+
+
+def _normalize_json_value(value: Any) -> Any:
+ """Convert SDK models and nested containers to JSON-style values."""
+ model_dump = getattr(value, "model_dump", None)
+ if callable(model_dump):
+ value = model_dump(mode="json", exclude_unset=True)
+ if isinstance(value, dict):
+ return {key: _normalize_json_value(item) for key, item in value.items()}
+ if isinstance(value, (list, tuple)):
+ return [_normalize_json_value(item) for item in value]
+ return value
+
+
+def _require_json_object(value: Any, context: str) -> dict[str, Any]:
+ """Normalize and validate a JSON object used for wire-fidelity replay."""
+ normalized = _normalize_json_value(value)
+ if not isinstance(normalized, dict):
+ raise ValueError(f"{context} must be a JSON object.")
+ try:
+ json.dumps(normalized, ensure_ascii=False, allow_nan=False)
+ except (TypeError, ValueError) as exc:
+ raise ValueError(f"{context} must contain only JSON-style values.") from exc
+ return normalized
+
+
+def _wire_item_from_fidelity(fidelity: dict[str, Any], context: str) -> dict[str, Any] | None:
+ """Extract a complete provider output item from a fidelity payload."""
+ wire_item = fidelity.get(_WIRE_ITEM_FIDELITY_KEY)
+ if wire_item is None:
+ return None
+ return _require_json_object(wire_item, f"{context} wire item")
+
+
+def _wire_content_text(wire_item: dict[str, Any], content_type: str) -> str | None:
+ """Concatenate text from matching content parts in a provider output item."""
+ content = wire_item.get("content")
+ if not isinstance(content, list):
+ return None
+
+ text_parts: list[str] = []
+ for part in content:
+ if not isinstance(part, dict) or part.get("type") != content_type:
+ continue
+ text = part.get("text")
+ if not isinstance(text, str):
+ return None
+ text_parts.append(text)
+ return "".join(text_parts) if text_parts else None
+
+
+def _json_values_equal(left: Any, right: Any) -> bool:
+ """Compare JSON-style values without conflating booleans and numbers."""
+ if isinstance(left, bool) or isinstance(right, bool):
+ return isinstance(left, bool) and isinstance(right, bool) and left == right
+ if isinstance(left, dict) and isinstance(right, dict):
+ return left.keys() == right.keys() and all(_json_values_equal(left[key], right[key]) for key in left)
+ if isinstance(left, list) and isinstance(right, list):
+ return len(left) == len(right) and all(
+ _json_values_equal(left_item, right_item) for left_item, right_item in zip(left, right, strict=True)
+ )
+ if isinstance(left, (int, float)) and isinstance(right, (int, float)):
+ return left == right
+ return type(left) is type(right) and left == right
+
+
+def _usage_from_response(response: Any) -> UsageMetadata | None:
+ usage = _field(response, "usage")
+ if usage is None:
+ return None
+
+ input_details = _field(usage, "input_tokens_details", {})
+ output_details = _field(usage, "output_tokens_details", {})
+ cached_tokens = _field(input_details, "cached_tokens", 0) or 0
+ reasoning_tokens = _field(output_details, "reasoning_tokens", 0) or 0
+ input_tokens = _field(usage, "input_tokens", 0) or 0
+ output_tokens = _field(usage, "output_tokens", 0) or 0
+ return {
+ "cached_tokens": cached_tokens,
+ "prompt_tokens": input_tokens - cached_tokens,
+ "thoughts_tokens": reasoning_tokens,
+ "response_tokens": output_tokens - reasoning_tokens,
+ }
+
+
+def _format_response_error(event_type: str, error: Any, response_id: Any = None) -> str:
+ normalized_error = _normalize_json_value(error)
+ details: list[str] = []
+ if isinstance(normalized_error, dict):
+ for field_name in ("code", "message", "param"):
+ if field_name in normalized_error and normalized_error[field_name] is not None:
+ details.append(f"{field_name}={normalized_error[field_name]!r}")
+ elif normalized_error is not None:
+ details.append(f"details={normalized_error!r}")
+ if not details:
+ details.append("no error details were provided")
+
+ response_context = f" for response {response_id!r}" if response_id is not None else ""
+ return f"MiniMax Responses API {event_type}{response_context}: {', '.join(details)}"
+
+
+class MiniMaxM3Client(LLMClient):
+ """MiniMax M3 client using MiniMax's Responses API."""
+
+ def __init__(self, model: str, api_key: str | None = None, base_url: str | None = None):
+ """Initialize a MiniMax M3 Responses client with a Subscription Key or API key."""
+ if model.lower() != "minimax-m3":
+ raise ValueError(f"{model} is not supported by MiniMaxM3Client.")
+ self._model = model
+ self._client = AsyncOpenAI(
+ api_key=api_key or os.getenv("MINIMAX_API_KEY"),
+ base_url=base_url or os.getenv("MINIMAX_BASE_URL") or _DEFAULT_BASE_URL,
+ )
+ self._history: list[UniMessage] = []
+
+ def _convert_thinking_level_to_effort(self, thinking_level: ThinkingLevel) -> str:
+ """Map AgentHub thinking levels to the MiniMax reasoning effort vocabulary."""
+ mapping = {
+ ThinkingLevel.NONE: "none",
+ ThinkingLevel.LOW: "low",
+ ThinkingLevel.MEDIUM: "medium",
+ ThinkingLevel.HIGH: "high",
+ ThinkingLevel.XHIGH: "high",
+ }
+ return mapping[thinking_level]
+
+ def _convert_tool_choice(self, tool_choice: ToolChoice) -> str:
+ """Validate MiniMax's supported automatic tool-selection modes."""
+ if tool_choice in ("auto", "none"):
+ return tool_choice
+ raise UnsupportedParameterError(
+ self.__class__.__name__,
+ "tool_choice",
+ "MiniMax Responses API does not support required or named tool selection.",
+ )
+
+ def transform_uni_config_to_model_config(self, config: UniConfig) -> dict[str, Any]:
+ """Transform universal configuration to MiniMax's Responses API payload."""
+ minimax_config: dict[str, Any] = {"model": self._model, "store": False}
+
+ if config.get("system_prompt") is not None:
+ minimax_config["instructions"] = config["system_prompt"]
+ if config.get("max_tokens") is not None:
+ minimax_config["max_output_tokens"] = config["max_tokens"]
+ if config.get("temperature") is not None:
+ temperature = config["temperature"]
+ if not 0 <= temperature <= 1:
+ raise UnsupportedParameterError(
+ self.__class__.__name__,
+ "temperature",
+ "MiniMax Responses API does not support temperatures outside the range 0 to 1.",
+ )
+ minimax_config["temperature"] = temperature
+ if config.get("thinking_level") is not None:
+ minimax_config["reasoning"] = {"effort": self._convert_thinking_level_to_effort(config["thinking_level"])}
+ if config.get("tools") is not None:
+ minimax_config["tools"] = [{"type": "function", **tool} for tool in config["tools"]]
+ if config.get("tool_choice") is not None:
+ minimax_config["tool_choice"] = self._convert_tool_choice(config["tool_choice"])
+ if config.get("prompt_caching") == PromptCaching.DISABLE:
+ raise UnsupportedParameterError(
+ self.__class__.__name__,
+ "prompt_caching",
+ "MiniMax Responses API does not support disabling its automatic prompt cache.",
+ )
+ if config.get("prompt_caching") == PromptCaching.ENHANCE:
+ raise UnsupportedParameterError(
+ self.__class__.__name__,
+ "prompt_caching",
+ "MiniMax Responses API does not support enhancing its automatic prompt cache.",
+ )
+
+ return minimax_config
+
+ def transform_uni_message_to_model_input(self, messages: list[UniMessage]) -> list[dict[str, Any]]:
+ """Transform universal messages to MiniMax Responses input items."""
+ input_list: list[dict[str, Any]] = []
+
+ for message in messages:
+ content_items: list[dict[str, Any]] = []
+ for item in message["content_items"]:
+ if item["type"] == "text":
+ content_items.append(
+ {
+ "type": "input_text" if message["role"] == "user" else "output_text",
+ "text": item["text"],
+ }
+ )
+ continue
+ if item["type"] == "image_url":
+ content_items.append({"type": "input_image", "image_url": item["image_url"]})
+ continue
+
+ if content_items:
+ input_list.append({"role": message["role"], "content": content_items})
+ content_items = []
+
+ if item["type"] == "thinking":
+ fidelity = _require_json_object(item.get("fidelity") or {}, "MiniMax reasoning fidelity")
+ wire_item = _wire_item_from_fidelity(fidelity, "MiniMax reasoning fidelity")
+ if wire_item is None:
+ wire_item = {
+ key: value
+ for key, value in fidelity.items()
+ if key not in {_WIRE_ITEM_FIDELITY_KEY, "phase"}
+ }
+ if (
+ not isinstance(wire_item.get("id"), str)
+ or not isinstance(wire_item.get("summary"), list)
+ or not isinstance(wire_item.get("content"), list)
+ ):
+ raise ValueError("MiniMax reasoning replay requires valid id, summary, and content fidelity.")
+
+ wire_text = _wire_content_text(wire_item, "reasoning_text")
+ reasoning_content = (
+ wire_item["content"]
+ if wire_text == item["thinking"]
+ else [{"type": "reasoning_text", "text": item["thinking"]}]
+ )
+ input_list.append({**wire_item, "type": "reasoning", "content": reasoning_content})
+ elif item["type"] == "tool_call":
+ arguments = json.dumps(item["arguments"], ensure_ascii=False, separators=(",", ":"))
+ raw_arguments = (item.get("fidelity") or {}).get("arguments")
+ if isinstance(raw_arguments, str):
+ parsed_arguments = parse_tool_call_arguments(
+ raw_arguments,
+ self.__class__.__name__,
+ item["name"],
+ item["tool_call_id"],
+ )
+ if _json_values_equal(parsed_arguments, item["arguments"]):
+ arguments = raw_arguments
+
+ input_list.append(
+ {
+ "type": "function_call",
+ "call_id": item["tool_call_id"],
+ "name": item["name"],
+ "arguments": arguments,
+ }
+ )
+ elif item["type"] == "tool_result":
+ output: str | list[dict[str, Any]] = item["text"]
+ if item.get("images"):
+ output = [{"type": "input_text", "text": item["text"]}]
+ output.extend({"type": "input_image", "image_url": image_url} for image_url in item["images"])
+ input_list.append(
+ {
+ "type": "function_call_output",
+ "call_id": item["tool_call_id"],
+ "output": output,
+ }
+ )
+ else:
+ raise ValueError(f"Unknown item: {item}")
+
+ if content_items:
+ input_list.append({"role": message["role"], "content": content_items})
+
+ return input_list
+
+ def transform_model_output_to_uni_event(self, model_output: Any) -> UniEvent:
+ """Transform a MiniMax streaming event to AgentHub's universal event format."""
+ event_type: EventType = "unused"
+ content_items: list[PartialContentItem] = []
+ usage_metadata: UsageMetadata | None = None
+ finish_reason: FinishReason | None = None
+ minimax_event_type = _field(model_output, "type")
+
+ if minimax_event_type == "response.output_text.delta":
+ event_type = "delta"
+ content_items.append(
+ {
+ "type": "text",
+ "text": _field(model_output, "delta", ""),
+ "fidelity": {"phase": _normalize_json_value(_field(model_output, "item_id"))},
+ }
+ )
+ elif minimax_event_type == "response.reasoning_text.delta":
+ event_type = "delta"
+ content_items.append({"type": "thinking", "thinking": _field(model_output, "delta", "")})
+ elif minimax_event_type == "response.output_item.added":
+ item = _field(model_output, "item")
+ if _field(item, "type") == "function_call":
+ event_type = "start"
+ content_items.append(
+ {
+ "type": "partial_tool_call",
+ "name": _field(item, "name", ""),
+ "arguments": "",
+ "tool_call_id": _field(item, "call_id", ""),
+ "fidelity": {
+ "item_id": _normalize_json_value(_field(item, "id")),
+ "output_index": _normalize_json_value(_field(model_output, "output_index")),
+ },
+ }
+ )
+ elif minimax_event_type == "response.function_call_arguments.delta":
+ event_type = "delta"
+ content_items.append(
+ {
+ "type": "partial_tool_call",
+ "name": "",
+ "arguments": _field(model_output, "delta", ""),
+ "tool_call_id": "",
+ "fidelity": {
+ "item_id": _normalize_json_value(_field(model_output, "item_id")),
+ "output_index": _normalize_json_value(_field(model_output, "output_index")),
+ },
+ }
+ )
+ elif minimax_event_type == "response.output_item.done":
+ item = _normalize_json_value(_field(model_output, "item"))
+ if not isinstance(item, dict):
+ raise ValueError(f"MiniMax output item must be a JSON object, got: {item!r}")
+
+ if item.get("type") == "reasoning":
+ event_type = "delta"
+ content_items.append(
+ {
+ "type": "thinking",
+ "thinking": "",
+ "fidelity": {_WIRE_ITEM_FIDELITY_KEY: item},
+ }
+ )
+ elif item.get("type") == "function_call":
+ event_type = "delta"
+ tool_name = item.get("name") or ""
+ tool_call_id = item.get("call_id") or ""
+ content_items.append(
+ {
+ "type": "tool_call",
+ "name": tool_name,
+ "arguments": parse_tool_call_arguments(
+ item.get("arguments"), self.__class__.__name__, tool_name, tool_call_id
+ ),
+ "tool_call_id": tool_call_id,
+ "fidelity": {"arguments": item.get("arguments")},
+ }
+ )
+ elif item.get("type") == "message":
+ event_type = "delta"
+ phase = item.get("id") or f"output-{_field(model_output, 'output_index', 0)}"
+ content_items.append(
+ {
+ "type": "text",
+ "text": "",
+ "fidelity": {"phase": phase},
+ }
+ )
+ elif minimax_event_type in {"response.completed", "response.incomplete"}:
+ event_type = "stop"
+ response = _field(model_output, "response")
+ usage_metadata = _usage_from_response(response)
+ if minimax_event_type == "response.completed":
+ output = _field(response, "output", []) or []
+ finish_reason = (
+ "tool_call" if any(_field(item, "type") == "function_call" for item in output) else "stop"
+ )
+ else:
+ incomplete_reason = _field(_field(response, "incomplete_details"), "reason")
+ finish_reason = {"max_output_tokens": "length", "content_filter": "stop"}.get(
+ incomplete_reason, "unknown"
+ )
+ elif minimax_event_type == "response.failed":
+ response = _field(model_output, "response")
+ raise RuntimeError(
+ _format_response_error(minimax_event_type, _field(response, "error"), _field(response, "id"))
+ )
+ elif minimax_event_type in {"error", "response.error"}:
+ response = _field(model_output, "response")
+ error = _field(model_output, "error")
+ if error is None and response is not None:
+ error = _field(response, "error")
+ raise RuntimeError(
+ _format_response_error(
+ minimax_event_type,
+ error if error is not None else model_output,
+ _field(response, "id") if response is not None else None,
+ )
+ )
+ elif minimax_event_type not in {
+ "response.created",
+ "response.in_progress",
+ "response.output_text.done",
+ "response.reasoning_text.done",
+ "response.function_call_arguments.done",
+ "response.content_part.added",
+ "response.content_part.done",
+ }:
+ raise ValueError(f"Unknown output: {model_output}")
+
+ return {
+ "role": "assistant",
+ "event_type": event_type,
+ "content_items": content_items,
+ "usage_metadata": usage_metadata,
+ "finish_reason": finish_reason,
+ }
+
+ async def _streaming_response_internal(
+ self, messages: list[UniMessage], config: UniConfig
+ ) -> AsyncIterator[UniEvent]:
+ """Stream MiniMax Responses events."""
+ minimax_config = self.transform_uni_config_to_model_config(config)
+ input_list = self.transform_uni_message_to_model_input(messages)
+ stream = await self._client.responses.create(**minimax_config, input=input_list, stream=True)
+
+ async for model_event in stream:
+ event = self.transform_model_output_to_uni_event(model_event)
+ if event["event_type"] != "unused":
+ yield event
diff --git a/src_py/agenthub/registry.py b/src_py/agenthub/registry.py
index 8115bd21..92c3dc82 100644
--- a/src_py/agenthub/registry.py
+++ b/src_py/agenthub/registry.py
@@ -61,6 +61,7 @@ class SupportedModel(TypedDict):
_DEEPSEEK = "https://api.deepseek.com"
_OPENROUTER = "https://openrouter.ai/api/v1"
_SILICONFLOW = "https://api.siliconflow.cn/v1"
+_MINIMAX = "https://api.minimax.io/v1"
# Display convention shared with the AgentHub apps: prices are stored in USD (official CNY
# list prices pre-converted at 7 CNY/USD), so requesting CNY shows the vendor's numbers.
@@ -187,6 +188,14 @@ def rate(value: float) -> float:
"context_window": 1050000,
"pricing": _usd(5.0, 30.0, cached=0.5),
},
+ {
+ "model": "MiniMax-M3",
+ "base_url": _MINIMAX,
+ "client": "minimax-m3",
+ "input_modalities": ["Text", "Image"],
+ "output_modalities": ["Text"],
+ "context_window": 1000000,
+ },
{
"model": "text-embedding-3-large",
"base_url": _OPENAI,
diff --git a/src_py/tests/test_client.py b/src_py/tests/test_client.py
index 6d4fb0bb..fe6eafaf 100644
--- a/src_py/tests/test_client.py
+++ b/src_py/tests/test_client.py
@@ -23,7 +23,7 @@
import httpx
import pytest
-from agenthub import AutoLLMClient, ThinkingLevel, list_supported_models
+from agenthub import AutoLLMClient, PromptCaching, ThinkingLevel, UnsupportedParameterError, list_supported_models
IMAGE = "https://cdn.britannica.com/80/120980-050-D1DA5C61/Poet-narcissus.jpg"
@@ -41,6 +41,7 @@ class Model:
support_embedding: bool = False
provider: Literal["official", "bedrock", "vertex", "siliconflow", "openrouter", "modelverse"] = "official"
client_type: str | None = None
+ requires_explicit_tool_prompt: bool = False
def __repr__(self) -> str:
return f"{self.name}:{self.provider}"
@@ -100,6 +101,9 @@ def __repr__(self) -> str:
if os.getenv("MOONSHOT_API_KEY"):
AVAILABLE_MODELS.append(Model(name="kimi-k3", support_temperature=False))
+if os.getenv("MINIMAX_API_KEY"):
+ AVAILABLE_MODELS.append(Model(name="MiniMax-M3", client_type="minimax-m3", requires_explicit_tool_prompt=True))
+
if os.getenv("DEEPSEEK_API_KEY"):
AVAILABLE_MODELS.append(
Model(name="deepseek-v4-flash", support_temperature=False, support_image_understanding=False)
@@ -398,6 +402,340 @@ async def test_unknown_model():
async def test_list_supported_models():
"""Test that the registry lists model entries accepted by AutoLLMClient."""
entries = list_supported_models()
+ minimax_entries = [entry for entry in entries if entry["base_url"] == "https://api.minimax.io/v1"]
+ assert minimax_entries == [
+ {
+ "model": "MiniMax-M3",
+ "base_url": "https://api.minimax.io/v1",
+ "client": "minimax-m3",
+ "input_modalities": ["Text", "Image"],
+ "output_modalities": ["Text"],
+ "context_window": 1000000,
+ }
+ ]
+
+ minimax_m3 = AutoLLMClient(model="MiniMax-M3", api_key="test-key")
+ assert minimax_m3._client.__class__.__name__ == "MiniMaxM3Client"
+ for client_type in (None, "minimax-m3"):
+ with pytest.raises(ValueError, match="not support"):
+ AutoLLMClient(model="MiniMax-M3-preview", api_key="test-key", client_type=client_type)
+
+ expected_m3_efforts = {
+ ThinkingLevel.NONE: "none",
+ ThinkingLevel.LOW: "low",
+ ThinkingLevel.MEDIUM: "medium",
+ ThinkingLevel.HIGH: "high",
+ ThinkingLevel.XHIGH: "high",
+ }
+ for thinking_level, effort in expected_m3_efforts.items():
+ config = minimax_m3.transform_uni_config_to_model_config({"thinking_level": thinking_level})
+ assert config["reasoning"] == {"effort": effort}
+ assert "prompt_caching" not in minimax_m3.transform_uni_config_to_model_config(
+ {"prompt_caching": PromptCaching.ENABLE}
+ )
+ for prompt_caching in (PromptCaching.DISABLE, PromptCaching.ENHANCE):
+ with pytest.raises(UnsupportedParameterError, match="does not support"):
+ minimax_m3.transform_uni_config_to_model_config({"prompt_caching": prompt_caching})
+ for tool_choice in ("required", ["lookup"]):
+ with pytest.raises(UnsupportedParameterError, match="does not support"):
+ minimax_m3.transform_uni_config_to_model_config({"tool_choice": tool_choice})
+
+ reasoning_wire_item = {
+ "id": "reasoning-1",
+ "status": "completed",
+ "summary": [],
+ "content": [{"type": "reasoning_text", "text": "reasoning"}],
+ "type": "reasoning",
+ }
+ function_wire_item = {
+ "id": "function-1",
+ "status": "completed",
+ "type": "function_call",
+ "call_id": "call-1",
+ "name": "lookup",
+ "arguments": '{"城市": "上海"}',
+ }
+ ordered_input = minimax_m3.transform_uni_message_to_model_input(
+ [
+ {
+ "role": "assistant",
+ "content_items": [
+ {"type": "text", "text": "before"},
+ {
+ "type": "thinking",
+ "thinking": "reasoning",
+ "fidelity": {"wire_item": reasoning_wire_item},
+ },
+ {
+ "type": "tool_call",
+ "name": "lookup",
+ "arguments": {"城市": "上海"},
+ "tool_call_id": "call-1",
+ },
+ {"type": "text", "text": "after"},
+ ],
+ }
+ ]
+ )
+ assert ordered_input == [
+ {"role": "assistant", "content": [{"type": "output_text", "text": "before"}]},
+ reasoning_wire_item,
+ {
+ "type": "function_call",
+ "call_id": "call-1",
+ "name": "lookup",
+ "arguments": '{"城市":"上海"}',
+ },
+ {"role": "assistant", "content": [{"type": "output_text", "text": "after"}]},
+ ]
+
+ reasoning_events = [
+ minimax_m3.transform_model_output_to_uni_event(
+ {"type": "response.reasoning_text.delta", "delta": "reasoning"}
+ ),
+ minimax_m3.transform_model_output_to_uni_event(
+ {"type": "response.output_item.done", "output_index": 0, "item": reasoning_wire_item}
+ ),
+ ]
+ replay_reasoning = minimax_m3.concat_uni_events_to_uni_message(reasoning_events)
+ assert replay_reasoning["content_items"] == [
+ {
+ "type": "thinking",
+ "thinking": "reasoning",
+ "fidelity": {"wire_item": reasoning_wire_item},
+ }
+ ]
+ assert minimax_m3.transform_uni_message_to_model_input([replay_reasoning]) == [reasoning_wire_item]
+
+ message_wire_item = {
+ "id": "message-1",
+ "type": "message",
+ "status": "completed",
+ "role": "assistant",
+ "content": [
+ {
+ "type": "output_text",
+ "text": "exact response",
+ "annotations": [],
+ "logprobs": None,
+ }
+ ],
+ "phase": None,
+ }
+ message_events = [
+ minimax_m3.transform_model_output_to_uni_event(
+ {
+ "type": "response.output_text.delta",
+ "item_id": "message-1",
+ "output_index": 0,
+ "content_index": 0,
+ "delta": "exact response",
+ }
+ ),
+ minimax_m3.transform_model_output_to_uni_event(
+ {"type": "response.output_item.done", "output_index": 0, "item": message_wire_item}
+ ),
+ ]
+ replay_message = minimax_m3.concat_uni_events_to_uni_message(message_events)
+ assert replay_message["content_items"] == [
+ {
+ "type": "text",
+ "text": "exact response",
+ "fidelity": {"phase": "message-1"},
+ }
+ ]
+ assert minimax_m3.transform_uni_message_to_model_input([replay_message]) == [
+ {"role": "assistant", "content": [{"type": "output_text", "text": "exact response"}]}
+ ]
+
+ second_function_wire_item = {
+ **function_wire_item,
+ "id": "function-2",
+ "call_id": "call-2",
+ "arguments": '{"城市": "北京"}',
+ }
+ function_events = [
+ minimax_m3.transform_model_output_to_uni_event(
+ {"type": "response.output_item.done", "output_index": index, "item": wire_item}
+ )
+ for index, wire_item in enumerate((function_wire_item, second_function_wire_item))
+ ]
+ replay_functions = minimax_m3.concat_uni_events_to_uni_message(function_events)
+ assert replay_functions["content_items"] == [
+ {
+ "type": "tool_call",
+ "name": "lookup",
+ "arguments": {"城市": "上海"},
+ "tool_call_id": "call-1",
+ "fidelity": {"arguments": '{"城市": "上海"}'},
+ },
+ {
+ "type": "tool_call",
+ "name": "lookup",
+ "arguments": {"城市": "北京"},
+ "tool_call_id": "call-2",
+ "fidelity": {"arguments": '{"城市": "北京"}'},
+ },
+ ]
+ assert minimax_m3.transform_uni_message_to_model_input([replay_functions]) == [
+ {
+ "type": "function_call",
+ "call_id": "call-1",
+ "name": "lookup",
+ "arguments": '{"城市": "上海"}',
+ },
+ {
+ "type": "function_call",
+ "call_id": "call-2",
+ "name": "lookup",
+ "arguments": '{"城市": "北京"}',
+ },
+ ]
+
+ precision_arguments = '{"large":9007199254740993,"decimal":1.0}'
+ precision_function_event = minimax_m3.transform_model_output_to_uni_event(
+ {
+ "type": "response.output_item.done",
+ "output_index": 0,
+ "item": {
+ "id": "function-precision",
+ "status": "completed",
+ "type": "function_call",
+ "call_id": "call-precision",
+ "name": "lookup",
+ "arguments": precision_arguments,
+ },
+ }
+ )
+ precision_message = minimax_m3.concat_uni_events_to_uni_message([precision_function_event])
+ assert minimax_m3.transform_uni_message_to_model_input([precision_message]) == [
+ {
+ "type": "function_call",
+ "call_id": "call-precision",
+ "name": "lookup",
+ "arguments": precision_arguments,
+ }
+ ]
+
+ serialized_arguments_input = minimax_m3.transform_uni_message_to_model_input(
+ [
+ {
+ "role": "assistant",
+ "content_items": [
+ {
+ "type": "tool_call",
+ "name": "lookup",
+ "arguments": {"value": True},
+ "tool_call_id": "call-bool",
+ "fidelity": {"arguments": '{"value":1}'},
+ }
+ ],
+ }
+ ]
+ )
+ assert serialized_arguments_input[0]["arguments"] == '{"value":true}'
+
+ with pytest.raises(ValueError, match="valid id, summary, and content"):
+ minimax_m3.transform_uni_message_to_model_input(
+ [
+ {
+ "role": "assistant",
+ "content_items": [
+ {
+ "type": "thinking",
+ "thinking": "invalid",
+ "fidelity": {
+ "wire_item": {
+ "id": 123,
+ "type": "reasoning",
+ "summary": "invalid",
+ "content": [],
+ }
+ },
+ }
+ ],
+ }
+ ]
+ )
+
+ completed_event = minimax_m3.transform_model_output_to_uni_event(
+ {
+ "type": "response.completed",
+ "response": {
+ "output": [function_wire_item, second_function_wire_item],
+ "usage": {
+ "input_tokens": 10,
+ "input_tokens_details": {"cached_tokens": 2},
+ "output_tokens": 6,
+ "output_tokens_details": {"reasoning_tokens": 1},
+ },
+ },
+ }
+ )
+ assert completed_event["finish_reason"] == "tool_call"
+ assert completed_event["usage_metadata"] == {
+ "cached_tokens": 2,
+ "prompt_tokens": 8,
+ "thoughts_tokens": 1,
+ "response_tokens": 5,
+ }
+
+ partial_start = minimax_m3.transform_model_output_to_uni_event(
+ {
+ "type": "response.output_item.added",
+ "output_index": 3,
+ "item": {
+ "id": "function-partial",
+ "type": "function_call",
+ "call_id": "call-partial",
+ "name": "lookup",
+ "arguments": "",
+ },
+ }
+ )
+ partial_delta = minimax_m3.transform_model_output_to_uni_event(
+ {
+ "type": "response.function_call_arguments.delta",
+ "item_id": "function-partial",
+ "output_index": 3,
+ "delta": '{"value":1}',
+ }
+ )
+ assert partial_start["content_items"][0]["fidelity"] == {
+ "item_id": "function-partial",
+ "output_index": 3,
+ }
+ assert partial_delta["content_items"][0]["fidelity"] == {
+ "item_id": "function-partial",
+ "output_index": 3,
+ }
+
+ incomplete_event = minimax_m3.transform_model_output_to_uni_event(
+ {
+ "type": "response.incomplete",
+ "response": {
+ "output": [],
+ "usage": None,
+ "incomplete_details": {"reason": "max_output_tokens"},
+ },
+ }
+ )
+ assert incomplete_event["finish_reason"] == "length"
+ with pytest.raises(RuntimeError, match="provider_failure"):
+ minimax_m3.transform_model_output_to_uni_event(
+ {
+ "type": "response.failed",
+ "response": {
+ "id": "response-1",
+ "error": {"code": "provider_failure", "message": "failed"},
+ },
+ }
+ )
+ with pytest.raises(RuntimeError, match="bad_request"):
+ minimax_m3.transform_model_output_to_uni_event(
+ {"type": "error", "error": {"code": "bad_request", "message": "invalid"}}
+ )
+
kimi = next(entry for entry in entries if entry["model"] == "kimi-k3")
assert kimi["base_url"] == "https://api.moonshot.cn/v1"
assert kimi["client"] == "kimi-k3"
@@ -495,10 +833,19 @@ async def test_tool_use(model: Model):
}
config = {"tools": [weather_tool]}
+ tool_name = None
+ tool_arguments = {}
tool_call_id = None
partial_tool_call_data = {}
- message1 = {"role": "user", "content_items": [{"type": "text", "text": "What is the weather in San Francisco?"}]}
+ tool_prompt = "What is the weather in San Francisco?"
+ if model.requires_explicit_tool_prompt:
+ tool_prompt = (
+ "You must invoke get_weather exactly once with location San Francisco. "
+ "Your only allowed action before the tool result is that function call; return no text before its result. "
+ "After the result is provided, answer using the returned weather."
+ )
+ message1 = {"role": "user", "content_items": [{"type": "text", "text": tool_prompt}]}
async for event in client.streaming_response_stateful(message=message1, config=config):
await _check_event_integrity(event)
for item in event["content_items"]:
@@ -641,7 +988,9 @@ async def test_tool_result_with_image(model: Model):
# Define a tool that returns an image
image_tool = {
"name": "get_image",
- "description": "Get an image URL",
+ "description": (
+ "Retrieve an image URL for a numeric seed." if model.requires_explicit_tool_prompt else "Get an image URL"
+ ),
"parameters": {
"type": "object",
"properties": {
@@ -655,12 +1004,16 @@ async def test_tool_result_with_image(model: Model):
}
config = {"tools": [image_tool]}
+ tool_name = None
tool_call_id = None
- message1 = {
- "role": "user",
- "content_items": [{"type": "text", "text": "Get me a random image and describe it briefly."}],
- }
+ tool_prompt = "Get me a random image and describe it briefly."
+ if model.requires_explicit_tool_prompt:
+ tool_prompt = (
+ "You must invoke the get_image function exactly once with seed 42. "
+ "Your only allowed action in this turn is that function call; return no text before its result."
+ )
+ message1 = {"role": "user", "content_items": [{"type": "text", "text": tool_prompt}]}
async for event in client.streaming_response_stateful(message=message1, config=config):
await _check_event_integrity(event)
for item in event["content_items"]:
@@ -676,7 +1029,11 @@ async def test_tool_result_with_image(model: Model):
"content_items": [
{
"type": "tool_result",
- "text": "Here is the result image:",
+ "text": (
+ "Here is the result image. Describe it briefly."
+ if model.requires_explicit_tool_prompt
+ else "Here is the result image:"
+ ),
"images": [IMAGE],
"tool_call_id": tool_call_id,
}
diff --git a/src_ts/src/autoClient.ts b/src_ts/src/autoClient.ts
index d4a491c4..6d3dd012 100644
--- a/src_ts/src/autoClient.ts
+++ b/src_ts/src/autoClient.ts
@@ -25,6 +25,7 @@ import { KimiK3Client } from "./kimi_k3";
import { OpenaiClient } from "./openai";
import { OpenaiEmbeddingClient } from "./openai_embedding";
import { DeepSeekV4Client } from "./deepseek_v4";
+import { MiniMaxM3Client } from "./minimax_m3";
import { UniConfig, UniEvent, UniMessage } from "./types";
/**
@@ -72,12 +73,11 @@ export class AutoLLMClient extends LLMClient {
baseUrl?: string | null,
clientType?: string | null,
): LLMClient {
- clientType = (
- clientType ||
- process.env.CLIENT_TYPE ||
- model.toLowerCase()
- ).toLowerCase();
+ clientType = (clientType || process.env.CLIENT_TYPE || model).toLowerCase();
+ if (clientType === "minimax-m3") {
+ return new MiniMaxM3Client({ model, apiKey, baseUrl });
+ }
// gemini-3.6 must be matched before the broader gemini-3 prefix below
if (
clientType.includes("gemini-3.6") ||
@@ -130,7 +130,10 @@ export class AutoLLMClient extends LLMClient {
} else {
throw new Error(
`${clientType} is not supported. ` +
- "Supported client types: gemini-3.6, gemini-3, claude-5, claude-4-8, claude-4-7, claude-4-6, gpt-5.5, gpt-5.4, glm-5.2, glm-5.1, kimi-k3, kimi-k2.6, kimi-k2.5, deepseek-v4, openai-embedding, openai.",
+ "Supported client types: minimax-m3, gemini-3.6, gemini-3, " +
+ "claude-5, claude-4-8, claude-4-7, " +
+ "claude-4-6, gpt-5.5, gpt-5.4, glm-5.2, glm-5.1, kimi-k3, kimi-k2.6, kimi-k2.5, " +
+ "deepseek-v4, openai-embedding, openai.",
);
}
}
diff --git a/src_ts/src/minimax_m3/client.ts b/src_ts/src/minimax_m3/client.ts
new file mode 100644
index 00000000..0f350f54
--- /dev/null
+++ b/src_ts/src/minimax_m3/client.ts
@@ -0,0 +1,641 @@
+// Copyright 2025 Prism Shadow. and/or its affiliates
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { isDeepStrictEqual } from "node:util";
+import OpenAI from "openai";
+import type {
+ Response,
+ ResponseCreateParamsStreaming,
+ ResponseStreamEvent,
+} from "openai/resources/responses/responses";
+import { LLMClient } from "../baseClient";
+import { parseToolCallArguments, UnsupportedParameterError } from "../errors";
+import {
+ EventType,
+ FinishReason,
+ PartialContentItem,
+ PromptCaching,
+ ThinkingLevel,
+ ToolChoice,
+ UniConfig,
+ UniEvent,
+ UniMessage,
+ UsageMetadata,
+} from "../types";
+
+const DEFAULT_BASE_URL = "https://api.minimax.io/v1";
+
+type JsonValue = string | number | boolean | null | JsonValue[] | JsonObject;
+
+interface JsonObject {
+ [key: string]: JsonValue;
+}
+
+type MiniMaxReasoningEffort = "none" | "low" | "medium" | "high";
+
+interface MiniMaxFunctionTool {
+ type: "function";
+ name: string;
+ description: string;
+ parameters?: Record;
+}
+
+interface MiniMaxResponseConfig {
+ model: string;
+ store: false;
+ instructions?: string;
+ max_output_tokens?: number;
+ temperature?: number;
+ reasoning?: { effort: MiniMaxReasoningEffort };
+ tools?: MiniMaxFunctionTool[];
+ tool_choice?: "auto" | "none";
+}
+
+interface MiniMaxInputTextContent extends JsonObject {
+ type: "input_text";
+ text: string;
+}
+
+interface MiniMaxOutputTextContent extends JsonObject {
+ type: "output_text";
+ text: string;
+}
+
+interface MiniMaxInputImageContent extends JsonObject {
+ type: "input_image";
+ image_url: string;
+}
+
+type MiniMaxMessageContent =
+ | MiniMaxInputTextContent
+ | MiniMaxOutputTextContent
+ | MiniMaxInputImageContent;
+
+type MiniMaxToolOutputContent =
+ | MiniMaxInputTextContent
+ | MiniMaxInputImageContent;
+
+interface MiniMaxMessageInput {
+ type?: "message";
+ role: UniMessage["role"];
+ content: JsonObject[];
+}
+
+interface MiniMaxReasoningInput extends JsonObject {
+ id: string;
+ type: "reasoning";
+ summary: JsonValue[];
+ content: JsonValue[];
+}
+
+interface MiniMaxFunctionCallInput extends JsonObject {
+ type: "function_call";
+ call_id: string;
+ name: string;
+ arguments: string;
+}
+
+interface MiniMaxFunctionCallOutputInput extends JsonObject {
+ type: "function_call_output";
+ call_id: string;
+ output: string | MiniMaxToolOutputContent[];
+}
+
+type MiniMaxInputItem =
+ | MiniMaxMessageInput
+ | MiniMaxReasoningInput
+ | MiniMaxFunctionCallInput
+ | MiniMaxFunctionCallOutputInput;
+
+interface MiniMaxResponseCreateParamsStreaming extends MiniMaxResponseConfig {
+ input: MiniMaxInputItem[];
+ stream: true;
+}
+
+const WIRE_ITEM_FIDELITY_KEY = "wire_item";
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+
+function isJsonValue(value: unknown): value is JsonValue {
+ if (
+ value === null ||
+ typeof value === "string" ||
+ typeof value === "boolean"
+ ) {
+ return true;
+ }
+ if (typeof value === "number") {
+ return Number.isFinite(value);
+ }
+ if (Array.isArray(value)) {
+ return value.every(isJsonValue);
+ }
+ return isJsonObject(value);
+}
+
+function isJsonObject(value: unknown): value is JsonObject {
+ if (!isRecord(value)) {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value);
+ if (prototype !== Object.prototype && prototype !== null) {
+ return false;
+ }
+ return Object.values(value).every(isJsonValue);
+}
+
+function requireJsonObject(value: unknown, context: string): JsonObject {
+ if (!isJsonObject(value)) {
+ throw new Error(`${context} must contain only JSON-style values.`);
+ }
+ return value;
+}
+
+function isJsonArray(value: JsonValue | undefined): value is JsonValue[] {
+ return Array.isArray(value);
+}
+
+function wireItemFromFidelity(
+ fidelity: unknown,
+ context: string,
+): JsonObject | undefined {
+ if (!isRecord(fidelity) || !(WIRE_ITEM_FIDELITY_KEY in fidelity)) {
+ return undefined;
+ }
+ return requireJsonObject(
+ fidelity[WIRE_ITEM_FIDELITY_KEY],
+ `${context} wire item`,
+ );
+}
+
+function legacyWireItemFromFidelity(fidelity: JsonObject): JsonObject {
+ const wireItem: JsonObject = {};
+ for (const [key, value] of Object.entries(fidelity)) {
+ if (key !== WIRE_ITEM_FIDELITY_KEY && key !== "phase") {
+ wireItem[key] = value;
+ }
+ }
+ return wireItem;
+}
+
+function wireContentText(
+ wireItem: JsonObject,
+ contentType: string,
+): string | undefined {
+ const content = wireItem.content;
+ if (!Array.isArray(content)) {
+ return undefined;
+ }
+
+ const textParts: string[] = [];
+ for (const part of content) {
+ if (!isJsonObject(part) || part.type !== contentType) {
+ continue;
+ }
+ if (typeof part.text !== "string") {
+ return undefined;
+ }
+ textParts.push(part.text);
+ }
+ return textParts.length > 0 ? textParts.join("") : undefined;
+}
+
+
+function transformUsage(response: Response): UsageMetadata | null {
+ const usage = response.usage;
+ if (!usage) {
+ return null;
+ }
+
+ const cachedTokens = usage.input_tokens_details.cached_tokens ?? 0;
+ const reasoningTokens = usage.output_tokens_details.reasoning_tokens ?? 0;
+ return {
+ cached_tokens: cachedTokens,
+ prompt_tokens: usage.input_tokens - cachedTokens,
+ thoughts_tokens: reasoningTokens,
+ response_tokens: usage.output_tokens - reasoningTokens,
+ };
+}
+
+function transformFinishReason(
+ response: Response,
+ eventType: "response.completed" | "response.incomplete",
+): FinishReason {
+ if (eventType === "response.incomplete") {
+ const incompleteReason = response.incomplete_details?.reason;
+ if (incompleteReason === "max_output_tokens") {
+ return "length";
+ }
+ if (incompleteReason === "content_filter") {
+ return "stop";
+ }
+ return "unknown";
+ }
+ if (response.output.some((item) => item.type === "function_call")) {
+ return "tool_call";
+ }
+ return "stop";
+}
+
+function responseFailure(response: Response): Error {
+ if (response.error) {
+ return new Error(
+ `MiniMax response ${response.id} failed (${response.error.code}): ${response.error.message}`,
+ );
+ }
+ return new Error(
+ `MiniMax response ${response.id} failed without error details from the API.`,
+ );
+}
+
+/** MiniMax M3 client using MiniMax's Responses API. */
+export class MiniMaxM3Client extends LLMClient {
+ protected _model: string;
+ private _client: OpenAI;
+
+ constructor(options: {
+ model: string;
+ apiKey?: string;
+ baseUrl?: string | null;
+ clientType?: string | null;
+ }) {
+ super();
+ if (options.model.toLowerCase() !== "minimax-m3") {
+ throw new Error(`${options.model} is not supported by MiniMaxM3Client.`);
+ }
+ this._model = options.model;
+ this._client = new OpenAI({
+ apiKey: options.apiKey || process.env.MINIMAX_API_KEY || undefined,
+ baseURL:
+ options.baseUrl || process.env.MINIMAX_BASE_URL || DEFAULT_BASE_URL,
+ });
+ }
+
+ private _convertThinkingLevelToEffort(
+ thinkingLevel: ThinkingLevel,
+ ): MiniMaxReasoningEffort {
+ const mapping: Record = {
+ [ThinkingLevel.NONE]: "none",
+ [ThinkingLevel.LOW]: "low",
+ [ThinkingLevel.MEDIUM]: "medium",
+ [ThinkingLevel.HIGH]: "high",
+ [ThinkingLevel.XHIGH]: "high",
+ };
+ return mapping[thinkingLevel];
+ }
+
+ private _convertToolChoice(toolChoice: ToolChoice): "auto" | "none" {
+ if (toolChoice === "auto" || toolChoice === "none") {
+ return toolChoice;
+ }
+ throw new UnsupportedParameterError({
+ client: this.constructor.name,
+ parameter: "tool_choice",
+ message: "MiniMax Responses API does not support required or named tool selection.",
+ });
+ }
+
+ transformUniConfigToModelConfig(config: UniConfig): MiniMaxResponseConfig {
+ const minimaxConfig: MiniMaxResponseConfig = {
+ model: this._model,
+ store: false,
+ };
+
+ if (config.system_prompt !== undefined) {
+ minimaxConfig.instructions = config.system_prompt;
+ }
+ if (config.max_tokens !== undefined) {
+ minimaxConfig.max_output_tokens = config.max_tokens;
+ }
+ if (config.temperature !== undefined) {
+ if (config.temperature < 0 || config.temperature > 1) {
+ throw new UnsupportedParameterError({
+ client: this.constructor.name,
+ parameter: "temperature",
+ message: "MiniMax Responses API does not support temperatures outside the range 0 to 1.",
+ });
+ }
+ minimaxConfig.temperature = config.temperature;
+ }
+ if (config.thinking_level !== undefined) {
+ minimaxConfig.reasoning = {
+ effort: this._convertThinkingLevelToEffort(config.thinking_level),
+ };
+ }
+ if (config.tools !== undefined) {
+ minimaxConfig.tools = config.tools.map((tool) => ({
+ type: "function",
+ name: tool.name,
+ description: tool.description,
+ ...(tool.parameters !== undefined
+ ? { parameters: tool.parameters }
+ : {}),
+ }));
+ }
+ if (config.tool_choice !== undefined) {
+ minimaxConfig.tool_choice = this._convertToolChoice(config.tool_choice);
+ }
+ if (config.prompt_caching === PromptCaching.DISABLE) {
+ throw new UnsupportedParameterError({
+ client: this.constructor.name,
+ parameter: "prompt_caching",
+ message: "MiniMax Responses API does not support disabling its automatic prompt cache.",
+ });
+ }
+ if (config.prompt_caching === PromptCaching.ENHANCE) {
+ throw new UnsupportedParameterError({
+ client: this.constructor.name,
+ parameter: "prompt_caching",
+ message: "MiniMax Responses API does not support enhancing its automatic prompt cache.",
+ });
+ }
+
+ return minimaxConfig;
+ }
+
+ transformUniMessageToModelInput(messages: UniMessage[]): MiniMaxInputItem[] {
+ const inputList: MiniMaxInputItem[] = [];
+
+ for (const message of messages) {
+ let contentItems: MiniMaxMessageContent[] = [];
+ const flushContentItems = (): void => {
+ if (contentItems.length > 0) {
+ inputList.push({ role: message.role, content: contentItems });
+ contentItems = [];
+ }
+ };
+
+ for (const item of message.content_items) {
+ if (item.type === "text") {
+ if (message.role === "user") {
+ contentItems.push({ type: "input_text", text: item.text });
+ } else {
+ contentItems.push({ type: "output_text", text: item.text });
+ }
+ } else if (item.type === "image_url") {
+ contentItems.push({ type: "input_image", image_url: item.image_url });
+ } else if (item.type === "thinking") {
+ flushContentItems();
+ const fidelity = requireJsonObject(
+ item.fidelity ?? {},
+ "MiniMax reasoning fidelity",
+ );
+ const wireItem =
+ wireItemFromFidelity(fidelity, "MiniMax reasoning fidelity") ??
+ legacyWireItemFromFidelity(fidelity);
+ const id = wireItem.id;
+ const summary = wireItem.summary;
+ const wireContent = wireItem.content;
+ if (
+ typeof id !== "string" ||
+ !isJsonArray(summary) ||
+ !isJsonArray(wireContent)
+ ) {
+ throw new Error(
+ "MiniMax reasoning replay requires valid id, summary, and content fidelity.",
+ );
+ }
+ const content =
+ wireContentText(wireItem, "reasoning_text") === item.thinking
+ ? wireContent
+ : [{ type: "reasoning_text", text: item.thinking }];
+ inputList.push({
+ ...wireItem,
+ id,
+ type: "reasoning",
+ summary,
+ content,
+ });
+ } else if (item.type === "tool_call") {
+ flushContentItems();
+ let serializedArguments = JSON.stringify(item.arguments);
+ if (serializedArguments === undefined) {
+ throw new Error(
+ `MiniMax tool call ${item.name} arguments could not be serialized.`,
+ );
+ }
+ const rawArguments = item.fidelity?.arguments;
+ if (
+ typeof rawArguments === "string" &&
+ isDeepStrictEqual(
+ parseToolCallArguments(
+ rawArguments,
+ this.constructor.name,
+ item.name,
+ item.tool_call_id,
+ ),
+ item.arguments,
+ )
+ ) {
+ serializedArguments = rawArguments;
+ }
+ inputList.push({
+ type: "function_call",
+ call_id: item.tool_call_id,
+ name: item.name,
+ arguments: serializedArguments,
+ });
+ } else if (item.type === "tool_result") {
+ flushContentItems();
+ let output: string | MiniMaxToolOutputContent[] = item.text;
+ if (item.images?.length) {
+ const outputItems: MiniMaxToolOutputContent[] = [
+ { type: "input_text", text: item.text },
+ ];
+ for (const imageUrl of item.images) {
+ outputItems.push({ type: "input_image", image_url: imageUrl });
+ }
+ output = outputItems;
+ }
+ inputList.push({
+ type: "function_call_output",
+ call_id: item.tool_call_id,
+ output,
+ });
+ } else {
+ throw new Error(`Unknown item: ${JSON.stringify(item)}`);
+ }
+ }
+
+ flushContentItems();
+ }
+
+ return inputList;
+ }
+
+ transformModelOutputToUniEvent(modelOutput: ResponseStreamEvent): UniEvent {
+ let eventType: EventType = "unused";
+ const contentItems: PartialContentItem[] = [];
+ let usageMetadata: UsageMetadata | null = null;
+ let finishReason: FinishReason | null = null;
+
+ switch (modelOutput.type) {
+ case "response.output_text.delta":
+ eventType = "delta";
+ contentItems.push({
+ type: "text",
+ text: modelOutput.delta,
+ fidelity: { phase: modelOutput.item_id },
+ });
+ break;
+ case "response.reasoning_text.delta":
+ eventType = "delta";
+ contentItems.push({
+ type: "thinking",
+ thinking: modelOutput.delta,
+ });
+ break;
+ case "response.output_item.added":
+ if (modelOutput.item.type === "function_call") {
+ if (modelOutput.item.id === undefined) {
+ throw new Error(
+ "MiniMax function-call start event is missing its output item id.",
+ );
+ }
+ eventType = "start";
+ contentItems.push({
+ type: "partial_tool_call",
+ name: modelOutput.item.name,
+ arguments: "",
+ tool_call_id: modelOutput.item.call_id,
+ fidelity: {
+ item_id: modelOutput.item.id,
+ output_index: modelOutput.output_index,
+ },
+ });
+ }
+ break;
+ case "response.function_call_arguments.delta":
+ eventType = "delta";
+ contentItems.push({
+ type: "partial_tool_call",
+ name: "",
+ arguments: modelOutput.delta,
+ tool_call_id: "",
+ fidelity: {
+ item_id: modelOutput.item_id,
+ output_index: modelOutput.output_index,
+ },
+ });
+ break;
+ case "response.output_item.done": {
+ const wireItem = requireJsonObject(
+ modelOutput.item,
+ "MiniMax output item",
+ );
+ if (modelOutput.item.type === "reasoning") {
+ eventType = "delta";
+ contentItems.push({
+ type: "thinking",
+ thinking: "",
+ fidelity: { [WIRE_ITEM_FIDELITY_KEY]: wireItem },
+ });
+ } else if (modelOutput.item.type === "function_call") {
+ eventType = "delta";
+ contentItems.push({
+ type: "tool_call",
+ name: modelOutput.item.name,
+ arguments: parseToolCallArguments(
+ modelOutput.item.arguments,
+ this.constructor.name,
+ modelOutput.item.name,
+ modelOutput.item.call_id,
+ ),
+ tool_call_id: modelOutput.item.call_id,
+ fidelity: { arguments: modelOutput.item.arguments },
+ });
+ } else if (modelOutput.item.type === "message") {
+ eventType = "delta";
+ const phase =
+ typeof wireItem.id === "string"
+ ? wireItem.id
+ : `output-${modelOutput.output_index}`;
+ contentItems.push({
+ type: "text",
+ text: "",
+ fidelity: { phase },
+ });
+ }
+ break;
+ }
+ case "response.completed":
+ case "response.incomplete":
+ eventType = "stop";
+ usageMetadata = transformUsage(modelOutput.response);
+ finishReason = transformFinishReason(
+ modelOutput.response,
+ modelOutput.type,
+ );
+ break;
+ case "response.failed":
+ throw responseFailure(modelOutput.response);
+ case "error": {
+ const code = modelOutput.code ? ` (${modelOutput.code})` : "";
+ const parameter = modelOutput.param
+ ? ` for parameter ${modelOutput.param}`
+ : "";
+ throw new Error(
+ `MiniMax stream error${code}${parameter}: ${modelOutput.message}`,
+ );
+ }
+ case "response.created":
+ case "response.in_progress":
+ case "response.output_text.done":
+ case "response.reasoning_text.done":
+ case "response.content_part.added":
+ case "response.content_part.done":
+ case "response.function_call_arguments.done":
+ break;
+ default:
+ throw new Error(`Unknown output: ${JSON.stringify(modelOutput)}`);
+ }
+
+ return {
+ role: "assistant",
+ event_type: eventType,
+ content_items: contentItems,
+ usage_metadata: usageMetadata,
+ finish_reason: finishReason,
+ };
+ }
+
+ async *_streamingResponseInternal(options: {
+ messages: UniMessage[];
+ config: UniConfig;
+ signal?: AbortSignal;
+ }): AsyncGenerator {
+ const minimaxConfig = this.transformUniConfigToModelConfig(options.config);
+ const inputList = this.transformUniMessageToModelInput(options.messages);
+ const params: MiniMaxResponseCreateParamsStreaming = {
+ ...minimaxConfig,
+ input: inputList,
+ stream: true,
+ };
+
+ // MiniMax accepts output_text assistant inputs and function tools without
+ // OpenAI's required strict field, so narrow the compatibility cast to this boundary.
+ const stream = await this._client.responses.create(
+ params as ResponseCreateParamsStreaming,
+ { signal: options.signal },
+ );
+ for await (const modelEvent of stream) {
+ const event = this.transformModelOutputToUniEvent(modelEvent);
+ if (event.event_type !== "unused") {
+ yield event;
+ }
+ }
+ }
+}
diff --git a/src_ts/src/minimax_m3/index.ts b/src_ts/src/minimax_m3/index.ts
new file mode 100644
index 00000000..e3ba3ad8
--- /dev/null
+++ b/src_ts/src/minimax_m3/index.ts
@@ -0,0 +1,15 @@
+// Copyright 2025 Prism Shadow. and/or its affiliates
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export { MiniMaxM3Client } from "./client";
diff --git a/src_ts/src/registry.ts b/src_ts/src/registry.ts
index 66af42f7..7fb207d1 100644
--- a/src_ts/src/registry.ts
+++ b/src_ts/src/registry.ts
@@ -57,6 +57,7 @@ const MOONSHOT = "https://api.moonshot.cn/v1";
const DEEPSEEK = "https://api.deepseek.com";
const OPENROUTER = "https://openrouter.ai/api/v1";
const SILICONFLOW = "https://api.siliconflow.cn/v1";
+const MINIMAX = "https://api.minimax.io/v1";
// Display convention shared with the AgentHub apps: prices are stored in USD
// (official CNY list prices pre-converted at 7 CNY/USD), so requesting CNY
@@ -182,6 +183,14 @@ const SUPPORTED_MODELS: SupportedModel[] = [
context_window: 1050000,
pricing: usd(5.0, 30.0, 0.5),
},
+ {
+ model: "MiniMax-M3",
+ base_url: MINIMAX,
+ client: "minimax-m3",
+ input_modalities: ["Text", "Image"],
+ output_modalities: ["Text"],
+ context_window: 1000000,
+ },
{
model: "text-embedding-3-large",
base_url: OPENAI,
diff --git a/src_ts/tests/client.test.ts b/src_ts/tests/client.test.ts
index caf1ee07..595922f9 100644
--- a/src_ts/tests/client.test.ts
+++ b/src_ts/tests/client.test.ts
@@ -14,7 +14,13 @@
import { AutoLLMClient } from "../src/autoClient";
import { listSupportedModels } from "../src/registry";
-import { ThinkingLevel, UniMessage, UniConfig, UniEvent } from "../src/types";
+import {
+ PromptCaching,
+ ThinkingLevel,
+ UniMessage,
+ UniConfig,
+ UniEvent,
+} from "../src/types";
import { expect, describe, test } from "@jest/globals";
const IMAGE =
@@ -30,6 +36,7 @@ interface Model {
supportAudioGeneration: boolean;
supportEmbedding: boolean;
clientType?: string;
+ requiresExplicitToolPrompt?: boolean;
provider:
| "official"
| "bedrock"
@@ -153,6 +160,21 @@ if (process.env.MOONSHOT_API_KEY) {
});
}
+if (process.env.MINIMAX_API_KEY) {
+ AVAILABLE_MODELS.push({
+ name: "MiniMax-M3",
+ supportTextGeneration: true,
+ supportTemperature: true,
+ supportImageUnderstanding: true,
+ supportImageGeneration: false,
+ supportAudioGeneration: false,
+ supportEmbedding: false,
+ clientType: "minimax-m3",
+ requiresExplicitToolPrompt: true,
+ provider: "official",
+ });
+}
+
if (process.env.DEEPSEEK_API_KEY) {
AVAILABLE_MODELS.push({
name: "deepseek-v4-flash",
@@ -659,11 +681,14 @@ if (AVAILABLE_MODELS.length > 0) {
let toolName: string | undefined;
let toolArguments: Record | undefined;
+ const toolPrompt = model.requiresExplicitToolPrompt
+ ? "You must invoke get_weather exactly once with location San Francisco. " +
+ "Your only allowed action before the tool result is that function call; return no text before its result. " +
+ "After the result is provided, answer using the returned weather."
+ : "What is the weather in San Francisco?";
const message1: UniMessage = {
role: "user",
- content_items: [
- { type: "text", text: "What is the weather in San Francisco?" },
- ],
+ content_items: [{ type: "text", text: toolPrompt }],
};
for await (const event of client.streamingResponseStateful({
message: message1,
@@ -850,7 +875,9 @@ if (AVAILABLE_MODELS.length > 0) {
const imageTool = {
name: "get_image",
- description: "Get an image URL",
+ description: model.requiresExplicitToolPrompt
+ ? "Retrieve an image URL for a numeric seed."
+ : "Get an image URL",
parameters: {
type: "object",
properties: {
@@ -867,14 +894,13 @@ if (AVAILABLE_MODELS.length > 0) {
let toolCallId: string | undefined;
let toolName: string | undefined;
+ const toolPrompt = model.requiresExplicitToolPrompt
+ ? "You must invoke the get_image function exactly once with seed 42. " +
+ "Your only allowed action in this turn is that function call; return no text before its result."
+ : "Get me a random image and describe it briefly.";
const message1: UniMessage = {
role: "user",
- content_items: [
- {
- type: "text",
- text: "Get me a random image and describe it briefly.",
- },
- ],
+ content_items: [{ type: "text", text: toolPrompt }],
};
for await (const event of client.streamingResponseStateful({
message: message1,
@@ -897,7 +923,9 @@ if (AVAILABLE_MODELS.length > 0) {
content_items: [
{
type: "tool_result",
- text: "Here is the result image:",
+ text: model.requiresExplicitToolPrompt
+ ? "Here is the result image. Describe it briefly."
+ : "Here is the result image:",
images: [IMAGE],
tool_call_id: toolCallId || "",
},
@@ -1084,6 +1112,391 @@ test("should list supported model entries", () => {
expect(glm52?.base_url).toBe("https://openrouter.ai/api/v1");
expect(glm52?.client).toBe("glm-5.2");
+ const minimaxEntries = entries.filter(
+ (entry) => entry.base_url === "https://api.minimax.io/v1",
+ );
+ expect(minimaxEntries).toEqual([
+ {
+ model: "MiniMax-M3",
+ base_url: "https://api.minimax.io/v1",
+ client: "minimax-m3",
+ input_modalities: ["Text", "Image"],
+ output_modalities: ["Text"],
+ context_window: 1000000,
+ },
+ ]);
+
+ const minimaxM3 = new AutoLLMClient({
+ model: "MiniMax-M3",
+ apiKey: "test-key",
+ });
+ for (const clientType of [undefined, "minimax-m3"]) {
+ expect(
+ () =>
+ new AutoLLMClient({
+ model: "MiniMax-M3-preview",
+ apiKey: "test-key",
+ clientType,
+ }),
+ ).toThrow("not supported");
+ }
+
+ const m3ThinkingEfforts = [
+ [ThinkingLevel.NONE, "none"],
+ [ThinkingLevel.LOW, "low"],
+ [ThinkingLevel.MEDIUM, "medium"],
+ [ThinkingLevel.HIGH, "high"],
+ [ThinkingLevel.XHIGH, "high"],
+ ] as const;
+ for (const [thinkingLevel, effort] of m3ThinkingEfforts) {
+ expect(
+ minimaxM3.transformUniConfigToModelConfig({
+ thinking_level: thinkingLevel,
+ }).reasoning,
+ ).toEqual({ effort });
+ }
+ expect(
+ minimaxM3.transformUniConfigToModelConfig({
+ prompt_caching: PromptCaching.ENABLE,
+ }),
+ ).not.toHaveProperty("prompt_caching");
+ for (const promptCaching of [
+ PromptCaching.DISABLE,
+ PromptCaching.ENHANCE,
+ ]) {
+ expect(() =>
+ minimaxM3.transformUniConfigToModelConfig({
+ prompt_caching: promptCaching,
+ }),
+ ).toThrow("does not support");
+ }
+ expect(() =>
+ minimaxM3.transformUniConfigToModelConfig({ tool_choice: "required" }),
+ ).toThrow("does not support");
+ expect(() =>
+ minimaxM3.transformUniConfigToModelConfig({ tool_choice: ["lookup"] }),
+ ).toThrow("does not support");
+
+ const reasoningWireItem = {
+ id: "reasoning-1",
+ status: "completed",
+ summary: [],
+ content: [{ type: "reasoning_text", text: "reasoning" }],
+ type: "reasoning",
+ };
+ const functionWireItem = {
+ id: "function-1",
+ status: "completed",
+ type: "function_call",
+ call_id: "call-1",
+ name: "lookup",
+ arguments: '{"城市": "上海"}',
+ };
+ expect(
+ minimaxM3.transformUniMessageToModelInput([
+ {
+ role: "assistant",
+ content_items: [
+ { type: "text", text: "before" },
+ {
+ type: "thinking",
+ thinking: "reasoning",
+ fidelity: { wire_item: reasoningWireItem },
+ },
+ {
+ type: "tool_call",
+ name: "lookup",
+ arguments: { 城市: "上海" },
+ tool_call_id: "call-1",
+ },
+ { type: "text", text: "after" },
+ ],
+ },
+ ]),
+ ).toEqual([
+ {
+ role: "assistant",
+ content: [{ type: "output_text", text: "before" }],
+ },
+ reasoningWireItem,
+ {
+ type: "function_call",
+ call_id: "call-1",
+ name: "lookup",
+ arguments: '{"城市":"上海"}',
+ },
+ {
+ role: "assistant",
+ content: [{ type: "output_text", text: "after" }],
+ },
+ ]);
+
+ const reasoningEvents = [
+ minimaxM3.transformModelOutputToUniEvent({
+ type: "response.reasoning_text.delta",
+ delta: "reasoning",
+ }),
+ minimaxM3.transformModelOutputToUniEvent({
+ type: "response.output_item.done",
+ output_index: 0,
+ item: reasoningWireItem,
+ }),
+ ];
+ const replayReasoning =
+ minimaxM3.concatUniEventsToUniMessage(reasoningEvents);
+ expect(replayReasoning.content_items).toEqual([
+ {
+ type: "thinking",
+ thinking: "reasoning",
+ fidelity: { wire_item: reasoningWireItem },
+ },
+ ]);
+ expect(
+ minimaxM3.transformUniMessageToModelInput([replayReasoning]),
+ ).toEqual([reasoningWireItem]);
+
+ const messageWireItem = {
+ id: "message-1",
+ type: "message",
+ status: "completed",
+ role: "assistant",
+ content: [
+ {
+ type: "output_text",
+ text: "exact response",
+ annotations: [],
+ logprobs: null,
+ },
+ ],
+ phase: null,
+ };
+ const messageEvents = [
+ minimaxM3.transformModelOutputToUniEvent({
+ type: "response.output_text.delta",
+ item_id: "message-1",
+ output_index: 0,
+ content_index: 0,
+ delta: "exact response",
+ }),
+ minimaxM3.transformModelOutputToUniEvent({
+ type: "response.output_item.done",
+ output_index: 0,
+ item: messageWireItem,
+ }),
+ ];
+ const replayMessage = minimaxM3.concatUniEventsToUniMessage(messageEvents);
+ expect(replayMessage.content_items).toEqual([
+ {
+ type: "text",
+ text: "exact response",
+ fidelity: { phase: "message-1" },
+ },
+ ]);
+ expect(minimaxM3.transformUniMessageToModelInput([replayMessage])).toEqual([
+ {
+ role: "assistant",
+ content: [{ type: "output_text", text: "exact response" }],
+ },
+ ]);
+
+ const secondFunctionWireItem = {
+ ...functionWireItem,
+ id: "function-2",
+ call_id: "call-2",
+ arguments: '{"城市": "北京"}',
+ };
+ const functionEvents = [functionWireItem, secondFunctionWireItem].map(
+ (wireItem, outputIndex) =>
+ minimaxM3.transformModelOutputToUniEvent({
+ type: "response.output_item.done",
+ output_index: outputIndex,
+ item: wireItem,
+ }),
+ );
+ const replayFunctions =
+ minimaxM3.concatUniEventsToUniMessage(functionEvents);
+ expect(replayFunctions.content_items).toEqual([
+ {
+ type: "tool_call",
+ name: "lookup",
+ arguments: { 城市: "上海" },
+ tool_call_id: "call-1",
+ fidelity: { arguments: '{"城市": "上海"}' },
+ },
+ {
+ type: "tool_call",
+ name: "lookup",
+ arguments: { 城市: "北京" },
+ tool_call_id: "call-2",
+ fidelity: { arguments: '{"城市": "北京"}' },
+ },
+ ]);
+ expect(
+ minimaxM3.transformUniMessageToModelInput([replayFunctions]),
+ ).toEqual([
+ {
+ type: "function_call",
+ call_id: "call-1",
+ name: "lookup",
+ arguments: '{"城市": "上海"}',
+ },
+ {
+ type: "function_call",
+ call_id: "call-2",
+ name: "lookup",
+ arguments: '{"城市": "北京"}',
+ },
+ ]);
+
+ const precisionArguments = '{"large":9007199254740993,"decimal":1.0}';
+ const precisionFunctionEvent = minimaxM3.transformModelOutputToUniEvent({
+ type: "response.output_item.done",
+ output_index: 0,
+ item: {
+ id: "function-precision",
+ status: "completed",
+ type: "function_call",
+ call_id: "call-precision",
+ name: "lookup",
+ arguments: precisionArguments,
+ },
+ });
+ const precisionMessage = minimaxM3.concatUniEventsToUniMessage([
+ precisionFunctionEvent,
+ ]);
+ expect(
+ minimaxM3.transformUniMessageToModelInput([precisionMessage]),
+ ).toEqual([
+ {
+ type: "function_call",
+ call_id: "call-precision",
+ name: "lookup",
+ arguments: precisionArguments,
+ },
+ ]);
+
+ const serializedArgumentsInput = minimaxM3.transformUniMessageToModelInput([
+ {
+ role: "assistant",
+ content_items: [
+ {
+ type: "tool_call",
+ name: "lookup",
+ arguments: { value: true },
+ tool_call_id: "call-bool",
+ fidelity: { arguments: '{"value":1}' },
+ },
+ ],
+ },
+ ]);
+ expect(serializedArgumentsInput[0].arguments).toBe('{"value":true}');
+
+ expect(() =>
+ minimaxM3.transformUniMessageToModelInput([
+ {
+ role: "assistant",
+ content_items: [
+ {
+ type: "thinking",
+ thinking: "invalid",
+ fidelity: {
+ wire_item: {
+ id: 123,
+ type: "reasoning",
+ summary: "invalid",
+ content: [],
+ },
+ },
+ },
+ ],
+ },
+ ]),
+ ).toThrow("valid id, summary, and content");
+
+ const completedEvent = minimaxM3.transformModelOutputToUniEvent({
+ type: "response.completed",
+ response: {
+ output: [functionWireItem, secondFunctionWireItem],
+ usage: {
+ input_tokens: 10,
+ input_tokens_details: { cached_tokens: 2 },
+ output_tokens: 6,
+ output_tokens_details: { reasoning_tokens: 1 },
+ },
+ },
+ });
+ expect(completedEvent.finish_reason).toBe("tool_call");
+ expect(completedEvent.usage_metadata).toEqual({
+ cached_tokens: 2,
+ prompt_tokens: 8,
+ thoughts_tokens: 1,
+ response_tokens: 5,
+ });
+
+ const partialStart = minimaxM3.transformModelOutputToUniEvent({
+ type: "response.output_item.added",
+ output_index: 3,
+ item: {
+ id: "function-partial",
+ type: "function_call",
+ call_id: "call-partial",
+ name: "lookup",
+ arguments: "",
+ },
+ });
+ const partialDelta = minimaxM3.transformModelOutputToUniEvent({
+ type: "response.function_call_arguments.delta",
+ item_id: "function-partial",
+ output_index: 3,
+ delta: '{"value":1}',
+ });
+ const partialStartItem = partialStart.content_items[0];
+ const partialDeltaItem = partialDelta.content_items[0];
+ expect(partialStartItem.type).toBe("partial_tool_call");
+ expect(partialDeltaItem.type).toBe("partial_tool_call");
+ if (
+ partialStartItem.type !== "partial_tool_call" ||
+ partialDeltaItem.type !== "partial_tool_call"
+ ) {
+ throw new Error("Expected MiniMax partial tool-call events.");
+ }
+ expect(partialStartItem.fidelity).toEqual({
+ item_id: "function-partial",
+ output_index: 3,
+ });
+ expect(partialDeltaItem.fidelity).toEqual({
+ item_id: "function-partial",
+ output_index: 3,
+ });
+
+ const incompleteEvent = minimaxM3.transformModelOutputToUniEvent({
+ type: "response.incomplete",
+ response: {
+ output: [],
+ usage: null,
+ incomplete_details: { reason: "max_output_tokens" },
+ },
+ });
+ expect(incompleteEvent.finish_reason).toBe("length");
+ expect(() =>
+ minimaxM3.transformModelOutputToUniEvent({
+ type: "response.failed",
+ response: {
+ id: "response-1",
+ error: { code: "provider_failure", message: "failed" },
+ },
+ }),
+ ).toThrow("provider_failure");
+ expect(() =>
+ minimaxM3.transformModelOutputToUniEvent({
+ type: "error",
+ code: "bad_request",
+ message: "invalid",
+ param: null,
+ }),
+ ).toThrow("bad_request");
+
+
for (const entry of entries) {
expect(entry.input_modalities.length).toBeGreaterThan(0);
expect(entry.output_modalities.length).toBeGreaterThan(0);