Non-streaming requests to reasoning-capable models (z-ai/glm-5.3) produce fragmented/corrupted content blocks
Summary
Non-streaming requests to reasoning-capable models routed through the openai_chat_completions
provider capability (tested with z-ai/glm-5.3 via OpenRouter) do not reliably deliver a valid,
complete Anthropic Messages response to the client. Root cause not fully isolated, but it is
specific to the non-stream code path — the same model/prompt succeeds every time when the client
requests stream: true.
Version: @musistudio/[email protected] (latest published at time of report)
Evidence
Real production traffic (70 historical requests, one machine, z-ai/glm-5.3 via OpenRouter,
client = Claude Code): every single non-streaming request failed (CCR's own request log marks
ok=0/status_code=0); every single streaming request from the same client/session succeeded.
Zero exceptions either direction. Confirmed this is not upstream/provider flakiness — pinned a batch
of 20 non-streaming requests to a single OpenRouter backend provider (Together) and all 20 still
failed identically, with the upstream itself returning 200 every time.
Repro
curl http://127.0.0.1:3456/v1/messages \
-H "content-type: application/json" \
-H "authorization: Bearer <local ccr key>" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: interleaved-thinking-2025-05-14" \
-d '{
"model": "OpenRouter/z-ai/glm-5.3[1m]",
"max_tokens": 300,
"thinking": {"type": "enabled", "budget_tokens": 200},
"messages": [{"role": "user", "content": "In one sentence, name a US state."}]
}'Without "stream": true in the request, the outbound leg to OpenRouter has no stream field at
all, and the client-visible response is later rejected/errors downstream. Add "stream": true to
the same request and it completes correctly every time.
Workarounds attempted
- Client sends
stream: truedirectly — works cleanly every time (proper single coalesced Anthropiccontent_block_start/content_block_delta/content_block_stopsequence). - Forcing the upstream leg to stream via a
providerPluginsconfig entry ({"providerName": "openrouter", "models": ["z-ai/glm-5.3"], "request": {"bodySet": {"stream": true}}}) while the client itself still requests non-streaming — this does forcestream: trueupstream successfully, but CCR's reassembly of that stream into a single JSON body for the non-streaming client is broken: instead of one coalescedthinkingcontent block, the returned message had 612 separate one-fragmentthinkingblocks, notextblock, andstop_reason: "max_tokens"(it exhausted the token budget emitting fragments before ever producing a real answer). Reverted this workaround — it's strictly worse than the original bug for real traffic. - Patching the compiled bundle directly — considered too fragile/invasive for production use,
not attempted; the correct client-facing SSE-generation logic (
content_block_startreused via anactiveBlockType/activeBlockIndexcheck) appears correct on inspection, so whatever finalizes a forced-stream response into a single JSON body for a non-streaming client is a separate, unidentified code path with its own bug.
Possibly related
- #1133 (reasoning_content not converted to a thinking block in non-streaming mode) — different symptom (missing conversion vs. fragmented conversion) but same general area: non-streaming + reasoning-content handling.
- #1397 (streaming reasoning transformer corrupts tool-call argument deltas) — same code family.
Impact
Any client that sends non-streaming requests to a reasoning-capable model through this provider capability will unpredictably fail or receive corrupted output. In our case this surfaced via Claude Code's own internal auto-mode permission classifier (which always sends non-streaming requests) — but any non-streaming caller to a reasoning model is exposed to this.
Source: musistudio/claude-code-router