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}