mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-03 05:12:15 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -80,6 +80,8 @@ export default definePluginEntry({
|
||||
},
|
||||
},
|
||||
capabilities: {
|
||||
anthropicToolSchemaMode: "openai-functions",
|
||||
anthropicToolChoiceMode: "openai-string-modes",
|
||||
openAiPayloadNormalizationMode: "moonshot-thinking",
|
||||
preserveAnthropicThinkingSignatures: false,
|
||||
},
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user