fix(moonshot): apply native thinking payload compatibility

This commit is contained in:
Peter Steinberger
2026-03-03 01:03:24 +00:00
parent 287606e445
commit ced267c5cb
3 changed files with 232 additions and 0 deletions

View File

@@ -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.<provider/model>.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.

View File

@@ -411,6 +411,92 @@ describe("applyExtraParamsToAgent", () => {
expect(payloads[0]?.thinking).toBe("off");
});
it("maps thinkingLevel=off to Moonshot thinking.type=disabled", () => {
const payloads: Record<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = {};
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
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<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = { tool_choice: "required" };
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
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<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = {};
options?.onPayload?.(payload);
payloads.push(payload);
return {} as ReturnType<StreamFn>;
};
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<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {

View File

@@ -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<string, unknown>).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<string, unknown>).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<string, unknown>;
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