diff --git a/CHANGELOG.md b/CHANGELOG.md index e73f6475ba5..417eaaa2556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.test.ts b/src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.test.ts new file mode 100644 index 00000000000..3edffebf62e --- /dev/null +++ b/src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.test.ts @@ -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); + }); +}); diff --git a/src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.ts b/src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.ts index 8c4f1119d61..ba2bd59a4c5 100644 --- a/src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.ts +++ b/src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.ts @@ -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 { diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index fe61895fd86..f26dbcd96b2 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -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,