diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md
index e788ee699ae1..8bcbeb3f9d6e 100644
--- a/docs/concepts/model-providers.md
+++ b/docs/concepts/model-providers.md
@@ -532,7 +532,7 @@ MiniMax is configured via `models.providers` because it uses custom endpoints:
See [/providers/minimax](/providers/minimax) for setup details, model options, and config snippets.
-On MiniMax's Anthropic-compatible streaming path, OpenClaw disables thinking by default unless you explicitly set it, and `/fast on` rewrites `MiniMax-M2.7` to `MiniMax-M2.7-highspeed`.
+On MiniMax's Anthropic-compatible streaming path, OpenClaw disables thinking by default for the M2.x family unless you explicitly set it; MiniMax-M3 (and M3.x) stays on the provider's omitted/adaptive thinking path by default. `/fast on` rewrites `MiniMax-M2.7` to `MiniMax-M2.7-highspeed`.
Plugin-owned capability split:
diff --git a/docs/gateway/config-tools.md b/docs/gateway/config-tools.md
index 37c4b426951e..c6dd3de72fcd 100644
--- a/docs/gateway/config-tools.md
+++ b/docs/gateway/config-tools.md
@@ -681,7 +681,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 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`.
+ 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 M2.x thinking by default unless you explicitly set `thinking` yourself; MiniMax-M3 (and M3.x) stays on the provider's omitted/adaptive thinking path by default. `/fast on` or `params.fastMode: true` rewrites `MiniMax-M2.7` to `MiniMax-M2.7-highspeed`.
diff --git a/docs/providers/minimax.md b/docs/providers/minimax.md
index ca871408dd3c..c04a99b5031f 100644
--- a/docs/providers/minimax.md
+++ b/docs/providers/minimax.md
@@ -176,7 +176,7 @@ Choose your preferred auth method and follow the setup steps.
```
- On the Anthropic-compatible streaming path, OpenClaw disables MiniMax thinking by default unless you explicitly set `thinking` yourself. MiniMax's streaming endpoint emits `reasoning_content` in OpenAI-style delta chunks instead of native Anthropic thinking blocks, which can leak internal reasoning into visible output if left enabled implicitly.
+ On the Anthropic-compatible streaming path, OpenClaw disables MiniMax M2.x thinking by default unless you explicitly set `thinking` yourself. M2.x's streaming endpoint emits `reasoning_content` in OpenAI-style delta chunks instead of native Anthropic thinking blocks, which can leak internal reasoning into visible output if left enabled implicitly. MiniMax-M3 (and forward-compatible M3.x) is exempt from this default: M3 emits proper Anthropic thinking blocks and requires thinking active to produce visible content, so OpenClaw keeps M3 on the provider's omitted/adaptive thinking path.
@@ -400,9 +400,11 @@ See [MiniMax Search](/tools/minimax-search) for full web search configuration an
- On `api: "anthropic-messages"`, OpenClaw injects `thinking: { type: "disabled" }` unless thinking is already explicitly set in params/config.
+ On `api: "anthropic-messages"`, OpenClaw injects `thinking: { type: "disabled" }` for MiniMax M2.x models unless thinking is already explicitly set in params/config.
- This prevents MiniMax's streaming endpoint from emitting `reasoning_content` in OpenAI-style delta chunks, which would leak internal reasoning into visible output.
+ This prevents M2.x's streaming endpoint from emitting `reasoning_content` in OpenAI-style delta chunks, which would leak internal reasoning into visible output.
+
+ MiniMax-M3 (and M3.x) is exempt: M3 emits proper Anthropic thinking blocks and returns an empty `content` array with `stop_reason: "end_turn"` when thinking is disabled, so the wrapper keeps M3 on the provider's omitted/adaptive thinking path.
diff --git a/docs/tools/thinking.md b/docs/tools/thinking.md
index 784bd21e2dbd..8dde80d2bf38 100644
--- a/docs/tools/thinking.md
+++ b/docs/tools/thinking.md
@@ -33,7 +33,7 @@ title: "Thinking levels"
- Custom OpenAI-compatible catalog entries can opt into `/think xhigh` by setting `models.providers..models[].compat.supportedReasoningEfforts` to include `"xhigh"`. This uses the same compat metadata that maps outbound OpenAI reasoning effort payloads, so menus, session validation, agent CLI, and `llm-task` agree with transport behavior.
- Stale configured OpenRouter Hunter Alpha refs skip proxy reasoning injection because that retired route could return final answer text through reasoning fields.
- Google Gemini maps `/think adaptive` to Gemini's provider-owned dynamic thinking. Gemini 3 requests omit a fixed `thinkingLevel`, while Gemini 2.5 requests send `thinkingBudget: -1`; fixed levels still map to the closest Gemini `thinkingLevel` or budget for that model family.
- - MiniMax (`minimax/*`) on the Anthropic-compatible streaming path defaults to `thinking: { type: "disabled" }` unless you explicitly set thinking in model params or request params. This avoids leaked `reasoning_content` deltas from MiniMax's non-native Anthropic stream format.
+ - MiniMax M2.x (`minimax/MiniMax-M2*`) on the Anthropic-compatible streaming path defaults to `thinking: { type: "disabled" }` unless you explicitly set thinking in model params or request params. This avoids leaked `reasoning_content` deltas from M2.x's non-native Anthropic stream format. MiniMax-M3 (and M3.x) is exempt: M3 emits proper Anthropic thinking blocks and returns empty content when thinking is disabled, so OpenClaw keeps M3 on the provider's omitted/adaptive thinking path.
- Z.AI (`zai/*`) only supports binary thinking (`on`/`off`). Any non-`off` level is treated as `on` (mapped to `low`).
- Moonshot (`moonshot/*`) maps `/think off` to `thinking: { type: "disabled" }` and any non-`off` level to `thinking: { type: "enabled" }`. When thinking is enabled, Moonshot only accepts `tool_choice` `auto|none`; OpenClaw normalizes incompatible values to `auto`.
diff --git a/extensions/minimax/index.test.ts b/extensions/minimax/index.test.ts
index eaad2957437b..fc1c1e44f4f2 100644
--- a/extensions/minimax/index.test.ts
+++ b/extensions/minimax/index.test.ts
@@ -71,6 +71,30 @@ describe("minimax provider hooks", () => {
).toBe("native");
});
+ it("defaults M3 thinking on while keeping M2.x thinking off by default", async () => {
+ const { providers } = await registerProviderPlugin({
+ plugin: minimaxProviderPlugin,
+ id: "minimax",
+ name: "MiniMax Provider",
+ });
+ const apiProvider = requireRegisteredProvider(providers, "minimax");
+ const portalProvider = requireRegisteredProvider(providers, "minimax-portal");
+
+ expect(apiProvider.resolveThinkingProfile?.({ modelId: "MiniMax-M3" } as never)).toMatchObject({
+ defaultLevel: "adaptive",
+ });
+ expect(
+ apiProvider.resolveThinkingProfile?.({ modelId: "MiniMax-M2.7" } as never),
+ ).toMatchObject({
+ defaultLevel: "off",
+ });
+ expect(
+ portalProvider.resolveThinkingProfile?.({ modelId: "MiniMax-M3" } as never),
+ ).toMatchObject({
+ defaultLevel: "adaptive",
+ });
+ });
+
it("keeps MiniMax auth setup metadata aligned across regions", async () => {
const { providers } = await registerProviderPlugin({
plugin: minimaxProviderPlugin,
diff --git a/extensions/minimax/provider-contract-api.ts b/extensions/minimax/provider-contract-api.ts
index ee4d82bf3ea6..99f066eedcaa 100644
--- a/extensions/minimax/provider-contract-api.ts
+++ b/extensions/minimax/provider-contract-api.ts
@@ -1,5 +1,6 @@
// Minimax API module exposes the plugin public contract.
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
+import { resolveMinimaxThinkingProfile } from "./thinking.js";
const noopAuth = async () => ({ profiles: [] });
const wizardGroup = {
@@ -15,6 +16,7 @@ export function createMinimaxProvider(): ProviderPlugin {
hookAliases: ["minimax-cn"],
docsPath: "/providers/minimax",
envVars: ["MINIMAX_API_KEY"],
+ resolveThinkingProfile: ({ modelId }) => resolveMinimaxThinkingProfile(modelId),
auth: [
{
id: "api-global",
@@ -53,6 +55,7 @@ export function createMinimaxPortalProvider(): ProviderPlugin {
hookAliases: ["minimax-portal-cn"],
docsPath: "/providers/minimax",
envVars: ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"],
+ resolveThinkingProfile: ({ modelId }) => resolveMinimaxThinkingProfile(modelId),
auth: [
{
id: "oauth",
diff --git a/extensions/minimax/provider-policy-api.ts b/extensions/minimax/provider-policy-api.ts
new file mode 100644
index 000000000000..87dfc9666022
--- /dev/null
+++ b/extensions/minimax/provider-policy-api.ts
@@ -0,0 +1,7 @@
+// MiniMax policy module exposes static provider policy before runtime registration.
+import type { ProviderDefaultThinkingPolicyContext } from "openclaw/plugin-sdk/core";
+import { resolveMinimaxThinkingProfile } from "./thinking.js";
+
+export function resolveThinkingProfile(context: ProviderDefaultThinkingPolicyContext) {
+ return resolveMinimaxThinkingProfile(context.modelId);
+}
diff --git a/extensions/minimax/provider-registration.ts b/extensions/minimax/provider-registration.ts
index 034561a7cd74..719e7cd71149 100644
--- a/extensions/minimax/provider-registration.ts
+++ b/extensions/minimax/provider-registration.ts
@@ -38,6 +38,7 @@ import {
buildMinimaxProvider,
resolveMinimaxCatalogBaseUrl,
} from "./provider-catalog.js";
+import { resolveMinimaxThinkingProfile } from "./thinking.js";
const API_PROVIDER_ID = "minimax";
const PORTAL_PROVIDER_ID = "minimax-portal";
@@ -64,6 +65,8 @@ const MINIMAX_PROVIDER_HOOKS = {
...HYBRID_ANTHROPIC_OPENAI_REPLAY_HOOKS,
...MINIMAX_FAST_MODE_STREAM_HOOKS,
resolveReasoningOutputMode: () => "native" as const,
+ resolveThinkingProfile: ({ modelId }: { modelId: string }) =>
+ resolveMinimaxThinkingProfile(modelId),
};
function getDefaultBaseUrl(region: MiniMaxRegion): string {
diff --git a/extensions/minimax/thinking.ts b/extensions/minimax/thinking.ts
new file mode 100644
index 000000000000..23e35e0cdbc9
--- /dev/null
+++ b/extensions/minimax/thinking.ts
@@ -0,0 +1,23 @@
+// MiniMax thinking policy keeps M3 active by default while preserving M2.x leak prevention.
+import type { ProviderThinkingProfile } from "openclaw/plugin-sdk/plugin-entry";
+
+const BUDGET_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"] as const;
+const ADAPTIVE_THINKING_LEVELS = ["off", "adaptive"] as const;
+
+export function resolveMinimaxThinkingProfile(
+ modelId: string,
+): ProviderThinkingProfile | undefined {
+ if (/^MiniMax-M3(\b|[-.])/i.test(modelId)) {
+ return {
+ levels: ADAPTIVE_THINKING_LEVELS.map((id) => ({ id })),
+ defaultLevel: "adaptive",
+ };
+ }
+ if (/^MiniMax-M2(?:\b|[-.])/i.test(modelId)) {
+ return {
+ levels: BUDGET_THINKING_LEVELS.map((id) => ({ id })),
+ defaultLevel: "off",
+ };
+ }
+ return undefined;
+}
diff --git a/src/agents/embedded-agent-runner-extraparams.test.ts b/src/agents/embedded-agent-runner-extraparams.test.ts
index 72b9830fc479..a603a27a8920 100644
--- a/src/agents/embedded-agent-runner-extraparams.test.ts
+++ b/src/agents/embedded-agent-runner-extraparams.test.ts
@@ -719,6 +719,153 @@ describe("applyExtraParamsToAgent", () => {
]);
});
+ it("removes implicit disabled thinking for MiniMax-M3 anthropic-messages payloads", () => {
+ const payloads: Record[] = [];
+ const baseStreamFn: StreamFn = (_model, _context, options) => {
+ const payload: Record = {
+ thinking: { type: "disabled" },
+ };
+ options?.onPayload?.(payload, _model);
+ payloads.push(payload);
+ return {} as ReturnType;
+ };
+ const agent = { streamFn: baseStreamFn };
+
+ applyExtraParamsToAgent(agent, undefined, "minimax", "MiniMax-M3");
+
+ const model = {
+ api: "anthropic-messages",
+ provider: "minimax",
+ id: "MiniMax-M3",
+ } as Model<"anthropic-messages">;
+ const context: Context = { messages: [] };
+ void agent.streamFn?.(model, context, {});
+
+ expect(payloads).toStrictEqual([{}]);
+ });
+
+ it("preserves explicit off thinking for MiniMax-M3 anthropic-messages payloads", () => {
+ const payloads: Record[] = [];
+ const baseStreamFn: StreamFn = (_model, _context, options) => {
+ const payload: Record = {
+ thinking: { type: "disabled" },
+ };
+ options?.onPayload?.(payload, _model);
+ payloads.push(payload);
+ return {} as ReturnType;
+ };
+ const agent = { streamFn: baseStreamFn };
+
+ applyExtraParamsToAgent(agent, undefined, "minimax", "MiniMax-M3", undefined, "off");
+
+ const model = {
+ api: "anthropic-messages",
+ provider: "minimax",
+ id: "MiniMax-M3",
+ } as Model<"anthropic-messages">;
+ const context: Context = { messages: [] };
+ void agent.streamFn?.(model, context, {});
+
+ expect(payloads).toStrictEqual([
+ {
+ thinking: { type: "disabled" },
+ },
+ ]);
+ });
+
+ it("rewrites MiniMax-M3 default budget thinking to adaptive", () => {
+ const payloads: Record[] = [];
+ const baseStreamFn: StreamFn = (_model, _context, options) => {
+ const payload: Record = {
+ thinking: { type: "enabled", budget_tokens: 1024 },
+ };
+ options?.onPayload?.(payload, _model);
+ payloads.push(payload);
+ return {} as ReturnType;
+ };
+ const agent = { streamFn: baseStreamFn };
+
+ applyExtraParamsToAgent(agent, undefined, "minimax", "MiniMax-M3", undefined, "adaptive");
+
+ const model = {
+ api: "anthropic-messages",
+ provider: "minimax",
+ id: "MiniMax-M3",
+ } as Model<"anthropic-messages">;
+ const context: Context = { messages: [] };
+ void agent.streamFn?.(model, context, {});
+
+ expect(payloads).toStrictEqual([
+ {
+ thinking: { type: "adaptive" },
+ },
+ ]);
+ });
+
+ it("restores explicit MiniMax-M3 maxTokens when rewriting budget thinking", () => {
+ const payloads: Record[] = [];
+ const baseStreamFn: StreamFn = (_model, _context, options) => {
+ const payload: Record = {
+ max_tokens: 8692,
+ thinking: { type: "enabled", budget_tokens: 8192 },
+ };
+ options?.onPayload?.(payload, _model);
+ payloads.push(payload);
+ return {} as ReturnType;
+ };
+ const agent = { streamFn: baseStreamFn };
+
+ applyExtraParamsToAgent(agent, undefined, "minimax", "MiniMax-M3", undefined, "adaptive");
+
+ const model = {
+ api: "anthropic-messages",
+ provider: "minimax",
+ id: "MiniMax-M3",
+ } as Model<"anthropic-messages">;
+ const context: Context = { messages: [] };
+ void agent.streamFn?.(model, context, { maxTokens: 500 });
+
+ expect(payloads).toStrictEqual([
+ {
+ max_tokens: 500,
+ thinking: { type: "adaptive" },
+ },
+ ]);
+ });
+
+ it("preserves downstream explicit MiniMax-M3 thinking overrides", () => {
+ const payloads: Record[] = [];
+ const baseStreamFn: StreamFn = (_model, _context, options) => {
+ const payload: Record = {
+ thinking: { type: "disabled" },
+ };
+ options?.onPayload?.(payload, _model);
+ payloads.push(payload);
+ return {} as ReturnType;
+ };
+ const agent = { streamFn: baseStreamFn };
+
+ applyExtraParamsToAgent(agent, undefined, "minimax", "MiniMax-M3");
+
+ const model = {
+ api: "anthropic-messages",
+ provider: "minimax",
+ id: "MiniMax-M3",
+ } as Model<"anthropic-messages">;
+ const context: Context = { messages: [] };
+ void agent.streamFn?.(model, context, {
+ onPayload: (payload) => {
+ (payload as Record).thinking = { type: "disabled" };
+ },
+ });
+
+ expect(payloads).toStrictEqual([
+ {
+ thinking: { type: "disabled" },
+ },
+ ]);
+ });
+
it("fills DeepSeek V4 reasoning_content for unowned OpenAI-compatible proxy models", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "opencode",
diff --git a/src/agents/embedded-agent-runner/extra-params.ts b/src/agents/embedded-agent-runner/extra-params.ts
index fb3539e4317a..3333a7c0138b 100644
--- a/src/agents/embedded-agent-runner/extra-params.ts
+++ b/src/agents/embedded-agent-runner/extra-params.ts
@@ -866,7 +866,7 @@ function applyPostPluginStreamWrappers(
// MiniMax's Anthropic-compatible stream can leak reasoning_content into the
// visible reply path because it does not emit native Anthropic thinking
// blocks. Disable thinking unless an earlier wrapper already set it.
- ctx.agent.streamFn = createMinimaxThinkingDisabledWrapper(ctx.agent.streamFn);
+ ctx.agent.streamFn = createMinimaxThinkingDisabledWrapper(ctx.agent.streamFn, ctx.thinkingLevel);
const rawChatTemplateKwargs = resolveAliasedParamValue(
[ctx.effectiveExtraParams, ctx.override],
diff --git a/src/agents/embedded-agent-runner/run.overflow-compaction.loop.test.ts b/src/agents/embedded-agent-runner/run.overflow-compaction.loop.test.ts
index 600ec7869cbf..f1cc55dc05cf 100644
--- a/src/agents/embedded-agent-runner/run.overflow-compaction.loop.test.ts
+++ b/src/agents/embedded-agent-runner/run.overflow-compaction.loop.test.ts
@@ -14,6 +14,7 @@ import {
mockedIsCompactionFailureError,
mockedIsLikelyContextOverflowError,
mockedLog,
+ mockedResolveModelAsync,
mockedRunEmbeddedAttempt,
mockedSessionLikelyHasOversizedToolResults,
mockedTruncateOversizedToolResultsInSession,
@@ -111,6 +112,34 @@ describe("overflow compaction in run loop", () => {
expect(result.meta.error).toBeUndefined();
});
+ it("uses provider thinking policy for configless embedded MiniMax-M3 runs", async () => {
+ mockedResolveModelAsync.mockResolvedValueOnce({
+ model: {
+ id: "MiniMax-M3",
+ provider: "minimax",
+ contextWindow: 1_000_000,
+ api: "anthropic-messages",
+ reasoning: true,
+ },
+ error: null,
+ authStorage: {
+ setRuntimeApiKey: vi.fn(),
+ },
+ modelRegistry: {},
+ });
+ mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null }));
+
+ await runEmbeddedAgent({
+ ...baseParams,
+ config: undefined,
+ provider: "minimax",
+ model: "MiniMax-M3",
+ runId: "run-configless-minimax-m3-thinking-default",
+ });
+
+ expect(requireMockCallArg(mockedRunEmbeddedAttempt, 0).thinkLevel).toBe("adaptive");
+ });
+
it("continues from transcript after compaction when the current inbound message was persisted", async () => {
const overflowError = makeOverflowError();
diff --git a/src/agents/embedded-agent-runner/run.ts b/src/agents/embedded-agent-runner/run.ts
index bb1e1e542a33..163a636bfd62 100644
--- a/src/agents/embedded-agent-runner/run.ts
+++ b/src/agents/embedded-agent-runner/run.ts
@@ -88,6 +88,7 @@ import {
resolveAuthProfileOrder,
shouldPreferExplicitConfigApiKeyAuth,
} from "../model-auth.js";
+import { resolveThinkingDefault } from "../model-thinking-default.js";
import { ensureOpenClawModelsJson } from "../models-config.js";
import {
OPENAI_PROVIDER_ID,
@@ -229,6 +230,31 @@ function isNoRealConversationCompactionNoop(params: {
);
}
+function resolveInitialThinkLevel(params: {
+ requested?: ThinkLevel;
+ config?: RunEmbeddedAgentParams["config"];
+ provider: string;
+ modelId: string;
+ model: { reasoning?: boolean };
+}): ThinkLevel {
+ if (params.requested) {
+ return params.requested;
+ }
+ return resolveThinkingDefault({
+ cfg: params.config ?? {},
+ provider: params.provider,
+ model: params.modelId,
+ catalog: [
+ {
+ provider: params.provider,
+ id: params.modelId,
+ name: params.modelId,
+ reasoning: params.model.reasoning,
+ },
+ ],
+ });
+}
+
async function resetNoRealConversationTokenSnapshot(params: {
config?: RunEmbeddedAgentParams["config"];
sessionKey?: string;
@@ -1030,7 +1056,13 @@ export async function runEmbeddedAgent(
let profileIndex = 0;
const traceAttempts: TraceAttempt[] = [];
- const initialThinkLevel = params.thinkLevel ?? "off";
+ const initialThinkLevel = resolveInitialThinkLevel({
+ requested: params.thinkLevel,
+ config: params.config,
+ provider,
+ modelId,
+ model: effectiveModel,
+ });
let thinkLevel = initialThinkLevel;
const attemptedThinking = new Set();
let apiKeyInfo: ApiKeyInfo | null = null;
@@ -1724,9 +1756,9 @@ export async function runEmbeddedAgent(
lastAssistant: sessionLastAssistant,
currentAttemptAssistant,
} = attempt;
- const setTerminalLifecycleMeta: NonNullable<
- typeof attempt.setTerminalLifecycleMeta
- > = (meta) => {
+ const setTerminalLifecycleMeta: NonNullable = (
+ meta,
+ ) => {
attempt.setTerminalLifecycleMeta?.({ ...meta, aborted });
};
const timedOutDuringToolExecution = attempt.timedOutDuringToolExecution ?? false;
diff --git a/src/llm/providers/stream-wrappers/minimax.test.ts b/src/llm/providers/stream-wrappers/minimax.test.ts
index cc1613f110f7..5b37d5011d70 100644
--- a/src/llm/providers/stream-wrappers/minimax.test.ts
+++ b/src/llm/providers/stream-wrappers/minimax.test.ts
@@ -1,13 +1,14 @@
-// MiniMax stream wrapper tests cover streamed text and reasoning conversion.
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import type { Context, Model } from "openclaw/plugin-sdk/llm";
import { describe, expect, it } from "vitest";
+import type { ThinkLevel } from "../../../auto-reply/thinking.js";
import { createMinimaxFastModeWrapper, createMinimaxThinkingDisabledWrapper } from "./minimax.js";
function captureThinkingPayload(params: {
provider: string;
api: string;
modelId: string;
+ thinkingLevel?: ThinkLevel;
}): unknown {
let capturedThinking: unknown = undefined;
const baseStreamFn: StreamFn = (model, context, options) => {
@@ -17,7 +18,7 @@ function captureThinkingPayload(params: {
return {} as ReturnType;
};
- const wrapped = createMinimaxThinkingDisabledWrapper(baseStreamFn);
+ const wrapped = createMinimaxThinkingDisabledWrapper(baseStreamFn, params.thinkingLevel);
void wrapped(
{
api: params.api,
@@ -72,6 +73,164 @@ describe("createMinimaxThinkingDisabledWrapper", () => {
).toBeUndefined();
});
+ it("does NOT disable thinking for MiniMax-M3 on anthropic-messages", () => {
+ // M3 emits Anthropic-shape thinking blocks and returns empty content
+ // when thinking is disabled; see isMinimaxModelRequiringThinking.
+ expect(
+ captureThinkingPayload({
+ provider: "minimax",
+ api: "anthropic-messages",
+ modelId: "MiniMax-M3",
+ }),
+ ).toBeUndefined();
+ });
+
+ it("does NOT disable thinking for MiniMax-M3 on minimax-portal", () => {
+ expect(
+ captureThinkingPayload({
+ provider: "minimax-portal",
+ api: "anthropic-messages",
+ modelId: "MiniMax-M3",
+ }),
+ ).toBeUndefined();
+ });
+
+ it("removes implicit disabled thinking for MiniMax-M3", () => {
+ let capturedThinking: unknown = undefined;
+ const baseStreamFn: StreamFn = (model, context, options) => {
+ const payload: Record = {
+ thinking: { type: "disabled" },
+ };
+ options?.onPayload?.(payload, model);
+ capturedThinking = payload.thinking;
+ return {} as ReturnType;
+ };
+
+ const wrapped = createMinimaxThinkingDisabledWrapper(baseStreamFn);
+ void wrapped(
+ {
+ api: "anthropic-messages",
+ provider: "minimax",
+ id: "MiniMax-M3",
+ } as Model<"anthropic-messages">,
+ { messages: [] } as Context,
+ {},
+ );
+
+ expect(capturedThinking).toBeUndefined();
+ });
+
+ it("preserves explicit off thinking for MiniMax-M3", () => {
+ let capturedThinking: unknown = undefined;
+ const baseStreamFn: StreamFn = (model, context, options) => {
+ const payload: Record = {
+ thinking: { type: "disabled" },
+ };
+ options?.onPayload?.(payload, model);
+ capturedThinking = payload.thinking;
+ return {} as ReturnType;
+ };
+
+ const wrapped = createMinimaxThinkingDisabledWrapper(baseStreamFn, "off");
+ void wrapped(
+ {
+ api: "anthropic-messages",
+ provider: "minimax",
+ id: "MiniMax-M3",
+ } as Model<"anthropic-messages">,
+ { messages: [] } as Context,
+ {},
+ );
+
+ expect(capturedThinking).toEqual({ type: "disabled" });
+ });
+
+ it("rewrites MiniMax-M3 default budget thinking to adaptive", () => {
+ let capturedThinking: unknown = undefined;
+ const baseStreamFn: StreamFn = (model, context, options) => {
+ const payload: Record = {
+ thinking: { type: "enabled", budget_tokens: 1024 },
+ };
+ options?.onPayload?.(payload, model);
+ capturedThinking = payload.thinking;
+ return {} as ReturnType;
+ };
+
+ const wrapped = createMinimaxThinkingDisabledWrapper(baseStreamFn, "adaptive");
+ void wrapped(
+ {
+ api: "anthropic-messages",
+ provider: "minimax",
+ id: "MiniMax-M3",
+ } as Model<"anthropic-messages">,
+ { messages: [] } as Context,
+ {},
+ );
+
+ expect(capturedThinking).toEqual({ type: "adaptive" });
+ });
+
+ it("restores explicit MiniMax-M3 maxTokens when rewriting budget thinking", () => {
+ let capturedPayload: Record | undefined;
+ const baseStreamFn: StreamFn = (model, context, options) => {
+ const payload: Record = {
+ max_tokens: 8692,
+ thinking: { type: "enabled", budget_tokens: 8192 },
+ };
+ options?.onPayload?.(payload, model);
+ capturedPayload = payload;
+ return {} as ReturnType;
+ };
+
+ const wrapped = createMinimaxThinkingDisabledWrapper(baseStreamFn, "adaptive");
+ void wrapped(
+ {
+ api: "anthropic-messages",
+ provider: "minimax",
+ id: "MiniMax-M3",
+ } as Model<"anthropic-messages">,
+ { messages: [] } as Context,
+ { maxTokens: 500 },
+ );
+
+ expect(capturedPayload).toMatchObject({
+ max_tokens: 500,
+ thinking: { type: "adaptive" },
+ });
+ });
+
+ it("preserves explicit enabled thinking for MiniMax-M3", () => {
+ let capturedThinking: unknown = undefined;
+ const baseStreamFn: StreamFn = (model, context, options) => {
+ const payload: Record = {
+ thinking: { type: "disabled" },
+ };
+ options?.onPayload?.(payload, model);
+ capturedThinking = payload.thinking;
+ return {} as ReturnType;
+ };
+
+ const wrapped = createMinimaxThinkingDisabledWrapper(baseStreamFn);
+ void wrapped(
+ {
+ api: "anthropic-messages",
+ provider: "minimax",
+ id: "MiniMax-M3",
+ } as Model<"anthropic-messages">,
+ { messages: [] } as Context,
+ {
+ onPayload: (payload) => {
+ (payload as Record).thinking = {
+ type: "enabled",
+ budget_tokens: 1024,
+ };
+ },
+ },
+ );
+
+ expect(capturedThinking).toEqual({ type: "enabled", budget_tokens: 1024 });
+ });
+
it("preserves an already-set thinking value", () => {
let capturedThinking: unknown = undefined;
const baseStreamFn: StreamFn = (model, context, options) => {
diff --git a/src/llm/providers/stream-wrappers/minimax.ts b/src/llm/providers/stream-wrappers/minimax.ts
index 678df71a7910..6a1fcdf5dad5 100644
--- a/src/llm/providers/stream-wrappers/minimax.ts
+++ b/src/llm/providers/stream-wrappers/minimax.ts
@@ -1,5 +1,5 @@
-// MiniMax stream wrapper normalizes MiniMax streamed text and reasoning output.
import type { StreamFn } from "../../../agents/runtime/index.js";
+import type { ThinkLevel } from "../../../auto-reply/thinking.js";
import { streamSimple } from "../../stream.js";
const MINIMAX_FAST_MODEL_IDS = new Map([
@@ -20,6 +20,47 @@ function isMinimaxAnthropicMessagesModel(model: { api?: unknown; provider?: unkn
);
}
+/**
+ * MiniMax-M3 (and any forward-compatible MiniMax-M3.x successor) emits proper
+ * Anthropic-shape thinking blocks (`content_block_start` with `type:"thinking"`
+ * + `thinking_delta`) and **requires** thinking to be active to produce any
+ * visible text. Pinning `thinking: { type: "disabled" }` on M3 makes the model
+ * return an empty content array with `stop_reason: "end_turn"` and 1 output
+ * token, observed against `https://api.minimax.io/anthropic/v1/messages`.
+ *
+ * The legacy MiniMax-M2.x family still needs the disable-thinking shim
+ * because their Anthropic-compat streams leak `reasoning_content` in
+ * OpenAI-style deltas (see {@link createMinimaxThinkingDisabledWrapper}).
+ */
+function isMinimaxModelRequiringThinking(model: { id?: unknown }): boolean {
+ const modelId = typeof model.id === "string" ? model.id.trim() : "";
+ return /^MiniMax-M3(\b|[-.])/i.test(modelId);
+}
+
+function isDisabledThinkingPayload(value: unknown): boolean {
+ return (
+ value !== null &&
+ typeof value === "object" &&
+ !Array.isArray(value) &&
+ (value as Record).type === "disabled"
+ );
+}
+
+function isEnabledThinkingPayload(value: unknown): boolean {
+ return (
+ value !== null &&
+ typeof value === "object" &&
+ !Array.isArray(value) &&
+ (value as Record).type === "enabled"
+ );
+}
+
+function resolvePositiveMaxTokens(value: unknown): number | undefined {
+ return typeof value === "number" && Number.isFinite(value) && value > 0
+ ? Math.floor(value)
+ : undefined;
+}
+
/** @deprecated MiniMax provider-owned stream helper; do not use from third-party plugins. */
export function createMinimaxFastModeWrapper(
baseStreamFn: StreamFn | undefined,
@@ -45,20 +86,30 @@ export function createMinimaxFastModeWrapper(
}
/**
- * MiniMax's Anthropic-compatible streaming endpoint returns reasoning_content
- * in OpenAI-style delta chunks ({delta: {content: "", reasoning_content: "..."}})
- * rather than the native Anthropic thinking block format. The shared Anthropic
- * provider cannot process this format and leaks the reasoning text as visible
- * content. Disable thinking in the outgoing payload so MiniMax does not produce
- * reasoning_content deltas during streaming.
+ * Legacy MiniMax (M2.x) Anthropic-compatible streaming endpoint returns
+ * reasoning_content in OpenAI-style delta chunks ({delta: {content: "",
+ * reasoning_content: "..."}}) rather than the native Anthropic thinking
+ * block format. The shared Anthropic provider cannot process this format
+ * and leaks the reasoning text as visible content. Disable thinking in the
+ * outgoing payload so MiniMax does not produce reasoning_content deltas
+ * during streaming.
+ *
+ * Skipped for MiniMax-M3 and M3.x, which emit proper Anthropic-shape thinking
+ * blocks and require thinking enabled to produce any visible content.
+ * The Anthropic transport builds `thinking: { type: "disabled" }` when no
+ * resolved thinking level exists, so M3 removes that implicit disabled payload.
+ * See {@link isMinimaxModelRequiringThinking}.
*/
-/** @deprecated MiniMax provider-owned stream helper; do not use from third-party plugins. */
-export function createMinimaxThinkingDisabledWrapper(baseStreamFn: StreamFn | undefined): StreamFn {
+export function createMinimaxThinkingDisabledWrapper(
+ baseStreamFn: StreamFn | undefined,
+ thinkingLevel?: ThinkLevel,
+): StreamFn {
const underlying = baseStreamFn ?? streamSimple;
return (model, context, options) => {
if (!isMinimaxAnthropicMessagesModel(model)) {
return underlying(model, context, options);
}
+ const requiresThinking = isMinimaxModelRequiringThinking(model);
const originalOnPayload = options?.onPayload;
return underlying(model, context, {
@@ -66,9 +117,24 @@ export function createMinimaxThinkingDisabledWrapper(baseStreamFn: StreamFn | un
onPayload: (payload) => {
if (payload && typeof payload === "object") {
const payloadObj = payload as Record;
- // Only inject if thinking is not already explicitly set.
- // This preserves unknown intentional override from other wrappers.
- if (payloadObj.thinking === undefined) {
+ if (requiresThinking) {
+ if (thinkingLevel === undefined && isDisabledThinkingPayload(payloadObj.thinking)) {
+ delete payloadObj.thinking;
+ } else if (
+ thinkingLevel !== "off" &&
+ (isEnabledThinkingPayload(payloadObj.thinking) ||
+ isDisabledThinkingPayload(payloadObj.thinking))
+ ) {
+ payloadObj.thinking = { type: "adaptive" };
+ const maxTokens = resolvePositiveMaxTokens(options?.maxTokens);
+ if (maxTokens !== undefined) {
+ payloadObj.max_tokens = maxTokens;
+ }
+ }
+ }
+ // M2.x only needs the shim when no earlier wrapper set thinking.
+ // Downstream payload hooks still run after this wrapper.
+ if (!requiresThinking && payloadObj.thinking === undefined) {
payloadObj.thinking = { type: "disabled" };
}
}
diff --git a/src/plugins/provider-public-artifacts.test.ts b/src/plugins/provider-public-artifacts.test.ts
index 168709f4a1e8..f92b862cf978 100644
--- a/src/plugins/provider-public-artifacts.test.ts
+++ b/src/plugins/provider-public-artifacts.test.ts
@@ -45,6 +45,19 @@ describe("provider public artifacts", () => {
).toBe(providerConfig);
});
+ it("loads MiniMax thinking policy before runtime registration", () => {
+ const surface = resolveBundledProviderPolicySurface("minimax");
+
+ expect(
+ surface?.resolveThinkingProfile?.({ provider: "minimax", modelId: "MiniMax-M2.7" })
+ ?.defaultLevel,
+ ).toBe("off");
+ expect(
+ surface?.resolveThinkingProfile?.({ provider: "minimax", modelId: "MiniMax-M3" })
+ ?.defaultLevel,
+ ).toBe("adaptive");
+ });
+
it("resolves multi-provider policy artifacts by manifest-owned provider id", async () => {
const bundledPluginsDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-provider-policy-"));
const pluginDir = path.join(bundledPluginsDir, "openai");