Skip to main content

openrouter_rs/api/
errors.rs

1use http::StatusCode;
2use serde::Deserialize;
3use serde_json::{Map, Value};
4
5use crate::error::{ApiErrorContext, ApiErrorKind, OpenRouterError};
6
7#[derive(Deserialize, Debug)]
8struct ApiErrorResponse {
9    error: ApiError,
10    openrouter_metadata: Option<Value>,
11    user_id: Option<String>,
12}
13
14#[derive(Deserialize, Debug)]
15struct ApiError {
16    code: Option<i64>,
17    message: String,
18    metadata: Option<ApiErrorMetadata>,
19}
20
21#[derive(Deserialize, Debug)]
22#[serde(untagged)]
23enum ApiErrorMetadata {
24    ModerationError(ModerationErrorMetadata),
25    ProviderError(ProviderErrorMetadata),
26    Raw(Value),
27}
28
29#[derive(Deserialize, Debug)]
30struct ModerationErrorMetadata {
31    reasons: Vec<String>,
32    flagged_input: String,
33    provider_name: String,
34    model_slug: String,
35}
36
37#[derive(Deserialize, Debug)]
38struct ProviderErrorMetadata {
39    provider_name: String,
40    raw: Value,
41}
42
43fn normalize_error_status(status: StatusCode, api_code: Option<i64>) -> StatusCode {
44    if !status.is_success() {
45        return status;
46    }
47
48    api_code
49        .and_then(|code| u16::try_from(code).ok())
50        .and_then(|code| StatusCode::try_from(code).ok())
51        .filter(|code| code.is_client_error() || code.is_server_error())
52        .unwrap_or(status)
53}
54
55fn build_api_error(
56    status: StatusCode,
57    request_id: Option<String>,
58    payload: ApiErrorResponse,
59) -> OpenRouterError {
60    let ApiErrorResponse {
61        error: api_error,
62        openrouter_metadata,
63        user_id,
64    } = payload;
65    let ApiError {
66        code,
67        message,
68        metadata,
69    } = api_error;
70    let status = normalize_error_status(status, code);
71
72    let (kind, normalized_metadata) = match metadata {
73        Some(ApiErrorMetadata::ModerationError(moderation)) => (
74            ApiErrorKind::Moderation {
75                reasons: moderation.reasons.clone(),
76                flagged_input: moderation.flagged_input.clone(),
77                provider_name: moderation.provider_name.clone(),
78                model_slug: moderation.model_slug.clone(),
79            },
80            Some(serde_json::json!({
81                "reasons": moderation.reasons,
82                "flagged_input": moderation.flagged_input,
83                "provider_name": moderation.provider_name,
84                "model_slug": moderation.model_slug,
85            })),
86        ),
87        Some(ApiErrorMetadata::ProviderError(provider)) => (
88            ApiErrorKind::Provider {
89                provider_name: provider.provider_name,
90                raw: provider.raw.clone(),
91            },
92            Some(provider.raw),
93        ),
94        Some(ApiErrorMetadata::Raw(raw)) => (ApiErrorKind::Generic, Some(raw)),
95        None => (ApiErrorKind::Generic, None),
96    };
97    let normalized_metadata =
98        merge_top_level_metadata(normalized_metadata, openrouter_metadata, user_id);
99
100    OpenRouterError::Api(Box::new(ApiErrorContext {
101        status,
102        api_code: code,
103        message,
104        request_id,
105        metadata: normalized_metadata,
106        kind,
107    }))
108}
109
110fn merge_top_level_metadata(
111    metadata: Option<Value>,
112    openrouter_metadata: Option<Value>,
113    user_id: Option<String>,
114) -> Option<Value> {
115    if openrouter_metadata.is_none() && user_id.is_none() {
116        return metadata;
117    }
118
119    let mut merged = match metadata {
120        Some(Value::Object(object)) => object,
121        Some(value) => {
122            let mut object = Map::new();
123            object.insert("error_metadata".to_string(), value);
124            object
125        }
126        None => Map::new(),
127    };
128
129    if let Some(openrouter_metadata) = openrouter_metadata {
130        merged.insert("openrouter_metadata".to_string(), openrouter_metadata);
131    }
132    if let Some(user_id) = user_id {
133        merged.insert("user_id".to_string(), Value::String(user_id));
134    }
135
136    Some(Value::Object(merged))
137}
138
139pub fn parse_api_error(
140    status: StatusCode,
141    request_id: Option<String>,
142    text: &str,
143) -> OpenRouterError {
144    match serde_json::from_str::<ApiErrorResponse>(text) {
145        Ok(payload) => build_api_error(status, request_id, payload),
146        Err(_) => OpenRouterError::Api(Box::new(ApiErrorContext {
147            status,
148            api_code: Some(i64::from(u16::from(status))),
149            message: text.to_string(),
150            request_id,
151            metadata: None,
152            kind: ApiErrorKind::Generic,
153        })),
154    }
155}
156
157pub fn unreadable_error_response(
158    status: StatusCode,
159    request_id: Option<String>,
160    error: &str,
161) -> OpenRouterError {
162    OpenRouterError::Api(Box::new(ApiErrorContext {
163        status,
164        api_code: Some(i64::from(u16::from(status))),
165        message: format!("Failed to read error response body: {error}"),
166        request_id,
167        metadata: Some(serde_json::json!({
168            "body_read_error": error,
169        })),
170        kind: ApiErrorKind::Generic,
171    }))
172}