Non-streaming requests to reasoning-capable models (z-ai/glm-5.3) produce fragmented/corrupted content blocks

Author: bb-nikCreated Sep 12, 2026Updated Sep 12, 2026

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

bash
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

  1. Client sends stream: true directly — works cleanly every time (proper single coalesced Anthropic content_block_start/content_block_delta/content_block_stop sequence).
  2. Forcing the upstream leg to stream via a providerPlugins config entry ({"providerName": "openrouter", "models": ["z-ai/glm-5.3"], "request": {"bodySet": {"stream": true}}}) while the client itself still requests non-streaming — this does force stream: true upstream successfully, but CCR's reassembly of that stream into a single JSON body for the non-streaming client is broken: instead of one coalesced thinking content block, the returned message had 612 separate one-fragment thinking blocks, no text block, and stop_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.
  3. Patching the compiled bundle directly — considered too fragile/invasive for production use, not attempted; the correct client-facing SSE-generation logic (content_block_start reused via an activeBlockType/activeBlockIndex check) 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