Skip to repository content862 lines · 28.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:55:35.512Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
open_router.rs
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