From df64c93a0ba60ed035720b6b8fd9717fba399441 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:12:47 +0800 Subject: [PATCH] Add MiniMax M3 provider metadata --- docs/providers/minimax.md | 230 +++++++----------- extensions/minimax/index.ts | 9 +- extensions/minimax/model-definitions.test.ts | 48 ++-- extensions/minimax/model-definitions.ts | 27 +- extensions/minimax/onboard.test.ts | 41 +++- extensions/minimax/onboard.ts | 8 +- extensions/minimax/openclaw.plugin.json | 8 +- extensions/minimax/provider-catalog.ts | 56 +---- extensions/minimax/provider-models.ts | 44 +++- .../models-config.providers.minimax.test.ts | 14 ++ src/agents/pi-embedded-runner/extra-params.ts | 15 +- .../minimax-stream-wrappers.test.ts | 60 +++++ .../minimax-stream-wrappers.ts | 35 +++ .../bundled-plugin-metadata.generated.ts | 8 +- 14 files changed, 356 insertions(+), 247 deletions(-) create mode 100644 src/agents/pi-embedded-runner/minimax-stream-wrappers.test.ts diff --git a/docs/providers/minimax.md b/docs/providers/minimax.md index f060c637de8..8f49d53674a 100644 --- a/docs/providers/minimax.md +++ b/docs/providers/minimax.md @@ -1,73 +1,77 @@ --- -summary: "Use MiniMax M2.5 in OpenClaw" +summary: "Use MiniMax M3 and M2.7 in OpenClaw" read_when: - You want MiniMax models in OpenClaw - - You need MiniMax setup guidance + - You need MiniMax setup or endpoint guidance title: "MiniMax" --- # MiniMax -MiniMax is an AI company that builds the **M2/M2.5** model family. The current -coding-focused release is **MiniMax M2.5** (December 23, 2025), built for -real-world complex tasks. +OpenClaw includes a bundled MiniMax provider for API-key and Token Plan OAuth setup. +MiniMax M3 is the default model, while MiniMax M2.7 remains available for existing +text-only workflows. -Source: [MiniMax M2.5 release note](https://www.minimax.io/news/minimax-m25) +Official references: -## Model overview (M2.5) +- [Model invocation](https://platform.minimax.io/docs/guides/text-generation) +- [Pay-as-you-go pricing](https://platform.minimax.io/docs/guides/pricing-paygo) -MiniMax highlights these improvements in M2.5: +## Model catalog -- Stronger **multi-language coding** (Rust, Java, Go, C++, Kotlin, Objective-C, TS/JS). -- Better **web/app development** and aesthetic output quality (including native mobile). -- Improved **composite instruction** handling for office-style workflows, building on - interleaved thinking and integrated constraint execution. -- **More concise responses** with lower token usage and faster iteration loops. -- Stronger **tool/agent framework** compatibility and context management (Claude Code, - Droid/Factory AI, Cline, Kilo Code, Roo Code, BlackBox). -- Higher-quality **dialogue and technical writing** outputs. +| Model ID | Context window | OpenClaw input declaration | Thinking behavior | +| ------------------------ | --------------------------------------: | -------------------------- | ---------------------------------- | +| `MiniMax-M3` | 1,000,000 total input and output tokens | Text and image | Supports `adaptive` and `disabled` | +| `MiniMax-M2.7` | 204,800 total input and output tokens | Text | Always on | +| `MiniMax-M2.7-highspeed` | 204,800 total input and output tokens | Text | Always on | -## MiniMax M2.5 vs MiniMax M2.5 Highspeed +The upstream M3 APIs also accept video content blocks. OpenClaw's current model input +declaration represents text and image inputs, which are the modalities the agent runtime +can pass directly to this provider. -- **Speed:** `MiniMax-M2.5-highspeed` is the official fast tier in MiniMax docs. -- **Cost:** MiniMax pricing lists the same input cost and a higher output cost for highspeed. -- **Current model IDs:** use `MiniMax-M2.5` or `MiniMax-M2.5-highspeed`. +M3 thinking defaults depend on the selected protocol. The Anthropic-compatible API leaves +thinking off when the parameter is omitted; the OpenAI-compatible API leaves it on. M2.7 +thinking cannot be disabled. OpenClaw normalizes enabled M3 thinking requests to +`{"type":"adaptive"}` for the Anthropic-compatible API. -## Choose a setup +## Configure with the wizard -### MiniMax OAuth (Coding Plan) — recommended - -**Best for:** quick setup with MiniMax Coding Plan via OAuth, no API key required. - -Enable the bundled OAuth plugin and authenticate: +For a global API key: ```bash -openclaw plugins enable minimax-portal-auth # skip if already loaded. -openclaw gateway restart # restart if gateway is already running -openclaw onboard --auth-choice minimax-portal +openclaw setup --wizard --auth-choice minimax-global-api ``` -You will be prompted to select an endpoint: +For a CN API key: -- **Global** - International users (`api.minimax.io`) -- **CN** - Users in China (`api.minimaxi.com`) +```bash +openclaw setup --wizard --auth-choice minimax-cn-api +``` -See [MiniMax OAuth plugin README](https://github.com/openclaw/openclaw/tree/main/extensions/minimax-portal-auth) for details. +For Token Plan OAuth: -### MiniMax M2.5 (API key) +```bash +openclaw models auth login --provider minimax-portal --set-default +``` -**Best for:** hosted MiniMax with Anthropic-compatible API. +The wizard uses MiniMax M3 and the Anthropic-compatible endpoint for the selected region. -Configure via CLI: +## Manual API-key configuration -- Run `openclaw configure` -- Select **Model/auth** -- Choose **MiniMax M2.5** +The global Anthropic-compatible configuration is: ```json5 { env: { MINIMAX_API_KEY: "sk-..." }, - agents: { defaults: { model: { primary: "minimax/MiniMax-M2.5" } } }, + agents: { + defaults: { + model: { primary: "minimax/MiniMax-M3" }, + models: { + "minimax/MiniMax-M3": { alias: "minimax" }, + "minimax/MiniMax-M2.7": { alias: "minimax-m2.7" }, + }, + }, + }, models: { mode: "merge", providers: { @@ -75,24 +79,25 @@ Configure via CLI: baseUrl: "https://api.minimax.io/anthropic", apiKey: "${MINIMAX_API_KEY}", api: "anthropic-messages", + authHeader: true, models: [ { - id: "MiniMax-M2.5", - name: "MiniMax M2.5", + id: "MiniMax-M3", + name: "MiniMax M3", reasoning: true, - input: ["text"], - cost: { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0.12 }, - contextWindow: 200000, - maxTokens: 8192, + input: ["text", "image"], + cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 }, + contextWindow: 1000000, + maxTokens: 131072, }, { - id: "MiniMax-M2.5-highspeed", - name: "MiniMax M2.5 Highspeed", + id: "MiniMax-M2.7", + name: "MiniMax M2.7", reasoning: true, input: ["text"], - cost: { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0.12 }, - contextWindow: 200000, - maxTokens: 8192, + cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 }, + contextWindow: 204800, + maxTokens: 131072, }, ], }, @@ -101,116 +106,67 @@ Configure via CLI: } ``` -### MiniMax M2.5 as fallback (example) - -**Best for:** keep your strongest latest-generation model as primary, fail over to MiniMax M2.5. -Example below uses Opus as a concrete primary; swap to your preferred latest-gen primary model. - -```json5 -{ - env: { MINIMAX_API_KEY: "sk-..." }, - agents: { - defaults: { - models: { - "anthropic/claude-opus-4-6": { alias: "primary" }, - "minimax/MiniMax-M2.5": { alias: "minimax" }, - }, - model: { - primary: "anthropic/claude-opus-4-6", - fallbacks: ["minimax/MiniMax-M2.5"], - }, - }, - }, -} -``` +## Region and protocol endpoints -### Optional: Local via LM Studio (manual) +OpenClaw stores one `baseUrl` and one `api` value per provider entry. Use one matching pair +from this table; no additional endpoint fields are required. -**Best for:** local inference with LM Studio. -We have seen strong results with MiniMax M2.5 on powerful hardware (e.g. a -desktop/server) using LM Studio's local server. +| Region | Protocol | `baseUrl` | `api` | +| ------ | -------------------- | ------------------------------------ | -------------------- | +| Global | Anthropic-compatible | `https://api.minimax.io/anthropic` | `anthropic-messages` | +| Global | OpenAI-compatible | `https://api.minimax.io/v1` | `openai-completions` | +| CN | Anthropic-compatible | `https://api.minimaxi.com/anthropic` | `anthropic-messages` | +| CN | OpenAI-compatible | `https://api.minimaxi.com/v1` | `openai-completions` | -Configure manually via `openclaw.json`: +For an OpenAI-compatible CN override: ```json5 { - agents: { - defaults: { - model: { primary: "lmstudio/minimax-m2.5-gs32" }, - models: { "lmstudio/minimax-m2.5-gs32": { alias: "Minimax" } }, - }, - }, models: { - mode: "merge", providers: { - lmstudio: { - baseUrl: "http://127.0.0.1:1234/v1", - apiKey: "lmstudio", - api: "openai-responses", - models: [ - { - id: "minimax-m2.5-gs32", - name: "MiniMax M2.5 GS32", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 196608, - maxTokens: 8192, - }, - ], + minimax: { + baseUrl: "https://api.minimaxi.com/v1", + api: "openai-completions", }, }, }, } ``` -## Configure via `openclaw configure` - -Use the interactive config wizard to set MiniMax without editing JSON: - -1. Run `openclaw configure`. -2. Select **Model/auth**. -3. Choose **MiniMax M2.5**. -4. Pick your default model when prompted. +Keep Anthropic Base URLs ending in `/anthropic`. The Anthropic SDK appends +`/v1/messages`; adding `/v1` to the configured Base URL would duplicate that path. The +OpenAI client appends `/chat/completions` to the `/v1` Base URL. -## Configuration options +## Pricing -- `models.providers.minimax.baseUrl`: prefer `https://api.minimax.io/anthropic` (Anthropic-compatible); `https://api.minimax.io/v1` is optional for OpenAI-compatible payloads. -- `models.providers.minimax.api`: prefer `anthropic-messages`; `openai-completions` is optional for OpenAI-compatible payloads. -- `models.providers.minimax.apiKey`: MiniMax API key (`MINIMAX_API_KEY`). -- `models.providers.minimax.models`: define `id`, `name`, `reasoning`, `contextWindow`, `maxTokens`, `cost`. -- `agents.defaults.models`: alias models you want in the allowlist. -- `models.mode`: keep `merge` if you want to add MiniMax alongside built-ins. +Catalog costs are USD per million tokens. M3 has input-length and service-tier pricing, +so preserve all applicable tiers when estimating an actual request: -## Notes +| Model and service tier | Input length | Input | Output | Cache read | Cache write | +| ---------------------- | --------------------------- | ----: | -----: | ---------: | ----------: | +| M3 standard | Up to 512K input tokens | $0.30 | $1.20 | $0.06 | Not listed | +| M3 standard | More than 512K input tokens | $0.60 | $2.40 | $0.12 | Not listed | +| M3 priority | Up to 512K input tokens | $0.45 | $1.80 | $0.09 | Not listed | +| M3 priority | More than 512K input tokens | $0.90 | $3.60 | $0.18 | Not listed | +| M2.7 standard | Up to its context limit | $0.30 | $1.20 | $0.06 | $0.375 | -- Model refs are `minimax/`. -- Recommended model IDs: `MiniMax-M2.5` and `MiniMax-M2.5-highspeed`. -- Coding Plan usage API: `https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains` (requires a coding plan key). -- Update pricing values in `models.json` if you need exact cost tracking. -- Referral link for MiniMax Coding Plan (10% off): [https://platform.minimax.io/subscribe/coding-plan?code=DbXJTRClnb&source=link](https://platform.minimax.io/subscribe/coding-plan?code=DbXJTRClnb&source=link) -- See [/concepts/model-providers](/concepts/model-providers) for provider rules. -- Use `openclaw models list` and `openclaw models set minimax/MiniMax-M2.5` to switch. +The flat M3 catalog cost uses the default standard tier for requests up to 512K input +tokens. Its `cacheWrite` value is `0` because the official M3 pricing table does not list a +prompt-cache write charge. Requests above 512K input tokens or requests using +`service_tier: "priority"` are billed at the corresponding table row. ## Troubleshooting -### “Unknown model: minimax/MiniMax-M2.5” - -This usually means the **MiniMax provider isn’t configured** (no provider entry -and no MiniMax auth profile/env key found). A fix for this detection is in -**2026.1.12** (unreleased at the time of writing). Fix by: - -- Upgrading to **2026.1.12** (or run from source `main`), then restarting the gateway. -- Running `openclaw configure` and selecting **MiniMax M2.5**, or -- Adding the `models.providers.minimax` block manually, or -- Setting `MINIMAX_API_KEY` (or a MiniMax auth profile) so the provider can be injected. +### Unknown model: minimax/MiniMax-M3 -Make sure the model id is **case‑sensitive**: +Confirm that the provider is configured through the wizard, JSON, or a MiniMax auth +profile. Model IDs are case-sensitive: -- `minimax/MiniMax-M2.5` -- `minimax/MiniMax-M2.5-highspeed` +- `minimax/MiniMax-M3` +- `minimax/MiniMax-M2.7` +- `minimax/MiniMax-M2.7-highspeed` -Then recheck with: +Then run: ```bash openclaw models list diff --git a/extensions/minimax/index.ts b/extensions/minimax/index.ts index 61729c23eb8..516eabf3d25 100644 --- a/extensions/minimax/index.ts +++ b/extensions/minimax/index.ts @@ -131,6 +131,7 @@ function createOAuthHandler(region: MiniMaxRegion) { agents: { defaults: { models: { + [portalModelRef(DEFAULT_MODEL)]: { alias: "minimax-m3" }, [portalModelRef("MiniMax-M2.7")]: { alias: "minimax-m2.7" }, [portalModelRef("MiniMax-M2.7-highspeed")]: { alias: "minimax-m2.7-highspeed", @@ -189,7 +190,7 @@ export default definePluginEntry({ choiceHint: "Global endpoint - api.minimax.io", groupId: "minimax", groupLabel: "MiniMax", - groupHint: "M2.7 (recommended)", + groupHint: "M3 (recommended)", }, }), createProviderApiKeyAuthMethod({ @@ -213,7 +214,7 @@ export default definePluginEntry({ choiceHint: "CN endpoint - api.minimaxi.com", groupId: "minimax", groupLabel: "MiniMax", - groupHint: "M2.7 (recommended)", + groupHint: "M3 (recommended)", }, }), ], @@ -255,7 +256,7 @@ export default definePluginEntry({ choiceHint: "Global endpoint - api.minimax.io", groupId: "minimax", groupLabel: "MiniMax", - groupHint: "M2.7 (recommended)", + groupHint: "M3 (recommended)", }, run: createOAuthHandler("global"), }, @@ -270,7 +271,7 @@ export default definePluginEntry({ choiceHint: "CN endpoint - api.minimaxi.com", groupId: "minimax", groupLabel: "MiniMax", - groupHint: "M2.7 (recommended)", + groupHint: "M3 (recommended)", }, run: createOAuthHandler("cn"), }, diff --git a/extensions/minimax/model-definitions.test.ts b/extensions/minimax/model-definitions.test.ts index ddc46187d4e..9e3aedc32f8 100644 --- a/extensions/minimax/model-definitions.test.ts +++ b/extensions/minimax/model-definitions.test.ts @@ -9,40 +9,56 @@ import { } 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 the default hosted model", () => { + expect(MINIMAX_HOSTED_MODEL_ID).toBe("MiniMax-M3"); }); - it("uses the higher upstream MiniMax context and token defaults", () => { - expect(DEFAULT_MINIMAX_CONTEXT_WINDOW).toBe(204800); - expect(DEFAULT_MINIMAX_MAX_TOKENS).toBe(131072); + it("uses the M3 context and standard pricing defaults", () => { + expect(DEFAULT_MINIMAX_CONTEXT_WINDOW).toBe(1_000_000); + expect(DEFAULT_MINIMAX_MAX_TOKENS).toBe(131_072); expect(MINIMAX_API_COST).toEqual({ input: 0.3, output: 1.2, cacheRead: 0.06, - cacheWrite: 0.375, + cacheWrite: 0, }); }); - it("builds catalog model with name and reasoning from catalog", () => { - const model = buildMinimaxModelDefinition({ - id: "MiniMax-M2.7", + it("builds M3 with multimodal input and adaptive reasoning capability", () => { + const model = buildMinimaxApiModelDefinition("MiniMax-M3"); + expect(model).toMatchObject({ + id: "MiniMax-M3", + name: "MiniMax M3", + reasoning: true, + input: ["text", "image"], cost: MINIMAX_API_COST, - contextWindow: DEFAULT_MINIMAX_CONTEXT_WINDOW, - maxTokens: DEFAULT_MINIMAX_MAX_TOKENS, + contextWindow: 1_000_000, + maxTokens: 131_072, }); + }); + + it("keeps M2.7 model-specific context and pricing", () => { + const model = buildMinimaxApiModelDefinition("MiniMax-M2.7"); expect(model).toMatchObject({ id: "MiniMax-M2.7", name: "MiniMax M2.7", reasoning: true, + input: ["text"], + cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 }, + contextWindow: 204_800, + maxTokens: 131_072, }); }); - it("builds API model definition with standard cost", () => { - const model = buildMinimaxApiModelDefinition("MiniMax-M2.7"); - expect(model.cost).toEqual(MINIMAX_API_COST); - expect(model.contextWindow).toBe(DEFAULT_MINIMAX_CONTEXT_WINDOW); - expect(model.maxTokens).toBe(DEFAULT_MINIMAX_MAX_TOKENS); + it("builds explicit definitions with catalog names", () => { + const model = buildMinimaxModelDefinition({ + id: "MiniMax-M3", + input: ["text", "image"], + cost: MINIMAX_API_COST, + contextWindow: DEFAULT_MINIMAX_CONTEXT_WINDOW, + maxTokens: DEFAULT_MINIMAX_MAX_TOKENS, + }); + expect(model).toMatchObject({ name: "MiniMax M3", input: ["text", "image"] }); }); it("falls back to generated name for unknown model id", () => { diff --git a/extensions/minimax/model-definitions.ts b/extensions/minimax/model-definitions.ts index 718dea466d1..6a9d9170e86 100644 --- a/extensions/minimax/model-definitions.ts +++ b/extensions/minimax/model-definitions.ts @@ -6,15 +6,11 @@ export const MINIMAX_API_BASE_URL = "https://api.minimax.io/anthropic"; 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 DEFAULT_MINIMAX_MAX_TOKENS = 131072; +const DEFAULT_MINIMAX_MODEL = MINIMAX_TEXT_MODEL_CATALOG[MINIMAX_DEFAULT_MODEL_ID]; +export const DEFAULT_MINIMAX_CONTEXT_WINDOW = DEFAULT_MINIMAX_MODEL.contextWindow; +export const DEFAULT_MINIMAX_MAX_TOKENS = DEFAULT_MINIMAX_MODEL.maxTokens; -export const MINIMAX_API_COST = { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0.375, -}; +export const MINIMAX_API_COST = { ...DEFAULT_MINIMAX_MODEL.cost }; export const MINIMAX_HOSTED_COST = { input: 0, output: 0, @@ -34,6 +30,7 @@ export function buildMinimaxModelDefinition(params: { id: string; name?: string; reasoning?: boolean; + input?: ModelDefinitionConfig["input"]; cost: ModelDefinitionConfig["cost"]; contextWindow: number; maxTokens: number; @@ -43,18 +40,22 @@ export function buildMinimaxModelDefinition(params: { id: params.id, name: params.name ?? catalog?.name ?? `MiniMax ${params.id}`, reasoning: params.reasoning ?? catalog?.reasoning ?? false, - input: ["text"], - cost: params.cost, + input: params.input ? [...params.input] : ["text"], + cost: { ...params.cost }, contextWindow: params.contextWindow, maxTokens: params.maxTokens, }; } export function buildMinimaxApiModelDefinition(modelId: string): ModelDefinitionConfig { + const catalog = MINIMAX_TEXT_MODEL_CATALOG[modelId as MinimaxCatalogId]; return buildMinimaxModelDefinition({ id: modelId, - cost: MINIMAX_API_COST, - contextWindow: DEFAULT_MINIMAX_CONTEXT_WINDOW, - maxTokens: DEFAULT_MINIMAX_MAX_TOKENS, + name: catalog?.name, + reasoning: catalog?.reasoning, + input: catalog?.input, + cost: catalog?.cost ?? MINIMAX_API_COST, + contextWindow: catalog?.contextWindow ?? DEFAULT_MINIMAX_CONTEXT_WINDOW, + maxTokens: catalog?.maxTokens ?? DEFAULT_MINIMAX_MAX_TOKENS, }); } diff --git a/extensions/minimax/onboard.test.ts b/extensions/minimax/onboard.test.ts index 331533f0481..fa1acc69988 100644 --- a/extensions/minimax/onboard.test.ts +++ b/extensions/minimax/onboard.test.ts @@ -8,7 +8,11 @@ import { createLegacyProviderConfig, EXPECTED_FALLBACKS, } from "../../test/helpers/extensions/onboard-config.js"; -import { applyMinimaxApiConfig, applyMinimaxApiProviderConfig } from "./onboard.js"; +import { + applyMinimaxApiConfig, + applyMinimaxApiConfigCn, + applyMinimaxApiProviderConfig, +} from "./onboard.js"; describe("minimax onboard", () => { it("adds minimax provider with correct settings", () => { @@ -20,9 +24,32 @@ describe("minimax onboard", () => { }); }); - it("keeps reasoning enabled for MiniMax-M2.7", () => { + it("uses the CN Anthropic endpoint for CN onboarding", () => { + const cfg = applyMinimaxApiConfigCn({}); + expect(cfg.models?.providers?.minimax).toMatchObject({ + baseUrl: "https://api.minimaxi.com/anthropic", + api: "anthropic-messages", + }); + }); + + it("uses M3 model metadata by default", () => { + const cfg = applyMinimaxApiConfig({}); + expect(cfg.models?.providers?.minimax?.models[0]).toMatchObject({ + id: "MiniMax-M3", + reasoning: true, + input: ["text", "image"], + contextWindow: 1_000_000, + }); + }); + + it("keeps M2.7 model-specific metadata when selected", () => { const cfg = applyMinimaxApiConfig({}, "MiniMax-M2.7"); - expect(cfg.models?.providers?.minimax?.models[0]?.reasoning).toBe(true); + expect(cfg.models?.providers?.minimax?.models[0]).toMatchObject({ + id: "MiniMax-M2.7", + reasoning: true, + input: ["text"], + contextWindow: 204_800, + }); }); it("preserves existing model params when adding alias", () => { @@ -31,7 +58,7 @@ describe("minimax onboard", () => { agents: { defaults: { models: { - "minimax/MiniMax-M2.7": { + "minimax/MiniMax-M3": { alias: "MiniMax", params: { custom: "value" }, }, @@ -39,9 +66,9 @@ describe("minimax onboard", () => { }, }, }, - "MiniMax-M2.7", + "MiniMax-M3", ); - expect(cfg.agents?.defaults?.models?.["minimax/MiniMax-M2.7"]).toMatchObject({ + expect(cfg.agents?.defaults?.models?.["minimax/MiniMax-M3"]).toMatchObject({ alias: "Minimax", params: { custom: "value" }, }); @@ -60,7 +87,7 @@ describe("minimax onboard", () => { expect(cfg.models?.providers?.minimax?.apiKey).toBe("old-key"); expect(cfg.models?.providers?.minimax?.models.map((m) => m.id)).toEqual([ "old-model", - "MiniMax-M2.7", + "MiniMax-M3", ]); }); diff --git a/extensions/minimax/onboard.ts b/extensions/minimax/onboard.ts index 86ece4348cd..40f94a267b3 100644 --- a/extensions/minimax/onboard.ts +++ b/extensions/minimax/onboard.ts @@ -61,7 +61,7 @@ function applyMinimaxApiConfigWithBaseUrl( export function applyMinimaxApiProviderConfig( cfg: OpenClawConfig, - modelId: string = "MiniMax-M2.7", + modelId: string = "MiniMax-M3", ): OpenClawConfig { return applyMinimaxApiProviderConfigWithBaseUrl(cfg, { providerId: "minimax", @@ -72,7 +72,7 @@ export function applyMinimaxApiProviderConfig( export function applyMinimaxApiConfig( cfg: OpenClawConfig, - modelId: string = "MiniMax-M2.7", + modelId: string = "MiniMax-M3", ): OpenClawConfig { return applyMinimaxApiConfigWithBaseUrl(cfg, { providerId: "minimax", @@ -83,7 +83,7 @@ export function applyMinimaxApiConfig( export function applyMinimaxApiProviderConfigCn( cfg: OpenClawConfig, - modelId: string = "MiniMax-M2.7", + modelId: string = "MiniMax-M3", ): OpenClawConfig { return applyMinimaxApiProviderConfigWithBaseUrl(cfg, { providerId: "minimax", @@ -94,7 +94,7 @@ export function applyMinimaxApiProviderConfigCn( export function applyMinimaxApiConfigCn( cfg: OpenClawConfig, - modelId: string = "MiniMax-M2.7", + modelId: string = "MiniMax-M3", ): OpenClawConfig { return applyMinimaxApiConfigWithBaseUrl(cfg, { providerId: "minimax", diff --git a/extensions/minimax/openclaw.plugin.json b/extensions/minimax/openclaw.plugin.json index 8f0da029322..e71c319af66 100644 --- a/extensions/minimax/openclaw.plugin.json +++ b/extensions/minimax/openclaw.plugin.json @@ -17,7 +17,7 @@ "choiceHint": "Global endpoint - api.minimax.io", "groupId": "minimax", "groupLabel": "MiniMax", - "groupHint": "M2.7 (recommended)" + "groupHint": "M3 (recommended)" }, { "provider": "minimax", @@ -28,7 +28,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 ", @@ -42,7 +42,7 @@ "choiceHint": "CN endpoint - api.minimaxi.com", "groupId": "minimax", "groupLabel": "MiniMax", - "groupHint": "M2.7 (recommended)" + "groupHint": "M3 (recommended)" }, { "provider": "minimax", @@ -53,7 +53,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 1ef5d931740..ea286a56183 100644 --- a/extensions/minimax/provider-catalog.ts +++ b/extensions/minimax/provider-catalog.ts @@ -1,57 +1,11 @@ -import type { - ModelDefinitionConfig, - ModelProviderConfig, -} from "openclaw/plugin-sdk/provider-model-shared"; -import { - MINIMAX_DEFAULT_MODEL_ID, - MINIMAX_TEXT_MODEL_CATALOG, - MINIMAX_TEXT_MODEL_ORDER, -} from "./provider-models.js"; +import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; +import { buildMinimaxApiModelDefinition } from "./model-definitions.js"; +import { MINIMAX_TEXT_MODEL_ORDER } from "./provider-models.js"; const MINIMAX_PORTAL_BASE_URL = "https://api.minimax.io/anthropic"; -const MINIMAX_DEFAULT_CONTEXT_WINDOW = 204800; -const MINIMAX_DEFAULT_MAX_TOKENS = 131072; -const MINIMAX_API_COST = { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0.375, -}; -function buildMinimaxModel(params: { - id: string; - name: string; - reasoning: boolean; - input: ModelDefinitionConfig["input"]; -}): ModelDefinitionConfig { - return { - id: params.id, - name: params.name, - reasoning: params.reasoning, - input: params.input, - cost: MINIMAX_API_COST, - contextWindow: MINIMAX_DEFAULT_CONTEXT_WINDOW, - maxTokens: MINIMAX_DEFAULT_MAX_TOKENS, - }; -} - -function buildMinimaxTextModel(params: { - id: string; - name: string; - reasoning: boolean; -}): ModelDefinitionConfig { - return buildMinimaxModel({ ...params, input: ["text"] }); -} - -function buildMinimaxCatalog(): ModelDefinitionConfig[] { - return MINIMAX_TEXT_MODEL_ORDER.map((id) => { - const model = MINIMAX_TEXT_MODEL_CATALOG[id]; - return buildMinimaxTextModel({ - id, - name: model.name, - reasoning: model.reasoning, - }); - }); +function buildMinimaxCatalog() { + return MINIMAX_TEXT_MODEL_ORDER.map((id) => buildMinimaxApiModelDefinition(id)); } export function buildMinimaxProvider(): ModelProviderConfig { diff --git a/extensions/minimax/provider-models.ts b/extensions/minimax/provider-models.ts index b4a026deda5..979d360265c 100644 --- a/extensions/minimax/provider-models.ts +++ b/extensions/minimax/provider-models.ts @@ -1,20 +1,52 @@ +import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared"; 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; + +type MinimaxModelCatalogEntry = Pick< + ModelDefinitionConfig, + "name" | "reasoning" | "input" | "cost" | "contextWindow" | "maxTokens" +>; 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 }, -} as const; + "MiniMax-M3": { + name: "MiniMax M3", + reasoning: true, + input: ["text", "image"], + cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 }, + contextWindow: 1_000_000, + maxTokens: 131_072, + }, + "MiniMax-M2.7": { + name: "MiniMax M2.7", + reasoning: true, + input: ["text"], + cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 }, + contextWindow: 204_800, + maxTokens: 131_072, + }, + "MiniMax-M2.7-highspeed": { + name: "MiniMax M2.7 Highspeed", + reasoning: true, + input: ["text"], + cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 }, + contextWindow: 204_800, + maxTokens: 131_072, + }, +} satisfies Record<(typeof MINIMAX_TEXT_MODEL_ORDER)[number], MinimaxModelCatalogEntry>; 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/src/agents/models-config.providers.minimax.test.ts b/src/agents/models-config.providers.minimax.test.ts index e4d31e05af2..ae8b77d7a3b 100644 --- a/src/agents/models-config.providers.minimax.test.ts +++ b/src/agents/models-config.providers.minimax.test.ts @@ -36,12 +36,26 @@ describe("minimax provider catalog", () => { const providers = await resolveImplicitProvidersForTest({ agentDir }); 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", ]); + expect(providers?.minimax?.models?.[0]).toMatchObject({ + id: "MiniMax-M3", + input: ["text", "image"], + contextWindow: 1_000_000, + cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 }, + }); + expect(providers?.minimax?.models?.[1]).toMatchObject({ + id: "MiniMax-M2.7", + input: ["text"], + contextWindow: 204_800, + cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 }, + }); }); }); diff --git a/src/agents/pi-embedded-runner/extra-params.ts b/src/agents/pi-embedded-runner/extra-params.ts index 7f474e5fc7c..1c79146e89e 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/pi-embedded-runner/extra-params.ts @@ -20,7 +20,11 @@ import { import { createBedrockNoCacheWrapper, isAnthropicBedrockModel } from "./bedrock-stream-wrappers.js"; import { createGoogleThinkingPayloadWrapper } from "./google-stream-wrappers.js"; import { log } from "./logger.js"; -import { createMinimaxFastModeWrapper } from "./minimax-stream-wrappers.js"; +import { + createMinimaxFastModeWrapper, + createMinimaxM3ThinkingWrapper, + shouldApplyMinimaxM3ThinkingCompat, +} from "./minimax-stream-wrappers.js"; import { createMoonshotThinkingWrapper, resolveMoonshotThinkingType, @@ -364,6 +368,15 @@ function applyPostPluginStreamWrappers( // upstream model-ID heuristics for Gemini 3.1 variants. ctx.agent.streamFn = createGoogleThinkingPayloadWrapper(ctx.agent.streamFn, ctx.thinkingLevel); + if ( + shouldApplyMinimaxM3ThinkingCompat({ + provider: ctx.provider, + modelId: ctx.modelId, + }) + ) { + ctx.agent.streamFn = createMinimaxM3ThinkingWrapper(ctx.agent.streamFn); + } + const anthropicFastMode = resolveAnthropicFastMode(ctx.effectiveExtraParams); if (anthropicFastMode !== undefined) { log.debug( diff --git a/src/agents/pi-embedded-runner/minimax-stream-wrappers.test.ts b/src/agents/pi-embedded-runner/minimax-stream-wrappers.test.ts new file mode 100644 index 00000000000..19b7ebc3784 --- /dev/null +++ b/src/agents/pi-embedded-runner/minimax-stream-wrappers.test.ts @@ -0,0 +1,60 @@ +import type { StreamFn } from "@mariozechner/pi-agent-core"; +import { describe, expect, it } from "vitest"; +import { + createMinimaxM3ThinkingWrapper, + shouldApplyMinimaxM3ThinkingCompat, +} from "./minimax-stream-wrappers.js"; + +function captureThinkingPayload(model: Parameters[0]) { + let captured: unknown; + const underlying: StreamFn = (payloadModel, _context, options) => { + const payload = { thinking: { type: "enabled", budget_tokens: 1024 } }; + options?.onPayload?.(payload, payloadModel); + captured = payload; + return {} as ReturnType; + }; + + void createMinimaxM3ThinkingWrapper(underlying)( + model, + { messages: [] } as Parameters[1], + {} as Parameters[2], + ); + return captured; +} + +describe("minimax stream wrappers", () => { + it("recognizes M3 on both MiniMax providers", () => { + expect(shouldApplyMinimaxM3ThinkingCompat({ provider: "minimax", modelId: "MiniMax-M3" })).toBe( + true, + ); + expect( + shouldApplyMinimaxM3ThinkingCompat({ + provider: "minimax-portal", + modelId: "MiniMax-M3", + }), + ).toBe(true); + expect( + shouldApplyMinimaxM3ThinkingCompat({ provider: "minimax", modelId: "MiniMax-M2.7" }), + ).toBe(false); + }); + + it("normalizes Anthropic M3 thinking to adaptive", () => { + expect( + captureThinkingPayload({ + api: "anthropic-messages", + provider: "minimax", + id: "MiniMax-M3", + } as Parameters[0]), + ).toEqual({ thinking: { type: "adaptive" } }); + }); + + it("leaves other model payloads unchanged", () => { + expect( + captureThinkingPayload({ + api: "anthropic-messages", + provider: "minimax", + id: "MiniMax-M2.7", + } as Parameters[0]), + ).toEqual({ thinking: { type: "enabled", budget_tokens: 1024 } }); + }); +}); diff --git a/src/agents/pi-embedded-runner/minimax-stream-wrappers.ts b/src/agents/pi-embedded-runner/minimax-stream-wrappers.ts index e8f83c809fa..4cb51fc067d 100644 --- a/src/agents/pi-embedded-runner/minimax-stream-wrappers.ts +++ b/src/agents/pi-embedded-runner/minimax-stream-wrappers.ts @@ -1,10 +1,45 @@ import type { StreamFn } from "@mariozechner/pi-agent-core"; import { streamSimple } from "@mariozechner/pi-ai"; +import { streamWithPayloadPatch } from "./stream-payload-utils.js"; const MINIMAX_FAST_MODEL_IDS = new Map([ ["MiniMax-M2.7", "MiniMax-M2.7-highspeed"], ]); +export function shouldApplyMinimaxM3ThinkingCompat(params: { + provider: string; + modelId: string; +}): boolean { + return ( + (params.provider === "minimax" || params.provider === "minimax-portal") && + params.modelId.trim().toLowerCase() === "minimax-m3" + ); +} + +export function createMinimaxM3ThinkingWrapper(baseStreamFn: StreamFn | undefined): StreamFn { + const underlying = baseStreamFn ?? streamSimple; + return (model, context, options) => { + if ( + model.api !== "anthropic-messages" || + !shouldApplyMinimaxM3ThinkingCompat({ provider: model.provider, modelId: model.id }) + ) { + return underlying(model, context, options); + } + + return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => { + const thinking = payloadObj.thinking; + if ( + thinking && + typeof thinking === "object" && + !Array.isArray(thinking) && + (thinking as Record).type === "enabled" + ) { + payloadObj.thinking = { type: "adaptive" }; + } + }); + }; +} + function resolveMinimaxFastModelId(modelId: unknown): string | undefined { if (typeof modelId !== "string") { return undefined; diff --git a/src/plugins/bundled-plugin-metadata.generated.ts b/src/plugins/bundled-plugin-metadata.generated.ts index 45bede0f5fe..976d7e95404 100644 --- a/src/plugins/bundled-plugin-metadata.generated.ts +++ b/src/plugins/bundled-plugin-metadata.generated.ts @@ -9713,7 +9713,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ choiceHint: "Global endpoint - api.minimax.io", groupId: "minimax", groupLabel: "MiniMax", - groupHint: "M2.7 (recommended)", + groupHint: "M3 (recommended)", }, { provider: "minimax", @@ -9724,7 +9724,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ 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 ", @@ -9738,7 +9738,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ choiceHint: "CN endpoint - api.minimaxi.com", groupId: "minimax", groupLabel: "MiniMax", - groupHint: "M2.7 (recommended)", + groupHint: "M3 (recommended)", }, { provider: "minimax", @@ -9749,7 +9749,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [ 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 ",