tokn_headers/schemas/overlays/
codex.rs1use crate::error::Error;
12use crate::keys;
13use crate::map::HeaderMap;
14use crate::name::HeaderName;
15use crate::schema::{from_inbound_or, opt_from_inbound, optional, put, put_opt, required, HeaderSchema};
16use crate::vars::TemplateVars;
17use serde::{Deserialize, Serialize};
18use smol_str::SmolStr;
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct CodexOverlay {
22 #[serde(rename = "OpenAI-Beta")]
23 pub openai_beta: SmolStr,
24 #[serde(rename = "OpenAI-Intent")]
25 pub openai_intent: Option<SmolStr>,
26 #[serde(rename = "chatgpt-account-id")]
27 pub chatgpt_account_id: Option<SmolStr>,
28 #[serde(rename = "X-Session-Id")]
29 pub session_id: Option<SmolStr>,
30}
31
32impl HeaderSchema for CodexOverlay {
33 fn parse(map: &HeaderMap) -> Result<Self, Error> {
34 Ok(Self {
35 openai_beta: required(map, &keys::OPENAI_BETA)?,
36 openai_intent: optional(map, &keys::OPENAI_INTENT),
37 chatgpt_account_id: optional(map, &keys::CHATGPT_ACCOUNT_ID),
38 session_id: optional(map, &keys::X_SESSION_ID),
39 })
40 }
41 fn dump(&self) -> HeaderMap {
42 let mut m = HeaderMap::new();
43 put(&mut m, &keys::OPENAI_BETA, &self.openai_beta);
44 put_opt(&mut m, &keys::OPENAI_INTENT, &self.openai_intent);
45 put_opt(&mut m, &keys::CHATGPT_ACCOUNT_ID, &self.chatgpt_account_id);
46 put_opt(&mut m, &keys::X_SESSION_ID, &self.session_id);
47 m
48 }
49 fn known_names() -> &'static [&'static HeaderName] {
50 static NAMES: [&HeaderName; 4] = [
51 &keys::OPENAI_BETA,
52 &keys::OPENAI_INTENT,
53 &keys::CHATGPT_ACCOUNT_ID,
54 &keys::X_SESSION_ID,
55 ];
56 &NAMES
57 }
58}
59
60impl CodexOverlay {
61 pub fn build(vars: &TemplateVars, inbound: &HeaderMap) -> Self {
64 Self {
65 openai_beta: from_inbound_or(inbound, &keys::OPENAI_BETA, || "responses=v1".into()),
66 openai_intent: opt_from_inbound(inbound, &keys::OPENAI_INTENT),
67 chatgpt_account_id: vars
68 .account_id
69 .clone()
70 .or_else(|| opt_from_inbound(inbound, &keys::CHATGPT_ACCOUNT_ID)),
71 session_id: vars
72 .session_id
73 .clone()
74 .or_else(|| opt_from_inbound(inbound, &keys::X_SESSION_ID)),
75 }
76 }
77
78 pub fn apply_to(&self, map: &mut HeaderMap, _vars: &TemplateVars) {
83 map.insert(&keys::OPENAI_BETA, self.openai_beta.to_string());
84 if let Some(v) = &self.openai_intent {
85 if !map.contains_key(&keys::OPENAI_INTENT) {
86 map.insert(&keys::OPENAI_INTENT, v.to_string());
87 }
88 }
89 if let Some(v) = &self.chatgpt_account_id {
90 if !map.contains_key(&keys::CHATGPT_ACCOUNT_ID) {
91 map.insert(&keys::CHATGPT_ACCOUNT_ID, v.to_string());
92 }
93 }
94 if let Some(v) = &self.session_id {
95 if !map.contains_key(&keys::X_SESSION_ID) {
96 map.insert(&keys::X_SESSION_ID, v.to_string());
97 }
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn round_trip() {
108 let h = CodexOverlay {
109 openai_beta: "responses=v1".into(),
110 openai_intent: Some("assistants".into()),
111 chatgpt_account_id: Some("acct_99".into()),
112 session_id: Some("ses_codex".into()),
113 };
114 assert_eq!(CodexOverlay::parse(&h.dump()).unwrap(), h);
115 }
116
117 #[test]
118 fn build_with_empty_inbound_uses_defaults() {
119 let h = CodexOverlay::build(&TemplateVars::default(), &HeaderMap::new());
120 assert_eq!(h.openai_beta.as_str(), "responses=v1");
121 assert!(h.openai_intent.is_none());
122 assert!(h.chatgpt_account_id.is_none());
123 assert!(h.session_id.is_none());
124 }
125
126 #[test]
127 fn build_passes_through_inbound() {
128 let mut inbound = HeaderMap::new();
129 inbound.insert(&keys::OPENAI_BETA, "responses=v2");
130 inbound.insert(&keys::OPENAI_INTENT, "assistants");
131 let h = CodexOverlay::build(&TemplateVars::default(), &inbound);
132 assert_eq!(h.openai_beta.as_str(), "responses=v2");
133 assert_eq!(h.openai_intent.as_deref(), Some("assistants"));
134 }
135
136 #[test]
137 fn build_uses_vars_for_correlation() {
138 let vars = TemplateVars {
139 session_id: Some("ses_xyz".into()),
140 account_id: Some("acct_abc".into()),
141 ..Default::default()
142 };
143 let h = CodexOverlay::build(&vars, &HeaderMap::new());
144 assert_eq!(h.session_id.as_deref(), Some("ses_xyz"));
145 assert_eq!(h.chatgpt_account_id.as_deref(), Some("acct_abc"));
146 }
147
148 #[test]
149 fn apply_to_overrides_managed_fields_and_skips_optionals_when_none() {
150 let mut map = HeaderMap::new();
151 map.insert(&keys::OPENAI_BETA, "stale=v0");
152 map.insert(&keys::X_SESSION_ID, "preexisting");
153
154 let overlay = CodexOverlay {
155 openai_beta: "responses=v1".into(),
156 openai_intent: None,
157 chatgpt_account_id: Some("acct_abc".into()),
158 session_id: Some("ses_xyz".into()),
159 };
160 overlay.apply_to(&mut map, &TemplateVars::default());
161
162 assert_eq!(map.get(&keys::OPENAI_BETA).unwrap().as_str(), "responses=v1");
163 assert_eq!(map.get(&keys::X_SESSION_ID).unwrap().as_str(), "preexisting");
165 assert_eq!(map.get(&keys::CHATGPT_ACCOUNT_ID).unwrap().as_str(), "acct_abc");
167 assert!(!map.contains_key(&keys::OPENAI_INTENT));
169 }
170}