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
30pub 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}