From 1db5a4378bd18cb1f7ace70060aff73623ebc36d Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 17 Jul 2026 23:13:29 -0500 Subject: [PATCH] test(ai): record MiniMax messages compatibility --- packages/ai/STATUS.md | 4 +- packages/ai/script/setup-recording-env.ts | 12 ++++ .../minimax-m3-anthropic-compatible-text.json | 40 +++++++++++++ ...max-m3-anthropic-compatible-tool-call.json | 41 +++++++++++++ ...max-m3-anthropic-compatible-tool-loop.json | 60 +++++++++++++++++++ .../ai/test/provider/golden.recorded.test.ts | 15 +++++ 6 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-text.json create mode 100644 packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-call.json create mode 100644 packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-loop.json diff --git a/packages/ai/STATUS.md b/packages/ai/STATUS.md index 85980d58cf91..24c98a426ac8 100644 --- a/packages/ai/STATUS.md +++ b/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. | diff --git a/packages/ai/script/setup-recording-env.ts b/packages/ai/script/setup-recording-env.ts index d32769b3cea5..281c9b8a8de5 100644 --- a/packages/ai/script/setup-recording-env.ts +++ b/packages/ai/script/setup-recording-env.ts @@ -161,6 +161,18 @@ const PROVIDERS: ReadonlyArray = [ 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", diff --git a/packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-text.json b/packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-text.json new file mode 100644 index 000000000000..cddad6972476 --- /dev/null +++ b/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" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-call.json b/packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-call.json new file mode 100644 index 000000000000..7e02d6e8cedd --- /dev/null +++ b/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" + } + } + ] +} diff --git a/packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-loop.json b/packages/ai/test/fixtures/recordings/anthropic-compatible-messages/minimax-m3-anthropic-compatible-tool-loop.json new file mode 100644 index 000000000000..e87e275c796a --- /dev/null +++ b/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" + } + } + ] +} diff --git a/packages/ai/test/provider/golden.recorded.test.ts b/packages/ai/test/provider/golden.recorded.test.ts index ae09cf7120fa..2ab4bd66c501 100644 --- a/packages/ai/test/provider/golden.recorded.test.ts +++ b/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",