fix(moonshot): apply native thinking payload compatibility
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user