Skip to main content

linger_openai_sdk/
error.rs

1use std::fmt;
2use std::time::Duration;
3
4/// EN: Case-insensitive HTTP headers retained for diagnostics and request construction.
5/// 中文:用于诊断和构造请求的大小写不敏感 HTTP 头集合。
6#[derive(Clone, Default, PartialEq, Eq)]
7pub struct HeaderMap {
8    entries: Vec<(String, String)>,
9}
10
11impl HeaderMap {
12    /// EN: Creates an empty header map.
13    /// 中文:创建空的请求头集合。
14    pub fn new() -> Self {
15        Self {
16            entries: Vec::new(),
17        }
18    }
19
20    /// EN: Creates headers from name/value pairs.
21    /// 中文:通过名称和值的键值对创建请求头集合。
22    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    /// EN: Inserts or replaces a header value using ASCII case-insensitive header names.
36    /// 中文:使用 ASCII 大小写不敏感的头名称插入或替换请求头。
37    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    /// EN: Returns a header value by ASCII case-insensitive name.
52    /// 中文:按 ASCII 大小写不敏感名称返回请求头值。
53    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    /// EN: Iterates over header names and values.
61    /// 中文:遍历请求头名称和值。
62    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
69/// EN: Converts owned or borrowed header pairs into SDK header storage.
70/// 中文:将拥有或借用的请求头键值对转换为 SDK 头存储。
71pub trait IntoHeaderPair {
72    /// EN: Converts the pair into owned strings.
73    /// 中文:将键值对转换为拥有所有权的字符串。
74    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/// EN: OpenAI request id returned by response headers.
129/// 中文:响应头返回的 OpenAI 请求 ID。
130#[derive(Clone, PartialEq, Eq, Hash)]
131pub struct RequestId(String);
132
133impl RequestId {
134    /// EN: Creates a request id wrapper.
135    /// 中文:创建请求 ID 包装类型。
136    pub fn new(value: impl Into<String>) -> Self {
137        Self(value.into())
138    }
139
140    /// EN: Returns the request id as a string slice.
141    /// 中文:以字符串切片返回请求 ID。
142    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/// EN: Structured OpenAI error body fields.
160/// 中文:结构化的 OpenAI 错误响应体字段。
161#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
162#[non_exhaustive]
163pub struct ApiErrorBody {
164    /// EN: Human-readable OpenAI error message.
165    /// 中文:OpenAI 返回的可读错误消息。
166    pub message: String,
167    /// EN: OpenAI error type, when present.
168    /// 中文:OpenAI 错误类型,如响应中存在。
169    #[serde(default)]
170    pub r#type: Option<String>,
171    /// EN: OpenAI error code, when present.
172    /// 中文:OpenAI 错误代码,如响应中存在。
173    #[serde(default)]
174    pub code: Option<String>,
175    /// EN: Request parameter associated with the error, when present.
176    /// 中文:与错误关联的请求参数,如响应中存在。
177    #[serde(default)]
178    pub param: Option<String>,
179}
180
181/// EN: Alias for the structured OpenAI API error body.
182/// 中文:结构化 OpenAI API 错误响应体的别名。
183pub type OpenAiError = ApiErrorBody;
184
185/// EN: Error returned by OpenAI with HTTP and diagnostic context.
186/// 中文:包含 HTTP 与诊断上下文的 OpenAI 错误。
187#[derive(Clone, Debug, PartialEq, Eq)]
188#[non_exhaustive]
189pub struct ApiError {
190    /// EN: HTTP response status.
191    /// 中文:HTTP 响应状态码。
192    pub status: u16,
193    /// EN: OpenAI request id, when returned.
194    /// 中文:OpenAI 请求 ID,如响应中存在。
195    pub request_id: Option<RequestId>,
196    /// EN: Redacted diagnostic response headers.
197    /// 中文:已脱敏的诊断响应头。
198    pub headers: HeaderMap,
199    /// EN: Structured OpenAI error fields.
200    /// 中文:结构化 OpenAI 错误字段。
201    pub error: ApiErrorBody,
202    /// EN: Safe raw body copy, when retained.
203    /// 中文:在安全时保留的原始响应体副本。
204    pub raw_body: Option<String>,
205}
206
207/// EN: Retry exhaustion details.
208/// 中文:重试耗尽时的详细信息。
209#[derive(Debug)]
210#[non_exhaustive]
211pub struct RetryExhausted {
212    /// EN: Number of attempts made.
213    /// 中文:已经执行的尝试次数。
214    pub attempts: usize,
215    /// EN: Last failure observed by the retry loop.
216    /// 中文:重试循环观察到的最后一次失败。
217    pub last_error: Box<LingerError>,
218}
219
220/// EN: Top-level SDK error categories.
221/// 中文:SDK 顶层错误类别。
222#[derive(Debug)]
223#[non_exhaustive]
224pub enum ErrorKind {
225    /// EN: HTTP transport failed before a complete response was available.
226    /// 中文:在完整响应可用前 HTTP 传输失败。
227    Transport { message: String },
228    /// EN: A configured timeout elapsed.
229    /// 中文:配置的超时时间已耗尽。
230    Timeout { message: String },
231    /// EN: Request serialization or response deserialization failed.
232    /// 中文:请求序列化或响应反序列化失败。
233    Serialization { message: String },
234    /// EN: Incremental stream parsing failed.
235    /// 中文:增量流解析失败。
236    Streaming { message: String },
237    /// EN: Retry policy exhausted all configured attempts.
238    /// 中文:重试策略耗尽了所有配置的尝试次数。
239    RetryExhausted(RetryExhausted),
240    /// EN: OpenAI returned a non-successful API response.
241    /// 中文:OpenAI 返回了非成功的 API 响应。
242    Api(ApiError),
243    /// EN: SDK configuration or builder input is invalid.
244    /// 中文:SDK 配置或构建器输入无效。
245    InvalidConfig { message: String },
246}
247
248/// EN: Structured error returned by SDK operations.
249/// 中文:SDK 操作返回的结构化错误。
250pub struct LingerError {
251    kind: Box<ErrorKind>,
252}
253
254impl LingerError {
255    /// EN: Creates a transport error.
256    /// 中文:创建传输错误。
257    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    /// EN: Creates a timeout error.
266    /// 中文:创建超时错误。
267    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    /// EN: Creates a serialization or deserialization error.
276    /// 中文:创建序列化或反序列化错误。
277    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    /// EN: Creates a streaming error.
286    /// 中文:创建流式解析错误。
287    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    /// EN: Creates an invalid configuration error.
296    /// 中文:创建无效配置错误。
297    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    /// EN: Creates an API error from response parts.
306    /// 中文:通过响应组成部分创建 API 错误。
307    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    /// EN: Creates a retry exhaustion error.
325    /// 中文:创建重试耗尽错误。
326    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    /// EN: Returns the structured error kind.
336    /// 中文:返回结构化错误类别。
337    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}