fix: suppress streaming JSON parse errors without hiding provider diagnostics

This commit is contained in:
vincentkoc
2026-04-27 22:40:58 +00:00
committed by Mason Huang
parent 638865b5d8
commit 7addcf4e79
4 changed files with 69 additions and 3 deletions

View File

@@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/errors: suppress malformed streaming tool-call JSON fragments before they reach chat surfaces while preserving provider request-validation diagnostics. Fixes #59076; keeps #59080 as duplicate coverage. (#59118) Thanks @singleGanghood.
- CLI/models: restore provider-filtered `models list --all --provider <id>` rows for providers without manifest/static catalog coverage, including Anthropic and Amazon Bedrock, while keeping the compatibility fallback off expensive availability and resolver paths. Thanks @shakkernerd.
- CLI/tools: keep the Gateway `tools.*` RPC namespace out of plugin command discovery and managed proxy startup, so stray commands like `openclaw tools effective` fail quickly instead of cold-loading plugin metadata. Refs #73477. Thanks @oromeis.
- CLI/status: keep default text `openclaw status --usage` on metadata-only channel scans unless `--deep` or `--all` is set, and send stray `openclaw tools --help` through the precomputed root-help fast path so latency-triage commands avoid plugin/runtime cold loads before printing. Refs #73477 and #74220. Thanks @oromeis and @NianJiuZst.

View File

@@ -369,13 +369,27 @@ describe("formatAssistantErrorText", () => {
);
});
it("sanitizes 'Unexpected token' JSON parse errors (#59076)", () => {
const msg = makeAssistantError("Unexpected token < in JSON at position 0");
it("sanitizes context-proven streaming 'Unexpected token' JSON parse errors (#59076)", () => {
const msg = makeAssistantError(
'Could not parse Anthropic SSE event content_block_delta: Unexpected token } in JSON at position 14; data={"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":"}"},"index":1}',
);
expect(formatAssistantErrorText(msg)).toBe(
"LLM streaming response contained a malformed fragment. Please try again.",
);
});
it("does not broadly rewrite non-streaming 'Unexpected token' JSON parse errors", () => {
const msg = makeAssistantError("Unexpected token < in JSON at position 0");
expect(formatAssistantErrorText(msg)).toBe("Unexpected token < in JSON at position 0");
});
it("does not rewrite non-streaming provider JSON request-validation diagnostics", () => {
const msg = makeAssistantError("Expected value in JSON at position 12 for messages.0.content");
expect(formatAssistantErrorText(msg)).toBe(
"Expected value in JSON at position 12 for messages.0.content",
);
});
it("keeps provider request-validation JSON diagnostics actionable", () => {
const msg = makeAssistantError(
'{"type":"error","error":{"type":"invalid_request_error","message":"Expected value in JSON at position 12 for messages.0.content"}}',

View File

@@ -0,0 +1,39 @@
import type { AssistantMessage } from "@mariozechner/pi-ai";
import { describe, expect, it } from "vitest";
import { makeAssistantMessageFixture } from "../test-helpers/assistant-message-fixtures.js";
import { formatAssistantErrorText } from "./errors.js";
describe("formatAssistantErrorText streaming JSON parse classification", () => {
const makeAssistantError = (errorMessage: string): AssistantMessage =>
makeAssistantMessageFixture({
errorMessage,
content: [{ type: "text", text: errorMessage }],
});
it("suppresses raw streaming tool-call fragment parse failures", () => {
const msg = makeAssistantError(
"Expected ',' or '}' after property value in JSON at position 334 (line 1 column 335)",
);
expect(formatAssistantErrorText(msg)).toBe(
"LLM streaming response contained a malformed fragment. Please try again.",
);
});
it("suppresses structured Anthropic tool-call delta parse failures", () => {
const msg = makeAssistantError(
'Could not parse Anthropic SSE event content_block_delta: Unexpected end of JSON input; data={"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":"{\\"path\\":"},"index":0}',
);
expect(formatAssistantErrorText(msg)).toBe(
"LLM streaming response contained a malformed fragment. Please try again.",
);
});
it("keeps non-streaming provider request-validation syntax diagnostics", () => {
const msg = makeAssistantError(
'{"type":"error","error":{"type":"invalid_request_error","message":"Expected value in JSON at position 12 for messages.0.content"}}',
);
expect(formatAssistantErrorText(msg)).toBe(
"LLM request rejected: Expected value in JSON at position 12 for messages.0.content",
);
});
});

View File

@@ -213,7 +213,19 @@ export function isStreamingJsonParseError(raw: string): boolean {
if (!raw) {
return false;
}
return /\b(?:expected|unexpected)\b.+\bin json\b.+\bposition\b/i.test(raw);
const trimmed = raw.trim();
if (
/\bcould not parse anthropic sse event\b/i.test(trimmed) &&
/\b(?:content_block_delta|input_json_delta|partial_json|tool_use)\b/i.test(trimmed) &&
(/\b(?:expected|unexpected|unterminated)\b.+\bin json\b.+\bposition\b/i.test(trimmed) ||
/\bunexpected end of json input\b/i.test(trimmed))
) {
return true;
}
return /^(?:Expected (?:',' or '\}' after property value|double-quoted property name|':' after property name|',' or '\]' after array element)|Unterminated string) in JSON at position \d+(?: \(line \d+ column \d+\))?$/i.test(
trimmed,
);
}
function hasRateLimitTpmHint(raw: string): boolean {