diff --git a/CHANGELOG.md b/CHANGELOG.md index e4b0d8fcf81..e09727ae2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai - Matrix/onboarding: restore guided setup in `openclaw channels add` and `openclaw configure --section channels`, while keeping custom plugin wizards on the shared `setupWizard` seam. (#59462) Thanks @gumadeiras. - Feishu/comment threads: harden document comment-thread delivery so whole-document comments fall back to `add_comment`, delayed reply lookups retry more reliably, and user-visible replies avoid reasoning/planning spillover. (#59129) Thanks @wittam-01. - Matrix/streaming: keep live partial previews for the current assistant block while preserving completed block updates as separate messages when `channels.matrix.blockStreaming` is enabled. (#59384) thanks @gumadeiras +- Kimi Coding/tools: normalize Anthropic tool payloads into the OpenAI-compatible function shape Kimi Coding expects so tool calls stop losing required arguments. (#59440) Thanks @obviyus. ## 2026.4.1-beta.1 diff --git a/extensions/kimi-coding/index.ts b/extensions/kimi-coding/index.ts index 4abc425ddb9..6aa4c17605d 100644 --- a/extensions/kimi-coding/index.ts +++ b/extensions/kimi-coding/index.ts @@ -80,6 +80,8 @@ export default definePluginEntry({ }, }, capabilities: { + anthropicToolSchemaMode: "openai-functions", + anthropicToolChoiceMode: "openai-string-modes", openAiPayloadNormalizationMode: "moonshot-thinking", preserveAnthropicThinkingSignatures: false, }, diff --git a/src/agents/pi-embedded-runner-extraparams.test.ts b/src/agents/pi-embedded-runner-extraparams.test.ts index ff29485f1ec..fe177501d1c 100644 --- a/src/agents/pi-embedded-runner-extraparams.test.ts +++ b/src/agents/pi-embedded-runner-extraparams.test.ts @@ -1090,7 +1090,7 @@ describe("applyExtraParamsToAgent", () => { }); }); - it("does not rewrite tool schema for Kimi (native Anthropic format)", () => { + it("rewrites tool schema for Kimi to the OpenAI function shape", () => { const payloads: Record[] = []; const baseStreamFn: StreamFn = (_model, _context, options) => { const payload: Record = { @@ -1127,16 +1127,22 @@ describe("applyExtraParamsToAgent", () => { expect(payloads).toHaveLength(1); expect(payloads[0]?.tools).toEqual([ { - name: "read", - description: "Read file", - input_schema: { - type: "object", - properties: { path: { type: "string" } }, - required: ["path"], + type: "function", + function: { + name: "read", + description: "Read file", + parameters: { + type: "object", + properties: { path: { type: "string" } }, + required: ["path"], + }, }, }, ]); - expect(payloads[0]?.tool_choice).toEqual({ type: "tool", name: "read" }); + expect(payloads[0]?.tool_choice).toEqual({ + type: "function", + function: { name: "read" }, + }); }); it("does not rewrite anthropic tool schema for non-kimi endpoints", () => { @@ -1287,6 +1293,63 @@ describe("applyExtraParamsToAgent", () => { ); }); + it("normalizes kimi-coding anthropic tool payloads to OpenAI function shape", () => { + const payloads: Record[] = []; + const baseStreamFn: StreamFn = (_model, _context, options) => { + const payload: Record = { + tools: [ + { + name: "exec", + description: "Execute a shell command", + input_schema: { + type: "object", + properties: { + command: { type: "string" }, + }, + required: ["command"], + }, + }, + ], + tool_choice: { type: "any" }, + }; + options?.onPayload?.(payload, _model); + payloads.push(payload); + return {} as ReturnType; + }; + const agent = { streamFn: baseStreamFn }; + + applyExtraParamsToAgent(agent, undefined, "kimi-coding", "k2p5", undefined, "low"); + + const model = { + api: "anthropic-messages", + provider: "kimi-coding", + id: "k2p5", + } as Model<"anthropic-messages">; + const context: Context = { messages: [] }; + void agent.streamFn?.(model, context, {}); + + expect(payloads).toHaveLength(1); + expect(payloads[0]).toMatchObject({ + tools: [ + { + type: "function", + function: { + name: "exec", + description: "Execute a shell command", + parameters: { + type: "object", + properties: { + command: { type: "string" }, + }, + required: ["command"], + }, + }, + }, + ], + tool_choice: "auto", + }); + }); + it("sanitizes invalid Atproxy Gemini negative thinking budgets", () => { const payloads: Record[] = []; const baseStreamFn: StreamFn = (_model, _context, options) => { diff --git a/src/agents/provider-capabilities.test.ts b/src/agents/provider-capabilities.test.ts index c39306231d5..beaa244d316 100644 --- a/src/agents/provider-capabilities.test.ts +++ b/src/agents/provider-capabilities.test.ts @@ -64,6 +64,7 @@ let shouldDropThinkingBlocksForModel: typeof import("./provider-capabilities.js" let shouldSanitizeGeminiThoughtSignaturesForModel: typeof import("./provider-capabilities.js").shouldSanitizeGeminiThoughtSignaturesForModel; let supportsOpenAiCompatTurnValidation: typeof import("./provider-capabilities.js").supportsOpenAiCompatTurnValidation; let usesMoonshotThinkingPayloadCompat: typeof import("./provider-capabilities.js").usesMoonshotThinkingPayloadCompat; +let providerCapabilityTesting: typeof import("./provider-capabilities.js").__testing; describe("resolveProviderCapabilities", () => { beforeAll(async () => { @@ -77,11 +78,13 @@ describe("resolveProviderCapabilities", () => { shouldSanitizeGeminiThoughtSignaturesForModel, supportsOpenAiCompatTurnValidation, usesMoonshotThinkingPayloadCompat, + __testing: providerCapabilityTesting, } = await import("./provider-capabilities.js")); }); beforeEach(() => { resolveProviderCapabilitiesWithPluginMock.mockClear(); + providerCapabilityTesting.resetDepsForTests(); }); it("returns provider-owned anthropic defaults for ordinary providers", () => { @@ -149,8 +152,8 @@ describe("resolveProviderCapabilities", () => { it("normalizes kimi aliases to the same capability set", () => { expect(resolveProviderCapabilities("kimi")).toEqual(resolveProviderCapabilities("kimi-code")); expect(resolveProviderCapabilities("kimi-code")).toEqual({ - anthropicToolSchemaMode: "native", - anthropicToolChoiceMode: "native", + anthropicToolSchemaMode: "openai-functions", + anthropicToolChoiceMode: "openai-string-modes", openAiPayloadNormalizationMode: "moonshot-thinking", providerFamily: "default", preserveAnthropicThinkingSignatures: false, @@ -203,9 +206,10 @@ describe("resolveProviderCapabilities", () => { expect(resolveTranscriptToolCallIdMode("mistral", "mistral-large-latest")).toBe("strict9"); }); - it("treats kimi aliases as native anthropic tool payload providers", () => { - expect(requiresOpenAiCompatibleAnthropicToolPayload("kimi")).toBe(false); - expect(requiresOpenAiCompatibleAnthropicToolPayload("kimi-code")).toBe(false); + it("treats kimi aliases as OpenAI-style anthropic tool payload providers", () => { + expect(requiresOpenAiCompatibleAnthropicToolPayload("kimi")).toBe(true); + expect(requiresOpenAiCompatibleAnthropicToolPayload("kimi-code")).toBe(true); + expect(requiresOpenAiCompatibleAnthropicToolPayload("kimi-coding")).toBe(true); expect(requiresOpenAiCompatibleAnthropicToolPayload("anthropic")).toBe(false); }); @@ -242,6 +246,9 @@ describe("resolveProviderCapabilities", () => { it("forwards config and workspace context to plugin capability lookup", () => { const config = { plugins: { enabled: true } }; const env = { OPENCLAW_HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv; + const lookup = vi.fn(() => undefined); + + providerCapabilityTesting.setResolveProviderCapabilitiesWithPluginForTest(lookup); resolveProviderCapabilities("anthropic", { config, @@ -249,7 +256,7 @@ describe("resolveProviderCapabilities", () => { env, }); - expect(resolveProviderCapabilitiesWithPluginMock).toHaveBeenLastCalledWith({ + expect(lookup).toHaveBeenLastCalledWith({ provider: "anthropic", config, workspaceDir: "/tmp/workspace", diff --git a/src/agents/provider-capabilities.ts b/src/agents/provider-capabilities.ts index 61192d7e382..6664e524bba 100644 --- a/src/agents/provider-capabilities.ts +++ b/src/agents/provider-capabilities.ts @@ -57,6 +57,8 @@ const PLUGIN_CAPABILITIES_FALLBACKS: Record