You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
SSE JSON parse failure with GLM-5.1 (Z.AI) — malformed JSON in content field kills stream #23442
When using GLM-5.1 via Z.AI's OpenAI-compatible API, the SSE stream occasionally contains invalid JSON. The model hallucinates SSE-formatted text (e.g. data: {"id":"...","choices":[...]}) as its actual response content. Z.AI's server does not properly escape the quotes in the content field, producing malformed JSON that fails parsing.
The inner " in the hallucinated "data: {"id":"..." breaks the outer JSON string boundary. Z.AI's server should be escaping these quotes (\") but isn't.
Where It Fails
EventSourceParserStream (eventsource-parser) correctly extracts the line after data:
safeParseJSON() in @ai-sdk/provider-utils/src/parse-json-event-stream.ts calls JSON.parse() on the malformed string → throws
The error propagates up through processor.ts and kills the stream
Suggested Fix
Skip-and-continue: When a chunk fails JSON parsing, log a warning and continue to the next chunk instead of killing the entire stream. The hallucinated SSE chunks are typically noise — the real content arrives in adjacent valid chunks. A single malformed chunk shouldn't terminate the conversation.
// In the TransformStream inside parseJsonEventStream:asynctransform({ data },controller){if(data==="[DONE]")return;try{controller.enqueue(awaitsafeParseJSON({text: data, schema }));}catch(err){// Provider sent malformed JSON in this chunk — skip it// rather than killing the entire stream.console.warn("SSE chunk JSON parse failed, skipping:",data.slice(0,200));}}
Environment
opencode v1.14.x
Provider: Z.AI (api.z.ai/api/coding/paas/v4)
Model: glm-5.1
Stream mode: SSE
Reproduction
Any multi-turn conversation with GLM-5.1 via opencode will eventually trigger this. The model sporadically hallucinates SSE chunks in its response text. The failure is intermittent but frequent enough to disrupt normal usage.
Workaround
We added content sanitization in our downstream provider layer that strips data: {...} artifacts from response content, but the fix belongs in the SSE parser itself so all providers benefit.
If your issue is specifically about the stream being killed on a parse error (rather than the malformed JSON itself), it may still warrant its own report — but please review the above to avoid duplicate effort.
Reproduction from MultiAgency body session (2026-04-30)
Running opencode 1.4.10 (homebrew, darwin-arm64) with GLM-5.1 via z.ai. During a long coding session with heavy MCP tool usage, the SSE stream dies with:
The malformed text shows two SSE data: frames concatenated — the first object is truncated mid-field ("object":"chat.c before "data:" from the next frame appears inline). This is the GLM-5 model generating truncated JSON in streaming chunks, not a network issue.
Environment:
opencode 1.4.10 (homebrew)
macOS darwin-arm64 (M1 Air)
Model: zai-coding-plan/glm-5.1
Session had 50+ tool calls before failure
The skip-and-continue approach would solve this — a single malformed chunk should not kill the entire conversation. The real content is in adjacent valid chunks.
We filed a detailed root-cause analysis on the z.ai side: zai-org/GLM-5#66
Two distinct failure modes identified
Truncated SSE chunks — the server emits a data: frame containing incomplete JSON (cut off mid-field, e.g. "object":"chat.c before the next data: appears inline). This is a server-side framing bug.
Hallucinated SSE content — the model generates data: {"id":"..."} as literal text in its response, and the server doesn't escape the inner quotes. This is a model output + server escaping issue.
Both produce the same symptom: JSON Parse error: Expected '}'.
Our environment (for the record)
opencode 1.4.10 (homebrew, darwin-arm64)
macOS M1 Air 8GB
Model: zai-coding-plan/glm-5.1 via api.z.ai
50+ tool calls before failure
Using MultiAgency body with 60+ MCP tools
The skip-and-continue approach suggested in the issue body would be a good client-side mitigation for both modes. But the root cause fix needs to happen server-side at z.ai.
We've seen this exact bug again with GLM-5.1 and have added a client-side resilience fix in our downstream SSE parser. The same pattern could be applied to opencode's parseJsonEventStream transform.
Bug pattern (confirmed)
Two SSE data: frames arrive as one line. The first JSON object is truncated mid-key (never gets a closing }), then data: appears as literal text inside the stream, followed by a complete second object.
The first object is cut off at "chat.completion.c (should be "chat.completion.chunk"). The \n\n SSE frame boundary is missing — both frames are on one continuous line.
Root cause
Server-side SSE buffering bug. The server holds a chunk in its write buffer without flushing, then writes the next chunk into the same buffer before flushing both as one line. The timestamps in our incident (1778724482 vs 1778726922) are ~40 minutes apart, confirming stale-buffer behavior.
When the SSE parser's brace-depth tracker finds zero complete objects (because the first JSON is truncated and never reaches depth 0), fall back to splitting the raw string on data: boundaries and retrying each fragment independently.
# Primary: brace-depth trackingfori, chinenumerate(data_str):
ifch=="{": depth+=1elifch=="}":
depth-=1ifdepth==0: chunks.append(json.loads(...))
# Fallback: if no complete objects found AND line contains "data: "ifnotchunksand"data: "indata_str:
forfragmentindata_str.split("data: "):
# retry brace-depth tracking on each fragment
This recovers the valid second chunk while correctly discarding the truncated first one. The key insight: the malformed first chunk is garbage anyway (truncated mid-key), so losing it is fine. The valid second chunk was previously lost because it was unreachable inside the malformed first object.
Suggested opencode fix
In the TransformStream inside parseJsonEventStream (or wherever safeParseJSON is called), add a pre-processing step:
// Before parsing, check if the line contains multiple "data: " prefixes// (indicating concatenated SSE frames from a buggy server)functionsplitMalformedSSE(data: string): string[]{if(!data.includes("data: "))return[data];returndata.split(/(?=data:)/).map(s=>s.replace(/^data:\s*/,"").trim()).filter(Boolean);}
Then iterate over the fragments and parse each one, catching JSON.parse errors per-fragment rather than per-line.
Related case: opencode-go sends a valid-JSON but semantically-empty event after [DONE]
Your proposed fix (skip-and-continue on malformed JSON) would solve one class of this problem. Here's a second, distinct case that needs the same defensive posture — but at a different level: ignoring events that arrive after [DONE].
What opencode-go sends:
data: {"id":"...","choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}]}
data: [DONE]
data: {"choices":[],"cost":"0"} ← valid JSON, but arrives after DONE
The trailing event is syntactically valid, so it won't be caught by a JSON parse guard. The problem is that the stream parser processes it as another completion chunk, sees choices: [], and overwrites the assembled message with empty content.
The TUI is unaffected (it appears to stop consuming after DONE). --format json mode — used by any programmatic consumer — processes the trailing event and the session never completes:
# opencode log shows content streaming fine:
INFO service=bus type=message.part.delta publishing ← repeated
INFO service=bus type=message.updated publishing
INFO service=bus type=session.status publishing
# ← exits here. No "exiting loop", no HTTP 200 back to caller.
Activity
github-actions commentedon Apr 19, 2026
This issue might be a duplicate of existing issues. Please check:
Expected '}'parse errorJSON Parse error: Unterminated string— same class of SSE/stream JSON parse failureIf your issue is specifically about the stream being killed on a parse error (rather than the malformed JSON itself), it may still warrant its own report — but please review the above to avoid duplicate effort.
darkhipo commentedon Apr 30, 2026
Reproduction from MultiAgency body session (2026-04-30)
Running opencode 1.4.10 (homebrew, darwin-arm64) with GLM-5.1 via z.ai. During a long coding session with heavy MCP tool usage, the SSE stream dies with:
The malformed text shows two SSE
data:frames concatenated — the first object is truncated mid-field ("object":"chat.cbefore"data:"from the next frame appears inline). This is the GLM-5 model generating truncated JSON in streaming chunks, not a network issue.Environment:
The skip-and-continue approach would solve this — a single malformed chunk should not kill the entire conversation. The real content is in adjacent valid chunks.
darkhipo commentedon Apr 30, 2026
Update: Filed with z.ai as well
We filed a detailed root-cause analysis on the z.ai side: zai-org/GLM-5#66
Two distinct failure modes identified
Truncated SSE chunks — the server emits a
data:frame containing incomplete JSON (cut off mid-field, e.g."object":"chat.cbefore the nextdata:appears inline). This is a server-side framing bug.Hallucinated SSE content — the model generates
data: {"id":"..."}as literal text in its response, and the server doesn't escape the inner quotes. This is a model output + server escaping issue.Both produce the same symptom:
JSON Parse error: Expected '}'.Our environment (for the record)
zai-coding-plan/glm-5.1viaapi.z.aiThe skip-and-continue approach suggested in the issue body would be a good client-side mitigation for both modes. But the root cause fix needs to happen server-side at z.ai.
water-in-stone commentedon May 2, 2026
@darkhipo There is a general method to handle this problem: we could use jsonrepair
by the way, jsonrepair is a popular tool used for fixing malformed JSON text from LLMs. Its key features include:
darkhipo commentedon May 14, 2026
Update: additional analysis + client-side workaround
We've seen this exact bug again with GLM-5.1 and have added a client-side resilience fix in our downstream SSE parser. The same pattern could be applied to opencode's
parseJsonEventStreamtransform.Bug pattern (confirmed)
Two SSE
data:frames arrive as one line. The first JSON object is truncated mid-key (never gets a closing}), thendata:appears as literal text inside the stream, followed by a complete second object.Example:
The first object is cut off at
"chat.completion.c(should be"chat.completion.chunk"). The\n\nSSE frame boundary is missing — both frames are on one continuous line.Root cause
Server-side SSE buffering bug. The server holds a chunk in its write buffer without flushing, then writes the next chunk into the same buffer before flushing both as one line. The timestamps in our incident (1778724482 vs 1778726922) are ~40 minutes apart, confirming stale-buffer behavior.
This affects at least:
Fix pattern: split-on-
data:fallbackWhen the SSE parser's brace-depth tracker finds zero complete objects (because the first JSON is truncated and never reaches depth 0), fall back to splitting the raw string on
data:boundaries and retrying each fragment independently.Our implementation (Python,
parse_sse_json_chunks):This recovers the valid second chunk while correctly discarding the truncated first one. The key insight: the malformed first chunk is garbage anyway (truncated mid-key), so losing it is fine. The valid second chunk was previously lost because it was unreachable inside the malformed first object.
Suggested opencode fix
In the
TransformStreaminsideparseJsonEventStream(or whereversafeParseJSONis called), add a pre-processing step:Then iterate over the fragments and parse each one, catching
JSON.parseerrors per-fragment rather than per-line.Related
parse_sse_json_chunksinsrc/multiaim_agents/provider/base.pywith 8 tests inTestParseSseJsonChunksCamiloFernando commentedon May 15, 2026
Related case: opencode-go sends a valid-JSON but semantically-empty event after
[DONE]Your proposed fix (skip-and-continue on malformed JSON) would solve one class of this problem. Here's a second, distinct case that needs the same defensive posture — but at a different level: ignoring events that arrive after
[DONE].What opencode-go sends:
The trailing event is syntactically valid, so it won't be caught by a JSON parse guard. The problem is that the stream parser processes it as another completion chunk, sees
choices: [], and overwrites the assembled message with empty content.Reproduce:
Symptom in
--format jsonmodeThe TUI is unaffected (it appears to stop consuming after DONE).
--format jsonmode — used by any programmatic consumer — processes the trailing event and the session never completes:A downstream consumer sees:
Suggested fix (complements #23442)
In addition to the skip-malformed-JSON guard, gate on
[DONE]:Both guards together make the parser resilient to the two known failure modes:
github-actions commentedon Jul 14, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.