Skip to content

Commit de3e8db

Browse files
NitjsefnieClaude Opus 5
andauthored
publish the per-request id as a request-id response header (#105)
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>
1 parent dc61029 commit de3e8db

1 file changed

Lines changed: 95 additions & 1 deletion

File tree

src/server.rs

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1783,13 +1783,31 @@ async fn dispatch_request(
17831783
response
17841784
}
17851785

1786+
/// Header Claude Code reads to populate the `requestId` field it writes into
1787+
/// every transcript record.
1788+
pub const REQUEST_ID_HEADER: &str = "request-id";
1789+
17861790
fn monitor_response_body(response: Response, guard: RequestMonitorGuard) -> Response {
17871791
let status = response.status();
17881792
let outcome = response
17891793
.extensions()
17901794
.get::<NativeResponseOutcome>()
17911795
.cloned();
1792-
let (parts, body) = response.into_parts();
1796+
let (mut parts, body) = response.into_parts();
1797+
// Publish the per-request id this proxy already mints. Anthropic returns
1798+
// `request-id`, and Claude Code records it as `requestId`; without it every
1799+
// downstream consumer that de-duplicates transcript records by request id
1800+
// (its own parser, usage dashboards) counts each request twice, because a
1801+
// transcript legitimately repeats a record and the id is what resolves it.
1802+
//
1803+
// Stamped here rather than at each `return` because every response path in
1804+
// this module funnels through this function, and it is applied to the parts
1805+
// before the body is streamed, so a streaming SSE response carries it too.
1806+
if !parts.headers.contains_key(REQUEST_ID_HEADER)
1807+
&& let Ok(value) = http::HeaderValue::from_str(&guard.req_id)
1808+
{
1809+
parts.headers.insert(REQUEST_ID_HEADER, value);
1810+
}
17931811
let stream = futures_util::stream::unfold(
17941812
(body, guard, outcome),
17951813
move |(mut body, mut guard, outcome)| async move {
@@ -2144,6 +2162,82 @@ fn _unused(session_state: Option<&SessionState>) {
21442162
let _ = session_state;
21452163
}
21462164

2165+
#[cfg(test)]
2166+
mod request_id_header_tests {
2167+
use super::{REQUEST_ID_HEADER, RequestMonitorGuard, monitor_response_body};
2168+
use axum::body::Body;
2169+
use axum::response::Response;
2170+
use http::{HeaderValue, StatusCode};
2171+
2172+
fn guard(req_id: &str) -> RequestMonitorGuard {
2173+
RequestMonitorGuard::new(None, req_id.to_string())
2174+
}
2175+
2176+
// Claude Code populates its transcript `requestId` from this header.
2177+
// Without it, consumers that de-duplicate transcript records by request id
2178+
// count every request twice, because a transcript legitimately repeats a
2179+
// record and the id is what resolves the repeat.
2180+
#[test]
2181+
fn stamps_the_request_id_on_a_response() {
2182+
let response = Response::builder()
2183+
.status(StatusCode::OK)
2184+
.body(Body::from("{}"))
2185+
.unwrap();
2186+
let stamped = monitor_response_body(response, guard("req-abc-123"));
2187+
assert_eq!(
2188+
stamped.headers().get(REQUEST_ID_HEADER).unwrap(),
2189+
"req-abc-123"
2190+
);
2191+
}
2192+
2193+
// Streaming responses carry it too: the header is applied to the parts
2194+
// before the body is streamed, which is why the body-level message id is
2195+
// not a substitute for SSE.
2196+
#[test]
2197+
fn stamps_a_streaming_response_before_the_body() {
2198+
let response = Response::builder()
2199+
.status(StatusCode::OK)
2200+
.header("content-type", "text/event-stream")
2201+
.body(Body::from("event: message_start\n"))
2202+
.unwrap();
2203+
let stamped = monitor_response_body(response, guard("req-stream-1"));
2204+
assert_eq!(
2205+
stamped.headers().get(REQUEST_ID_HEADER).unwrap(),
2206+
"req-stream-1"
2207+
);
2208+
}
2209+
2210+
// An upstream that already supplied one owns it; overwriting would relabel
2211+
// a real provider id with a local uuid.
2212+
#[test]
2213+
fn does_not_clobber_an_upstream_supplied_id() {
2214+
let response = Response::builder()
2215+
.status(StatusCode::OK)
2216+
.header(
2217+
REQUEST_ID_HEADER,
2218+
HeaderValue::from_static("upstream-owned"),
2219+
)
2220+
.body(Body::from("{}"))
2221+
.unwrap();
2222+
let stamped = monitor_response_body(response, guard("local-uuid"));
2223+
assert_eq!(
2224+
stamped.headers().get(REQUEST_ID_HEADER).unwrap(),
2225+
"upstream-owned"
2226+
);
2227+
}
2228+
2229+
// Error responses are de-duplicated by the same key as successes.
2230+
#[test]
2231+
fn stamps_error_responses_too() {
2232+
let response = Response::builder()
2233+
.status(StatusCode::TOO_MANY_REQUESTS)
2234+
.body(Body::from("{\"error\":\"rate limited\"}"))
2235+
.unwrap();
2236+
let stamped = monitor_response_body(response, guard("req-429"));
2237+
assert_eq!(stamped.headers().get(REQUEST_ID_HEADER).unwrap(), "req-429");
2238+
}
2239+
}
2240+
21472241
#[cfg(test)]
21482242
mod auto_review_tests {
21492243
use super::{apply_auto_review_model, headers_to_record, is_claude_auto_review_request};

0 commit comments

Comments
 (0)