Skip to content

[Bug] Stream gate falsely rejects valid Responses API compaction/tool-call streams as empty_stream; stream_gate_mode=off not honored when replay enabled #1410

Description

@tomiatimar

Summary

CCH's stream content gate (and the stream_gate_precommit path) falsely rejects valid OpenAI Responses API streams as empty_stream when the upstream response contains no response.output_text.delta frames. Observed for two cases:

  1. Compaction streamsgpt-5.6-sol returns a context-compaction response whose terminal response.completed frame has output:[{type:"compaction", encrypted_content}] and no text content.
  2. Tool-call streams — the model returns response.custom_tool_call_input.delta frames (tool-call argument streaming) with no text deltas.

In both cases the upstream returns HTTP 200 with a complete, well-formed SSE stream, but the gate classifies it as empty_stream and returns 502, which burns the provider's circuit breaker.

Reproduction

CCH → codex-type provider → POST /v1/responses with stream:true. Upstream (an OAuth aggregator forwarding to OpenAI Codex gpt-5.6-sol) returns 200 with a standard Responses API SSE sequence. When the model emits a compaction or a tool call, the stream contains reasoning/compaction/tool-call frames and zero response.output_text.delta frames.

Observed error (compaction case):

{
  "error": {
    "type": "stream_gate_precommit",
    "reason": "empty_stream",
    "family": "openai-responses",
    "frames_seen": 6,
    "buffered_bytes": 141511,
    "echo_excluded_bytes": 73346,
    "frame_preview": "{\"type\":\"response.completed\",\"response\":{\"id\":\"resp_...\",\"object\":\"response\",\"status\":\"completed\",\"model\":\"gpt-5.6-sol\",\"output\":[{\"id\":\"cmp_...\",\"type\":\"compaction\",\"encrypted_content\":\"...\"}]}}"
  }
}

For the tool-call case, a replay-payload capture of a rejected stream showed:

130 × response.custom_tool_call_input.delta
  3 × reasoning / custom_tool_call items
  0 × response.output_text.delta

→ same empty_stream rejection.

Root cause

The gate's content detector only recognizes response.output_text.delta (text deltas) as "valid content". When a stream contains only compaction and/or tool-call frames, the detector reaches a terminal frame without ever seeing "valid content" → classifies as empty_stream.

Additionally, the precommit gate is not controlled by stream_gate_mode. From the compiled source, the rejection path fires when:

if ("enforce" === resolveStreamGateMode() || replayState?.role === "owner") { ... reject ... }

So even with stream_gate_mode = off (or shadow), the precommit gate still rejects for any request whose replayState.role === "owner" — i.e. virtually every primary request while request Replay is enabled. This matches the admin UI description: "Replay owner … regardless of mode, always retains the pre-content safety check."

Why this isn't covered by #1404

#1404 ("fix(proxy): support remote compaction v2 passthrough") was meant to fix #1398 (compaction streams rejected as empty_stream). We are running a build that contains #1404 (compaction passthrough code present), yet compaction streams are still rejected by the precommit gate. It looks like #1404 handled the compaction wire path but did not teach the gate's content detector to recognize compaction/tool-call frames as valid content.

Workaround (verified)

  1. Disable request Replay (replay_enabled = false) so requests no longer carry replayState.role === "owner".
  2. Set stream_gate_mode = off.

With both, the precommit gate no longer force-runs and compaction/tool-call streams pass through. Verified: empty_stream rejections drop to 0 and the provider circuit stops burning. (Cost: loses Replay's concurrent-request dedup and reconnect-tail. Does not affect prompt cache — that's orthogonal, via prompt_cache_key.)

Impact

Any codex / Responses-API provider whose upstream performs context compaction or tool calls will be intermittently rejected, burning its circuit breaker and eventually returning 503 "no available providers" to the client. For tool-heavy clients (Claude Code / Codex CLI) this is near-constant — in our case the provider accrued 60+ empty_stream failures and the codex path was effectively unusable until the workaround above.

Environment

  • claude-code-hub v0.9.2 (image contains fix(proxy): support remote compaction v2 passthrough #1404)
  • Provider type: codex, endpoint /v1/responses
  • Reproduced with stream_gate_mode = enforce (default); switching to off/shadow does not help while Replay is enabled
  • Upstream: OAuth aggregator forwarding to OpenAI Codex gpt-5.6-sol

Ask

  • Could the gate's "valid content" detection be extended to treat Responses-API compaction output (type: compaction) and tool-call streams (response.custom_tool_call_input.delta) as valid content?
  • And/or should the precommit gate honor stream_gate_mode = off even for replay owners (or expose a separate toggle)?

Happy to provide fuller SSE captures if useful. Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions