Skip to main content

ccs_proxy/capture/
extract.rs

1//! Pull out structured metadata (request id, model, usage) from raw
2//! request/response shapes.
3
4use 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/// Extract the Claude Code working-directory marker from a request body's
31/// `system` field. Looks for a line starting with `Primary working directory:`
32/// (case-sensitive, anchored to line start) and returns the trimmed path.
33/// Handles both `system: "..."` (string) and
34/// `system: [{"type":"text","text":"..."}]` (block list) shapes.
35pub fn extract_cwd(body: &Value) -> Option<String> {
36    let system = body.get("system")?;
37    if let Some(s) = system.as_str() {
38        return scan_system_text(s);
39    }
40    if let Some(arr) = system.as_array() {
41        for block in arr {
42            if let Some(text) = block.get("text").and_then(|v| v.as_str())
43                && let Some(found) = scan_system_text(text)
44            {
45                return Some(found);
46            }
47        }
48    }
49    None
50}
51
52fn scan_system_text(text: &str) -> Option<String> {
53    const MARKER: &str = "Primary working directory:";
54    for line in text.lines() {
55        if let Some(rest) = line.strip_prefix(MARKER) {
56            let trimmed = rest.trim();
57            if !trimmed.is_empty() {
58                return Some(trimmed.to_string());
59            }
60        }
61    }
62    None
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use serde_json::json;
69
70    #[test]
71    fn picks_anthropic_request_id() {
72        let h = BTreeMap::from([
73            ("Anthropic-Request-Id".to_string(), "req_01H8".to_string()),
74            ("content-type".to_string(), "text/event-stream".to_string()),
75        ]);
76        assert_eq!(extract_request_id(&h), Some("req_01H8".into()));
77    }
78
79    #[test]
80    fn picks_x_request_id_when_anthropic_missing() {
81        let h = BTreeMap::from([("X-Request-Id".to_string(), "abc".to_string())]);
82        assert_eq!(extract_request_id(&h), Some("abc".into()));
83    }
84
85    #[test]
86    fn returns_none_when_no_id_header() {
87        let h = BTreeMap::from([("content-type".to_string(), "application/json".to_string())]);
88        assert_eq!(extract_request_id(&h), None);
89    }
90
91    #[test]
92    fn extracts_model_from_body() {
93        let body = json!({"model": "claude-sonnet-4-6", "messages": []});
94        assert_eq!(
95            extract_model_from_request_body(&body),
96            Some("claude-sonnet-4-6".into())
97        );
98    }
99
100    #[test]
101    fn no_model_when_field_absent() {
102        let body = json!({"messages": []});
103        assert_eq!(extract_model_from_request_body(&body), None);
104    }
105
106    #[test]
107    fn cwd_from_string_system() {
108        let body = json!({
109            "system": "You are Claude Code.\nPrimary working directory: /Users/me/proj\nMore text.",
110        });
111        assert_eq!(extract_cwd(&body), Some("/Users/me/proj".into()));
112    }
113
114    #[test]
115    fn cwd_from_block_list_system() {
116        let body = json!({
117            "system": [
118                {"type": "text", "text": "header"},
119                {"type": "text", "text": "intro\nPrimary working directory: /tmp/x y z\ntail"},
120            ],
121        });
122        assert_eq!(extract_cwd(&body), Some("/tmp/x y z".into()));
123    }
124
125    #[test]
126    fn cwd_ignores_prose_mention() {
127        let body = json!({
128            "system": "Consider the user's working directory when answering questions.",
129        });
130        assert_eq!(extract_cwd(&body), None);
131    }
132
133    #[test]
134    fn cwd_returns_none_when_no_system() {
135        let body = json!({"messages": []});
136        assert_eq!(extract_cwd(&body), None);
137    }
138
139    #[test]
140    fn cwd_takes_first_match_only() {
141        let body = json!({
142            "system": "Primary working directory: /a\nPrimary working directory: /b",
143        });
144        assert_eq!(extract_cwd(&body), Some("/a".into()));
145    }
146}