1use crate::error::Error;
9use crate::keys;
10use crate::map::HeaderMap;
11use crate::name::HeaderName;
12use crate::schema::{from_inbound_or, opt_from_inbound, optional, put, put_opt, required, HeaderSchema};
13use crate::vars::TemplateVars;
14use serde::{Deserialize, Serialize};
15use smol_str::SmolStr;
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct ClaudeCodeHeaders {
19 #[serde(rename = "User-Agent")]
20 pub user_agent: SmolStr,
21 #[serde(rename = "Accept", skip_serializing_if = "Option::is_none")]
22 pub accept: Option<SmolStr>,
23 #[serde(rename = "Anthropic-Version")]
24 pub anthropic_version: Option<SmolStr>,
25 #[serde(rename = "Anthropic-Beta")]
26 pub anthropic_beta: Option<SmolStr>,
27 #[serde(rename = "X-Stainless-Lang", skip_serializing_if = "Option::is_none")]
28 pub stainless_lang: Option<SmolStr>,
29 #[serde(rename = "X-Stainless-Package-Version", skip_serializing_if = "Option::is_none")]
30 pub stainless_package_version: Option<SmolStr>,
31 #[serde(rename = "X-Stainless-OS", skip_serializing_if = "Option::is_none")]
32 pub stainless_os: Option<SmolStr>,
33 #[serde(rename = "X-Stainless-Arch", skip_serializing_if = "Option::is_none")]
34 pub stainless_arch: Option<SmolStr>,
35 #[serde(rename = "X-Stainless-Runtime", skip_serializing_if = "Option::is_none")]
36 pub stainless_runtime: Option<SmolStr>,
37 #[serde(rename = "X-Stainless-Runtime-Version", skip_serializing_if = "Option::is_none")]
38 pub stainless_runtime_version: Option<SmolStr>,
39 #[serde(rename = "X-Stainless-Retry-Count", skip_serializing_if = "Option::is_none")]
40 pub stainless_retry_count: Option<SmolStr>,
41 #[serde(rename = "X-Stainless-Timeout", skip_serializing_if = "Option::is_none")]
42 pub stainless_timeout: Option<SmolStr>,
43 #[serde(rename = "x-stainless-helper-method", skip_serializing_if = "Option::is_none")]
44 pub stainless_helper_method: Option<SmolStr>,
45 #[serde(rename = "X-App", skip_serializing_if = "Option::is_none")]
46 pub app: Option<SmolStr>,
47 #[serde(
48 rename = "Anthropic-Dangerous-Direct-Browser-Access",
49 skip_serializing_if = "Option::is_none"
50 )]
51 pub direct_browser_access: Option<SmolStr>,
52 #[serde(rename = "x-client-request-id", skip_serializing_if = "Option::is_none")]
53 pub client_request_id: Option<SmolStr>,
54 #[serde(rename = "X-Session-Id")]
55 pub session_id: Option<SmolStr>,
56 #[serde(rename = "X-Interaction-Id")]
57 pub interaction_id: Option<SmolStr>,
58}
59
60impl HeaderSchema for ClaudeCodeHeaders {
61 fn parse(map: &HeaderMap) -> Result<Self, Error> {
62 Ok(Self {
63 user_agent: required(map, &keys::USER_AGENT)?,
64 accept: optional(map, &keys::ACCEPT),
65 anthropic_version: optional(map, &keys::ANTHROPIC_VERSION),
66 anthropic_beta: optional(map, &keys::ANTHROPIC_BETA),
67 stainless_lang: optional(map, &keys::X_STAINLESS_LANG),
68 stainless_package_version: optional(map, &keys::X_STAINLESS_PACKAGE_VERSION),
69 stainless_os: optional(map, &keys::X_STAINLESS_OS),
70 stainless_arch: optional(map, &keys::X_STAINLESS_ARCH),
71 stainless_runtime: optional(map, &keys::X_STAINLESS_RUNTIME),
72 stainless_runtime_version: optional(map, &keys::X_STAINLESS_RUNTIME_VERSION),
73 stainless_retry_count: optional(map, &keys::X_STAINLESS_RETRY_COUNT),
74 stainless_timeout: optional(map, &keys::X_STAINLESS_TIMEOUT),
75 stainless_helper_method: optional(map, &keys::X_STAINLESS_HELPER_METHOD),
76 app: optional(map, &keys::X_APP),
77 direct_browser_access: optional(map, &keys::ANTHROPIC_DANGEROUS_DIRECT_BROWSER_ACCESS),
78 client_request_id: optional(map, &keys::X_CLIENT_REQUEST_ID),
79 session_id: optional(map, &keys::X_SESSION_ID),
80 interaction_id: optional(map, &keys::X_INTERACTION_ID),
81 })
82 }
83 fn dump(&self) -> HeaderMap {
84 let mut m = HeaderMap::new();
85 put(&mut m, &keys::USER_AGENT, &self.user_agent);
86 put_opt(&mut m, &keys::ACCEPT, &self.accept);
87 put_opt(&mut m, &keys::ANTHROPIC_VERSION, &self.anthropic_version);
88 put_opt(&mut m, &keys::ANTHROPIC_BETA, &self.anthropic_beta);
89 put_opt(&mut m, &keys::X_STAINLESS_LANG, &self.stainless_lang);
90 put_opt(
91 &mut m,
92 &keys::X_STAINLESS_PACKAGE_VERSION,
93 &self.stainless_package_version,
94 );
95 put_opt(&mut m, &keys::X_STAINLESS_OS, &self.stainless_os);
96 put_opt(&mut m, &keys::X_STAINLESS_ARCH, &self.stainless_arch);
97 put_opt(&mut m, &keys::X_STAINLESS_RUNTIME, &self.stainless_runtime);
98 put_opt(
99 &mut m,
100 &keys::X_STAINLESS_RUNTIME_VERSION,
101 &self.stainless_runtime_version,
102 );
103 put_opt(&mut m, &keys::X_STAINLESS_RETRY_COUNT, &self.stainless_retry_count);
104 put_opt(&mut m, &keys::X_STAINLESS_TIMEOUT, &self.stainless_timeout);
105 put_opt(&mut m, &keys::X_STAINLESS_HELPER_METHOD, &self.stainless_helper_method);
106 put_opt(&mut m, &keys::X_APP, &self.app);
107 put_opt(
108 &mut m,
109 &keys::ANTHROPIC_DANGEROUS_DIRECT_BROWSER_ACCESS,
110 &self.direct_browser_access,
111 );
112 put_opt(&mut m, &keys::X_CLIENT_REQUEST_ID, &self.client_request_id);
113 put_opt(&mut m, &keys::X_SESSION_ID, &self.session_id);
114 put_opt(&mut m, &keys::X_INTERACTION_ID, &self.interaction_id);
115 m
116 }
117 fn known_names() -> &'static [&'static HeaderName] {
118 static NAMES: [&HeaderName; 18] = [
119 &keys::USER_AGENT,
120 &keys::ACCEPT,
121 &keys::ANTHROPIC_VERSION,
122 &keys::ANTHROPIC_BETA,
123 &keys::X_STAINLESS_LANG,
124 &keys::X_STAINLESS_PACKAGE_VERSION,
125 &keys::X_STAINLESS_OS,
126 &keys::X_STAINLESS_ARCH,
127 &keys::X_STAINLESS_RUNTIME,
128 &keys::X_STAINLESS_RUNTIME_VERSION,
129 &keys::X_STAINLESS_RETRY_COUNT,
130 &keys::X_STAINLESS_TIMEOUT,
131 &keys::X_STAINLESS_HELPER_METHOD,
132 &keys::X_APP,
133 &keys::ANTHROPIC_DANGEROUS_DIRECT_BROWSER_ACCESS,
134 &keys::X_CLIENT_REQUEST_ID,
135 &keys::X_SESSION_ID,
136 &keys::X_INTERACTION_ID,
137 ];
138 &NAMES
139 }
140}
141
142impl ClaudeCodeHeaders {
143 pub fn build(vars: &TemplateVars, inbound: &HeaderMap) -> Self {
146 Self {
147 user_agent: from_inbound_or(inbound, &keys::USER_AGENT, || {
148 "claude-cli/2.1.92 (external, cli)".into()
149 }),
150 accept: Some(from_inbound_or(inbound, &keys::ACCEPT, || "application/json".into())),
151 anthropic_version: Some(from_inbound_or(inbound, &keys::ANTHROPIC_VERSION, || {
152 "2023-06-01".into()
153 })),
154 anthropic_beta: Some(from_inbound_or(inbound, &keys::ANTHROPIC_BETA, || {
155 "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
156 .into()
157 })),
158 stainless_lang: Some(from_inbound_or(inbound, &keys::X_STAINLESS_LANG, || "js".into())),
159 stainless_package_version: Some(from_inbound_or(inbound, &keys::X_STAINLESS_PACKAGE_VERSION, || {
160 "0.70.0".into()
161 })),
162 stainless_os: Some(from_inbound_or(inbound, &keys::X_STAINLESS_OS, || "Linux".into())),
163 stainless_arch: Some(from_inbound_or(inbound, &keys::X_STAINLESS_ARCH, || "arm64".into())),
164 stainless_runtime: Some(from_inbound_or(inbound, &keys::X_STAINLESS_RUNTIME, || "node".into())),
165 stainless_runtime_version: Some(from_inbound_or(inbound, &keys::X_STAINLESS_RUNTIME_VERSION, || {
166 "v24.13.0".into()
167 })),
168 stainless_retry_count: Some(from_inbound_or(inbound, &keys::X_STAINLESS_RETRY_COUNT, || "0".into())),
169 stainless_timeout: Some(from_inbound_or(inbound, &keys::X_STAINLESS_TIMEOUT, || "600".into())),
170 stainless_helper_method: opt_from_inbound(inbound, &keys::X_STAINLESS_HELPER_METHOD),
171 app: Some(from_inbound_or(inbound, &keys::X_APP, || "cli".into())),
172 direct_browser_access: Some(from_inbound_or(
173 inbound,
174 &keys::ANTHROPIC_DANGEROUS_DIRECT_BROWSER_ACCESS,
175 || "true".into(),
176 )),
177 client_request_id: vars
178 .request_id
179 .clone()
180 .or_else(|| opt_from_inbound(inbound, &keys::X_CLIENT_REQUEST_ID)),
181 session_id: vars
182 .session_id
183 .clone()
184 .or_else(|| opt_from_inbound(inbound, &keys::X_SESSION_ID)),
185 interaction_id: vars
186 .interaction_id
187 .clone()
188 .or_else(|| opt_from_inbound(inbound, &keys::X_INTERACTION_ID)),
189 }
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn round_trip() {
199 let h = ClaudeCodeHeaders {
200 user_agent: "claude-code/1.2.3".into(),
201 accept: Some("application/json".into()),
202 anthropic_version: Some("2023-06-01".into()),
203 anthropic_beta: Some("messages-2023-12-15".into()),
204 stainless_lang: Some("js".into()),
205 stainless_package_version: Some("0.70.0".into()),
206 stainless_os: Some("Linux".into()),
207 stainless_arch: Some("arm64".into()),
208 stainless_runtime: Some("node".into()),
209 stainless_runtime_version: Some("v24.13.0".into()),
210 stainless_retry_count: Some("0".into()),
211 stainless_timeout: Some("600".into()),
212 stainless_helper_method: Some("stream".into()),
213 app: Some("cli".into()),
214 direct_browser_access: Some("true".into()),
215 client_request_id: Some("req-1".into()),
216 session_id: Some("ses_cc".into()),
217 interaction_id: Some("int_99".into()),
218 };
219 assert_eq!(ClaudeCodeHeaders::parse(&h.dump()).unwrap(), h);
220 }
221
222 #[test]
223 fn build_with_empty_inbound_uses_defaults() {
224 let h = ClaudeCodeHeaders::build(&TemplateVars::default(), &HeaderMap::new());
225 assert_eq!(h.user_agent.as_str(), "claude-cli/2.1.92 (external, cli)");
226 assert_eq!(h.accept.as_deref(), Some("application/json"));
227 assert_eq!(h.anthropic_version.as_deref(), Some("2023-06-01"));
228 assert_eq!(
229 h.anthropic_beta.as_deref(),
230 Some(
231 "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14"
232 )
233 );
234 assert_eq!(h.stainless_lang.as_deref(), Some("js"));
235 assert_eq!(h.stainless_package_version.as_deref(), Some("0.70.0"));
236 assert_eq!(h.stainless_os.as_deref(), Some("Linux"));
237 assert_eq!(h.stainless_arch.as_deref(), Some("arm64"));
238 assert_eq!(h.stainless_runtime.as_deref(), Some("node"));
239 assert_eq!(h.stainless_runtime_version.as_deref(), Some("v24.13.0"));
240 assert_eq!(h.stainless_retry_count.as_deref(), Some("0"));
241 assert_eq!(h.stainless_timeout.as_deref(), Some("600"));
242 assert_eq!(h.app.as_deref(), Some("cli"));
243 assert_eq!(h.direct_browser_access.as_deref(), Some("true"));
244 assert!(h.stainless_helper_method.is_none());
245 assert!(h.client_request_id.is_none());
246 assert!(h.session_id.is_none());
247 assert!(h.interaction_id.is_none());
248 }
249
250 #[test]
251 fn build_passes_through_inbound() {
252 let mut inbound = HeaderMap::new();
253 inbound.insert(&keys::USER_AGENT, "claude-cli/2.0");
254 inbound.insert(&keys::ANTHROPIC_VERSION, "2023-06-01");
255 let h = ClaudeCodeHeaders::build(&TemplateVars::default(), &inbound);
256 assert_eq!(h.user_agent.as_str(), "claude-cli/2.0");
257 assert_eq!(h.anthropic_version.as_deref(), Some("2023-06-01"));
258 }
259
260 #[test]
261 fn build_uses_vars_for_correlation() {
262 let vars = TemplateVars {
263 session_id: Some("ses_xyz".into()),
264 request_id: Some("req_xyz".into()),
265 interaction_id: Some("int_42".into()),
266 ..Default::default()
267 };
268 let h = ClaudeCodeHeaders::build(&vars, &HeaderMap::new());
269 assert_eq!(h.client_request_id.as_deref(), Some("req_xyz"));
270 assert_eq!(h.session_id.as_deref(), Some("ses_xyz"));
271 assert_eq!(h.interaction_id.as_deref(), Some("int_42"));
272 }
273}