From d7b8faa7bf010c8857dac72a03859bee6ff8b434 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Sat, 4 Apr 2026 08:33:38 +0530 Subject: [PATCH] fix: keep Kimi anthropic tool payloads native (#60391) (thanks @Eric-Guo) --- CHANGELOG.md | 1 + extensions/kimi-coding/index.ts | 4 +- extensions/kimi-coding/stream.ts | 7 --- .../pi-embedded-runner-extraparams.test.ts | 46 ++++++++++--------- 4 files changed, 28 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bc3560cc0c..58214c67cf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,7 @@ Docs: https://docs.openclaw.ai - Plugins/media understanding: enable bundled Groq and Deepgram providers by default so configured audio transcription models load without extra plugin activation config. (#59982) Thanks @yxjsxy. - Providers/OpenAI Codex: add forward-compat `openai-codex/gpt-5.4-mini` synthesis across provider runtime, model catalog, and model listing so Codex mini works before bundled Pi catalog updates land. - Plugins/marketplace: block remote marketplace symlink escapes without rewriting ordinary local marketplace install paths. (#60556) Thanks @eleqtrizit. +- Plugins/Kimi Coding: keep native Anthropic tool payloads on the Kimi coding endpoint while still parsing tagged tool-call text on the response path, so tool calls execute again instead of echoing raw markup. (#60391) Thanks @Eric-Guo. ## 2026.4.2 diff --git a/extensions/kimi-coding/index.ts b/extensions/kimi-coding/index.ts index 9b7c0607f75..bb8e290f1e5 100644 --- a/extensions/kimi-coding/index.ts +++ b/extensions/kimi-coding/index.ts @@ -2,7 +2,7 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; import { applyKimiCodeConfig, KIMI_CODING_MODEL_REF } from "./onboard.js"; import { buildKimiCodingProvider } from "./provider-catalog.js"; -import { wrapKimiProviderStream } from "./stream.js"; +import { createKimiToolCallMarkupWrapper } from "./stream.js"; const PLUGIN_ID = "kimi"; const PROVIDER_ID = "kimi"; @@ -87,7 +87,7 @@ export default definePluginEntry({ }, }, buildReplayPolicy: () => buildKimiReplayPolicy(), - wrapStreamFn: (ctx) => wrapKimiProviderStream(ctx.streamFn), + wrapStreamFn: (ctx) => createKimiToolCallMarkupWrapper(ctx.streamFn), }); }, }); diff --git a/extensions/kimi-coding/stream.ts b/extensions/kimi-coding/stream.ts index 312ebacc321..d497aed55d0 100644 --- a/extensions/kimi-coding/stream.ts +++ b/extensions/kimi-coding/stream.ts @@ -1,6 +1,5 @@ import type { StreamFn } from "@mariozechner/pi-agent-core"; import { streamSimple } from "@mariozechner/pi-ai"; -import { createOpenAIAnthropicToolPayloadCompatibilityWrapper } from "openclaw/plugin-sdk/provider-stream"; const TOOL_CALLS_SECTION_BEGIN = "<|tool_calls_section_begin|>"; const TOOL_CALLS_SECTION_END = "<|tool_calls_section_end|>"; @@ -180,9 +179,3 @@ export function createKimiToolCallMarkupWrapper(baseStreamFn: StreamFn | undefin return wrapKimiTaggedToolCalls(maybeStream); }; } - -export function wrapKimiProviderStream(baseStreamFn: StreamFn | undefined): StreamFn { - return createKimiToolCallMarkupWrapper( - createOpenAIAnthropicToolPayloadCompatibilityWrapper(baseStreamFn), - ); -} diff --git a/src/agents/pi-embedded-runner-extraparams.test.ts b/src/agents/pi-embedded-runner-extraparams.test.ts index 52a4aa0f3fd..c1c538959b1 100644 --- a/src/agents/pi-embedded-runner-extraparams.test.ts +++ b/src/agents/pi-embedded-runner-extraparams.test.ts @@ -54,10 +54,7 @@ import { resolveExtraParams, resolvePreparedExtraParams, } from "./pi-embedded-runner.js"; -import { - createAnthropicToolPayloadCompatibilityWrapper, - createOpenAIAnthropicToolPayloadCompatibilityWrapper, -} from "./pi-embedded-runner/anthropic-family-tool-payload-compat.js"; +import { createAnthropicToolPayloadCompatibilityWrapper } from "./pi-embedded-runner/anthropic-family-tool-payload-compat.js"; import { createBedrockNoCacheWrapper, isAnthropicBedrockModel, @@ -164,8 +161,14 @@ beforeEach(() => { params.context.thinkingLevel, ); } + if (params.provider === "test-anthropic-tool-compat") { + return createAnthropicToolPayloadCompatibilityWrapper(params.context.streamFn, { + toolSchemaMode: "openai-functions", + toolChoiceMode: "openai-string-modes", + }); + } if (params.provider === "kimi") { - return createOpenAIAnthropicToolPayloadCompatibilityWrapper(params.context.streamFn); + return params.context.streamFn; } if (params.provider === "minimax" || params.provider === "minimax-portal") { return createMinimaxFastModeWrapper( @@ -1222,7 +1225,7 @@ describe("applyExtraParamsToAgent", () => { }); }); - it("rewrites anthropic tool payloads for Kimi", () => { + it("keeps anthropic tool payloads native for Kimi", () => { const payloads: Record[] = []; const baseStreamFn: StreamFn = (_model, _context, options) => { const payload: Record = { @@ -1259,22 +1262,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"], - }, + 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" }, - }); + expect(payloads[0]?.tool_choice).toEqual({ type: "tool", name: "read" }); }); it("does not rewrite anthropic tool schema for non-kimi endpoints", () => { @@ -1377,11 +1374,18 @@ describe("applyExtraParamsToAgent", () => { }; const agent = { streamFn: baseStreamFn }; - applyExtraParamsToAgent(agent, undefined, "kimi", "proxy-model", undefined, "low"); + applyExtraParamsToAgent( + agent, + undefined, + "test-anthropic-tool-compat", + "proxy-model", + undefined, + "low", + ); const model = { api: "anthropic-messages", - provider: "kimi", + provider: "test-anthropic-tool-compat", id: "proxy-model", } as Model<"anthropic-messages">; const context: Context = { messages: [] };