Skip to main content

tokn_headers/schemas/personas/
copilot_cli.rs

1//! Headers emitted by the GitHub Copilot CLI client (`copilot/<ver>`).
2//!
3//! Field set verified against one captured request:
4//!
5//! ```text
6//! POST https://api.business.githubcopilot.com/responses
7//! user-agent: copilot/1.0.25 (client/sdk win32 v22.19.0) term/unknown
8//! copilot-integration-id: copilot-developer-cli
9//! ```
10//!
11//! The Copilot CLI is built on the OpenAI Stainless-generated JS SDK; the
12//! `x-stainless-*` family is therefore present on every call and modelled as
13//! required. `x-agent-task-id` is required as well — it correlates
14//! multi-turn agent invocations and was present on the captured POST
15//! /responses call. If future captures show it absent on simpler endpoints
16//! (e.g. /models), demote to optional.
17
18use 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  // Transport
30  #[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  // Copilot identity / contract
46  #[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  // Per-call correlation
56  #[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  // Stainless SDK fingerprint — present on this SDK-flavoured capture but
66  // demoted to optional: a future Copilot-CLI build using a non-Stainless
67  // transport (or a hand-rolled HTTP path for `gh copilot suggest`/`explain`)
68  // would omit them entirely.
69  #[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  // Body framing — present on POST, absent on GET.
85  #[serde(rename = "Content-Length", skip_serializing_if = "Option::is_none")]
86  pub content_length: Option<SmolStr>,
87
88  // Plausibly per-endpoint optional.
89  #[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  /// Build a [`CopilotCliHeaders`] from inbound transport headers and
198  /// correlation [`TemplateVars`]. Defaults derived from the captured POST
199  /// `/responses` request documented at the module top.
200  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    // 15 required fields written (22 - 7 stainless now optional).
313    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}