From ced267c5cb0a15bb2bd219123c02262d9b828c63 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 3 Mar 2026 01:03:24 +0000 Subject: [PATCH] fix(moonshot): apply native thinking payload compatibility --- docs/providers/moonshot.md | 32 +++++ .../pi-embedded-runner-extraparams.test.ts | 86 +++++++++++++ src/agents/pi-embedded-runner/extra-params.ts | 114 ++++++++++++++++++ 3 files changed, 232 insertions(+) diff --git a/docs/providers/moonshot.md b/docs/providers/moonshot.md index a62b2f7ae..3e8217bbe 100644 --- a/docs/providers/moonshot.md +++ b/docs/providers/moonshot.md @@ -146,3 +146,35 @@ Note: Moonshot and Kimi Coding are separate providers. Keys are not interchangea - If Moonshot publishes different context limits for a model, adjust `contextWindow` accordingly. - Use `https://api.moonshot.ai/v1` for the international endpoint, and `https://api.moonshot.cn/v1` for the China endpoint. + +## Native thinking mode (Moonshot) + +Moonshot Kimi supports binary native thinking: + +- `thinking: { type: "enabled" }` +- `thinking: { type: "disabled" }` + +Configure it per model via `agents.defaults.models..params`: + +```json5 +{ + agents: { + defaults: { + models: { + "moonshot/kimi-k2.5": { + params: { + thinking: { type: "disabled" }, + }, + }, + }, + }, + }, +} +``` + +OpenClaw also maps runtime `/think` levels for Moonshot: + +- `/think off` -> `thinking.type=disabled` +- any non-off thinking level -> `thinking.type=enabled` + +When Moonshot thinking is enabled, `tool_choice` must be `auto` or `none`. OpenClaw normalizes incompatible `tool_choice` values to `auto` for compatibility. diff --git a/src/agents/pi-embedded-runner-extraparams.test.ts b/src/agents/pi-embedded-runner-extraparams.test.ts index 8e114cf25..2c1398d6e 100644 --- a/src/agents/pi-embedded-runner-extraparams.test.ts +++ b/src/agents/pi-embedded-runner-extraparams.test.ts @@ -411,6 +411,92 @@ describe("applyExtraParamsToAgent", () => { expect(payloads[0]?.thinking).toBe("off"); }); + it("maps thinkingLevel=off to Moonshot thinking.type=disabled", () => { + const payloads: Record[] = []; + const baseStreamFn: StreamFn = (_model, _context, options) => { + const payload: Record = {}; + options?.onPayload?.(payload); + payloads.push(payload); + return {} as ReturnType; + }; + const agent = { streamFn: baseStreamFn }; + + applyExtraParamsToAgent(agent, undefined, "moonshot", "kimi-k2.5", undefined, "off"); + + const model = { + api: "openai-completions", + provider: "moonshot", + id: "kimi-k2.5", + } as Model<"openai-completions">; + const context: Context = { messages: [] }; + void agent.streamFn?.(model, context, {}); + + expect(payloads).toHaveLength(1); + expect(payloads[0]?.thinking).toEqual({ type: "disabled" }); + }); + + it("maps non-off thinking levels to Moonshot thinking.type=enabled and normalizes tool_choice", () => { + const payloads: Record[] = []; + const baseStreamFn: StreamFn = (_model, _context, options) => { + const payload: Record = { tool_choice: "required" }; + options?.onPayload?.(payload); + payloads.push(payload); + return {} as ReturnType; + }; + const agent = { streamFn: baseStreamFn }; + + applyExtraParamsToAgent(agent, undefined, "moonshot", "kimi-k2.5", undefined, "low"); + + const model = { + api: "openai-completions", + provider: "moonshot", + id: "kimi-k2.5", + } as Model<"openai-completions">; + const context: Context = { messages: [] }; + void agent.streamFn?.(model, context, {}); + + expect(payloads).toHaveLength(1); + expect(payloads[0]?.thinking).toEqual({ type: "enabled" }); + expect(payloads[0]?.tool_choice).toBe("auto"); + }); + + it("respects explicit Moonshot thinking param from model config", () => { + const payloads: Record[] = []; + const baseStreamFn: StreamFn = (_model, _context, options) => { + const payload: Record = {}; + options?.onPayload?.(payload); + payloads.push(payload); + return {} as ReturnType; + }; + const agent = { streamFn: baseStreamFn }; + const cfg = { + agents: { + defaults: { + models: { + "moonshot/kimi-k2.5": { + params: { + thinking: { type: "disabled" }, + }, + }, + }, + }, + }, + }; + + applyExtraParamsToAgent(agent, cfg, "moonshot", "kimi-k2.5", undefined, "high"); + + const model = { + api: "openai-completions", + provider: "moonshot", + id: "kimi-k2.5", + } as Model<"openai-completions">; + const context: Context = { messages: [] }; + void agent.streamFn?.(model, context, {}); + + expect(payloads).toHaveLength(1); + expect(payloads[0]?.thinking).toEqual({ type: "disabled" }); + }); + it("removes invalid negative Google thinkingBudget and maps Gemini 3.1 to thinkingLevel", () => { const payloads: Record[] = []; const baseStreamFn: StreamFn = (_model, _context, options) => { diff --git a/src/agents/pi-embedded-runner/extra-params.ts b/src/agents/pi-embedded-runner/extra-params.ts index 4258f758d..f57bd272d 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/pi-embedded-runner/extra-params.ts @@ -560,6 +560,107 @@ function createSiliconFlowThinkingWrapper(baseStreamFn: StreamFn | undefined): S }; } +type MoonshotThinkingType = "enabled" | "disabled"; + +function normalizeMoonshotThinkingType(value: unknown): MoonshotThinkingType | undefined { + if (typeof value === "boolean") { + return value ? "enabled" : "disabled"; + } + if (typeof value === "string") { + const normalized = value.trim().toLowerCase(); + if ( + normalized === "enabled" || + normalized === "enable" || + normalized === "on" || + normalized === "true" + ) { + return "enabled"; + } + if ( + normalized === "disabled" || + normalized === "disable" || + normalized === "off" || + normalized === "false" + ) { + return "disabled"; + } + return undefined; + } + if (value && typeof value === "object" && !Array.isArray(value)) { + const typeValue = (value as Record).type; + return normalizeMoonshotThinkingType(typeValue); + } + return undefined; +} + +function resolveMoonshotThinkingType(params: { + configuredThinking: unknown; + thinkingLevel?: ThinkLevel; +}): MoonshotThinkingType | undefined { + const configured = normalizeMoonshotThinkingType(params.configuredThinking); + if (configured) { + return configured; + } + if (!params.thinkingLevel) { + return undefined; + } + return params.thinkingLevel === "off" ? "disabled" : "enabled"; +} + +function isMoonshotToolChoiceCompatible(toolChoice: unknown): boolean { + if (toolChoice == null) { + return true; + } + if (toolChoice === "auto" || toolChoice === "none") { + return true; + } + if (typeof toolChoice === "object" && !Array.isArray(toolChoice)) { + const typeValue = (toolChoice as Record).type; + return typeValue === "auto" || typeValue === "none"; + } + return false; +} + +/** + * Moonshot Kimi supports native binary thinking mode: + * - { thinking: { type: "enabled" } } + * - { thinking: { type: "disabled" } } + * + * When thinking is enabled, Moonshot only accepts tool_choice auto|none. + * Normalize incompatible values to auto instead of failing the request. + */ +function createMoonshotThinkingWrapper( + baseStreamFn: StreamFn | undefined, + thinkingType?: MoonshotThinkingType, +): StreamFn { + const underlying = baseStreamFn ?? streamSimple; + return (model, context, options) => { + const originalOnPayload = options?.onPayload; + return underlying(model, context, { + ...options, + onPayload: (payload) => { + if (payload && typeof payload === "object") { + const payloadObj = payload as Record; + let effectiveThinkingType = normalizeMoonshotThinkingType(payloadObj.thinking); + + if (thinkingType) { + payloadObj.thinking = { type: thinkingType }; + effectiveThinkingType = thinkingType; + } + + if ( + effectiveThinkingType === "enabled" && + !isMoonshotToolChoiceCompatible(payloadObj.tool_choice) + ) { + payloadObj.tool_choice = "auto"; + } + } + originalOnPayload?.(payload); + }, + }); + }; +} + /** * Create a streamFn wrapper that adds OpenRouter app attribution headers * and injects reasoning.effort based on the configured thinking level. @@ -808,6 +909,19 @@ export function applyExtraParamsToAgent( agent.streamFn = createSiliconFlowThinkingWrapper(agent.streamFn); } + if (provider === "moonshot") { + const moonshotThinkingType = resolveMoonshotThinkingType({ + configuredThinking: merged?.thinking, + thinkingLevel, + }); + if (moonshotThinkingType) { + log.debug( + `applying Moonshot thinking=${moonshotThinkingType} payload wrapper for ${provider}/${modelId}`, + ); + } + agent.streamFn = createMoonshotThinkingWrapper(agent.streamFn, moonshotThinkingType); + } + if (provider === "openrouter") { log.debug(`applying OpenRouter app attribution headers for ${provider}/${modelId}`); // "auto" is a dynamic routing model — we don't know which underlying model