Skip to content

Streaming responses drop the upstream request-id header (only ratelimit/x-codex forwarded) #1100

@flin2

Description

@flin2

Summary

On the streaming (SSE) path, the proxy rebuilds the downstream response headers from an allowlist that keeps only rate-limit and Codex headers (headroom/proxy/handlers/streaming.py, _stream_response, allowlist at ~lines 1083-1086, returned on the StreamingResponse at ~line 1338 on main):

forwarded_headers = {
    k: v
    for k, v in upstream_response.headers.items()
    if "ratelimit" in k.lower() or k.lower().startswith("x-codex")
}

Anthropic's request-id response header is therefore never forwarded to streaming clients. Clients that record it lose it — most visibly Claude Code, which writes a requestId into each transcript turn from that header.

This is the same class of omission already fixed for other headers on this exact allowlist:

request-id is the next header clients need but never receive. The non-streaming/buffered path already forwards all upstream headers (dict(upstream_response.headers)), so only streaming clients are affected.

Impact

  • Claude Code transcripts produced through Headroom have no requestId field.
  • Downstream usage/cost tools that deduplicate transcript rows by the provider response identity (messageId + requestId) can no longer dedup and over-count tokens. CodexBar, for example, requires both messageId and requestId; without requestId it counts the same message.id once per file it appears in — inflating Claude token/cost roughly 2.5x for agent-heavy workloads (subagents / fork / resume copy a message across many files). ccusage is unaffected only because it treats requestId as optional.
  • request-id is standard response metadata used for support tickets and debugging; silently dropping it on the streaming path is surprising and asymmetric with the non-streaming path.

Reproduction (no private data needed)

  1. Direct:
    ANTHROPIC_BASE_URL=https://api.anthropic.com claude -p "hi" --output-format json
    The newest ~/.claude/projects/**/<session>.jsonl assistant row has a top-level "requestId": "req_…".
  2. Through Headroom:
    headroom proxy --port 8787 &
    ANTHROPIC_BASE_URL=http://127.0.0.1:8787 claude -p "hi" --output-format json
    The resulting assistant row has no requestId.

In my logs, requestId presence dropped from ~100% to ~0% on the day I routed Claude Code through Headroom (2026-06-12); mixed days are bimodal per session (direct = present, proxied = absent), confirming the proxy rather than a Claude Code version change.

Proposed fix

Add the request-id family (request-id, anthropic-request-id, x-request-id) to the streaming allowlist — the same one-line widening you already applied for *ratelimit* (#57) and x-codex-* (#794).

One check before I send a PR: the allowlist is deny-by-default and a test currently asserts x-request-id is dropped on this path (tests/test_proxy_streaming_ratelimit_headers.py). Is dropping non-allowlisted response headers on the streaming path a deliberate policy, or is request-id simply one you haven't needed to forward yet? Happy to open a PR (allowlist change + the test update) if you're open to it.

Environment

Reproduced on main (v0.26.0) and on headroom-ai 0.25.0. Verified that a patched build restores requestId end-to-end on the _stream_response path. (The backend/compression streaming returns, _stream_*_via_backend at ~lines 1469/1685, forward no response headers at all and would need a separate change, since the upstream response isn't in scope at their return.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions