tokn_headers/schemas/personas/
opencode.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)]
22pub struct OpencodeHeaders {
23 #[serde(rename = "User-Agent")]
25 pub user_agent: SmolStr,
26 #[serde(rename = "Authorization")]
27 pub authorization: SmolStr,
28 #[serde(rename = "Host", skip_serializing_if = "Option::is_none")]
33 pub host: Option<SmolStr>,
34 #[serde(rename = "Accept")]
35 pub accept: SmolStr,
36 #[serde(rename = "Accept-Encoding")]
37 pub accept_encoding: SmolStr,
38 #[serde(rename = "Connection")]
39 pub connection: SmolStr,
40 #[serde(rename = "Content-Type")]
41 pub content_type: SmolStr,
42
43 #[serde(rename = "Content-Length", skip_serializing_if = "Option::is_none")]
45 pub content_length: Option<SmolStr>,
46
47 #[serde(rename = "X-Session-Affinity", skip_serializing_if = "Option::is_none")]
50 pub session_affinity: Option<SmolStr>,
51 #[serde(rename = "X-Parent-Session-Id", skip_serializing_if = "Option::is_none")]
52 pub parent_session_id: Option<SmolStr>,
53}
54
55impl HeaderSchema for OpencodeHeaders {
56 fn parse(map: &HeaderMap) -> Result<Self, Error> {
57 Ok(Self {
58 user_agent: required(map, &keys::USER_AGENT)?,
59 authorization: required(map, &keys::AUTHORIZATION)?,
60 host: optional(map, &keys::HOST),
61 accept: required(map, &keys::ACCEPT)?,
62 accept_encoding: required(map, &keys::ACCEPT_ENCODING)?,
63 connection: required(map, &keys::CONNECTION)?,
64 content_type: required(map, &keys::CONTENT_TYPE)?,
65 content_length: optional(map, &keys::CONTENT_LENGTH),
66 session_affinity: optional(map, &keys::X_SESSION_AFFINITY),
67 parent_session_id: optional(map, &keys::X_PARENT_SESSION_ID),
68 })
69 }
70 fn dump(&self) -> HeaderMap {
71 let mut m = HeaderMap::new();
72 put(&mut m, &keys::USER_AGENT, &self.user_agent);
73 put(&mut m, &keys::AUTHORIZATION, &self.authorization);
74 put_opt(&mut m, &keys::HOST, &self.host);
75 put(&mut m, &keys::ACCEPT, &self.accept);
76 put(&mut m, &keys::ACCEPT_ENCODING, &self.accept_encoding);
77 put(&mut m, &keys::CONNECTION, &self.connection);
78 put(&mut m, &keys::CONTENT_TYPE, &self.content_type);
79 put_opt(&mut m, &keys::CONTENT_LENGTH, &self.content_length);
80 put_opt(&mut m, &keys::X_SESSION_AFFINITY, &self.session_affinity);
81 put_opt(&mut m, &keys::X_PARENT_SESSION_ID, &self.parent_session_id);
82 m
83 }
84 fn known_names() -> &'static [&'static HeaderName] {
85 static NAMES: [&HeaderName; 10] = [
86 &keys::USER_AGENT,
87 &keys::AUTHORIZATION,
88 &keys::HOST,
89 &keys::ACCEPT,
90 &keys::ACCEPT_ENCODING,
91 &keys::CONNECTION,
92 &keys::CONTENT_TYPE,
93 &keys::CONTENT_LENGTH,
94 &keys::X_SESSION_AFFINITY,
95 &keys::X_PARENT_SESSION_ID,
96 ];
97 &NAMES
98 }
99}
100
101impl OpencodeHeaders {
102 pub fn build(vars: &TemplateVars, inbound: &HeaderMap) -> Self {
107 Self {
108 user_agent: from_inbound_or(inbound, &keys::USER_AGENT, || {
109 "opencode/1.14.28 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.13".into()
110 }),
111 authorization: from_inbound_or(inbound, &keys::AUTHORIZATION, || "<missing>".into()),
112 host: None,
113 accept: from_inbound_or(inbound, &keys::ACCEPT, || "*/*".into()),
114 accept_encoding: from_inbound_or(inbound, &keys::ACCEPT_ENCODING, || "gzip, deflate, br, zstd".into()),
115 connection: from_inbound_or(inbound, &keys::CONNECTION, || "keep-alive".into()),
116 content_type: from_inbound_or(inbound, &keys::CONTENT_TYPE, || "application/json".into()),
117 content_length: opt_from_inbound(inbound, &keys::CONTENT_LENGTH),
118 session_affinity: vars
119 .session_id
120 .clone()
121 .or_else(|| opt_from_inbound(inbound, &keys::X_SESSION_AFFINITY)),
122 parent_session_id: opt_from_inbound(inbound, &keys::X_PARENT_SESSION_ID),
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 fn sample() -> OpencodeHeaders {
132 OpencodeHeaders {
133 user_agent: "opencode/1.14.28 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.13".into(),
134 authorization: "<redacted>".into(),
135 host: Some("api.deepseek.com".into()),
136 accept: "*/*".into(),
137 accept_encoding: "gzip, deflate, br, zstd".into(),
138 connection: "keep-alive".into(),
139 content_type: "application/json".into(),
140 content_length: Some("4429".into()),
141 session_affinity: Some("ses_1dddd2016ffed1A1u3yj5LmNWC".into()),
142 parent_session_id: None,
143 }
144 }
145
146 #[test]
147 fn opencode_round_trip() {
148 let h = sample();
149 let parsed = OpencodeHeaders::parse(&h.dump()).unwrap();
150 assert_eq!(parsed, h);
151 }
152
153 #[test]
154 fn opencode_optional_fields_omitted_when_none() {
155 let mut h = sample();
156 h.content_length = None;
157 h.session_affinity = None;
158 h.parent_session_id = None;
159 let m = h.dump();
160 assert_eq!(m.len(), 7);
162 assert!(!m.contains_key(&keys::CONTENT_LENGTH));
163 assert!(!m.contains_key(&keys::X_SESSION_AFFINITY));
164 }
165
166 #[test]
167 fn opencode_missing_required_returns_error() {
168 let m = HeaderMap::new();
169 assert!(matches!(OpencodeHeaders::parse(&m), Err(Error::MissingHeader { .. })));
170 }
171
172 #[test]
173 fn build_with_empty_inbound_uses_defaults() {
174 let h = OpencodeHeaders::build(&TemplateVars::default(), &HeaderMap::new());
175 assert_eq!(
176 h.user_agent.as_str(),
177 "opencode/1.14.28 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.13"
178 );
179 assert_eq!(h.authorization.as_str(), "<missing>");
180 assert!(
181 h.host.is_none(),
182 "no inbound Host => no persona-default Host (would leak to wire)"
183 );
184 assert_eq!(h.accept.as_str(), "*/*");
185 assert_eq!(h.accept_encoding.as_str(), "gzip, deflate, br, zstd");
186 assert_eq!(h.connection.as_str(), "keep-alive");
187 assert_eq!(h.content_type.as_str(), "application/json");
188 assert!(h.content_length.is_none());
189 assert!(h.session_affinity.is_none());
190 assert!(h.parent_session_id.is_none());
191 }
192
193 #[test]
194 fn build_passes_through_inbound() {
195 let mut inbound = HeaderMap::new();
196 inbound.insert(&keys::USER_AGENT, "custom-ua/9.9");
197 inbound.insert(&keys::AUTHORIZATION, "Bearer secret");
198 inbound.insert(&keys::CONTENT_LENGTH, "1234");
199 inbound.insert(&keys::HOST, "api.deepseek.com");
200 let h = OpencodeHeaders::build(&TemplateVars::default(), &inbound);
201 assert_eq!(h.user_agent.as_str(), "custom-ua/9.9");
202 assert_eq!(h.authorization.as_str(), "Bearer secret");
203 assert_eq!(h.content_length.as_deref(), Some("1234"));
204 assert_eq!(h.host.as_deref(), None);
205 }
206
207 #[test]
208 fn build_uses_vars_for_correlation() {
209 let vars = TemplateVars {
210 session_id: Some("ses_xyz".into()),
211 ..Default::default()
212 };
213 let h = OpencodeHeaders::build(&vars, &HeaderMap::new());
214 assert_eq!(h.session_affinity.as_deref(), Some("ses_xyz"));
215 }
216}