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)
- 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_…".
- 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.)
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 theStreamingResponseat ~line 1338 onmain):Anthropic's
request-idresponse header is therefore never forwarded to streaming clients. Clients that record it lose it — most visibly Claude Code, which writes arequestIdinto each transcript turn from that header.This is the same class of omission already fixed for other headers on this exact allowlist:
*ratelimit*forwarding on the streaming path.x-codex-*(fixing [BUG] Codex session/weekly usage never updates on streaming SSE and WebSocket transports #577, where Codex session/weekly usage never updated on streaming SSE).request-idis 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
requestIdfield.messageId+requestId) can no longer dedup and over-count tokens. CodexBar, for example, requires bothmessageIdandrequestId; withoutrequestIdit counts the samemessage.idonce 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).ccusageis unaffected only because it treatsrequestIdas optional.request-idis 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)
ANTHROPIC_BASE_URL=https://api.anthropic.com claude -p "hi" --output-format json~/.claude/projects/**/<session>.jsonlassistant row has a top-level"requestId": "req_…".requestId.In my logs,
requestIdpresence 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-idfamily (request-id,anthropic-request-id,x-request-id) to the streaming allowlist — the same one-line widening you already applied for*ratelimit*(#57) andx-codex-*(#794).One check before I send a PR: the allowlist is deny-by-default and a test currently asserts
x-request-idis 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 isrequest-idsimply 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 onheadroom-ai0.25.0. Verified that a patched build restoresrequestIdend-to-end on the_stream_responsepath. (The backend/compression streaming returns,_stream_*_via_backendat ~lines 1469/1685, forward no response headers at all and would need a separate change, since the upstream response isn't in scope at theirreturn.)