vtcode_core/llm/providers/openai/
errors.rs1use reqwest::StatusCode;
7use reqwest::header::HeaderMap;
8use serde_json::Value;
9
10use crate::config::constants::models;
11use crate::llm::providers::common::extract_header;
12
13#[derive(Debug, Default, PartialEq, Eq)]
14struct OpenAIErrorDetails {
15 message: Option<String>,
16 code: Option<String>,
17 error_type: Option<String>,
18 param: Option<String>,
19}
20
21fn parse_openai_error_details(body: &str) -> OpenAIErrorDetails {
22 let Ok(json) = serde_json::from_str::<Value>(body) else {
23 return OpenAIErrorDetails::default();
24 };
25
26 let error = json.get("error").unwrap_or(&json);
27 OpenAIErrorDetails {
28 message: error
29 .get("message")
30 .and_then(Value::as_str)
31 .map(ToOwned::to_owned)
32 .filter(|value| !value.trim().is_empty())
33 .or_else(|| {
34 json.get("detail")
36 .and_then(Value::as_str)
37 .map(ToOwned::to_owned)
38 .filter(|value| !value.trim().is_empty())
39 }),
40 code: error
41 .get("code")
42 .and_then(Value::as_str)
43 .map(ToOwned::to_owned)
44 .filter(|value| !value.trim().is_empty()),
45 error_type: error
46 .get("type")
47 .and_then(Value::as_str)
48 .map(ToOwned::to_owned)
49 .filter(|value| !value.trim().is_empty()),
50 param: error
51 .get("param")
52 .and_then(Value::as_str)
53 .map(ToOwned::to_owned)
54 .filter(|value| !value.trim().is_empty()),
55 }
56}
57
58pub fn is_model_not_found(status: StatusCode, error_text: &str) -> bool {
60 if !matches!(
61 status,
62 StatusCode::NOT_FOUND | StatusCode::BAD_REQUEST | StatusCode::UNPROCESSABLE_ENTITY
63 ) {
64 return false;
65 }
66
67 let lower = error_text.to_ascii_lowercase();
68 lower.contains("model_not_found")
69 || (lower.contains("model") && lower.contains("does not exist"))
70 || (lower.contains("model") && lower.contains("not found"))
71 || lower.contains("unknown model")
72}
73
74pub fn fallback_model_if_not_found(model: &str) -> Option<String> {
76 match model {
77 m if m == models::openai::GPT_5_MINI => Some(models::openai::GPT_5.to_string()),
78 m if m == models::openai::GPT_5_NANO => Some(models::openai::GPT_5_MINI.to_string()),
79 _ => Some(models::openai::DEFAULT_MODEL.to_string()),
80 }
81}
82
83pub fn format_openai_error(
85 status: StatusCode,
86 body: &str,
87 headers: &HeaderMap,
88 context: &str,
89 client_request_id: Option<&str>,
90) -> String {
91 let request_id = extract_header(
92 headers,
93 &["x-request-id", "request-id", "openai-request-id"],
94 )
95 .unwrap_or_else(|| "<none>".to_string());
96 let effective_client_request_id = client_request_id
97 .map(str::trim)
98 .filter(|value| !value.is_empty())
99 .map(ToOwned::to_owned)
100 .or_else(|| extract_header(headers, &["x-client-request-id"]));
101 let organization_id = extract_header(headers, &["openai-organization", "x-organization-id"]);
102 let retry_after = extract_header(headers, &["retry-after"]);
103 let error_details = parse_openai_error_details(body);
104 let trimmed_body: String = body.chars().take(2_000).collect();
105 let mut metadata_parts = vec![format!("request_id={request_id}")];
106 if let Some(client_request_id) = effective_client_request_id {
107 metadata_parts.push(format!("client_request_id={client_request_id}"));
108 }
109 if let Some(code) = error_details.code.as_deref() {
110 metadata_parts.push(format!("code={code}"));
111 }
112 if let Some(error_type) = error_details.error_type.as_deref() {
113 metadata_parts.push(format!("type={error_type}"));
114 }
115 if let Some(param) = error_details.param.as_deref() {
116 metadata_parts.push(format!("param={param}"));
117 }
118 if let Some(retry_after) = retry_after.as_deref() {
119 metadata_parts.push(format!("retry_after={retry_after}"));
120 }
121 if let Some(organization_id) = organization_id.as_deref() {
122 metadata_parts.push(format!("organization={organization_id}"));
123 }
124
125 let mut formatted = format!(
126 "{} (status {}) [{}]",
127 context,
128 status,
129 metadata_parts.join(" ")
130 );
131 if let Some(message) = error_details.message.as_deref() {
132 formatted.push_str(&format!(" Message: {message}"));
133 }
134 if !trimmed_body.is_empty() && error_details.message.as_deref() != Some(trimmed_body.as_str()) {
135 formatted.push_str(&format!(" Body: {trimmed_body}"));
136 }
137 formatted
138}
139
140pub fn is_responses_api_unsupported(status: StatusCode, body: &str) -> bool {
142 let lower = body.to_ascii_lowercase();
143
144 (status == StatusCode::NOT_FOUND && lower.trim().is_empty())
145 || lower.contains("not enabled for the responses api")
146 || lower.contains("responses api")
147 && (lower.contains("unsupported") || lower.contains("not supported"))
148 || lower.contains("invalid api parameter")
149 || lower.contains("unsupported parameter")
150 || lower.contains("1210")
151 || lower.contains("invalid_request_error")
152}
153
154pub fn is_flex_service_tier_unsupported(status: StatusCode, body: &str) -> bool {
156 if status != StatusCode::BAD_REQUEST {
157 return false;
158 }
159
160 let lower = body.to_ascii_lowercase();
161 lower.contains("flex")
162 && (lower.contains("flex is not available for this model")
163 || (lower.contains("service tier") || lower.contains("service_tier"))
164 && (lower.contains("not available") || lower.contains("unsupported")))
165}
166
167#[cfg(test)]
168mod tests {
169 use super::{
170 OpenAIErrorDetails, format_openai_error, is_flex_service_tier_unsupported,
171 is_model_not_found, is_responses_api_unsupported, parse_openai_error_details,
172 };
173 use reqwest::StatusCode;
174 use reqwest::header::{HeaderMap, HeaderValue};
175
176 #[test]
177 fn model_not_found_requires_model_specific_body() {
178 assert!(!is_model_not_found(StatusCode::NOT_FOUND, ""));
179 assert!(is_model_not_found(StatusCode::NOT_FOUND, "model_not_found"));
180 assert!(is_model_not_found(
181 StatusCode::BAD_REQUEST,
182 "The requested model does not exist"
183 ));
184 }
185
186 #[test]
187 fn responses_api_unsupported_keeps_blank_404_fallback() {
188 assert!(is_responses_api_unsupported(StatusCode::NOT_FOUND, ""));
189 assert!(is_responses_api_unsupported(
190 StatusCode::BAD_REQUEST,
191 "This endpoint is not enabled for the Responses API"
192 ));
193 assert!(!is_responses_api_unsupported(
194 StatusCode::NOT_FOUND,
195 "model_not_found"
196 ));
197 }
198
199 #[test]
200 fn flex_service_tier_detection_matches_backend_error() {
201 assert!(is_flex_service_tier_unsupported(
202 StatusCode::BAD_REQUEST,
203 r#"{"error":{"message":"Flex is not available for this model.","type":"invalid_request_error"}}"#
204 ));
205 assert!(!is_flex_service_tier_unsupported(
206 StatusCode::BAD_REQUEST,
207 r#"{"error":{"message":"Bad request","type":"invalid_request_error"}}"#
208 ));
209 assert!(!is_flex_service_tier_unsupported(
210 StatusCode::TOO_MANY_REQUESTS,
211 r#"{"error":{"message":"Flex is not available for this model.","type":"invalid_request_error"}}"#
212 ));
213 }
214
215 #[test]
216 fn parse_openai_error_details_extracts_message_and_codes() {
217 let details = parse_openai_error_details(
218 r#"{"error":{"message":"Bad request","type":"invalid_request_error","param":"text.verbosity","code":"unsupported_parameter"}}"#,
219 );
220
221 assert_eq!(details.message.as_deref(), Some("Bad request"));
222 assert_eq!(details.error_type.as_deref(), Some("invalid_request_error"));
223 assert_eq!(details.param.as_deref(), Some("text.verbosity"));
224 assert_eq!(details.code.as_deref(), Some("unsupported_parameter"));
225 }
226
227 #[test]
228 fn parse_openai_error_details_handles_empty_body() {
229 let details = parse_openai_error_details("");
230
231 assert_eq!(details, OpenAIErrorDetails::default());
232 }
233
234 #[test]
235 fn format_openai_error_surfaces_debugging_metadata() {
236 let mut headers = HeaderMap::new();
237 headers.insert("x-request-id", HeaderValue::from_static("req_123"));
238 headers.insert("retry-after", HeaderValue::from_static("30"));
239 headers.insert("openai-organization", HeaderValue::from_static("org_456"));
240
241 let formatted = format_openai_error(
242 StatusCode::BAD_REQUEST,
243 r#"{"error":{"message":"Bad request","type":"invalid_request_error","param":"text.verbosity","code":"unsupported_parameter"}}"#,
244 &headers,
245 "Responses API error",
246 Some("vtcode-abc"),
247 );
248
249 assert!(formatted.contains("request_id=req_123"));
250 assert!(formatted.contains("client_request_id=vtcode-abc"));
251 assert!(formatted.contains("retry_after=30"));
252 assert!(formatted.contains("organization=org_456"));
253 assert!(formatted.contains("type=invalid_request_error"));
254 assert!(formatted.contains("code=unsupported_parameter"));
255 assert!(formatted.contains("param=text.verbosity"));
256 assert!(formatted.contains("Message: Bad request"));
257 }
258}