From 0369672691cfb2b37f6f34e5e86f79c7620d6366 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 31 May 2026 21:47:47 -0400 Subject: [PATCH] feat(minimax): add m3 model support (#88860) --- .../openclaw-live-and-e2e-checks-reusable.yml | 4 +- docs/concepts/model-providers.md | 6 +- docs/gateway/config-tools.md | 18 ++--- docs/help/faq-models.md | 11 ++-- docs/help/testing-live.md | 12 ++-- docs/providers/minimax.md | 43 +++++++----- docs/reference/wizard.md | 2 +- docs/start/wizard-cli-reference.md | 2 +- extensions/minimax/index.test.ts | 66 ++++++++++++++++--- extensions/minimax/model-definitions.test.ts | 46 ++++++++----- extensions/minimax/model-definitions.ts | 16 ++++- extensions/minimax/onboard.test.ts | 12 ++-- extensions/minimax/onboard.ts | 9 +-- extensions/minimax/openclaw.plugin.json | 8 +-- extensions/minimax/provider-catalog.ts | 12 ++-- extensions/minimax/provider-contract-api.ts | 2 +- extensions/minimax/provider-models.ts | 30 +++++++-- extensions/minimax/provider-registration.ts | 56 ++++++++++++++-- src/agents/live-model-filter.ts | 4 +- src/agents/minimax-docs.test.ts | 2 +- src/agents/minimax.live.test.ts | 6 +- src/agents/model-compat.test.ts | 9 +-- .../models-config.providers.minimax.test.ts | 11 ++++ .../provider-discovery-contract.ts | 1 + .../package-acceptance-workflow.test.ts | 6 +- 25 files changed, 279 insertions(+), 115 deletions(-) diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index ede7ef5f6417..a6bd4bcac3d9 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -1953,7 +1953,7 @@ jobs: profiles: stable full - suite_id: native-live-src-gateway-profiles-minimax label: Native live gateway profiles MiniMax - command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M2.7,minimax-portal/MiniMax-M2.7 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles + command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M3,minimax-portal/MiniMax-M3 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles timeout_minutes: 60 profile_env_only: false profiles: stable full @@ -2252,7 +2252,7 @@ jobs: profiles: stable full - suite_id: live-gateway-minimax-docker label: Docker live gateway MiniMax - command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M2.7,minimax-portal/MiniMax-M2.7 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh + command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M3,minimax-portal/MiniMax-M3 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh timeout_minutes: 40 profile_env_only: false profiles: stable full diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index 021210d06ec4..8af22bee223e 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -303,7 +303,7 @@ See [/providers/kilocode](/providers/kilocode) for setup details. | Hugging Face Inference | `huggingface` | `HUGGINGFACE_HUB_TOKEN` or `HF_TOKEN` | `huggingface/deepseek-ai/DeepSeek-R1` | | Kilo Gateway | `kilocode` | `KILOCODE_API_KEY` | `kilocode/kilo/auto` | | Kimi Coding | `kimi` | `KIMI_API_KEY` or `KIMICODE_API_KEY` | `kimi/kimi-for-coding` | -| MiniMax | `minimax` / `minimax-portal` | `MINIMAX_API_KEY` / `MINIMAX_OAUTH_TOKEN` | `minimax/MiniMax-M2.7` | +| MiniMax | `minimax` / `minimax-portal` | `MINIMAX_API_KEY` / `MINIMAX_OAUTH_TOKEN` | `minimax/MiniMax-M3` | | Mistral | `mistral` | `MISTRAL_API_KEY` | `mistral/mistral-large-latest` | | Moonshot | `moonshot` | `MOONSHOT_API_KEY` | `moonshot/kimi-k2.6` | | NVIDIA | `nvidia` | `NVIDIA_API_KEY` | `nvidia/nvidia/nemotron-3-super-120b-a12b` | @@ -331,7 +331,7 @@ See [/providers/kilocode](/providers/kilocode) for setup details. Gemini-backed refs follow the same proxy-Gemini sanitation path; `kilocode/kilo/auto` and other proxy-reasoning-unsupported refs skip proxy reasoning injection. - API-key onboarding writes explicit text-only M2.7 chat model definitions; image understanding stays on the plugin-owned `MiniMax-VL-01` media provider. + API-key onboarding writes explicit M3 and M2.7 chat model definitions; image understanding stays on the plugin-owned `MiniMax-VL-01` media provider. Model ids use a `nvidia//` namespace (for example `nvidia/nvidia/nemotron-...` alongside `nvidia/moonshotai/kimi-k2.5`); pickers preserve the literal `/` composition while the canonical key sent to the API stays single-prefixed. @@ -537,7 +537,7 @@ On MiniMax's Anthropic-compatible streaming path, OpenClaw disables thinking by Plugin-owned capability split: -- Text/chat defaults stay on `minimax/MiniMax-M2.7` +- Text/chat defaults stay on `minimax/MiniMax-M3` - Image generation is `minimax/image-01` or `minimax-portal/image-01` - Image understanding is plugin-owned `MiniMax-VL-01` on both MiniMax auth paths - Web search stays on provider id `minimax` diff --git a/docs/gateway/config-tools.md b/docs/gateway/config-tools.md index 1dadc87d1d4e..472b2d284ee7 100644 --- a/docs/gateway/config-tools.md +++ b/docs/gateway/config-tools.md @@ -645,14 +645,14 @@ Interactive custom-provider onboarding infers image input for common vision mode See [Local Models](/gateway/local-models). TL;DR: run a large local model via LM Studio Responses API on serious hardware; keep hosted models merged for fallback. - + ```json5 { agents: { defaults: { - model: { primary: "minimax/MiniMax-M2.7" }, + model: { primary: "minimax/MiniMax-M3" }, models: { - "minimax/MiniMax-M2.7": { alias: "Minimax" }, + "minimax/MiniMax-M3": { alias: "Minimax" }, }, }, }, @@ -665,12 +665,12 @@ Interactive custom-provider onboarding infers image input for common vision mode api: "anthropic-messages", models: [ { - id: "MiniMax-M2.7", - name: "MiniMax M2.7", + id: "MiniMax-M3", + name: "MiniMax M3", reasoning: true, - input: ["text"], - cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 }, - contextWindow: 204800, + input: ["text", "image"], + cost: { input: 0.6, output: 2.4, cacheRead: 0.12, cacheWrite: 0 }, + contextWindow: 1000000, maxTokens: 131072, }, ], @@ -680,7 +680,7 @@ Interactive custom-provider onboarding infers image input for common vision mode } ``` - Set `MINIMAX_API_KEY`. Shortcuts: `openclaw onboard --auth-choice minimax-global-api` or `openclaw onboard --auth-choice minimax-cn-api`. The model catalog defaults to M2.7 only. On the Anthropic-compatible streaming path, OpenClaw disables MiniMax thinking by default unless you explicitly set `thinking` yourself. `/fast on` or `params.fastMode: true` rewrites `MiniMax-M2.7` to `MiniMax-M2.7-highspeed`. + Set `MINIMAX_API_KEY`. Shortcuts: `openclaw onboard --auth-choice minimax-global-api` or `openclaw onboard --auth-choice minimax-cn-api`. The model catalog defaults to M3 and also includes the M2.7 variants. On the Anthropic-compatible streaming path, OpenClaw disables MiniMax thinking by default unless you explicitly set `thinking` yourself. `/fast on` or `params.fastMode: true` rewrites `MiniMax-M2.7` to `MiniMax-M2.7-highspeed`. diff --git a/docs/help/faq-models.md b/docs/help/faq-models.md index e2dfcc31c51a..791b2c1236f4 100644 --- a/docs/help/faq-models.md +++ b/docs/help/faq-models.md @@ -215,7 +215,7 @@ troubleshooting, see the main [FAQ](/help/faq). - + This means the **provider isn't configured** (no MiniMax provider config or auth profile was found), so the model can't be resolved. @@ -227,8 +227,9 @@ troubleshooting, see the main [FAQ](/help/faq). (`MINIMAX_API_KEY` for `minimax`, `MINIMAX_OAUTH_TOKEN` or stored MiniMax OAuth for `minimax-portal`). 3. Use the exact model id (case-sensitive) for your auth path: - `minimax/MiniMax-M2.7` or `minimax/MiniMax-M2.7-highspeed` for API-key - setup, or `minimax-portal/MiniMax-M2.7` / + `minimax/MiniMax-M3`, `minimax/MiniMax-M2.7`, or + `minimax/MiniMax-M2.7-highspeed` for API-key setup, or + `minimax-portal/MiniMax-M3`, `minimax-portal/MiniMax-M2.7`, or `minimax-portal/MiniMax-M2.7-highspeed` for OAuth setup. 4. Run: @@ -253,9 +254,9 @@ troubleshooting, see the main [FAQ](/help/faq). env: { MINIMAX_API_KEY: "sk-...", OPENAI_API_KEY: "sk-..." }, agents: { defaults: { - model: { primary: "minimax/MiniMax-M2.7" }, + model: { primary: "minimax/MiniMax-M3" }, models: { - "minimax/MiniMax-M2.7": { alias: "minimax" }, + "minimax/MiniMax-M3": { alias: "minimax" }, "openai/gpt-5.5": { alias: "gpt" }, }, }, diff --git a/docs/help/testing-live.md b/docs/help/testing-live.md index de42bba84070..a5d0ca913d94 100644 --- a/docs/help/testing-live.md +++ b/docs/help/testing-live.md @@ -73,7 +73,7 @@ Live tests are split into two layers so we can isolate failures: - `pnpm test:live` (or `OPENCLAW_LIVE_TEST=1` if invoking Vitest directly) - Set `OPENCLAW_LIVE_MODELS=modern`, `small`, or `all` (alias for modern) to actually run this suite; otherwise it skips to keep `pnpm test:live` focused on gateway smoke - How to select models: - - `OPENCLAW_LIVE_MODELS=modern` to run the modern allowlist (Opus/Sonnet 4.6+, GPT-5.2 + Codex, Gemini 3, DeepSeek V4, GLM 4.7, MiniMax M2.7, Grok 4.3) + - `OPENCLAW_LIVE_MODELS=modern` to run the modern allowlist (Opus/Sonnet 4.6+, GPT-5.2 + Codex, Gemini 3, DeepSeek V4, GLM 4.7, MiniMax M3, Grok 4.3) - `OPENCLAW_LIVE_MODELS=small` to run the constrained small-model allowlist (Qwen 8B/9B local-compatible routes, Ollama Gemma, OpenRouter Qwen/GLM, and Z.AI GLM) - `OPENCLAW_LIVE_MODELS=all` is an alias for the modern allowlist - or `OPENCLAW_LIVE_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,..."` (comma allowlist) @@ -109,7 +109,7 @@ Live tests are split into two layers so we can isolate failures: - How to enable: - `pnpm test:live` (or `OPENCLAW_LIVE_TEST=1` if invoking Vitest directly) - How to select models: - - Default: modern allowlist (Opus/Sonnet 4.6+, GPT-5.2 + Codex, Gemini 3, DeepSeek V4, GLM 4.7, MiniMax M2.7, Grok 4.3) + - Default: modern allowlist (Opus/Sonnet 4.6+, GPT-5.2 + Codex, Gemini 3, DeepSeek V4, GLM 4.7, MiniMax M3, Grok 4.3) - `OPENCLAW_LIVE_GATEWAY_MODELS=all` is an alias for the modern allowlist - Or set `OPENCLAW_LIVE_GATEWAY_MODELS="provider/model"` (or comma list) to narrow - Modern/all gateway sweeps default to a curated high-signal cap; set `OPENCLAW_LIVE_GATEWAY_MAX_MODELS=0` for an exhaustive modern sweep or a positive number for a smaller cap. @@ -351,7 +351,7 @@ Narrow, explicit allowlists are fastest and least flaky: - `OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` - Tool calling across several providers: - - `OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,google/gemini-3-flash-preview,deepseek/deepseek-v4-flash,zai/glm-5.1,minimax/MiniMax-M2.7" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` + - `OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,google/gemini-3-flash-preview,deepseek/deepseek-v4-flash,zai/glm-5.1,minimax/MiniMax-M3" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` - Google focus (Gemini API key + Antigravity): - Gemini (API key): `OPENCLAW_LIVE_GATEWAY_MODELS="google/gemini-3-flash-preview" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` @@ -385,10 +385,10 @@ This is the "common models" run we expect to keep working: - Google (Antigravity): `google-antigravity/claude-opus-4-6-thinking` and `google-antigravity/gemini-3-flash` - DeepSeek: `deepseek/deepseek-v4-flash` and `deepseek/deepseek-v4-pro` - Z.AI (GLM): `zai/glm-5.1` -- MiniMax: `minimax/MiniMax-M2.7` +- MiniMax: `minimax/MiniMax-M3` Run gateway smoke with tools + image: -`OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,google/gemini-3.1-pro-preview,google/gemini-3-flash-preview,google-antigravity/claude-opus-4-6-thinking,google-antigravity/gemini-3-flash,deepseek/deepseek-v4-flash,zai/glm-5.1,minimax/MiniMax-M2.7" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` +`OPENCLAW_LIVE_GATEWAY_MODELS="openai/gpt-5.5,anthropic/claude-opus-4-6,google/gemini-3.1-pro-preview,google/gemini-3-flash-preview,google-antigravity/claude-opus-4-6-thinking,google-antigravity/gemini-3-flash,deepseek/deepseek-v4-flash,zai/glm-5.1,minimax/MiniMax-M3" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` ### Baseline: tool calling (Read + optional Exec) @@ -399,7 +399,7 @@ Pick at least one per provider family: - Google: `google/gemini-3-flash-preview` (or `google/gemini-3.1-pro-preview`) - DeepSeek: `deepseek/deepseek-v4-flash` - Z.AI (GLM): `zai/glm-5.1` -- MiniMax: `minimax/MiniMax-M2.7` +- MiniMax: `minimax/MiniMax-M3` Optional additional coverage (nice to have): diff --git a/docs/providers/minimax.md b/docs/providers/minimax.md index 83d3d353918f..ca871408dd3c 100644 --- a/docs/providers/minimax.md +++ b/docs/providers/minimax.md @@ -6,7 +6,7 @@ read_when: title: "MiniMax" --- -OpenClaw's MiniMax provider defaults to **MiniMax M2.7**. +OpenClaw's MiniMax provider defaults to **MiniMax M3**. MiniMax also provides: @@ -26,7 +26,8 @@ Provider split: | Model | Type | Description | | ------------------------ | ---------------- | ---------------------------------------- | -| `MiniMax-M2.7` | Chat (reasoning) | Default hosted reasoning model | +| `MiniMax-M3` | Chat (reasoning) | Default hosted reasoning model | +| `MiniMax-M2.7` | Chat (reasoning) | Previous hosted reasoning model | | `MiniMax-M2.7-highspeed` | Chat (reasoning) | Faster M2.7 reasoning tier | | `MiniMax-VL-01` | Vision | Image understanding model | | `image-01` | Image generation | Text-to-image and image-to-image editing | @@ -79,7 +80,7 @@ Choose your preferred auth method and follow the setup steps. - OAuth setups use the `minimax-portal` provider id. Model refs follow the form `minimax-portal/MiniMax-M2.7`. + OAuth setups use the `minimax-portal` provider id. Model refs follow the form `minimax-portal/MiniMax-M3`. @@ -131,7 +132,7 @@ Choose your preferred auth method and follow the setup steps. ```json5 { env: { MINIMAX_API_KEY: "sk-..." }, - agents: { defaults: { model: { primary: "minimax/MiniMax-M2.7" } } }, + agents: { defaults: { model: { primary: "minimax/MiniMax-M3" } } }, models: { mode: "merge", providers: { @@ -140,6 +141,15 @@ Choose your preferred auth method and follow the setup steps. apiKey: "${MINIMAX_API_KEY}", api: "anthropic-messages", models: [ + { + id: "MiniMax-M3", + name: "MiniMax M3", + reasoning: true, + input: ["text", "image"], + cost: { input: 0.6, output: 2.4, cacheRead: 0.12, cacheWrite: 0 }, + contextWindow: 1000000, + maxTokens: 131072, + }, { id: "MiniMax-M2.7", name: "MiniMax M2.7", @@ -170,7 +180,7 @@ Choose your preferred auth method and follow the setup steps. - API-key setups use the `minimax` provider id. Model refs follow the form `minimax/MiniMax-M2.7`. + API-key setups use the `minimax` provider id. Model refs follow the form `minimax/MiniMax-M3`. @@ -243,9 +253,10 @@ through the CN endpoint; the default global endpoint is `https://api.minimax.io`. When onboarding or API-key setup writes explicit `models.providers.minimax` -entries, OpenClaw materializes `MiniMax-M2.7` and -`MiniMax-M2.7-highspeed` as text-only chat models. Image understanding is -exposed separately through the plugin-owned `MiniMax-VL-01` media provider. +entries, OpenClaw materializes `MiniMax-M3`, `MiniMax-M2.7`, and +`MiniMax-M2.7-highspeed` as chat models. M3 advertises text and image input; +image understanding remains exposed separately through the plugin-owned +`MiniMax-VL-01` media provider. See [Image Generation](/tools/image-generation) for shared tool parameters, provider selection, and failover behavior. @@ -353,7 +364,7 @@ catalog: | `minimax-portal` | `MiniMax-VL-01` | That is why automatic media routing can use MiniMax image understanding even -when the bundled text-provider catalog still shows text-only M2.7 chat refs. +when the bundled text-provider catalog also includes M3 image-capable chat refs. ### Web search @@ -437,12 +448,12 @@ See [MiniMax Search](/tools/minimax-search) for full web search configuration an - Model refs follow the auth path: - API-key setup: `minimax/` - OAuth setup: `minimax-portal/` -- Default chat model: `MiniMax-M2.7` -- Alternate chat model: `MiniMax-M2.7-highspeed` -- Onboarding and direct API-key setup write text-only model definitions for both M2.7 variants +- Default chat model: `MiniMax-M3` +- Alternate chat models: `MiniMax-M2.7`, `MiniMax-M2.7-highspeed` +- Onboarding and direct API-key setup write model definitions for M3 and both M2.7 variants - Image understanding uses the plugin-owned `MiniMax-VL-01` media provider - Update pricing values in `models.json` if you need exact cost tracking -- Use `openclaw models list` to confirm the current provider id, then switch with `openclaw models set minimax/MiniMax-M2.7` or `openclaw models set minimax-portal/MiniMax-M2.7` +- Use `openclaw models list` to confirm the current provider id, then switch with `openclaw models set minimax/MiniMax-M3` or `openclaw models set minimax-portal/MiniMax-M3` Referral link for MiniMax Coding Plan (10% off): [MiniMax Coding Plan](https://platform.minimax.io/subscribe/coding-plan?code=DbXJTRClnb&source=link) @@ -455,7 +466,7 @@ See [Model providers](/concepts/model-providers) for provider rules. ## Troubleshooting - + This usually means the **MiniMax provider is not configured** (no matching provider entry and no MiniMax auth profile/env key found). A fix for this detection is in **2026.1.12**. Fix by: - Upgrading to **2026.1.12** (or run from source `main`), then restarting the gateway. @@ -465,8 +476,8 @@ See [Model providers](/concepts/model-providers) for provider rules. Make sure the model id is **case-sensitive**: - - API-key path: `minimax/MiniMax-M2.7` or `minimax/MiniMax-M2.7-highspeed` - - OAuth path: `minimax-portal/MiniMax-M2.7` or `minimax-portal/MiniMax-M2.7-highspeed` + - API-key path: `minimax/MiniMax-M3`, `minimax/MiniMax-M2.7`, or `minimax/MiniMax-M2.7-highspeed` + - OAuth path: `minimax-portal/MiniMax-M3`, `minimax-portal/MiniMax-M2.7`, or `minimax-portal/MiniMax-M2.7-highspeed` Then recheck with: diff --git a/docs/reference/wizard.md b/docs/reference/wizard.md index f385567bc2bb..6a2fa4a89cbf 100644 --- a/docs/reference/wizard.md +++ b/docs/reference/wizard.md @@ -47,7 +47,7 @@ For a high-level overview, see [Onboarding (CLI)](/start/wizard). - More detail: [Vercel AI Gateway](/providers/vercel-ai-gateway) - **Cloudflare AI Gateway**: prompts for Account ID, Gateway ID, and `CLOUDFLARE_AI_GATEWAY_API_KEY`. - More detail: [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway) - - **MiniMax**: config is auto-written; hosted default is `MiniMax-M2.7`. + - **MiniMax**: config is auto-written; hosted default is `MiniMax-M3`. API-key setup uses `minimax/...`, and OAuth setup uses `minimax-portal/...`. - More detail: [MiniMax](/providers/minimax) diff --git a/docs/start/wizard-cli-reference.md b/docs/start/wizard-cli-reference.md index 29ee25327dc4..7b04eb98d9ec 100644 --- a/docs/start/wizard-cli-reference.md +++ b/docs/start/wizard-cli-reference.md @@ -182,7 +182,7 @@ What you set: More detail: [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway). - Config is auto-written. Hosted default is `MiniMax-M2.7`; API-key setup uses + Config is auto-written. Hosted default is `MiniMax-M3`; API-key setup uses `minimax/...`, and OAuth setup uses `minimax-portal/...`. More detail: [MiniMax](/providers/minimax). diff --git a/extensions/minimax/index.test.ts b/extensions/minimax/index.test.ts index 4f2c6415f878..f3b8a821ff71 100644 --- a/extensions/minimax/index.test.ts +++ b/extensions/minimax/index.test.ts @@ -6,7 +6,7 @@ import { registerProviderPlugin, requireRegisteredProvider, } from "openclaw/plugin-sdk/plugin-test-runtime"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { registerMinimaxProviders } from "./provider-registration.js"; import { createMiniMaxWebSearchProvider } from "./src/minimax-web-search-provider.js"; @@ -26,6 +26,10 @@ const minimaxProviderPlugin = { }, }; +afterEach(() => { + vi.unstubAllEnvs(); +}); + describe("minimax provider hooks", () => { it("declares CN provider auth aliases in the manifest", () => { const pluginJson = JSON.parse( @@ -91,7 +95,7 @@ describe("minimax provider hooks", () => { hint: "Global endpoint - api.minimax.io", choiceId: "minimax-global-api", groupId: "minimax", - groupHint: "M2.7 (recommended)", + groupHint: "M3 (recommended)", }, { id: "api-cn", @@ -99,7 +103,7 @@ describe("minimax provider hooks", () => { hint: "CN endpoint - api.minimaxi.com", choiceId: "minimax-cn-api", groupId: "minimax", - groupHint: "M2.7 (recommended)", + groupHint: "M3 (recommended)", }, ]); @@ -119,7 +123,7 @@ describe("minimax provider hooks", () => { hint: "Global endpoint - api.minimax.io", choiceId: "minimax-global-oauth", groupId: "minimax", - groupHint: "M2.7 (recommended)", + groupHint: "M3 (recommended)", }, { id: "oauth-cn", @@ -127,7 +131,7 @@ describe("minimax provider hooks", () => { hint: "CN endpoint - api.minimaxi.com", choiceId: "minimax-cn-oauth", groupId: "minimax", - groupHint: "M2.7 (recommended)", + groupHint: "M3 (recommended)", }, ]); }); @@ -173,7 +177,7 @@ describe("minimax provider hooks", () => { }); }); - it("keeps M2.7 on the Anthropic Messages route used by the empty-history guard", async () => { + it("lists M3 on the Anthropic Messages route used by the empty-history guard", async () => { const { providers } = await registerProviderPlugin({ plugin: minimaxProviderPlugin, id: "minimax", @@ -193,13 +197,55 @@ describe("minimax provider hooks", () => { expect(provider?.api).toBe("anthropic-messages"); expect(provider?.authHeader).toBe(true); expect(provider?.baseUrl).toBe("https://api.minimax.io/anthropic"); - const model = provider?.models.find((entry: { id?: string }) => entry.id === "MiniMax-M2.7"); - expect(model?.id).toBe("MiniMax-M2.7"); - expect(model?.input).toEqual(["text"]); - expect(model?.name).toBe("MiniMax M2.7"); + const model = provider?.models.find((entry: { id?: string }) => entry.id === "MiniMax-M3"); + expect(model?.id).toBe("MiniMax-M3"); + expect(model?.input).toEqual(["text", "image"]); + expect(model?.name).toBe("MiniMax M3"); expect(model?.reasoning).toBe(true); }); + it("resolves M3 through the dynamic model hook before agent discovery", async () => { + const { providers } = await registerProviderPlugin({ + plugin: minimaxProviderPlugin, + id: "minimax", + name: "MiniMax Provider", + }); + const apiProvider = requireRegisteredProvider(providers, "minimax"); + + const model = apiProvider.resolveDynamicModel?.({ + provider: "minimax", + modelId: "MiniMax-M3", + providerConfig: {}, + } as never); + + expect(model).toMatchObject({ + provider: "minimax", + id: "MiniMax-M3", + api: "anthropic-messages", + baseUrl: "https://api.minimax.io/anthropic", + input: ["text", "image"], + contextWindow: 1_000_000, + }); + }); + + it("keeps MINIMAX_API_HOST endpoint overrides on dynamic M3 resolution", async () => { + vi.stubEnv("MINIMAX_API_HOST", "https://api.minimaxi.com"); + const { providers } = await registerProviderPlugin({ + plugin: minimaxProviderPlugin, + id: "minimax", + name: "MiniMax Provider", + }); + const apiProvider = requireRegisteredProvider(providers, "minimax"); + + const model = apiProvider.resolveDynamicModel?.({ + provider: "minimax", + modelId: "MiniMax-M3", + providerConfig: {}, + } as never); + + expect(model?.baseUrl).toBe("https://api.minimaxi.com/anthropic"); + }); + it("owns fast-mode stream wrapping for MiniMax transports", async () => { const { providers } = await registerProviderPlugin({ plugin: minimaxProviderPlugin, diff --git a/extensions/minimax/model-definitions.test.ts b/extensions/minimax/model-definitions.test.ts index cf014420fbf4..cb7a39e9d29d 100644 --- a/extensions/minimax/model-definitions.test.ts +++ b/extensions/minimax/model-definitions.test.ts @@ -7,40 +7,43 @@ import { MINIMAX_API_COST, MINIMAX_API_HIGHSPEED_COST, MINIMAX_HOSTED_MODEL_ID, + MINIMAX_M27_API_COST, MINIMAX_M25_API_COST, MINIMAX_M25_API_HIGHSPEED_COST, + MINIMAX_M3_CONTEXT_WINDOW, } from "./model-definitions.js"; describe("minimax model definitions", () => { - it("uses M2.7 as default hosted model", () => { - expect(MINIMAX_HOSTED_MODEL_ID).toBe("MiniMax-M2.7"); + it("uses M3 as default hosted model", () => { + expect(MINIMAX_HOSTED_MODEL_ID).toBe("MiniMax-M3"); }); - it("uses the higher upstream MiniMax context and token defaults", () => { + it("uses the current upstream MiniMax context, token, and pricing defaults", () => { + expect(MINIMAX_M3_CONTEXT_WINDOW).toBe(1_000_000); expect(DEFAULT_MINIMAX_CONTEXT_WINDOW).toBe(204800); expect(DEFAULT_MINIMAX_MAX_TOKENS).toBe(131072); expect(MINIMAX_API_COST).toEqual({ - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0.375, + input: 0.6, + output: 2.4, + cacheRead: 0.12, + cacheWrite: 0, }); }); - it("builds catalog model with name and reasoning from catalog for M2.7", () => { + it("builds catalog model with M3 metadata from the catalog", () => { const model = buildMinimaxModelDefinition({ - id: "MiniMax-M2.7", + id: "MiniMax-M3", cost: MINIMAX_API_COST, - contextWindow: DEFAULT_MINIMAX_CONTEXT_WINDOW, + contextWindow: MINIMAX_M3_CONTEXT_WINDOW, maxTokens: DEFAULT_MINIMAX_MAX_TOKENS, }); expect(model).toEqual({ - contextWindow: DEFAULT_MINIMAX_CONTEXT_WINDOW, + contextWindow: MINIMAX_M3_CONTEXT_WINDOW, cost: MINIMAX_API_COST, - id: "MiniMax-M2.7", - input: ["text"], + id: "MiniMax-M3", + input: ["text", "image"], maxTokens: DEFAULT_MINIMAX_MAX_TOKENS, - name: "MiniMax M2.7", + name: "MiniMax M3", reasoning: true, }); }); @@ -63,12 +66,12 @@ describe("minimax model definitions", () => { }); }); - it("builds API model definition with standard cost for M2.7", () => { - const model = buildMinimaxApiModelDefinition("MiniMax-M2.7"); + it("builds API model definition with standard cost for M3", () => { + const model = buildMinimaxApiModelDefinition("MiniMax-M3"); expect(model.cost).toEqual(MINIMAX_API_COST); - expect(model.contextWindow).toBe(DEFAULT_MINIMAX_CONTEXT_WINDOW); + expect(model.contextWindow).toBe(MINIMAX_M3_CONTEXT_WINDOW); expect(model.maxTokens).toBe(DEFAULT_MINIMAX_MAX_TOKENS); - expect(model.input).toEqual(["text"]); + expect(model.input).toEqual(["text", "image"]); }); it("falls back to generated name for unknown model id", () => { @@ -77,6 +80,13 @@ describe("minimax model definitions", () => { expect(model.reasoning).toBe(false); }); + it("keeps M2.7 on its existing price and text-only metadata", () => { + const model = buildMinimaxApiModelDefinition("MiniMax-M2.7"); + expect(model.input).toEqual(["text"]); + expect(model.cost).toEqual(MINIMAX_M27_API_COST); + expect(model.contextWindow).toBe(DEFAULT_MINIMAX_CONTEXT_WINDOW); + }); + it("keeps M2.7 text-only on the Anthropic-compatible chat path", () => { const model = buildMinimaxApiModelDefinition("MiniMax-M2.7"); expect(model.input).toEqual(["text"]); diff --git a/extensions/minimax/model-definitions.ts b/extensions/minimax/model-definitions.ts index 2ce490c10cb2..55288d41978b 100644 --- a/extensions/minimax/model-definitions.ts +++ b/extensions/minimax/model-definitions.ts @@ -7,9 +7,16 @@ export const MINIMAX_CN_API_BASE_URL = "https://api.minimaxi.com/anthropic"; export const MINIMAX_HOSTED_MODEL_ID = MINIMAX_DEFAULT_MODEL_ID; export const MINIMAX_HOSTED_MODEL_REF = `minimax/${MINIMAX_HOSTED_MODEL_ID}`; export const DEFAULT_MINIMAX_CONTEXT_WINDOW = 204800; +export const MINIMAX_M3_CONTEXT_WINDOW = 1_000_000; export const DEFAULT_MINIMAX_MAX_TOKENS = 131072; export const MINIMAX_API_COST = { + input: 0.6, + output: 2.4, + cacheRead: 0.12, + cacheWrite: 0, +}; +export const MINIMAX_M27_API_COST = { input: 0.3, output: 1.2, cacheRead: 0.06, @@ -49,6 +56,9 @@ export const MINIMAX_LM_STUDIO_COST = { type MinimaxCatalogId = keyof typeof MINIMAX_TEXT_MODEL_CATALOG; export function resolveMinimaxApiCost(modelId: string): ModelDefinitionConfig["cost"] { + if (modelId === "MiniMax-M2.7") { + return MINIMAX_M27_API_COST; + } if (modelId === "MiniMax-M2.5-highspeed") { return MINIMAX_M25_API_HIGHSPEED_COST; } @@ -74,7 +84,7 @@ export function buildMinimaxModelDefinition(params: { id: params.id, name: params.name ?? catalog?.name ?? `MiniMax ${params.id}`, reasoning: params.reasoning ?? catalog?.reasoning ?? false, - input: ["text"], + input: [...(catalog?.input ?? ["text"])], cost: params.cost, contextWindow: params.contextWindow, maxTokens: params.maxTokens, @@ -85,7 +95,9 @@ export function buildMinimaxApiModelDefinition(modelId: string): ModelDefinition return buildMinimaxModelDefinition({ id: modelId, cost: resolveMinimaxApiCost(modelId), - contextWindow: DEFAULT_MINIMAX_CONTEXT_WINDOW, + contextWindow: + MINIMAX_TEXT_MODEL_CATALOG[modelId as MinimaxCatalogId]?.contextWindow ?? + DEFAULT_MINIMAX_CONTEXT_WINDOW, maxTokens: DEFAULT_MINIMAX_MAX_TOKENS, }); } diff --git a/extensions/minimax/onboard.test.ts b/extensions/minimax/onboard.test.ts index 90139fd17be0..054bbda5cf68 100644 --- a/extensions/minimax/onboard.test.ts +++ b/extensions/minimax/onboard.test.ts @@ -14,16 +14,16 @@ describe("minimax onboard", () => { baseUrl: "https://api.minimax.io/anthropic", api: "anthropic-messages", authHeader: true, - models: [buildMinimaxApiModelDefinition("MiniMax-M2.7")], + models: [buildMinimaxApiModelDefinition("MiniMax-M3")], }); - expect(cfg.agents?.defaults?.models?.["minimax/MiniMax-M2.7"]).toEqual({ + expect(cfg.agents?.defaults?.models?.["minimax/MiniMax-M3"]).toEqual({ alias: "Minimax", }); - expect(cfg.agents?.defaults?.model).toEqual({ primary: "minimax/MiniMax-M2.7" }); + expect(cfg.agents?.defaults?.model).toEqual({ primary: "minimax/MiniMax-M3" }); }); - it("keeps reasoning enabled for MiniMax-M2.7", () => { - const cfg = applyMinimaxApiConfig({}, "MiniMax-M2.7"); + it("keeps reasoning enabled for MiniMax-M3", () => { + const cfg = applyMinimaxApiConfig({}, "MiniMax-M3"); expect(cfg.models?.providers?.minimax?.models[0]?.reasoning).toBe(true); }); @@ -73,7 +73,7 @@ describe("minimax onboard", () => { legacyApi: "openai-completions", }); expect(provider?.authHeader).toBe(true); - expect(provider?.models.map((m) => m.id)).toEqual(["old-model", "MiniMax-M2.7"]); + expect(provider?.models.map((m) => m.id)).toEqual(["old-model", "MiniMax-M3"]); }); it("preserves other providers when adding minimax", () => { diff --git a/extensions/minimax/onboard.ts b/extensions/minimax/onboard.ts index b4779f43fd57..4bad3b7325b9 100644 --- a/extensions/minimax/onboard.ts +++ b/extensions/minimax/onboard.ts @@ -9,6 +9,7 @@ import { MINIMAX_API_BASE_URL, MINIMAX_CN_API_BASE_URL, } from "./model-definitions.js"; +import { MINIMAX_DEFAULT_MODEL_ID } from "./provider-models.js"; type MinimaxApiProviderConfigParams = { providerId: string; @@ -61,7 +62,7 @@ function applyMinimaxApiConfigWithBaseUrl( export function applyMinimaxApiProviderConfig( cfg: OpenClawConfig, - modelId = "MiniMax-M2.7", + modelId = MINIMAX_DEFAULT_MODEL_ID, ): OpenClawConfig { return applyMinimaxApiProviderConfigWithBaseUrl(cfg, { providerId: "minimax", @@ -72,7 +73,7 @@ export function applyMinimaxApiProviderConfig( export function applyMinimaxApiConfig( cfg: OpenClawConfig, - modelId = "MiniMax-M2.7", + modelId = MINIMAX_DEFAULT_MODEL_ID, ): OpenClawConfig { return applyMinimaxApiConfigWithBaseUrl(cfg, { providerId: "minimax", @@ -83,7 +84,7 @@ export function applyMinimaxApiConfig( export function applyMinimaxApiProviderConfigCn( cfg: OpenClawConfig, - modelId = "MiniMax-M2.7", + modelId = MINIMAX_DEFAULT_MODEL_ID, ): OpenClawConfig { return applyMinimaxApiProviderConfigWithBaseUrl(cfg, { providerId: "minimax", @@ -94,7 +95,7 @@ export function applyMinimaxApiProviderConfigCn( export function applyMinimaxApiConfigCn( cfg: OpenClawConfig, - modelId = "MiniMax-M2.7", + modelId = MINIMAX_DEFAULT_MODEL_ID, ): OpenClawConfig { return applyMinimaxApiConfigWithBaseUrl(cfg, { providerId: "minimax", diff --git a/extensions/minimax/openclaw.plugin.json b/extensions/minimax/openclaw.plugin.json index 9d072b294434..f1965b3247db 100644 --- a/extensions/minimax/openclaw.plugin.json +++ b/extensions/minimax/openclaw.plugin.json @@ -34,7 +34,7 @@ "choiceHint": "Global endpoint - api.minimax.io", "groupId": "minimax", "groupLabel": "MiniMax", - "groupHint": "M2.7 (recommended)" + "groupHint": "M3 (recommended)" }, { "provider": "minimax", @@ -45,7 +45,7 @@ "choiceHint": "Global endpoint - api.minimax.io", "groupId": "minimax", "groupLabel": "MiniMax", - "groupHint": "M2.7 (recommended)", + "groupHint": "M3 (recommended)", "optionKey": "minimaxApiKey", "cliFlag": "--minimax-api-key", "cliOption": "--minimax-api-key ", @@ -59,7 +59,7 @@ "choiceHint": "CN endpoint - api.minimaxi.com", "groupId": "minimax", "groupLabel": "MiniMax", - "groupHint": "M2.7 (recommended)" + "groupHint": "M3 (recommended)" }, { "provider": "minimax", @@ -70,7 +70,7 @@ "choiceHint": "CN endpoint - api.minimaxi.com", "groupId": "minimax", "groupLabel": "MiniMax", - "groupHint": "M2.7 (recommended)", + "groupHint": "M3 (recommended)", "optionKey": "minimaxApiKey", "cliFlag": "--minimax-api-key", "cliOption": "--minimax-api-key ", diff --git a/extensions/minimax/provider-catalog.ts b/extensions/minimax/provider-catalog.ts index 2a5faeddf651..91e15e07d0ba 100644 --- a/extensions/minimax/provider-catalog.ts +++ b/extensions/minimax/provider-catalog.ts @@ -3,14 +3,13 @@ import type { ModelProviderConfig, } from "openclaw/plugin-sdk/provider-model-shared"; import { - DEFAULT_MINIMAX_CONTEXT_WINDOW, DEFAULT_MINIMAX_MAX_TOKENS, MINIMAX_API_BASE_URL, resolveMinimaxApiCost, } from "./model-definitions.js"; import { MINIMAX_TEXT_MODEL_CATALOG, MINIMAX_TEXT_MODEL_ORDER } from "./provider-models.js"; -function resolveMinimaxCatalogBaseUrl(env: NodeJS.ProcessEnv = process.env): string { +export function resolveMinimaxCatalogBaseUrl(env: NodeJS.ProcessEnv = process.env): string { const rawHost = env.MINIMAX_API_HOST?.trim(); if (!rawHost) { return MINIMAX_API_BASE_URL; @@ -34,6 +33,7 @@ function buildMinimaxModel(params: { reasoning: boolean; input: ModelDefinitionConfig["input"]; cost: ModelDefinitionConfig["cost"]; + contextWindow: number; }): ModelDefinitionConfig { return { id: params.id, @@ -41,7 +41,7 @@ function buildMinimaxModel(params: { reasoning: params.reasoning, input: params.input, cost: params.cost, - contextWindow: DEFAULT_MINIMAX_CONTEXT_WINDOW, + contextWindow: params.contextWindow, maxTokens: DEFAULT_MINIMAX_MAX_TOKENS, }; } @@ -50,9 +50,11 @@ function buildMinimaxTextModel(params: { id: string; name: string; reasoning: boolean; + input: ModelDefinitionConfig["input"]; cost: ModelDefinitionConfig["cost"]; + contextWindow: number; }): ModelDefinitionConfig { - return buildMinimaxModel({ ...params, input: ["text"] }); + return buildMinimaxModel(params); } function buildMinimaxCatalog(): ModelDefinitionConfig[] { @@ -62,7 +64,9 @@ function buildMinimaxCatalog(): ModelDefinitionConfig[] { id, name: model.name, reasoning: model.reasoning, + input: [...model.input], cost: resolveMinimaxApiCost(id), + contextWindow: model.contextWindow, }); }); } diff --git a/extensions/minimax/provider-contract-api.ts b/extensions/minimax/provider-contract-api.ts index dd7bead4f835..13b6c687d365 100644 --- a/extensions/minimax/provider-contract-api.ts +++ b/extensions/minimax/provider-contract-api.ts @@ -4,7 +4,7 @@ const noopAuth = async () => ({ profiles: [] }); const wizardGroup = { groupId: "minimax", groupLabel: "MiniMax", - groupHint: "M2.7 (recommended)", + groupHint: "M3 (recommended)", } as const; export function createMinimaxProvider(): ProviderPlugin { diff --git a/extensions/minimax/provider-models.ts b/extensions/minimax/provider-models.ts index b4a026deda51..691c4d32e0e1 100644 --- a/extensions/minimax/provider-models.ts +++ b/extensions/minimax/provider-models.ts @@ -1,20 +1,40 @@ import { matchesExactOrPrefix } from "openclaw/plugin-sdk/provider-model-shared"; -export const MINIMAX_DEFAULT_MODEL_ID = "MiniMax-M2.7"; +export const MINIMAX_DEFAULT_MODEL_ID = "MiniMax-M3"; export const MINIMAX_DEFAULT_MODEL_REF = `minimax/${MINIMAX_DEFAULT_MODEL_ID}`; -export const MINIMAX_TEXT_MODEL_ORDER = ["MiniMax-M2.7", "MiniMax-M2.7-highspeed"] as const; +export const MINIMAX_TEXT_MODEL_ORDER = [ + "MiniMax-M3", + "MiniMax-M2.7", + "MiniMax-M2.7-highspeed", +] as const; export const MINIMAX_TEXT_MODEL_CATALOG = { - "MiniMax-M2.7": { name: "MiniMax M2.7", reasoning: true }, - "MiniMax-M2.7-highspeed": { name: "MiniMax M2.7 Highspeed", reasoning: true }, + "MiniMax-M3": { + name: "MiniMax M3", + reasoning: true, + input: ["text", "image"], + contextWindow: 1_000_000, + }, + "MiniMax-M2.7": { + name: "MiniMax M2.7", + reasoning: true, + input: ["text"], + contextWindow: 204800, + }, + "MiniMax-M2.7-highspeed": { + name: "MiniMax M2.7 Highspeed", + reasoning: true, + input: ["text"], + contextWindow: 204800, + }, } as const; export const MINIMAX_TEXT_MODEL_REFS = MINIMAX_TEXT_MODEL_ORDER.map( (modelId) => `minimax/${modelId}`, ); -const MINIMAX_MODERN_MODEL_MATCHERS = ["minimax-m2.7"] as const; +const MINIMAX_MODERN_MODEL_MATCHERS = ["minimax-m3", "minimax-m2.7"] as const; export function isMiniMaxModernModelId(modelId: string): boolean { return matchesExactOrPrefix(modelId, MINIMAX_MODERN_MODEL_MATCHERS); diff --git a/extensions/minimax/provider-registration.ts b/extensions/minimax/provider-registration.ts index 7ac15403be14..965964947bdb 100644 --- a/extensions/minimax/provider-registration.ts +++ b/extensions/minimax/provider-registration.ts @@ -5,6 +5,8 @@ import type { ProviderAuthContext, ProviderAuthResult, ProviderCatalogContext, + ProviderResolveDynamicModelContext, + ProviderRuntimeModel, } from "openclaw/plugin-sdk/plugin-entry"; import { MINIMAX_OAUTH_MARKER, @@ -14,14 +16,27 @@ import { import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth"; import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared"; -import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; +import { + buildProviderReplayFamilyHooks, + normalizeModelCompat, +} from "openclaw/plugin-sdk/provider-model-shared"; import { MINIMAX_FAST_MODE_STREAM_HOOKS } from "openclaw/plugin-sdk/provider-stream-family"; import { fetchMinimaxUsage } from "openclaw/plugin-sdk/provider-usage"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; -import { isMiniMaxModernModelId, MINIMAX_DEFAULT_MODEL_ID } from "./api.js"; +import { + isMiniMaxModernModelId, + MINIMAX_DEFAULT_MODEL_ID, + MINIMAX_TEXT_MODEL_CATALOG, + MINIMAX_TEXT_MODEL_ORDER, +} from "./api.js"; +import { DEFAULT_MINIMAX_MAX_TOKENS, resolveMinimaxApiCost } from "./model-definitions.js"; import type { MiniMaxRegion } from "./oauth.js"; import { applyMinimaxApiConfig, applyMinimaxApiConfigCn } from "./onboard.js"; -import { buildMinimaxPortalProvider, buildMinimaxProvider } from "./provider-catalog.js"; +import { + buildMinimaxPortalProvider, + buildMinimaxProvider, + resolveMinimaxCatalogBaseUrl, +} from "./provider-catalog.js"; const API_PROVIDER_ID = "minimax"; const PORTAL_PROVIDER_ID = "minimax-portal"; @@ -38,7 +53,7 @@ const MINIMAX_USAGE_ENV_VAR_KEYS = [ const MINIMAX_WIZARD_GROUP = { groupId: "minimax", groupLabel: "MiniMax", - groupHint: "M2.7 (recommended)", + groupHint: "M3 (recommended)", } as const; const HYBRID_ANTHROPIC_OPENAI_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ family: "hybrid-anthropic-openai", @@ -86,6 +101,35 @@ function buildPortalProviderCatalog(params: { baseUrl: string; apiKey: string }) }; } +function findMinimaxCatalogModel(modelId: string) { + const normalizedModelId = modelId.trim().toLowerCase(); + const catalogId = MINIMAX_TEXT_MODEL_ORDER.find((id) => id.toLowerCase() === normalizedModelId); + return catalogId ? { id: catalogId, model: MINIMAX_TEXT_MODEL_CATALOG[catalogId] } : undefined; +} + +function resolveMinimaxDynamicModel(params: { + providerId: string; + ctx: ProviderResolveDynamicModelContext; +}): ProviderRuntimeModel | undefined { + const catalogModel = findMinimaxCatalogModel(params.ctx.modelId); + if (!catalogModel) { + return undefined; + } + return normalizeModelCompat({ + id: catalogModel.id, + name: catalogModel.model.name, + provider: params.providerId, + api: "anthropic-messages", + baseUrl: + normalizeOptionalString(params.ctx.providerConfig?.baseUrl) ?? resolveMinimaxCatalogBaseUrl(), + reasoning: catalogModel.model.reasoning, + input: [...catalogModel.model.input], + cost: resolveMinimaxApiCost(catalogModel.id), + contextWindow: catalogModel.model.contextWindow, + maxTokens: DEFAULT_MINIMAX_MAX_TOKENS, + }); +} + function resolveApiCatalog(ctx: ProviderCatalogContext) { const apiKey = ctx.resolveProviderApiKey(API_PROVIDER_ID).apiKey; if (!apiKey) { @@ -165,6 +209,7 @@ function createOAuthHandler(region: MiniMaxRegion) { agents: { defaults: { models: { + [portalModelRef("MiniMax-M3")]: { alias: "minimax-m3" }, [portalModelRef("MiniMax-M2.7")]: { alias: "minimax-m2.7" }, [portalModelRef("MiniMax-M2.7-highspeed")]: { alias: "minimax-m2.7-highspeed", @@ -267,6 +312,7 @@ export function buildMinimaxApiProviderPlugin(): ProviderPlugin { return apiKey ? { token: apiKey } : null; }, ...MINIMAX_PROVIDER_HOOKS, + resolveDynamicModel: (ctx) => resolveMinimaxDynamicModel({ providerId: API_PROVIDER_ID, ctx }), isModernModelRef: ({ modelId }) => isMiniMaxModernModelId(modelId), fetchUsageSnapshot: async (ctx) => await fetchMinimaxUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn, { @@ -292,6 +338,8 @@ export function buildMinimaxPortalProviderPlugin(): ProviderPlugin { }, auth: [createMinimaxOAuthMethod("global"), createMinimaxOAuthMethod("cn")], ...MINIMAX_PROVIDER_HOOKS, + resolveDynamicModel: (ctx) => + resolveMinimaxDynamicModel({ providerId: PORTAL_PROVIDER_ID, ctx }), isModernModelRef: ({ modelId }) => isMiniMaxModernModelId(modelId), }; } diff --git a/src/agents/live-model-filter.ts b/src/agents/live-model-filter.ts index 1ebb444f69c5..a8342f0b1027 100644 --- a/src/agents/live-model-filter.ts +++ b/src/agents/live-model-filter.ts @@ -19,7 +19,7 @@ const HIGH_SIGNAL_LIVE_MODEL_PRIORITY = [ "anthropic/claude-opus-4-6", "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-pro", - "minimax/minimax-m2.7", + "minimax/minimax-m3", "openai/gpt-5.5", "openrouter/openai/gpt-5.2-chat", "openrouter/minimax/minimax-m2.7", @@ -28,7 +28,7 @@ const HIGH_SIGNAL_LIVE_MODEL_PRIORITY = [ "xai/grok-4.3", "zai/glm-5.1", "fireworks/accounts/fireworks/models/glm-5p1", - "minimax-portal/minimax-m2.7", + "minimax-portal/minimax-m3", ] as const; const SMALL_LIVE_MODEL_PRIORITY = [ diff --git a/src/agents/minimax-docs.test.ts b/src/agents/minimax-docs.test.ts index 52e1b16730a0..3f7a09e07c80 100644 --- a/src/agents/minimax-docs.test.ts +++ b/src/agents/minimax-docs.test.ts @@ -13,7 +13,7 @@ const minimaxDoc = fs.readFileSync(path.join(repoRoot, "docs/providers/minimax.m describe("MiniMax docs sync", () => { it("keeps the live-testing guide on the current MiniMax default", () => { - expect(testingLiveDoc).toContain("MiniMax M2.7"); + expect(testingLiveDoc).toContain("MiniMax M3"); expect(testingLiveDoc).toContain(MINIMAX_DEFAULT_MODEL_REF); }); diff --git a/src/agents/minimax.live.test.ts b/src/agents/minimax.live.test.ts index 990f6876a9fd..0da92eebf45f 100644 --- a/src/agents/minimax.live.test.ts +++ b/src/agents/minimax.live.test.ts @@ -8,7 +8,7 @@ import { const MINIMAX_KEY = process.env.MINIMAX_API_KEY ?? ""; const MINIMAX_BASE_URL = process.env.MINIMAX_BASE_URL?.trim() || "https://api.minimax.io/anthropic"; -const MINIMAX_MODEL = process.env.MINIMAX_MODEL?.trim() || "MiniMax-M2.7"; +const MINIMAX_MODEL = process.env.MINIMAX_MODEL?.trim() || "MiniMax-M3"; const LIVE = isLiveTestEnabled(["MINIMAX_LIVE_TEST"]); const describeLive = LIVE && MINIMAX_KEY ? describe : describe.skip; @@ -36,10 +36,10 @@ describeLive("minimax live", () => { provider: "minimax", baseUrl: MINIMAX_BASE_URL, reasoning: false, - input: ["text"], + input: ["text", "image"], // Pricing: placeholder values (per 1M tokens, multiplied by 1000 for display) cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 }, - contextWindow: 200000, + contextWindow: 1_000_000, maxTokens: 8192, }; const probeResult = await runMinimaxTextProbe(model, 128); diff --git a/src/agents/model-compat.test.ts b/src/agents/model-compat.test.ts index 5ef586bf8830..0bdcaab3de7f 100644 --- a/src/agents/model-compat.test.ts +++ b/src/agents/model-compat.test.ts @@ -524,6 +524,7 @@ describe("isHighSignalLiveModelRef", () => { expect( isHighSignalLiveModelRef({ provider: "openrouter", id: "minimax/minimax-m2.1:free" }), ).toBe(false); + expect(isHighSignalLiveModelRef({ provider: "minimax", id: "MiniMax-M3" })).toBe(true); expect(isHighSignalLiveModelRef({ provider: "minimax", id: "MiniMax-M2.7" })).toBe(true); expect(isHighSignalLiveModelRef({ provider: "openrouter", id: "minimax/minimax-m2.7" })).toBe( true, @@ -656,7 +657,7 @@ describe("isPrioritizedHighSignalLiveModelRef", () => { { provider: "anthropic", id: "claude-opus-4-6" }, { provider: "deepseek", id: "deepseek-v4-flash" }, { provider: "deepseek", id: "deepseek-v4-pro" }, - { provider: "minimax", id: "minimax-m2.7" }, + { provider: "minimax", id: "minimax-m3" }, { provider: "openai", id: "gpt-5.5" }, { provider: "openrouter", id: "openai/gpt-5.2-chat" }, { provider: "openrouter", id: "minimax/minimax-m2.7" }, @@ -665,7 +666,7 @@ describe("isPrioritizedHighSignalLiveModelRef", () => { { provider: "xai", id: "grok-4.3" }, { provider: "zai", id: "glm-5.1" }, { provider: "fireworks", id: "accounts/fireworks/models/glm-5p1" }, - { provider: "minimax-portal", id: "minimax-m2.7" }, + { provider: "minimax-portal", id: "minimax-m3" }, ]); }); }); @@ -733,7 +734,7 @@ describe("selectHighSignalLiveItems", () => { { provider: "openai", id: "gpt-5.5" }, { provider: "deepseek", id: "deepseek-v4-flash" }, { provider: "deepseek", id: "deepseek-v4-pro" }, - { provider: "minimax", id: "minimax-m2.7" }, + { provider: "minimax", id: "minimax-m3" }, ]; expect( @@ -746,7 +747,7 @@ describe("selectHighSignalLiveItems", () => { ).toEqual([ { provider: "deepseek", id: "deepseek-v4-flash" }, { provider: "deepseek", id: "deepseek-v4-pro" }, - { provider: "minimax", id: "minimax-m2.7" }, + { provider: "minimax", id: "minimax-m3" }, ]); }); diff --git a/src/agents/models-config.providers.minimax.test.ts b/src/agents/models-config.providers.minimax.test.ts index cffdcdd582ee..de2bf0f7fcd1 100644 --- a/src/agents/models-config.providers.minimax.test.ts +++ b/src/agents/models-config.providers.minimax.test.ts @@ -2,6 +2,15 @@ import { describe, expect, it } from "vitest"; function buildMinimaxCatalog() { return [ + { + id: "MiniMax-M3", + cost: { + input: 0.6, + output: 2.4, + cacheRead: 0.12, + cacheWrite: 0, + }, + }, { id: "MiniMax-M2.7", cost: { @@ -30,10 +39,12 @@ describe("minimax provider catalog", () => { "minimax-portal": { models: buildMinimaxCatalog() }, }; expect(providers?.minimax?.models?.map((model) => model.id)).toEqual([ + "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed", ]); expect(providers?.["minimax-portal"]?.models?.map((model) => model.id)).toEqual([ + "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed", ]); diff --git a/src/plugin-sdk/test-helpers/provider-discovery-contract.ts b/src/plugin-sdk/test-helpers/provider-discovery-contract.ts index 1c89c31f5280..306045cd0e9b 100644 --- a/src/plugin-sdk/test-helpers/provider-discovery-contract.ts +++ b/src/plugin-sdk/test-helpers/provider-discovery-contract.ts @@ -717,6 +717,7 @@ export function describeMinimaxProviderDiscoveryContract( apiKey: "minimax-key", }); const ids = providerModelIds(provider); + expect(ids).toContain("MiniMax-M3"); expect(ids).toContain("MiniMax-M2.7"); expect(ids).toContain("MiniMax-M2.7-highspeed"); }); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index e9faf6de7a75..9f3745d810c9 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -255,9 +255,7 @@ describe("package acceptance workflow", () => { expect(crabboxConfig.jobs?.changed?.command).toContain( "commit -q --no-gpg-sign -m remote-check-tree", ); - expect(crabboxConfig.jobs?.changed?.command).toContain( - "env CI=1 corepack pnpm check --timed", - ); + expect(crabboxConfig.jobs?.changed?.command).toContain("env CI=1 corepack pnpm check --timed"); expect(crabboxConfig.ssh?.user).toBe("crabbox"); expect(crabboxConfig.ssh?.port).toBe("22"); }); @@ -655,7 +653,7 @@ describe("package artifact reuse", () => { "OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles", ); expect(workflow).toContain( - "OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M2.7,minimax-portal/MiniMax-M2.7 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2", + "OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M3,minimax-portal/MiniMax-M3 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2", ); expect(workflow).toMatch( /suite_id: native-live-src-gateway-profiles-fireworks[\s\S]*?timeout_minutes: 30[\s\S]*?advisory: true/u,