open_router.rs

  1use anyhow::{Result, anyhow};
  2use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
  3use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, http};
  4use serde::{Deserialize, Serialize};
  5use serde_json::Value;
  6use std::{convert::TryFrom, io, time::Duration};
  7use strum::EnumString;
  8use thiserror::Error;
  9use util::serde::default_true;
 10
 11pub const OPEN_ROUTER_API_URL: &str = "https://openrouter.ai/api/v1";
 12
 13fn extract_retry_after(headers: &http::HeaderMap) -> Option<std::time::Duration> {
 14    if let Some(reset) = headers.get("X-RateLimit-Reset") {
 15        if let Ok(s) = reset.to_str() {
 16            if let Ok(epoch_ms) = s.parse::<u64>() {
 17                let now = std::time::SystemTime::now()
 18                    .duration_since(std::time::UNIX_EPOCH)
 19                    .unwrap_or_default()
 20                    .as_millis() as u64;
 21                if epoch_ms > now {
 22                    return Some(std::time::Duration::from_millis(epoch_ms - now));
 23                }
 24            }
 25        }
 26    }
 27    None
 28}
 29
 30fn is_none_or_empty<T: AsRef<[U]>, U>(opt: &Option<T>) -> bool {
 31    opt.as_ref().is_none_or(|v| v.as_ref().is_empty())
 32}
 33
 34#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
 35#[serde(rename_all = "lowercase")]
 36pub enum Role {
 37    User,
 38    Assistant,
 39    System,
 40    Tool,
 41}
 42
 43impl TryFrom<String> for Role {
 44    type Error = anyhow::Error;
 45
 46    fn try_from(value: String) -> Result<Self> {
 47        match value.as_str() {
 48            "user" => Ok(Self::User),
 49            "assistant" => Ok(Self::Assistant),
 50            "system" => Ok(Self::System),
 51            "tool" => Ok(Self::Tool),
 52            _ => Err(anyhow!("invalid role '{value}'")),
 53        }
 54    }
 55}
 56
 57impl From<Role> for String {
 58    fn from(val: Role) -> Self {
 59        match val {
 60            Role::User => "user".to_owned(),
 61            Role::Assistant => "assistant".to_owned(),
 62            Role::System => "system".to_owned(),
 63            Role::Tool => "tool".to_owned(),
 64        }
 65    }
 66}
 67
 68#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 69#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
 70#[serde(rename_all = "lowercase")]
 71pub enum DataCollection {
 72    Allow,
 73    Disallow,
 74}
 75
 76impl Default for DataCollection {
 77    fn default() -> Self {
 78        Self::Allow
 79    }
 80}
 81
 82#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 83#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
 84pub struct Provider {
 85    #[serde(skip_serializing_if = "Option::is_none")]
 86    order: Option<Vec<String>>,
 87    #[serde(default = "default_true")]
 88    allow_fallbacks: bool,
 89    #[serde(default)]
 90    require_parameters: bool,
 91    #[serde(default)]
 92    data_collection: DataCollection,
 93    #[serde(skip_serializing_if = "Option::is_none")]
 94    only: Option<Vec<String>>,
 95    #[serde(skip_serializing_if = "Option::is_none")]
 96    ignore: Option<Vec<String>>,
 97    #[serde(skip_serializing_if = "Option::is_none")]
 98    quantizations: Option<Vec<String>>,
 99    #[serde(skip_serializing_if = "Option::is_none")]
100    sort: Option<String>,
101}
102
103#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
104#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
105pub struct Model {
106    pub name: String,
107    pub display_name: Option<String>,
108    pub max_tokens: u64,
109    pub supports_tools: Option<bool>,
110    pub supports_images: Option<bool>,
111    #[serde(default)]
112    pub mode: ModelMode,
113    pub provider: Option<Provider>,
114}
115
116#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
117#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
118pub enum ModelMode {
119    #[default]
120    Default,
121    Thinking {
122        budget_tokens: Option<u32>,
123    },
124}
125
126impl Model {
127    pub fn default_fast() -> Self {
128        Self::new(
129            "openrouter/auto",
130            Some("Auto Router"),
131            Some(2000000),
132            Some(true),
133            Some(false),
134            Some(ModelMode::Default),
135            None,
136        )
137    }
138
139    pub fn default() -> Self {
140        Self::default_fast()
141    }
142
143    pub fn new(
144        name: &str,
145        display_name: Option<&str>,
146        max_tokens: Option<u64>,
147        supports_tools: Option<bool>,
148        supports_images: Option<bool>,
149        mode: Option<ModelMode>,
150        provider: Option<Provider>,
151    ) -> Self {
152        Self {
153            name: name.to_owned(),
154            display_name: display_name.map(|s| s.to_owned()),
155            max_tokens: max_tokens.unwrap_or(2000000),
156            supports_tools,
157            supports_images,
158            mode: mode.unwrap_or(ModelMode::Default),
159            provider,
160        }
161    }
162
163    pub fn id(&self) -> &str {
164        &self.name
165    }
166
167    pub fn display_name(&self) -> &str {
168        self.display_name.as_ref().unwrap_or(&self.name)
169    }
170
171    pub fn max_token_count(&self) -> u64 {
172        self.max_tokens
173    }
174
175    pub fn max_output_tokens(&self) -> Option<u64> {
176        None
177    }
178
179    pub fn supports_tool_calls(&self) -> bool {
180        self.supports_tools.unwrap_or(false)
181    }
182
183    pub fn supports_parallel_tool_calls(&self) -> bool {
184        false
185    }
186}
187
188#[derive(Debug, Serialize, Deserialize)]
189pub struct Request {
190    pub model: String,
191    pub messages: Vec<RequestMessage>,
192    pub stream: bool,
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub max_tokens: Option<u64>,
195    #[serde(default, skip_serializing_if = "Vec::is_empty")]
196    pub stop: Vec<String>,
197    pub temperature: f32,
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub tool_choice: Option<ToolChoice>,
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub parallel_tool_calls: Option<bool>,
202    #[serde(default, skip_serializing_if = "Vec::is_empty")]
203    pub tools: Vec<ToolDefinition>,
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub reasoning: Option<Reasoning>,
206    pub usage: RequestUsage,
207    pub provider: Option<Provider>,
208}
209
210#[derive(Debug, Default, Serialize, Deserialize)]
211pub struct RequestUsage {
212    pub include: bool,
213}
214
215#[derive(Debug, Serialize, Deserialize)]
216#[serde(rename_all = "lowercase")]
217pub enum ToolChoice {
218    Auto,
219    Required,
220    None,
221    #[serde(untagged)]
222    Other(ToolDefinition),
223}
224
225#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
226#[derive(Clone, Deserialize, Serialize, Debug)]
227#[serde(tag = "type", rename_all = "snake_case")]
228pub enum ToolDefinition {
229    #[allow(dead_code)]
230    Function { function: FunctionDefinition },
231}
232
233#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
234#[derive(Clone, Debug, Serialize, Deserialize)]
235pub struct FunctionDefinition {
236    pub name: String,
237    pub description: Option<String>,
238    pub parameters: Option<Value>,
239}
240
241#[derive(Debug, Serialize, Deserialize)]
242pub struct Reasoning {
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub effort: Option<String>,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub max_tokens: Option<u32>,
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub exclude: Option<bool>,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub enabled: Option<bool>,
251}
252
253#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
254#[serde(tag = "role", rename_all = "lowercase")]
255pub enum RequestMessage {
256    Assistant {
257        content: Option<MessageContent>,
258        #[serde(default, skip_serializing_if = "Vec::is_empty")]
259        tool_calls: Vec<ToolCall>,
260    },
261    User {
262        content: MessageContent,
263    },
264    System {
265        content: MessageContent,
266    },
267    Tool {
268        content: MessageContent,
269        tool_call_id: String,
270    },
271}
272
273#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
274#[serde(untagged)]
275pub enum MessageContent {
276    Plain(String),
277    Multipart(Vec<MessagePart>),
278}
279
280impl MessageContent {
281    pub fn empty() -> Self {
282        Self::Plain(String::new())
283    }
284
285    pub fn push_part(&mut self, part: MessagePart) {
286        match self {
287            Self::Plain(text) if text.is_empty() => {
288                *self = Self::Multipart(vec![part]);
289            }
290            Self::Plain(text) => {
291                let text_part = MessagePart::Text {
292                    text: std::mem::take(text),
293                };
294                *self = Self::Multipart(vec![text_part, part]);
295            }
296            Self::Multipart(parts) => parts.push(part),
297        }
298    }
299}
300
301impl From<Vec<MessagePart>> for MessageContent {
302    fn from(parts: Vec<MessagePart>) -> Self {
303        if parts.len() == 1
304            && let MessagePart::Text { text } = &parts[0]
305        {
306            return Self::Plain(text.clone());
307        }
308        Self::Multipart(parts)
309    }
310}
311
312impl From<String> for MessageContent {
313    fn from(text: String) -> Self {
314        Self::Plain(text)
315    }
316}
317
318impl From<&str> for MessageContent {
319    fn from(text: &str) -> Self {
320        Self::Plain(text.to_string())
321    }
322}
323
324impl MessageContent {
325    pub fn as_text(&self) -> Option<&str> {
326        match self {
327            Self::Plain(text) => Some(text),
328            Self::Multipart(parts) if parts.len() == 1 => {
329                if let MessagePart::Text { text } = &parts[0] {
330                    Some(text)
331                } else {
332                    None
333                }
334            }
335            _ => None,
336        }
337    }
338
339    pub fn to_text(&self) -> String {
340        match self {
341            Self::Plain(text) => text.clone(),
342            Self::Multipart(parts) => parts
343                .iter()
344                .filter_map(|part| {
345                    if let MessagePart::Text { text } = part {
346                        Some(text.as_str())
347                    } else {
348                        None
349                    }
350                })
351                .collect::<Vec<_>>()
352                .join(""),
353        }
354    }
355}
356
357#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
358#[serde(tag = "type", rename_all = "snake_case")]
359pub enum MessagePart {
360    Text {
361        text: String,
362    },
363    #[serde(rename = "image_url")]
364    Image {
365        image_url: String,
366    },
367}
368
369#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
370pub struct ToolCall {
371    pub id: String,
372    #[serde(flatten)]
373    pub content: ToolCallContent,
374}
375
376#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
377#[serde(tag = "type", rename_all = "lowercase")]
378pub enum ToolCallContent {
379    Function { function: FunctionContent },
380}
381
382#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
383pub struct FunctionContent {
384    pub name: String,
385    pub arguments: String,
386}
387
388#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
389pub struct ResponseMessageDelta {
390    pub role: Option<Role>,
391    pub content: Option<String>,
392    pub reasoning: Option<String>,
393    #[serde(default, skip_serializing_if = "is_none_or_empty")]
394    pub tool_calls: Option<Vec<ToolCallChunk>>,
395}
396
397#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
398pub struct ToolCallChunk {
399    pub index: usize,
400    pub id: Option<String>,
401    pub function: Option<FunctionChunk>,
402}
403
404#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
405pub struct FunctionChunk {
406    pub name: Option<String>,
407    pub arguments: Option<String>,
408}
409
410#[derive(Serialize, Deserialize, Debug)]
411pub struct Usage {
412    pub prompt_tokens: u64,
413    pub completion_tokens: u64,
414    pub total_tokens: u64,
415}
416
417#[derive(Serialize, Deserialize, Debug)]
418pub struct ChoiceDelta {
419    pub index: u32,
420    pub delta: ResponseMessageDelta,
421    pub finish_reason: Option<String>,
422}
423
424#[derive(Serialize, Deserialize, Debug)]
425pub struct ResponseStreamEvent {
426    #[serde(default, skip_serializing_if = "Option::is_none")]
427    pub id: Option<String>,
428    pub created: u32,
429    pub model: String,
430    pub choices: Vec<ChoiceDelta>,
431    pub usage: Option<Usage>,
432}
433
434#[derive(Serialize, Deserialize, Debug)]
435pub struct Response {
436    pub id: String,
437    pub object: String,
438    pub created: u64,
439    pub model: String,
440    pub choices: Vec<Choice>,
441    pub usage: Usage,
442}
443
444#[derive(Serialize, Deserialize, Debug)]
445pub struct Choice {
446    pub index: u32,
447    pub message: RequestMessage,
448    pub finish_reason: Option<String>,
449}
450
451#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
452pub struct ListModelsResponse {
453    pub data: Vec<ModelEntry>,
454}
455
456#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
457pub struct ModelEntry {
458    pub id: String,
459    pub name: String,
460    pub created: usize,
461    pub description: String,
462    #[serde(default, skip_serializing_if = "Option::is_none")]
463    pub context_length: Option<u64>,
464    #[serde(default, skip_serializing_if = "Vec::is_empty")]
465    pub supported_parameters: Vec<String>,
466    #[serde(default, skip_serializing_if = "Option::is_none")]
467    pub architecture: Option<ModelArchitecture>,
468}
469
470#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
471pub struct ModelArchitecture {
472    #[serde(default, skip_serializing_if = "Vec::is_empty")]
473    pub input_modalities: Vec<String>,
474}
475
476pub async fn stream_completion(
477    client: &dyn HttpClient,
478    api_url: &str,
479    api_key: &str,
480    request: Request,
481) -> Result<BoxStream<'static, Result<ResponseStreamEvent, OpenRouterError>>, OpenRouterError> {
482    let uri = format!("{api_url}/chat/completions");
483    let request_builder = HttpRequest::builder()
484        .method(Method::POST)
485        .uri(uri)
486        .header("Content-Type", "application/json")
487        .header("Authorization", format!("Bearer {}", api_key))
488        .header("HTTP-Referer", "https://zed.dev")
489        .header("X-Title", "Zed Editor");
490
491    let request = request_builder
492        .body(AsyncBody::from(
493            serde_json::to_string(&request).map_err(OpenRouterError::SerializeRequest)?,
494        ))
495        .map_err(OpenRouterError::BuildRequestBody)?;
496    let mut response = client
497        .send(request)
498        .await
499        .map_err(OpenRouterError::HttpSend)?;
500
501    if response.status().is_success() {
502        let reader = BufReader::new(response.into_body());
503        Ok(reader
504            .lines()
505            .filter_map(|line| async move {
506                match line {
507                    Ok(line) => {
508                        if line.starts_with(':') {
509                            return None;
510                        }
511
512                        let line = line.strip_prefix("data: ")?;
513                        if line == "[DONE]" {
514                            None
515                        } else {
516                            match serde_json::from_str::<ResponseStreamEvent>(line) {
517                                Ok(response) => Some(Ok(response)),
518                                Err(error) => {
519                                    if line.trim().is_empty() {
520                                        None
521                                    } else {
522                                        Some(Err(OpenRouterError::DeserializeResponse(error)))
523                                    }
524                                }
525                            }
526                        }
527                    }
528                    Err(error) => Some(Err(OpenRouterError::ReadResponse(error))),
529                }
530            })
531            .boxed())
532    } else {
533        let code = ApiErrorCode::from_status(response.status().as_u16());
534
535        let mut body = String::new();
536        response
537            .body_mut()
538            .read_to_string(&mut body)
539            .await
540            .map_err(OpenRouterError::ReadResponse)?;
541
542        let error_response = match serde_json::from_str::<OpenRouterErrorResponse>(&body) {
543            Ok(OpenRouterErrorResponse { error }) => error,
544            Err(_) => OpenRouterErrorBody {
545                code: response.status().as_u16(),
546                message: body,
547                metadata: None,
548            },
549        };
550
551        match code {
552            ApiErrorCode::RateLimitError => {
553                let retry_after = extract_retry_after(response.headers());
554                Err(OpenRouterError::RateLimit {
555                    retry_after: retry_after.unwrap_or_else(|| std::time::Duration::from_secs(60)),
556                })
557            }
558            ApiErrorCode::OverloadedError => {
559                let retry_after = extract_retry_after(response.headers());
560                Err(OpenRouterError::ServerOverloaded { retry_after })
561            }
562            _ => Err(OpenRouterError::ApiError(ApiError {
563                code: code,
564                message: error_response.message,
565            })),
566        }
567    }
568}
569
570pub async fn list_models(
571    client: &dyn HttpClient,
572    api_url: &str,
573    api_key: &str,
574) -> Result<Vec<Model>, OpenRouterError> {
575    let uri = format!("{api_url}/models/user");
576    let request_builder = HttpRequest::builder()
577        .method(Method::GET)
578        .uri(uri)
579        .header("Accept", "application/json")
580        .header("Authorization", format!("Bearer {}", api_key))
581        .header("HTTP-Referer", "https://zed.dev")
582        .header("X-Title", "Zed Editor");
583
584    let request = request_builder
585        .body(AsyncBody::default())
586        .map_err(OpenRouterError::BuildRequestBody)?;
587    let mut response = client
588        .send(request)
589        .await
590        .map_err(OpenRouterError::HttpSend)?;
591
592    let mut body = String::new();
593    response
594        .body_mut()
595        .read_to_string(&mut body)
596        .await
597        .map_err(OpenRouterError::ReadResponse)?;
598
599    if response.status().is_success() {
600        let response: ListModelsResponse =
601            serde_json::from_str(&body).map_err(OpenRouterError::DeserializeResponse)?;
602
603        let models = response
604            .data
605            .into_iter()
606            .map(|entry| Model {
607                name: entry.id,
608                // OpenRouter returns display names in the format "provider_name: model_name".
609                // When displayed in the UI, these names can get truncated from the right.
610                // Since users typically already know the provider, we extract just the model name
611                // portion (after the colon) to create a more concise and user-friendly label
612                // for the model dropdown in the agent panel.
613                display_name: Some(
614                    entry
615                        .name
616                        .split(':')
617                        .next_back()
618                        .unwrap_or(&entry.name)
619                        .trim()
620                        .to_string(),
621                ),
622                max_tokens: entry.context_length.unwrap_or(2000000),
623                supports_tools: Some(entry.supported_parameters.contains(&"tools".to_string())),
624                supports_images: Some(
625                    entry
626                        .architecture
627                        .as_ref()
628                        .map(|arch| arch.input_modalities.contains(&"image".to_string()))
629                        .unwrap_or(false),
630                ),
631                mode: if entry
632                    .supported_parameters
633                    .contains(&"reasoning".to_string())
634                {
635                    ModelMode::Thinking {
636                        budget_tokens: Some(4_096),
637                    }
638                } else {
639                    ModelMode::Default
640                },
641                provider: None,
642            })
643            .collect();
644
645        Ok(models)
646    } else {
647        let code = ApiErrorCode::from_status(response.status().as_u16());
648
649        let mut body = String::new();
650        response
651            .body_mut()
652            .read_to_string(&mut body)
653            .await
654            .map_err(OpenRouterError::ReadResponse)?;
655
656        let error_response = match serde_json::from_str::<OpenRouterErrorResponse>(&body) {
657            Ok(OpenRouterErrorResponse { error }) => error,
658            Err(_) => OpenRouterErrorBody {
659                code: response.status().as_u16(),
660                message: body,
661                metadata: None,
662            },
663        };
664
665        match code {
666            ApiErrorCode::RateLimitError => {
667                let retry_after = extract_retry_after(response.headers());
668                Err(OpenRouterError::RateLimit {
669                    retry_after: retry_after.unwrap_or_else(|| std::time::Duration::from_secs(60)),
670                })
671            }
672            ApiErrorCode::OverloadedError => {
673                let retry_after = extract_retry_after(response.headers());
674                Err(OpenRouterError::ServerOverloaded { retry_after })
675            }
676            _ => Err(OpenRouterError::ApiError(ApiError {
677                code: code,
678                message: error_response.message,
679            })),
680        }
681    }
682}
683
684#[derive(Debug)]
685pub enum OpenRouterError {
686    /// Failed to serialize the HTTP request body to JSON
687    SerializeRequest(serde_json::Error),
688
689    /// Failed to construct the HTTP request body
690    BuildRequestBody(http::Error),
691
692    /// Failed to send the HTTP request
693    HttpSend(anyhow::Error),
694
695    /// Failed to deserialize the response from JSON
696    DeserializeResponse(serde_json::Error),
697
698    /// Failed to read from response stream
699    ReadResponse(io::Error),
700
701    /// Rate limit exceeded
702    RateLimit { retry_after: Duration },
703
704    /// Server overloaded
705    ServerOverloaded { retry_after: Option<Duration> },
706
707    /// API returned an error response
708    ApiError(ApiError),
709}
710
711#[derive(Debug, Serialize, Deserialize)]
712pub struct OpenRouterErrorBody {
713    pub code: u16,
714    pub message: String,
715    #[serde(default, skip_serializing_if = "Option::is_none")]
716    pub metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
717}
718
719#[derive(Debug, Serialize, Deserialize)]
720pub struct OpenRouterErrorResponse {
721    pub error: OpenRouterErrorBody,
722}
723
724#[derive(Debug, Serialize, Deserialize, Error)]
725#[error("OpenRouter API Error: {code}: {message}")]
726pub struct ApiError {
727    pub code: ApiErrorCode,
728    pub message: String,
729}
730
731/// An OpenROuter API error code.
732/// <https://openrouter.ai/docs/api-reference/errors#error-codes>
733#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString, Serialize, Deserialize)]
734#[strum(serialize_all = "snake_case")]
735pub enum ApiErrorCode {
736    /// 400: Bad Request (invalid or missing params, CORS)
737    InvalidRequestError,
738    /// 401: Invalid credentials (OAuth session expired, disabled/invalid API key)
739    AuthenticationError,
740    /// 402: Your account or API key has insufficient credits. Add more credits and retry the request.
741    PaymentRequiredError,
742    /// 403: Your chosen model requires moderation and your input was flagged
743    PermissionError,
744    /// 408: Your request timed out
745    RequestTimedOut,
746    /// 429: You are being rate limited
747    RateLimitError,
748    /// 502: Your chosen model is down or we received an invalid response from it
749    ApiError,
750    /// 503: There is no available model provider that meets your routing requirements
751    OverloadedError,
752}
753
754impl std::fmt::Display for ApiErrorCode {
755    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
756        let s = match self {
757            ApiErrorCode::InvalidRequestError => "invalid_request_error",
758            ApiErrorCode::AuthenticationError => "authentication_error",
759            ApiErrorCode::PaymentRequiredError => "payment_required_error",
760            ApiErrorCode::PermissionError => "permission_error",
761            ApiErrorCode::RequestTimedOut => "request_timed_out",
762            ApiErrorCode::RateLimitError => "rate_limit_error",
763            ApiErrorCode::ApiError => "api_error",
764            ApiErrorCode::OverloadedError => "overloaded_error",
765        };
766        write!(f, "{s}")
767    }
768}
769
770impl ApiErrorCode {
771    pub fn from_status(status: u16) -> Self {
772        match status {
773            400 => ApiErrorCode::InvalidRequestError,
774            401 => ApiErrorCode::AuthenticationError,
775            402 => ApiErrorCode::PaymentRequiredError,
776            403 => ApiErrorCode::PermissionError,
777            408 => ApiErrorCode::RequestTimedOut,
778            429 => ApiErrorCode::RateLimitError,
779            502 => ApiErrorCode::ApiError,
780            503 => ApiErrorCode::OverloadedError,
781            _ => ApiErrorCode::ApiError,
782        }
783    }
784}