ccs_proxy/capture/
extract.rs1use serde_json::Value;
5use std::collections::BTreeMap;
6
7pub fn extract_request_id(response_headers: &BTreeMap<String, String>) -> Option<String> {
8 const CANDIDATES: &[&str] = &[
9 "anthropic-request-id",
10 "x-request-id",
11 "request-id",
12 "openai-request-id",
13 ];
14 for name in CANDIDATES {
15 for (k, v) in response_headers.iter() {
16 if k.eq_ignore_ascii_case(name) && !v.is_empty() {
17 return Some(v.clone());
18 }
19 }
20 }
21 None
22}
23
24pub fn extract_model_from_request_body(body: &Value) -> Option<String> {
25 body.get("model")
26 .and_then(|v| v.as_str())
27 .map(|s| s.to_string())
28}
29
30#[cfg(test)]
31mod tests {
32 use super::*;
33 use serde_json::json;
34
35 #[test]
36 fn picks_anthropic_request_id() {
37 let h = BTreeMap::from([
38 ("Anthropic-Request-Id".to_string(), "req_01H8".to_string()),
39 ("content-type".to_string(), "text/event-stream".to_string()),
40 ]);
41 assert_eq!(extract_request_id(&h), Some("req_01H8".into()));
42 }
43
44 #[test]
45 fn picks_x_request_id_when_anthropic_missing() {
46 let h = BTreeMap::from([("X-Request-Id".to_string(), "abc".to_string())]);
47 assert_eq!(extract_request_id(&h), Some("abc".into()));
48 }
49
50 #[test]
51 fn returns_none_when_no_id_header() {
52 let h = BTreeMap::from([("content-type".to_string(), "application/json".to_string())]);
53 assert_eq!(extract_request_id(&h), None);
54 }
55
56 #[test]
57 fn extracts_model_from_body() {
58 let body = json!({"model": "claude-sonnet-4-6", "messages": []});
59 assert_eq!(
60 extract_model_from_request_body(&body),
61 Some("claude-sonnet-4-6".into())
62 );
63 }
64
65 #[test]
66 fn no_model_when_field_absent() {
67 let body = json!({"messages": []});
68 assert_eq!(extract_model_from_request_body(&body), None);
69 }
70}