fix(agents): repair malformed tool-call args on openai-completions

This commit is contained in:
Ted Li
2026-04-22 10:56:38 -07:00
committed by Peter Steinberger
parent e54d0634c5
commit 49c7319ea5
4 changed files with 46 additions and 6 deletions

View File

@@ -96,6 +96,7 @@ Docs: https://docs.openclaw.ai
- Auto-reply/media: share one run-scoped reply media context between streamed block delivery and final payload filtering, so a local `MEDIA:` attachment is staged once and duplicate media sends are suppressed reliably. (#68111) Thanks @ayeshakhalid192007-dev.
- Plugins/gateway hooks: expose startup config, workspace dir, and a live cron getter on the typed `gateway_start` hook, and move memory-core managed dreaming off the internal `gateway:startup` bridge so cron reconciliation stays on the public plugin hook path. Thanks @vincentkoc.
- Plugins/config: read plugin trust decisions from the source config snapshot when a resolved runtime snapshot is active, so `plugins.allow` remains enforced and `doctor`/gateway startup no longer warn that the allowlist is empty when it is configured. Fixes #70161. Also fixes #70141.
- Agents/openai-completions: enable malformed streamed tool-call argument repair for self-hosted OpenAI-compatible backends such as Kimi/SGLang, so fragmented tool-call arguments no longer reach tools as empty or unusable objects. Fixes #69672.
- Gateway/restart: preserve group and channel chat context when resuming an agent turn after a Gateway restart, so continuation replies keep the same prompt, routing, and tool-status behavior as the original conversation.
- Gateway/pairing: shared-secret loopback CLI clients now silently auto-approve `metadata-upgrade` pairing (platform / device family refresh) instead of being disconnected with `1008 pairing required`. This matches the scope-upgrade and role-upgrade behavior added in #69431 and unblocks non-interactive CLI automation when a paired-device record has a stale platform string (e.g. device key replicated across hosts, install migrated between OSes, or platform-string format changed between OpenClaw versions). Browser / Control-UI clients keep the existing approval-required flow for metadata changes.
- Gateway/pairing: treat any forwarded-header evidence (`Forwarded`, `X-Forwarded-*`, or `X-Real-IP`) as proxied WebSocket traffic before pairing locality checks, so reverse-proxy topologies cannot use the loopback shared-secret helper auto-pairing path.

View File

@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { shouldRepairMalformedToolCallArguments } from "./attempt.tool-call-argument-repair.js";
describe("shouldRepairMalformedToolCallArguments", () => {
it("keeps the repair enabled for kimi providers on anthropic-messages", () => {
expect(
shouldRepairMalformedToolCallArguments({
provider: "kimi-coding",
modelApi: "anthropic-messages",
}),
).toBe(true);
});
it("enables the repair for openai-completions even when the provider is not kimi", () => {
expect(
shouldRepairMalformedToolCallArguments({
provider: "openai-compatible",
modelApi: "openai-completions",
}),
).toBe(true);
});
it("does not enable the repair for unrelated non-kimi transports", () => {
expect(
shouldRepairMalformedToolCallArguments({
provider: "openai-compatible",
modelApi: "openai-responses",
}),
).toBe(false);
});
});

View File

@@ -240,7 +240,7 @@ function wrapStreamRepairMalformedToolCallArguments(
if (!loggedRepairIndices.has(event.contentIndex) && repair.kind === "repaired") {
loggedRepairIndices.add(event.contentIndex);
log.warn(
`repairing Kimi tool call arguments with ${repair.leadingPrefix.length} leading chars and ${repair.trailingSuffix.length} trailing chars`,
`repairing malformed tool call arguments with ${repair.leadingPrefix.length} leading chars and ${repair.trailingSuffix.length} trailing chars`,
);
}
} else {
@@ -294,8 +294,14 @@ export function wrapStreamFnRepairMalformedToolCallArguments(baseFn: StreamFn):
};
}
export function shouldRepairMalformedAnthropicToolCallArguments(provider?: string): boolean {
return normalizeProviderId(provider ?? "") === "kimi";
export function shouldRepairMalformedToolCallArguments(params: {
provider?: string;
modelApi?: string | null;
}): boolean {
return (
normalizeProviderId(params.provider ?? "") === "kimi" ||
params.modelApi === "openai-completions"
);
}
export function wrapStreamFnDecodeXaiToolCallArguments(baseFn: StreamFn): StreamFn {

View File

@@ -238,7 +238,7 @@ import {
shouldUseOpenAIWebSocketTransport,
} from "./attempt.thread-helpers.js";
import {
shouldRepairMalformedAnthropicToolCallArguments,
shouldRepairMalformedToolCallArguments,
wrapStreamFnDecodeXaiToolCallArguments,
wrapStreamFnRepairMalformedToolCallArguments,
} from "./attempt.tool-call-argument-repair.js";
@@ -1464,8 +1464,10 @@ export async function runEmbeddedAttempt(
);
if (
params.model.api === "anthropic-messages" &&
shouldRepairMalformedAnthropicToolCallArguments(params.provider)
shouldRepairMalformedToolCallArguments({
provider: params.provider,
modelApi: params.model.api,
})
) {
activeSession.agent.streamFn = wrapStreamFnRepairMalformedToolCallArguments(
activeSession.agent.streamFn,