From 51bae75120485d305b0bac00d59a2d80280590c2 Mon Sep 17 00:00:00 2001 From: opriz Date: Mon, 9 Mar 2026 21:28:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(kimi-coding):=20fix=20kimi=20tool=20format:?= =?UTF-8?q?=20use=20native=20Anthropic=20tool=20schema=20instead=20of=20Op?= =?UTF-8?q?enAI=20=E2=80=A6=20(openclaw#40008)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified: - pnpm install --frozen-lockfile - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: opriz <51957849+opriz@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> --- CHANGELOG.md | 1 + src/agents/models-config.providers.static.ts | 3 - .../pi-embedded-runner-extraparams.test.ts | 76 ++----------------- src/agents/provider-capabilities.test.ts | 10 +-- src/agents/provider-capabilities.ts | 4 +- src/config/zod-schema.core.ts | 1 + 6 files changed, 17 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b94dca8be1..29d4917ed2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai - macOS/LaunchAgent install: tighten LaunchAgent directory and plist permissions during install so launchd bootstrap does not fail when the target home path or generated plist inherited group/world-writable modes. - Gateway/Control UI: keep dashboard auth tokens in session-scoped browser storage so same-tab refreshes preserve remote token auth without restoring long-lived localStorage token persistence, while scoping tokens to the selected gateway URL and fragment-only bootstrap flow. (#40892) thanks @velvet-shark. +- Models/Kimi Coding: send `anthropic-messages` tools in native Anthropic format again so `kimi-coding` stops degrading tool calls into XML/plain-text pseudo invocations instead of real `tool_use` blocks. (#38669, #39907, #40552) Thanks @opriz. ## 2026.3.8 diff --git a/src/agents/models-config.providers.static.ts b/src/agents/models-config.providers.static.ts index 638943cc4a4..0a766fe983e 100644 --- a/src/agents/models-config.providers.static.ts +++ b/src/agents/models-config.providers.static.ts @@ -233,9 +233,6 @@ export function buildKimiCodingProvider(): ProviderConfig { cost: KIMI_CODING_DEFAULT_COST, contextWindow: KIMI_CODING_DEFAULT_CONTEXT_WINDOW, maxTokens: KIMI_CODING_DEFAULT_MAX_TOKENS, - compat: { - requiresOpenAiAnthropicToolPayload: true, - }, }, ], }; diff --git a/src/agents/pi-embedded-runner-extraparams.test.ts b/src/agents/pi-embedded-runner-extraparams.test.ts index 18513167a33..c0541116075 100644 --- a/src/agents/pi-embedded-runner-extraparams.test.ts +++ b/src/agents/pi-embedded-runner-extraparams.test.ts @@ -732,7 +732,7 @@ describe("applyExtraParamsToAgent", () => { expect(payloads[0]?.thinking).toEqual({ type: "disabled" }); }); - it("normalizes kimi-coding anthropic tools to OpenAI function format", () => { + it("does not rewrite tool schema for kimi-coding (native Anthropic format)", () => { const payloads: Record[] = []; const baseStreamFn: StreamFn = (_model, _context, options) => { const payload: Record = { @@ -746,14 +746,6 @@ describe("applyExtraParamsToAgent", () => { required: ["path"], }, }, - { - type: "function", - function: { - name: "exec", - description: "Run command", - parameters: { type: "object", properties: {} }, - }, - }, ], tool_choice: { type: "tool", name: "read" }, }; @@ -777,68 +769,16 @@ describe("applyExtraParamsToAgent", () => { expect(payloads).toHaveLength(1); expect(payloads[0]?.tools).toEqual([ { - type: "function", - function: { - name: "read", - description: "Read file", - parameters: { - type: "object", - properties: { path: { type: "string" } }, - required: ["path"], - }, - }, - }, - { - type: "function", - function: { - name: "exec", - description: "Run command", - parameters: { type: "object", properties: {} }, + name: "read", + description: "Read file", + input_schema: { + type: "object", + properties: { path: { type: "string" } }, + required: ["path"], }, }, ]); - expect(payloads[0]?.tool_choice).toEqual({ - type: "function", - function: { name: "read" }, - }); - }); - - it.each([ - { input: { type: "auto" }, expected: "auto" }, - { input: { type: "none" }, expected: "none" }, - { input: { type: "required" }, expected: "required" }, - ])("normalizes anthropic tool_choice %j for kimi-coding endpoints", ({ input, expected }) => { - const payloads: Record[] = []; - const baseStreamFn: StreamFn = (_model, _context, options) => { - const payload: Record = { - tools: [ - { - name: "read", - description: "Read file", - input_schema: { type: "object", properties: {} }, - }, - ], - tool_choice: input, - }; - 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", - baseUrl: "https://api.kimi.com/coding/", - } as Model<"anthropic-messages">; - const context: Context = { messages: [] }; - void agent.streamFn?.(model, context, {}); - - expect(payloads).toHaveLength(1); - expect(payloads[0]?.tool_choice).toBe(expected); + expect(payloads[0]?.tool_choice).toEqual({ type: "tool", name: "read" }); }); it("does not rewrite anthropic tool schema for non-kimi endpoints", () => { diff --git a/src/agents/provider-capabilities.test.ts b/src/agents/provider-capabilities.test.ts index 5f97ac95746..5e162c87794 100644 --- a/src/agents/provider-capabilities.test.ts +++ b/src/agents/provider-capabilities.test.ts @@ -31,8 +31,8 @@ describe("resolveProviderCapabilities", () => { resolveProviderCapabilities("kimi-code"), ); expect(resolveProviderCapabilities("kimi-code")).toEqual({ - anthropicToolSchemaMode: "openai-functions", - anthropicToolChoiceMode: "openai-string-modes", + anthropicToolSchemaMode: "native", + anthropicToolChoiceMode: "native", providerFamily: "default", preserveAnthropicThinkingSignatures: false, openAiCompatTurnValidation: true, @@ -66,9 +66,9 @@ describe("resolveProviderCapabilities", () => { expect(resolveTranscriptToolCallIdMode("mistral", "mistral-large-latest")).toBe("strict9"); }); - it("treats kimi aliases as anthropic tool payload compatibility providers", () => { - expect(requiresOpenAiCompatibleAnthropicToolPayload("kimi-coding")).toBe(true); - expect(requiresOpenAiCompatibleAnthropicToolPayload("kimi-code")).toBe(true); + it("treats kimi aliases as native anthropic tool payload providers", () => { + expect(requiresOpenAiCompatibleAnthropicToolPayload("kimi-coding")).toBe(false); + expect(requiresOpenAiCompatibleAnthropicToolPayload("kimi-code")).toBe(false); expect(requiresOpenAiCompatibleAnthropicToolPayload("anthropic")).toBe(false); }); diff --git a/src/agents/provider-capabilities.ts b/src/agents/provider-capabilities.ts index d12a3f0b94e..62007b810f8 100644 --- a/src/agents/provider-capabilities.ts +++ b/src/agents/provider-capabilities.ts @@ -33,9 +33,9 @@ const PROVIDER_CAPABILITIES: Record> = { "amazon-bedrock": { providerFamily: "anthropic", }, + // kimi-coding natively supports Anthropic tool framing (input_schema); + // converting to OpenAI format causes XML text fallback instead of tool_use blocks. "kimi-coding": { - anthropicToolSchemaMode: "openai-functions", - anthropicToolChoiceMode: "openai-string-modes", preserveAnthropicThinkingSignatures: false, }, mistral: { diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts index 7ddef789282..23accd81637 100644 --- a/src/config/zod-schema.core.ts +++ b/src/config/zod-schema.core.ts @@ -198,6 +198,7 @@ export const ModelCompatSchema = z requiresAssistantAfterToolResult: z.boolean().optional(), requiresThinkingAsText: z.boolean().optional(), requiresMistralToolIds: z.boolean().optional(), + requiresOpenAiAnthropicToolPayload: z.boolean().optional(), }) .strict() .optional();