diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba14e3c5c1..4590f8c087c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Ollama/Kimi Cloud: apply the Moonshot Kimi payload compatibility wrapper to Ollama-hosted Kimi models like `kimi-k2.5:cloud`, so tool routing no longer breaks when thinking is enabled. (#41519) Thanks @vincentkoc. - Models/Kimi Coding: send the built-in `User-Agent: claude-code/0.1.0` header by default for `kimi-coding` while still allowing explicit provider headers to override it, so Kimi Code subscription auth can work without a local header-injection proxy. (#30099) Thanks @Amineelfarssi and @vincentkoc. - Security/plugins: disable implicit workspace plugin auto-load so cloned repositories cannot execute workspace plugin code without an explicit trust decision. (`GHSA-99qw-6mr3-36qr`)(#44174) Thanks @lintsinghua and @vincentkoc. - Moonshot CN API: respect explicit `baseUrl` (api.moonshot.cn) in implicit provider resolution so platform.moonshot.cn API keys authenticate correctly instead of returning HTTP 401. (#33637) Thanks @chengzhichao-xydt. diff --git a/src/agents/pi-embedded-runner-extraparams.test.ts b/src/agents/pi-embedded-runner-extraparams.test.ts index 1785abfb843..04ada5e9ba6 100644 --- a/src/agents/pi-embedded-runner-extraparams.test.ts +++ b/src/agents/pi-embedded-runner-extraparams.test.ts @@ -695,6 +695,33 @@ describe("applyExtraParamsToAgent", () => { expect(payloads[0]?.tool_choice).toBe("auto"); }); + it("disables thinking instead of broadening pinned Moonshot tool_choice", () => { + const payloads: Record[] = []; + const baseStreamFn: StreamFn = (_model, _context, options) => { + const payload: Record = { + tool_choice: { type: "tool", name: "read" }, + }; + options?.onPayload?.(payload, _model); + 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: "disabled" }); + expect(payloads[0]?.tool_choice).toEqual({ type: "tool", name: "read" }); + }); + it("respects explicit Moonshot thinking param from model config", () => { const payloads: Record[] = []; const baseStreamFn: StreamFn = (_model, _context, options) => { @@ -732,6 +759,85 @@ describe("applyExtraParamsToAgent", () => { expect(payloads[0]?.thinking).toEqual({ type: "disabled" }); }); + it("applies Moonshot payload compatibility to Ollama Kimi cloud models", () => { + const payloads: Record[] = []; + const baseStreamFn: StreamFn = (_model, _context, options) => { + const payload: Record = { tool_choice: "required" }; + options?.onPayload?.(payload, _model); + payloads.push(payload); + return {} as ReturnType; + }; + const agent = { streamFn: baseStreamFn }; + + applyExtraParamsToAgent(agent, undefined, "ollama", "kimi-k2.5:cloud", undefined, "low"); + + const model = { + api: "openai-completions", + provider: "ollama", + id: "kimi-k2.5:cloud", + } 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("maps thinkingLevel=off for Ollama Kimi cloud models through Moonshot compatibility", () => { + const payloads: Record[] = []; + const baseStreamFn: StreamFn = (_model, _context, options) => { + const payload: Record = {}; + options?.onPayload?.(payload, _model); + payloads.push(payload); + return {} as ReturnType; + }; + const agent = { streamFn: baseStreamFn }; + + applyExtraParamsToAgent(agent, undefined, "ollama", "kimi-k2.5:cloud", undefined, "off"); + + const model = { + api: "openai-completions", + provider: "ollama", + id: "kimi-k2.5:cloud", + } 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("disables thinking instead of broadening pinned Ollama Kimi cloud tool_choice", () => { + const payloads: Record[] = []; + const baseStreamFn: StreamFn = (_model, _context, options) => { + const payload: Record = { + tool_choice: { type: "function", function: { name: "read" } }, + }; + options?.onPayload?.(payload, _model); + payloads.push(payload); + return {} as ReturnType; + }; + const agent = { streamFn: baseStreamFn }; + + applyExtraParamsToAgent(agent, undefined, "ollama", "kimi-k2.5:cloud", undefined, "low"); + + const model = { + api: "openai-completions", + provider: "ollama", + id: "kimi-k2.5:cloud", + } as Model<"openai-completions">; + const context: Context = { messages: [] }; + void agent.streamFn?.(model, context, {}); + + expect(payloads).toHaveLength(1); + expect(payloads[0]?.thinking).toEqual({ type: "disabled" }); + expect(payloads[0]?.tool_choice).toEqual({ + type: "function", + function: { name: "read" }, + }); + }); + it("does not rewrite tool schema for kimi-coding (native Anthropic format)", () => { 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 8f36792f393..56ee8946cbd 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/pi-embedded-runner/extra-params.ts @@ -16,6 +16,7 @@ import { createMoonshotThinkingWrapper, createSiliconFlowThinkingWrapper, resolveMoonshotThinkingType, + shouldApplyMoonshotPayloadCompat, shouldApplySiliconFlowThinkingOffCompat, } from "./moonshot-stream-wrappers.js"; import { @@ -373,7 +374,7 @@ export function applyExtraParamsToAgent( agent.streamFn = createSiliconFlowThinkingWrapper(agent.streamFn); } - if (provider === "moonshot") { + if (shouldApplyMoonshotPayloadCompat({ provider, modelId })) { const moonshotThinkingType = resolveMoonshotThinkingType({ configuredThinking: merged?.thinking, thinkingLevel, diff --git a/src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts b/src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts index 282b0960a9d..c066a168a0f 100644 --- a/src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts +++ b/src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts @@ -35,6 +35,14 @@ function isMoonshotToolChoiceCompatible(toolChoice: unknown): boolean { return false; } +function isPinnedToolChoice(toolChoice: unknown): boolean { + if (!toolChoice || typeof toolChoice !== "object" || Array.isArray(toolChoice)) { + return false; + } + const typeValue = (toolChoice as Record).type; + return typeValue === "tool" || typeValue === "function"; +} + export function shouldApplySiliconFlowThinkingOffCompat(params: { provider: string; modelId: string; @@ -47,6 +55,27 @@ export function shouldApplySiliconFlowThinkingOffCompat(params: { ); } +export function shouldApplyMoonshotPayloadCompat(params: { + provider: string; + modelId: string; +}): boolean { + const normalizedProvider = params.provider.trim().toLowerCase(); + const normalizedModelId = params.modelId.trim().toLowerCase(); + + if (normalizedProvider === "moonshot") { + return true; + } + + // Ollama Cloud exposes Kimi variants through OpenAI-compatible model IDs such + // as `kimi-k2.5:cloud`, but they still need the same payload normalization as + // native Moonshot endpoints when thinking/tool_choice are enabled together. + return ( + normalizedProvider === "ollama" && + normalizedModelId.startsWith("kimi-k") && + normalizedModelId.includes(":cloud") + ); +} + export function createSiliconFlowThinkingWrapper(baseStreamFn: StreamFn | undefined): StreamFn { const underlying = baseStreamFn ?? streamSimple; return (model, context, options) => { @@ -103,7 +132,11 @@ export function createMoonshotThinkingWrapper( effectiveThinkingType === "enabled" && !isMoonshotToolChoiceCompatible(payloadObj.tool_choice) ) { - payloadObj.tool_choice = "auto"; + if (payloadObj.tool_choice === "required") { + payloadObj.tool_choice = "auto"; + } else if (isPinnedToolChoice(payloadObj.tool_choice)) { + payloadObj.thinking = { type: "disabled" }; + } } } return originalOnPayload?.(payload, model);