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}