Skip to main content

objectiveai_api/chat/completions/upstream/openrouter/
error.rs

1//! Error types for OpenRouter provider operations.
2
3use serde::{Deserialize, Serialize};
4
5/// Errors that can occur when communicating with the OpenRouter provider.
6#[derive(thiserror::Error, Debug)]
7pub enum Error {
8    /// Error returned by the OpenRouter provider.
9    #[error("provider error: {0}")]
10    OpenRouterProviderError(#[from] OpenRouterProviderError),
11    /// The provider returned an empty stream with no chunks.
12    #[error("received an empty stream")]
13    EmptyStream,
14    /// Failed to deserialize a response from OpenRouter.
15    #[error("deserialization error: {0}")]
16    DeserializationError(#[from] serde_path_to_error::Error<serde_json::Error>),
17    /// The provider returned a non-success HTTP status code.
18    #[error("received bad status code: {code}, body: {body}")]
19    BadStatus {
20        /// The HTTP status code received.
21        code: reqwest::StatusCode,
22        /// The response body, parsed as JSON if possible.
23        body: serde_json::Value,
24    },
25    /// Error occurred while fetching or processing the SSE stream.
26    #[error("error fetching stream: {0}")]
27    StreamError(#[from] reqwest_eventsource::Error),
28    /// The stream timed out waiting for chunks.
29    #[error("error fetching stream: timeout")]
30    StreamTimeout,
31    /// Failed to fetch an Ensemble LLM definition.
32    #[error("fetch Ensemble LLM error: {0}")]
33    FetchEnsembleLlm(objectiveai::error::ResponseError),
34    /// The requested Ensemble LLM was not found.
35    #[error("Ensemble LLM not found")]
36    EnsembleLlmNotFound,
37    /// The user has insufficient credits to complete the request.
38    #[error("insufficient credits")]
39    InsufficientCredits,
40    /// The Ensemble LLM configuration is invalid.
41    #[error("invalid Ensemble LLM: {0}")]
42    InvalidEnsembleLlm(String),
43}
44
45impl objectiveai::error::StatusError for Error {
46    fn status(&self) -> u16 {
47        match self {
48            Error::OpenRouterProviderError(e) => e.status(),
49            Error::EmptyStream => 500,
50            Error::DeserializationError(_) => 500,
51            Error::BadStatus { code, .. } => code.as_u16(),
52            Error::StreamError(reqwest_eventsource::Error::Transport(e)) => {
53                e.status().map(|s| s.as_u16()).unwrap_or(500)
54            }
55            Error::StreamError(reqwest_eventsource::Error::InvalidStatusCode(code, _)) => {
56                code.as_u16()
57            }
58            Error::StreamError(_) => 500,
59            Error::StreamTimeout => 500,
60            Error::FetchEnsembleLlm(e) => e.status(),
61            Error::InsufficientCredits => 402,
62            Error::InvalidEnsembleLlm(_) => 400,
63            Error::EnsembleLlmNotFound => 404,
64        }
65    }
66
67    fn message(&self) -> Option<serde_json::Value> {
68        Some(serde_json::json!({
69            "kind": "openrouter",
70            "error": match self {
71                Error::OpenRouterProviderError(e) => serde_json::json!({
72                    "kind": "provider_error",
73                    "error": e.message(),
74                }),
75                Error::EmptyStream => serde_json::json!({
76                    "kind": "empty_stream",
77                    "error": "received an empty stream",
78                }),
79                Error::DeserializationError(e) => serde_json::json!({
80                    "kind": "deserialization",
81                    "error": e.to_string(),
82                }),
83                Error::BadStatus { body, .. } => serde_json::json!({
84                    "kind": "bad_status",
85                    "error": body,
86                }),
87                Error::StreamError(e) => serde_json::json!({
88                    "kind": "stream_error",
89                    "error": e.to_string(),
90                }),
91                Error::StreamTimeout => serde_json::json!({
92                    "kind": "stream_timeout",
93                    "error": "error fetching stream: timeout",
94                }),
95                Error::FetchEnsembleLlm(e) => serde_json::json!({
96                    "kind": "fetch_ensemble_llm",
97                    "error": e.message(),
98                }),
99                Error::InsufficientCredits => serde_json::json!({
100                    "kind": "insufficient_credits",
101                    "error": "the user has insufficient credits",
102                }),
103                Error::InvalidEnsembleLlm(msg) => serde_json::json!({
104                    "kind": "invalid_ensemble_llm",
105                    "error": msg,
106                }),
107                Error::EnsembleLlmNotFound => serde_json::json!({
108                    "kind": "ensemble_llm_not_found",
109                    "error": "Ensemble LLM not found",
110                }),
111            },
112        }))
113    }
114}
115
116/// Error response from OpenRouter containing provider error details.
117#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
118#[error("{}", &serde_json::to_string(self).unwrap_or_default())]
119pub struct OpenRouterProviderError {
120    /// The inner error details from the provider.
121    pub error: OpenRouterProviderErrorInner,
122    /// Optional user ID associated with the error.
123    pub user_id: Option<String>,
124}
125
126impl objectiveai::error::StatusError for OpenRouterProviderError {
127    fn status(&self) -> u16 {
128        self.error.status()
129    }
130
131    fn message(&self) -> Option<serde_json::Value> {
132        self.error.message()
133    }
134}
135
136/// Inner error details from the OpenRouter provider.
137#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
138#[error("{}", &serde_json::to_string(self).unwrap_or_default())]
139pub struct OpenRouterProviderErrorInner {
140    /// The HTTP status code from the provider, if available.
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub code: Option<u16>,
143    /// The error message from the provider.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub message: Option<serde_json::Value>,
146    /// Additional metadata about the error.
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub metadata: Option<serde_json::Value>,
149}
150
151impl objectiveai::error::StatusError for OpenRouterProviderErrorInner {
152    fn status(&self) -> u16 {
153        self.code
154            .unwrap_or(reqwest::StatusCode::INTERNAL_SERVER_ERROR.as_u16())
155    }
156
157    fn message(&self) -> Option<serde_json::Value> {
158        Some(serde_json::json!({
159            "kind": "provider",
160            "message": self.message,
161            "metadata": self.metadata,
162        }))
163    }
164}