Skip to content

Publish the per-request id as a request-id response header - #105

Open
Nitjsefnie wants to merge 1 commit into
raine:mainfrom
Nitjsefnie-OSC:request-id-header
Open

Publish the per-request id as a request-id response header#105
Nitjsefnie wants to merge 1 commit into
raine:mainfrom
Nitjsefnie-OSC:request-id-header

Conversation

@Nitjsefnie

Copy link
Copy Markdown

Summary

Stamps a request-id header on proxy responses so Claude Code records a usable requestId in transcripts. Without it, consumers that de-duplicate transcript records by request id count every request twice.

Related Issues and Pull Requests

Fixes #104

Changes

  • src/server.rs: added REQUEST_ID_HEADER and stamped it in monitor_response_body, using the req_id that dispatch_request already mints and RequestMonitorGuard already carries.
  • Applied at that one function rather than at each return, because every response path in the module funnels through it, and it is applied to the response parts before the body is streamed, so a streaming SSE response carries the header too.
  • An upstream-supplied request-id is preserved rather than overwritten.
  • src/server.rs: added request_id_header_tests covering a plain response, a streaming response, an upstream-supplied header, and an error response.

Testing

cargo fmt --check, cargo clippy --all-targets -- -D warnings, and cargo test all pass on this branch. The four new tests pass.

Verified end to end against a live proxy on Linux with the codex provider:

  • Before: response headers were content-type, transfer-encoding, date; a proxied Claude Code session produced 4 usage-bearing transcript records, 0 with requestId; parse_session.py --cache reported 4 turns and fresh: 47,986.
  • After: the response carries request-id: 288ebaeb-9b8a-4a6a-8ebf-0b6372caa94c; an equivalent session produced 4 usage-bearing records, 4 with requestId, resolving to 2 unique ids; the same parser reported 2 turns and fresh: 23,966, with real ids in its requestId column.

The parser was not modified — the id was the only thing missing.

Follow-ups / Known Limitations

The header is stamped for responses that pass through monitor_response_body. Any future response path that bypasses it would need the same treatment.

Footer

Generated by Claude Opus 5 (implementation, testing, verification)

Claude Code records the `request-id` response header into every transcript
record as `requestId`. This proxy never sent one, so proxied transcripts carry
no request id at all: measured on a real session, 0 of 4 usage-bearing records
had one, against 753 of 756 in a native Anthropic session.

That matters because a transcript legitimately repeats a record, and the
request id is what downstream consumers use to resolve the repeat. Without it
they double-count. Claude Code's own parser reported 4 turns and 47,986 fresh
input tokens for a session whose true figures were 2 turns and 23,966; with the
header present it reports both correctly, unchanged.

The id already existed. `dispatch_request` mints `req_id` per request and
threads it through logging and the monitor; it simply never reached the
response. `RequestMonitorGuard` already carries it, and every response path in
this module funnels through `monitor_response_body`, which destructures the
response parts before streaming the body. Stamping it there covers all return
sites at once, and applies to a streaming SSE response as well, which the
body-level `message.id` cannot do because a stream's id arrives inside
`message_start` rather than in the headers.

An upstream-supplied header is preserved rather than overwritten: relabelling a
real provider id with a local uuid would lose the more useful value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Responses carry no request-id header, so proxied Claude Code transcripts have no request id

1 participant