1use crate::error::Error;
19use crate::keys;
20use crate::map::HeaderMap;
21use crate::name::HeaderName;
22use crate::schema::{from_inbound_or, opt_from_inbound, optional, put, put_opt, required, HeaderSchema};
23use crate::vars::TemplateVars;
24use serde::{Deserialize, Serialize};
25use smol_str::SmolStr;
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct CopilotCliHeaders {
29 #[serde(rename = "User-Agent")]
31 pub user_agent: SmolStr,
32 #[serde(rename = "Authorization")]
33 pub authorization: SmolStr,
34 #[serde(rename = "Content-Type")]
35 pub content_type: SmolStr,
36 #[serde(rename = "Accept")]
37 pub accept: SmolStr,
38 #[serde(rename = "Accept-Encoding")]
39 pub accept_encoding: SmolStr,
40 #[serde(rename = "Accept-Language")]
41 pub accept_language: SmolStr,
42 #[serde(rename = "Sec-Fetch-Mode")]
43 pub sec_fetch_mode: SmolStr,
44
45 #[serde(rename = "Copilot-Integration-Id")]
47 pub copilot_integration_id: SmolStr,
48 #[serde(rename = "OpenAI-Intent")]
49 pub openai_intent: SmolStr,
50 #[serde(rename = "X-Initiator")]
51 pub initiator: SmolStr,
52 #[serde(rename = "X-GitHub-Api-Version")]
53 pub github_api_version: SmolStr,
54
55 #[serde(rename = "X-Interaction-Id")]
57 pub interaction_id: SmolStr,
58 #[serde(rename = "X-Interaction-Type")]
59 pub interaction_type: SmolStr,
60 #[serde(rename = "X-Client-Session-Id")]
61 pub client_session_id: SmolStr,
62 #[serde(rename = "X-Agent-Task-Id")]
63 pub agent_task_id: SmolStr,
64
65 #[serde(rename = "X-Stainless-Retry-Count", skip_serializing_if = "Option::is_none")]
70 pub stainless_retry_count: Option<SmolStr>,
71 #[serde(rename = "X-Stainless-Lang", skip_serializing_if = "Option::is_none")]
72 pub stainless_lang: Option<SmolStr>,
73 #[serde(rename = "X-Stainless-Package-Version", skip_serializing_if = "Option::is_none")]
74 pub stainless_package_version: Option<SmolStr>,
75 #[serde(rename = "X-Stainless-OS", skip_serializing_if = "Option::is_none")]
76 pub stainless_os: Option<SmolStr>,
77 #[serde(rename = "X-Stainless-Arch", skip_serializing_if = "Option::is_none")]
78 pub stainless_arch: Option<SmolStr>,
79 #[serde(rename = "X-Stainless-Runtime", skip_serializing_if = "Option::is_none")]
80 pub stainless_runtime: Option<SmolStr>,
81 #[serde(rename = "X-Stainless-Runtime-Version", skip_serializing_if = "Option::is_none")]
82 pub stainless_runtime_version: Option<SmolStr>,
83
84 #[serde(rename = "Content-Length", skip_serializing_if = "Option::is_none")]
86 pub content_length: Option<SmolStr>,
87
88 #[serde(rename = "Cookie", skip_serializing_if = "Option::is_none")]
90 pub cookie: Option<SmolStr>,
91 #[serde(rename = "X-Request-Id", skip_serializing_if = "Option::is_none")]
92 pub request_id: Option<SmolStr>,
93}
94
95impl HeaderSchema for CopilotCliHeaders {
96 fn parse(map: &HeaderMap) -> Result<Self, Error> {
97 Ok(Self {
98 user_agent: required(map, &keys::USER_AGENT)?,
99 authorization: required(map, &keys::AUTHORIZATION)?,
100 content_type: required(map, &keys::CONTENT_TYPE)?,
101 accept: required(map, &keys::ACCEPT)?,
102 accept_encoding: required(map, &keys::ACCEPT_ENCODING)?,
103 accept_language: required(map, &keys::ACCEPT_LANGUAGE)?,
104 sec_fetch_mode: required(map, &keys::SEC_FETCH_MODE)?,
105 copilot_integration_id: required(map, &keys::COPILOT_INTEGRATION_ID)?,
106 openai_intent: required(map, &keys::OPENAI_INTENT)?,
107 initiator: required(map, &keys::X_INITIATOR)?,
108 github_api_version: required(map, &keys::X_GITHUB_API_VERSION)?,
109 interaction_id: required(map, &keys::X_INTERACTION_ID)?,
110 interaction_type: required(map, &keys::X_INTERACTION_TYPE)?,
111 client_session_id: required(map, &keys::X_CLIENT_SESSION_ID)?,
112 agent_task_id: required(map, &keys::X_AGENT_TASK_ID)?,
113 stainless_retry_count: optional(map, &keys::X_STAINLESS_RETRY_COUNT),
114 stainless_lang: optional(map, &keys::X_STAINLESS_LANG),
115 stainless_package_version: optional(map, &keys::X_STAINLESS_PACKAGE_VERSION),
116 stainless_os: optional(map, &keys::X_STAINLESS_OS),
117 stainless_arch: optional(map, &keys::X_STAINLESS_ARCH),
118 stainless_runtime: optional(map, &keys::X_STAINLESS_RUNTIME),
119 stainless_runtime_version: optional(map, &keys::X_STAINLESS_RUNTIME_VERSION),
120 content_length: optional(map, &keys::CONTENT_LENGTH),
121 cookie: optional(map, &keys::COOKIE),
122 request_id: optional(map, &keys::X_REQUEST_ID),
123 })
124 }
125
126 fn dump(&self) -> HeaderMap {
127 let mut m = HeaderMap::new();
128 put(&mut m, &keys::USER_AGENT, &self.user_agent);
129 put(&mut m, &keys::AUTHORIZATION, &self.authorization);
130 put(&mut m, &keys::CONTENT_TYPE, &self.content_type);
131 put(&mut m, &keys::ACCEPT, &self.accept);
132 put(&mut m, &keys::ACCEPT_ENCODING, &self.accept_encoding);
133 put(&mut m, &keys::ACCEPT_LANGUAGE, &self.accept_language);
134 put(&mut m, &keys::SEC_FETCH_MODE, &self.sec_fetch_mode);
135 put(&mut m, &keys::COPILOT_INTEGRATION_ID, &self.copilot_integration_id);
136 put(&mut m, &keys::OPENAI_INTENT, &self.openai_intent);
137 put(&mut m, &keys::X_INITIATOR, &self.initiator);
138 put(&mut m, &keys::X_GITHUB_API_VERSION, &self.github_api_version);
139 put(&mut m, &keys::X_INTERACTION_ID, &self.interaction_id);
140 put(&mut m, &keys::X_INTERACTION_TYPE, &self.interaction_type);
141 put(&mut m, &keys::X_CLIENT_SESSION_ID, &self.client_session_id);
142 put(&mut m, &keys::X_AGENT_TASK_ID, &self.agent_task_id);
143 put_opt(&mut m, &keys::X_STAINLESS_RETRY_COUNT, &self.stainless_retry_count);
144 put_opt(&mut m, &keys::X_STAINLESS_LANG, &self.stainless_lang);
145 put_opt(
146 &mut m,
147 &keys::X_STAINLESS_PACKAGE_VERSION,
148 &self.stainless_package_version,
149 );
150 put_opt(&mut m, &keys::X_STAINLESS_OS, &self.stainless_os);
151 put_opt(&mut m, &keys::X_STAINLESS_ARCH, &self.stainless_arch);
152 put_opt(&mut m, &keys::X_STAINLESS_RUNTIME, &self.stainless_runtime);
153 put_opt(
154 &mut m,
155 &keys::X_STAINLESS_RUNTIME_VERSION,
156 &self.stainless_runtime_version,
157 );
158 put_opt(&mut m, &keys::CONTENT_LENGTH, &self.content_length);
159 put_opt(&mut m, &keys::COOKIE, &self.cookie);
160 put_opt(&mut m, &keys::X_REQUEST_ID, &self.request_id);
161 m
162 }
163
164 fn known_names() -> &'static [&'static HeaderName] {
165 static NAMES: [&HeaderName; 25] = [
166 &keys::USER_AGENT,
167 &keys::AUTHORIZATION,
168 &keys::CONTENT_TYPE,
169 &keys::ACCEPT,
170 &keys::ACCEPT_ENCODING,
171 &keys::ACCEPT_LANGUAGE,
172 &keys::SEC_FETCH_MODE,
173 &keys::COPILOT_INTEGRATION_ID,
174 &keys::OPENAI_INTENT,
175 &keys::X_INITIATOR,
176 &keys::X_GITHUB_API_VERSION,
177 &keys::X_INTERACTION_ID,
178 &keys::X_INTERACTION_TYPE,
179 &keys::X_CLIENT_SESSION_ID,
180 &keys::X_AGENT_TASK_ID,
181 &keys::X_STAINLESS_RETRY_COUNT,
182 &keys::X_STAINLESS_LANG,
183 &keys::X_STAINLESS_PACKAGE_VERSION,
184 &keys::X_STAINLESS_OS,
185 &keys::X_STAINLESS_ARCH,
186 &keys::X_STAINLESS_RUNTIME,
187 &keys::X_STAINLESS_RUNTIME_VERSION,
188 &keys::CONTENT_LENGTH,
189 &keys::COOKIE,
190 &keys::X_REQUEST_ID,
191 ];
192 &NAMES
193 }
194}
195
196impl CopilotCliHeaders {
197 pub fn build(vars: &TemplateVars, inbound: &HeaderMap) -> Self {
201 Self {
202 user_agent: from_inbound_or(inbound, &keys::USER_AGENT, || {
203 "copilot/1.0.25 (client/sdk win32 v22.19.0) term/unknown".into()
204 }),
205 authorization: from_inbound_or(inbound, &keys::AUTHORIZATION, || "<missing>".into()),
206 content_type: from_inbound_or(inbound, &keys::CONTENT_TYPE, || "application/json".into()),
207 accept: from_inbound_or(inbound, &keys::ACCEPT, || "application/json".into()),
208 accept_encoding: from_inbound_or(inbound, &keys::ACCEPT_ENCODING, || "br, gzip, deflate".into()),
209 accept_language: from_inbound_or(inbound, &keys::ACCEPT_LANGUAGE, || "*".into()),
210 sec_fetch_mode: from_inbound_or(inbound, &keys::SEC_FETCH_MODE, || "cors".into()),
211 copilot_integration_id: from_inbound_or(inbound, &keys::COPILOT_INTEGRATION_ID, || {
212 "copilot-developer-cli".into()
213 }),
214 openai_intent: from_inbound_or(inbound, &keys::OPENAI_INTENT, || "conversation-agent".into()),
215 initiator: from_inbound_or(inbound, &keys::X_INITIATOR, || "user".into()),
216 github_api_version: from_inbound_or(inbound, &keys::X_GITHUB_API_VERSION, || "2026-01-09".into()),
217 interaction_id: vars.interaction_id.clone().unwrap_or_else(|| {
218 from_inbound_or(inbound, &keys::X_INTERACTION_ID, || {
219 "00000000-0000-0000-0000-000000000000".into()
220 })
221 }),
222 interaction_type: from_inbound_or(inbound, &keys::X_INTERACTION_TYPE, || "conversation-user".into()),
223 client_session_id: vars.session_id.clone().unwrap_or_else(|| {
224 from_inbound_or(inbound, &keys::X_CLIENT_SESSION_ID, || {
225 "00000000-0000-0000-0000-000000000000".into()
226 })
227 }),
228 agent_task_id: from_inbound_or(inbound, &keys::X_AGENT_TASK_ID, || {
229 "00000000-0000-0000-0000-000000000000".into()
230 }),
231 stainless_retry_count: opt_from_inbound(inbound, &keys::X_STAINLESS_RETRY_COUNT),
232 stainless_lang: opt_from_inbound(inbound, &keys::X_STAINLESS_LANG),
233 stainless_package_version: opt_from_inbound(inbound, &keys::X_STAINLESS_PACKAGE_VERSION),
234 stainless_os: opt_from_inbound(inbound, &keys::X_STAINLESS_OS),
235 stainless_arch: opt_from_inbound(inbound, &keys::X_STAINLESS_ARCH),
236 stainless_runtime: opt_from_inbound(inbound, &keys::X_STAINLESS_RUNTIME),
237 stainless_runtime_version: opt_from_inbound(inbound, &keys::X_STAINLESS_RUNTIME_VERSION),
238 content_length: opt_from_inbound(inbound, &keys::CONTENT_LENGTH),
239 cookie: opt_from_inbound(inbound, &keys::COOKIE),
240 request_id: vars
241 .request_id
242 .clone()
243 .or_else(|| opt_from_inbound(inbound, &keys::X_REQUEST_ID)),
244 }
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 fn captured() -> CopilotCliHeaders {
253 CopilotCliHeaders {
254 user_agent: "copilot/1.0.25 (client/sdk win32 v22.19.0) term/unknown".into(),
255 authorization: "Bearer gho_xxxx".into(),
256 content_type: "application/json".into(),
257 accept: "application/json".into(),
258 accept_encoding: "br, gzip, deflate".into(),
259 accept_language: "*".into(),
260 sec_fetch_mode: "cors".into(),
261 copilot_integration_id: "copilot-developer-cli".into(),
262 openai_intent: "conversation-agent".into(),
263 initiator: "user".into(),
264 github_api_version: "2026-01-09".into(),
265 interaction_id: "6342b4bb-3441-4440-aae6-884912e51b08".into(),
266 interaction_type: "conversation-user".into(),
267 client_session_id: "07e363b0-ea39-48e6-a45e-fe5c6066e50d".into(),
268 agent_task_id: "08a2565a-6ab6-4613-ae6d-3b0814061942".into(),
269 stainless_retry_count: Some("0".into()),
270 stainless_lang: Some("js".into()),
271 stainless_package_version: Some("5.20.1".into()),
272 stainless_os: Some("Windows".into()),
273 stainless_arch: Some("x64".into()),
274 stainless_runtime: Some("node".into()),
275 stainless_runtime_version: Some("v22.19.0".into()),
276 content_length: Some("58364".into()),
277 cookie: None,
278 request_id: None,
279 }
280 }
281
282 #[test]
283 fn round_trip_matches_capture() {
284 let h = captured();
285 assert_eq!(CopilotCliHeaders::parse(&h.dump()).unwrap(), h);
286 }
287
288 #[test]
289 fn missing_required_errors() {
290 let m = HeaderMap::new();
291 assert!(matches!(CopilotCliHeaders::parse(&m), Err(Error::MissingHeader { .. })));
292 }
293
294 #[test]
295 fn optional_fields_omitted_when_none() {
296 let mut h = captured();
297 h.content_length = None;
298 h.cookie = None;
299 h.request_id = None;
300 h.stainless_retry_count = None;
301 h.stainless_lang = None;
302 h.stainless_package_version = None;
303 h.stainless_os = None;
304 h.stainless_arch = None;
305 h.stainless_runtime = None;
306 h.stainless_runtime_version = None;
307 let m = h.dump();
308 assert!(!m.contains_key(&keys::CONTENT_LENGTH));
309 assert!(!m.contains_key(&keys::COOKIE));
310 assert!(!m.contains_key(&keys::X_REQUEST_ID));
311 assert!(!m.contains_key(&keys::X_STAINLESS_LANG));
312 assert_eq!(m.len(), 15);
314 }
315
316 #[test]
317 fn build_with_empty_inbound_uses_defaults() {
318 let h = CopilotCliHeaders::build(&TemplateVars::default(), &HeaderMap::new());
319 assert_eq!(
320 h.user_agent.as_str(),
321 "copilot/1.0.25 (client/sdk win32 v22.19.0) term/unknown"
322 );
323 assert_eq!(h.authorization.as_str(), "<missing>");
324 assert_eq!(h.copilot_integration_id.as_str(), "copilot-developer-cli");
325 assert_eq!(h.openai_intent.as_str(), "conversation-agent");
326 assert_eq!(h.initiator.as_str(), "user");
327 assert_eq!(h.github_api_version.as_str(), "2026-01-09");
328 assert!(h.stainless_lang.is_none());
329 assert!(h.cookie.is_none());
330 assert!(h.request_id.is_none());
331 assert!(h.content_length.is_none());
332 }
333
334 #[test]
335 fn build_passes_through_inbound() {
336 let mut inbound = HeaderMap::new();
337 inbound.insert(&keys::USER_AGENT, "copilot/2.0");
338 inbound.insert(&keys::AUTHORIZATION, "Bearer gho_abc");
339 inbound.insert(&keys::X_STAINLESS_LANG, "js");
340 let h = CopilotCliHeaders::build(&TemplateVars::default(), &inbound);
341 assert_eq!(h.user_agent.as_str(), "copilot/2.0");
342 assert_eq!(h.authorization.as_str(), "Bearer gho_abc");
343 assert_eq!(h.stainless_lang.as_deref(), Some("js"));
344 }
345
346 #[test]
347 fn build_uses_vars_for_correlation() {
348 let vars = TemplateVars {
349 session_id: Some("ses_xyz".into()),
350 interaction_id: Some("int_42".into()),
351 request_id: Some("req_99".into()),
352 ..Default::default()
353 };
354 let h = CopilotCliHeaders::build(&vars, &HeaderMap::new());
355 assert_eq!(h.client_session_id.as_str(), "ses_xyz");
356 assert_eq!(h.interaction_id.as_str(), "int_42");
357 assert_eq!(h.request_id.as_deref(), Some("req_99"));
358 }
359}