1use std::fmt;
2use std::time::Duration;
3
4#[derive(Clone, Default, PartialEq, Eq)]
7pub struct HeaderMap {
8 entries: Vec<(String, String)>,
9}
10
11impl HeaderMap {
12 pub fn new() -> Self {
15 Self {
16 entries: Vec::new(),
17 }
18 }
19
20 pub fn from_pairs<I, P>(pairs: I) -> Self
23 where
24 I: IntoIterator<Item = P>,
25 P: IntoHeaderPair,
26 {
27 let mut headers = Self::new();
28 for pair in pairs {
29 let (name, value) = pair.into_header_pair();
30 headers.insert(name, value);
31 }
32 headers
33 }
34
35 pub fn insert(&mut self, name: impl Into<String>, value: impl Into<String>) {
38 let name = name.into();
39 let value = value.into();
40 if let Some((_, existing)) = self
41 .entries
42 .iter_mut()
43 .find(|(existing, _)| existing.eq_ignore_ascii_case(&name))
44 {
45 *existing = value;
46 return;
47 }
48 self.entries.push((name, value));
49 }
50
51 pub fn get(&self, name: &str) -> Option<&str> {
54 self.entries
55 .iter()
56 .find(|(existing, _)| existing.eq_ignore_ascii_case(name))
57 .map(|(_, value)| value.as_str())
58 }
59
60 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
63 self.entries
64 .iter()
65 .map(|(name, value)| (name.as_str(), value.as_str()))
66 }
67}
68
69pub trait IntoHeaderPair {
72 fn into_header_pair(self) -> (String, String);
75}
76
77impl<K, V> IntoHeaderPair for (K, V)
78where
79 K: ToString,
80 V: ToString,
81{
82 fn into_header_pair(self) -> (String, String) {
83 (self.0.to_string(), self.1.to_string())
84 }
85}
86
87impl<K, V> IntoHeaderPair for &(K, V)
88where
89 K: ToString,
90 V: ToString,
91{
92 fn into_header_pair(self) -> (String, String) {
93 (self.0.to_string(), self.1.to_string())
94 }
95}
96
97impl<const N: usize> From<[(&str, &str); N]> for HeaderMap {
98 fn from(value: [(&str, &str); N]) -> Self {
99 Self::from_pairs(value)
100 }
101}
102
103impl fmt::Debug for HeaderMap {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 let redacted: Vec<(&str, &str)> = self
106 .entries
107 .iter()
108 .map(|(name, value)| {
109 let value = if is_sensitive_header(name) {
110 "<redacted>"
111 } else {
112 value.as_str()
113 };
114 (name.as_str(), value)
115 })
116 .collect();
117 f.debug_list().entries(redacted).finish()
118 }
119}
120
121fn is_sensitive_header(name: &str) -> bool {
122 name.eq_ignore_ascii_case("authorization")
123 || name.eq_ignore_ascii_case("openai-organization")
124 || name.eq_ignore_ascii_case("openai-project")
125 || name.eq_ignore_ascii_case("openai-webhook-secret")
126}
127
128#[derive(Clone, PartialEq, Eq, Hash)]
131pub struct RequestId(String);
132
133impl RequestId {
134 pub fn new(value: impl Into<String>) -> Self {
137 Self(value.into())
138 }
139
140 pub fn as_str(&self) -> &str {
143 &self.0
144 }
145}
146
147impl fmt::Debug for RequestId {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 f.debug_tuple("RequestId").field(&self.0).finish()
150 }
151}
152
153impl fmt::Display for RequestId {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 f.write_str(&self.0)
156 }
157}
158
159#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
162#[non_exhaustive]
163pub struct ApiErrorBody {
164 pub message: String,
167 #[serde(default)]
170 pub r#type: Option<String>,
171 #[serde(default)]
174 pub code: Option<String>,
175 #[serde(default)]
178 pub param: Option<String>,
179}
180
181pub type OpenAiError = ApiErrorBody;
184
185#[derive(Clone, Debug, PartialEq, Eq)]
188#[non_exhaustive]
189pub struct ApiError {
190 pub status: u16,
193 pub request_id: Option<RequestId>,
196 pub headers: HeaderMap,
199 pub error: ApiErrorBody,
202 pub raw_body: Option<String>,
205}
206
207#[derive(Debug)]
210#[non_exhaustive]
211pub struct RetryExhausted {
212 pub attempts: usize,
215 pub last_error: Box<LingerError>,
218}
219
220#[derive(Debug)]
223#[non_exhaustive]
224pub enum ErrorKind {
225 Transport { message: String },
228 Timeout { message: String },
231 Serialization { message: String },
234 Streaming { message: String },
237 RetryExhausted(RetryExhausted),
240 Api(ApiError),
243 InvalidConfig { message: String },
246}
247
248pub struct LingerError {
251 kind: Box<ErrorKind>,
252}
253
254impl LingerError {
255 pub fn transport(message: impl Into<String>) -> Self {
258 Self {
259 kind: Box::new(ErrorKind::Transport {
260 message: message.into(),
261 }),
262 }
263 }
264
265 pub fn timeout(message: impl Into<String>) -> Self {
268 Self {
269 kind: Box::new(ErrorKind::Timeout {
270 message: message.into(),
271 }),
272 }
273 }
274
275 pub fn serialization(message: impl Into<String>) -> Self {
278 Self {
279 kind: Box::new(ErrorKind::Serialization {
280 message: message.into(),
281 }),
282 }
283 }
284
285 pub fn streaming(message: impl Into<String>) -> Self {
288 Self {
289 kind: Box::new(ErrorKind::Streaming {
290 message: message.into(),
291 }),
292 }
293 }
294
295 pub fn invalid_config(message: impl Into<String>) -> Self {
298 Self {
299 kind: Box::new(ErrorKind::InvalidConfig {
300 message: message.into(),
301 }),
302 }
303 }
304
305 pub fn api(
308 status: u16,
309 headers: HeaderMap,
310 request_id: Option<RequestId>,
311 error: ApiErrorBody,
312 ) -> Self {
313 Self {
314 kind: Box::new(ErrorKind::Api(ApiError {
315 status,
316 request_id,
317 headers,
318 error,
319 raw_body: None,
320 })),
321 }
322 }
323
324 pub fn retry_exhausted(attempts: usize, last_error: LingerError) -> Self {
327 Self {
328 kind: Box::new(ErrorKind::RetryExhausted(RetryExhausted {
329 attempts,
330 last_error: Box::new(last_error),
331 })),
332 }
333 }
334
335 pub fn kind(&self) -> &ErrorKind {
338 self.kind.as_ref()
339 }
340}
341
342impl fmt::Debug for LingerError {
343 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344 f.debug_struct("LingerError")
345 .field("kind", &self.kind)
346 .finish()
347 }
348}
349
350impl fmt::Display for LingerError {
351 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352 match self.kind.as_ref() {
353 ErrorKind::Transport { message } => write!(f, "transport error: {message}"),
354 ErrorKind::Timeout { message } => write!(f, "timeout error: {message}"),
355 ErrorKind::Serialization { message } => write!(f, "serialization error: {message}"),
356 ErrorKind::Streaming { message } => write!(f, "streaming error: {message}"),
357 ErrorKind::RetryExhausted(details) => {
358 write!(f, "retry exhausted after {} attempts", details.attempts)
359 }
360 ErrorKind::Api(error) => write!(
361 f,
362 "OpenAI API error {}: {}",
363 error.status, error.error.message
364 ),
365 ErrorKind::InvalidConfig { message } => {
366 write!(f, "invalid SDK configuration: {message}")
367 }
368 }
369 }
370}
371
372impl std::error::Error for LingerError {}
373
374impl From<serde_json::Error> for LingerError {
375 fn from(value: serde_json::Error) -> Self {
376 Self::serialization(value.to_string())
377 }
378}
379
380pub(crate) fn request_id_from_headers(headers: &HeaderMap) -> Option<RequestId> {
381 headers
382 .get("x-request-id")
383 .or_else(|| headers.get("openai-request-id"))
384 .map(RequestId::new)
385}
386
387pub(crate) fn parse_retry_after_seconds(value: &str) -> Option<Duration> {
388 let seconds = value.trim().parse::<u64>().ok()?;
389 Some(Duration::from_secs(seconds))
390}
391
392pub(crate) fn parse_api_error(status: u16, headers: HeaderMap, body: &[u8]) -> LingerError {
393 #[derive(serde::Deserialize)]
394 struct Wire {
395 error: ApiErrorBody,
396 }
397
398 let request_id = request_id_from_headers(&headers);
399 match serde_json::from_slice::<Wire>(body) {
400 Ok(parsed) => LingerError::api(status, headers, request_id, parsed.error),
401 Err(_) => LingerError::api(
402 status,
403 headers,
404 request_id,
405 ApiErrorBody {
406 message: format!(
407 "OpenAI API returned HTTP status {status} with an unparseable error body"
408 ),
409 r#type: None,
410 code: None,
411 param: None,
412 },
413 ),
414 }
415}