Ver Fonte

test(ai): record MiniMax messages compatibility (#37604)

Aiden Cline há 4 semanas atrás
pai
commit
deb5b144c3

+ 2 - 2
packages/ai/STATUS.md

@@ -1,6 +1,6 @@
 # LLM Provider Parity Status
 
-Last reviewed: 2026-07-16
+Last reviewed: 2026-07-17
 
 This file tracks the gap between the native `@opencode-ai/ai` package and the AI SDK provider packages that opencode still depends on for many catalog/runtime paths.
 
@@ -20,7 +20,7 @@ This file tracks the gap between the native `@opencode-ai/ai` package and the AI
 | OpenAI Responses WebSocket         | `src/protocols/openai-responses.ts`, `src/route/transport/websocket.ts`                        | Present as `OpenAI.responsesWebSocket(...)`.                                                                                                                             | Runner/catalog support explicitly must not downgrade WebSocket routes; broader runtime selection is not complete.                                                                                                                                         |
 | OpenAI-compatible Chat             | `src/protocols/openai-compatible-chat.ts`, `src/providers/openai-compatible.ts`                | Usable for generic Chat and several profiles: Baseten, Cerebras, DeepInfra, DeepSeek, Fireworks, Groq, TogetherAI.                                                       | Family quirks are mostly endpoint defaults, not full typed behavior.                                                                                                                                                                                      |
 | OpenAI-compatible Responses        | `src/protocols/openai-compatible-responses.ts`, `src/providers/openai-compatible-responses.ts` | Usable for deployments that implement the OpenAI Responses wire protocol.                                                                                                | No named family profiles or recorded deployment coverage yet.                                                                                                                                                                                             |
-| Anthropic-compatible Messages      | `src/protocols/anthropic-messages.ts`, `src/providers/anthropic-compatible.ts`                 | Usable for deployments that implement the Anthropic Messages wire protocol. Named Anthropic composes this base.                                                          | No named compatible family profiles or recorded deployment coverage yet.                                                                                                                                                                                  |
+| Anthropic-compatible Messages      | `src/protocols/anthropic-messages.ts`, `src/providers/anthropic-compatible.ts`                 | Usable for deployments that implement the Anthropic Messages wire protocol. Named Anthropic composes this base; MiniMax M3 has recorded text and tool-loop coverage.       | No named compatible family profiles yet.                                                                                                                                                                                                                  |
 | Anthropic Messages                 | `src/protocols/anthropic-messages.ts`, `src/providers/anthropic.ts`                            | Usable. Supports tools, thinking, cache control, images, server-hosted tool events, and usage.                                                                           | Provider option surface is small. Beta/header handling, metadata, and newer Messages fields need a typed parity pass.                                                                                                                                     |
 | Gemini Developer API               | `src/protocols/gemini.ts`, `src/providers/google.ts`                                           | Usable for Google API key flow. Supports text, images, tools, thinking signatures, and cache usage.                                                                      | This is not Vertex. Typed provider options are narrow; many Gemini request fields currently require raw `http.body` overlays.                                                                                                                             |
 | Vertex Gemini                      | `src/protocols/gemini.ts`, `src/providers/google-vertex.ts`                                    | Usable through API-key express mode, explicit OAuth tokens, or ADC with project/location endpoint derivation, including tuned `endpoints/...` deployments.               | Core runner/catalog mapping and recorded provider coverage are missing.                                                                                                                                                                                   |

+ 12 - 0
packages/ai/script/setup-recording-env.ts

@@ -161,6 +161,18 @@ const PROVIDERS: ReadonlyArray<Provider> = [
     vars: [{ name: "TOGETHER_AI_API_KEY" }],
     validate: (env) => validateBearer("https://api.together.xyz/v1/models", Redacted.make(env.TOGETHER_AI_API_KEY)),
   },
+  {
+    id: "minimax",
+    label: "MiniMax",
+    tier: "compatible",
+    note: "Anthropic-compatible Messages text/tool recorded tests",
+    vars: [{ name: "MINIMAX_API_KEY" }],
+    validate: (env) =>
+      HttpClientRequest.get("https://api.minimax.io/anthropic/v1/models").pipe(
+        HttpClientRequest.setHeader("x-api-key", Redacted.value(Redacted.make(env.MINIMAX_API_KEY))),
+        executeRequest,
+      ),
+  },
   {
     id: "mistral",
     label: "Mistral",

+ 40 - 0
packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-text.json

@@ -0,0 +1,40 @@
+{
+  "version": 1,
+  "metadata": {
+    "provider": "minimax",
+    "protocol": "anthropic-messages",
+    "route": "anthropic-messages",
+    "transport": "http",
+    "model": "MiniMax-M3",
+    "tags": [
+      "prefix:anthropic-compatible-messages",
+      "provider:minimax",
+      "protocol:anthropic-messages",
+      "text",
+      "golden"
+    ],
+    "name": "anthropic-compatible-messages/minimax-m3-anthropic-compatible-text",
+    "recordedAt": "2026-07-18T03:42:22.893Z"
+  },
+  "interactions": [
+    {
+      "transport": "http",
+      "request": {
+        "method": "POST",
+        "url": "https://api.minimax.io/anthropic/v1/messages",
+        "headers": {
+          "anthropic-version": "2023-06-01",
+          "content-type": "application/json"
+        },
+        "body": "{\"model\":\"MiniMax-M3\",\"system\":[{\"type\":\"text\",\"text\":\"You are concise.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Reply exactly with: Hello!\"}]}],\"stream\":true,\"max_tokens\":40,\"temperature\":0}"
+      },
+      "response": {
+        "status": 200,
+        "headers": {
+          "content-type": "text/event-stream; charset=utf-8"
+        },
+        "body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"1a0b363d0882af316faebcec4d4855a8\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":53,\"output_tokens\":0,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":114,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"!\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"input_tokens\":53,\"output_tokens\":2,\"cache_read_input_tokens\":114,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
+      }
+    }
+  ]
+}

+ 41 - 0
packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-call.json

@@ -0,0 +1,41 @@
+{
+  "version": 1,
+  "metadata": {
+    "provider": "minimax",
+    "protocol": "anthropic-messages",
+    "route": "anthropic-messages",
+    "transport": "http",
+    "model": "MiniMax-M3",
+    "tags": [
+      "prefix:anthropic-compatible-messages",
+      "provider:minimax",
+      "protocol:anthropic-messages",
+      "tool",
+      "tool-call",
+      "golden"
+    ],
+    "name": "anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-call",
+    "recordedAt": "2026-07-18T03:42:23.876Z"
+  },
+  "interactions": [
+    {
+      "transport": "http",
+      "request": {
+        "method": "POST",
+        "url": "https://api.minimax.io/anthropic/v1/messages",
+        "headers": {
+          "anthropic-version": "2023-06-01",
+          "content-type": "application/json"
+        },
+        "body": "{\"model\":\"MiniMax-M3\",\"system\":[{\"type\":\"text\",\"text\":\"Call tools exactly as requested.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Call get_weather with city exactly Paris.\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"tool_choice\":{\"type\":\"tool\",\"name\":\"get_weather\"},\"stream\":true,\"max_tokens\":80,\"temperature\":0}"
+      },
+      "response": {
+        "status": 200,
+        "headers": {
+          "content-type": "text/event-stream; charset=utf-8"
+        },
+        "body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"6731ecc323233459d1792df9a733dd98\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":404,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"call_function_vkxtif4epmvm_1\",\"name\":\"get_weather\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\": \\\"Paris\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"input_tokens\":290,\"output_tokens\":27,\"cache_read_input_tokens\":114,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
+      }
+    }
+  ]
+}

+ 60 - 0
packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-loop.json

@@ -0,0 +1,60 @@
+{
+  "version": 1,
+  "metadata": {
+    "provider": "minimax",
+    "protocol": "anthropic-messages",
+    "route": "anthropic-messages",
+    "transport": "http",
+    "model": "MiniMax-M3",
+    "tags": [
+      "prefix:anthropic-compatible-messages",
+      "provider:minimax",
+      "protocol:anthropic-messages",
+      "tool",
+      "tool-loop",
+      "golden"
+    ],
+    "name": "anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-loop",
+    "recordedAt": "2026-07-18T03:42:25.248Z"
+  },
+  "interactions": [
+    {
+      "transport": "http",
+      "request": {
+        "method": "POST",
+        "url": "https://api.minimax.io/anthropic/v1/messages",
+        "headers": {
+          "anthropic-version": "2023-06-01",
+          "content-type": "application/json"
+        },
+        "body": "{\"model\":\"MiniMax-M3\",\"system\":[{\"type\":\"text\",\"text\":\"Use the get_weather tool exactly once. After the tool result, reply exactly: Paris is sunny.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"stream\":true,\"max_tokens\":80,\"temperature\":0}"
+      },
+      "response": {
+        "status": 200,
+        "headers": {
+          "content-type": "text/event-stream; charset=utf-8"
+        },
+        "body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"3807fa12f9ecb9357df511e099da6da0\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":417,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"call_function_yr64rwmre4gr_1\",\"name\":\"get_weather\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\": \\\"Paris\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\"}\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"input_tokens\":303,\"output_tokens\":27,\"cache_read_input_tokens\":114,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
+      }
+    },
+    {
+      "transport": "http",
+      "request": {
+        "method": "POST",
+        "url": "https://api.minimax.io/anthropic/v1/messages",
+        "headers": {
+          "anthropic-version": "2023-06-01",
+          "content-type": "application/json"
+        },
+        "body": "{\"model\":\"MiniMax-M3\",\"system\":[{\"type\":\"text\",\"text\":\"Use the get_weather tool exactly once. After the tool result, reply exactly: Paris is sunny.\"}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"call_function_yr64rwmre4gr_1\",\"name\":\"get_weather\",\"input\":{\"city\":\"Paris\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"call_function_yr64rwmre4gr_1\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"stream\":true,\"max_tokens\":80,\"temperature\":0}"
+      },
+      "response": {
+        "status": 200,
+        "headers": {
+          "content-type": "text/event-stream; charset=utf-8"
+        },
+        "body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"92f8a1e86f29946eb2699d40a088fc08\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"MiniMax-M3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":41,\"output_tokens\":0,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":430,\"service_tier\":\"standard\"},\"service_tier\":\"standard\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Paris\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" is sunny.\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"input_tokens\":41,\"output_tokens\":4,\"cache_read_input_tokens\":430,\"service_tier\":\"standard\"}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"
+      }
+    }
+  ]
+}

+ 15 - 0
packages/ai/test/provider/golden.recorded.test.ts

@@ -1,4 +1,5 @@
 import * as Anthropic from "../../src/providers/anthropic"
+import * as AnthropicCompatible from "../../src/providers/anthropic-compatible"
 import { CloudflareAIGateway, CloudflareWorkersAI } from "../../src/providers/cloudflare"
 import * as Google from "../../src/providers/google"
 import * as OpenAI from "../../src/providers/openai"
@@ -17,6 +18,11 @@ const anthropic = Anthropic.configure({
 })
 const anthropicHaiku = anthropic.model("claude-haiku-4-5-20251001")
 const anthropicOpus = anthropic.model("claude-opus-4-7")
+const minimax = AnthropicCompatible.configure({
+  apiKey: process.env.MINIMAX_API_KEY ?? "fixture",
+  baseURL: "https://api.minimax.io/anthropic/v1",
+  provider: "minimax",
+}).model("MiniMax-M3")
 const google = Google.configure({ apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY ?? "fixture" })
 const gemini = google.model("gemini-2.5-flash")
 const xai = XAI.configure({ apiKey: process.env.XAI_API_KEY ?? "fixture" })
@@ -108,6 +114,15 @@ describeRecordedGoldenScenarios([
       { id: "image-tool-result", temperature: false, maxTokens: 40 },
     ],
   },
+  {
+    name: "MiniMax M3 Anthropic-compatible",
+    prefix: "anthropic-compatible-messages",
+    protocol: "anthropic-messages",
+    model: minimax,
+    requires: ["MINIMAX_API_KEY"],
+    options: { redact: { allowRequestHeaders: ["anthropic-version"] } },
+    scenarios: ["text", "tool-call", "tool-loop"],
+  },
   {
     name: "Gemini 2.5 Flash",
     prefix: "gemini",