fix: normalize kimi anthropic tool payloads (#59440)

* fix: normalize kimi anthropic tool payloads

* fix: normalize kimi anthropic tool payloads (#59440)
This commit is contained in:
Ayaan Zaidi
2026-04-02 13:39:51 +05:30
committed by GitHub
parent 53f1c9968a
commit b441cd2f4f
5 changed files with 89 additions and 14 deletions

View File

@@ -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

View File

@@ -80,6 +80,8 @@ export default definePluginEntry({
},
},
capabilities: {
anthropicToolSchemaMode: "openai-functions",
anthropicToolChoiceMode: "openai-string-modes",
openAiPayloadNormalizationMode: "moonshot-thinking",
preserveAnthropicThinkingSignatures: false,
},

View File

@@ -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<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = {
@@ -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<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {
const payload: Record<string, unknown> = {
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<StreamFn>;
};
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<string, unknown>[] = [];
const baseStreamFn: StreamFn = (_model, _context, options) => {

View File

@@ -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",

View File

@@ -57,6 +57,8 @@ const PLUGIN_CAPABILITIES_FALLBACKS: Record<string, Partial<ProviderCapabilities
openAiPayloadNormalizationMode: "moonshot-thinking",
},
kimi: {
anthropicToolSchemaMode: "openai-functions",
anthropicToolChoiceMode: "openai-string-modes",
openAiPayloadNormalizationMode: "moonshot-thinking",
},
opencode: {