Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:55:35.512Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

open_router.rs

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