elph_ai/utils/
error_body.rs1use serde_json::Value;
2
3pub const MAX_PROVIDER_ERROR_BODY_CHARS: usize = 4000;
4
5#[derive(Debug, Clone)]
6pub struct NormalizedProviderError {
7 pub status: Option<u16>,
8 pub body: Option<String>,
9 pub message: String,
10 pub message_carries_body: bool,
11}
12
13#[derive(Debug)]
15pub struct ProviderSdkError {
16 pub message: String,
17 pub status_code: Option<u16>,
18 pub status: Option<u16>,
19 pub body: Option<String>,
20 pub parsed_error: Option<Value>,
21 pub bedrock_metadata_http_status: Option<u16>,
22 pub bedrock_response_status_code: Option<u16>,
23 pub bedrock_response_body: Option<String>,
24}
25
26impl std::fmt::Display for ProviderSdkError {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 write!(f, "{}", self.message)
29 }
30}
31
32impl std::error::Error for ProviderSdkError {}
33
34#[derive(Debug)]
36pub struct ThrownValue(pub Value);
37
38impl std::fmt::Display for ThrownValue {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 write!(f, "{}", safe_json_stringify(&self.0))
41 }
42}
43
44impl std::error::Error for ThrownValue {}
45
46pub fn normalize_provider_error(error: &anyhow::Error) -> NormalizedProviderError {
47 if let Some(thrown) = error.downcast_ref::<ThrownValue>() {
48 return NormalizedProviderError {
49 status: None,
50 body: None,
51 message: safe_json_stringify(&thrown.0),
52 message_carries_body: false,
53 };
54 }
55
56 if let Some(sdk) = error.downcast_ref::<ProviderSdkError>() {
57 let status = extract_status(sdk);
58 let body = extract_body(sdk);
59 let message_carries_body = body.as_ref().is_none_or(|b| sdk.message.contains(b));
60 return NormalizedProviderError {
61 status,
62 body,
63 message: sdk.message.clone(),
64 message_carries_body,
65 };
66 }
67
68 let message = error.to_string();
69 let status = error
70 .downcast_ref::<reqwest::Error>()
71 .and_then(|e| e.status())
72 .map(|s| s.as_u16());
73
74 NormalizedProviderError {
75 status,
76 body: None,
77 message,
78 message_carries_body: false,
79 }
80}
81
82fn extract_status(error: &ProviderSdkError) -> Option<u16> {
83 error
84 .status_code
85 .or(error.status)
86 .or(error.bedrock_metadata_http_status)
87 .or(error.bedrock_response_status_code)
88}
89
90fn extract_body(error: &ProviderSdkError) -> Option<String> {
91 let body_text = pick_body_text(error)?;
92 let trimmed = body_text.trim();
93 if trimmed.is_empty() {
94 return None;
95 }
96 Some(truncate_error_text(trimmed, MAX_PROVIDER_ERROR_BODY_CHARS))
97}
98
99fn pick_body_text(error: &ProviderSdkError) -> Option<String> {
100 if let Some(body) = &error.body {
101 return Some(body.clone());
102 }
103 if is_non_empty_object(error.parsed_error.as_ref()) {
104 return Some(safe_json_stringify(error.parsed_error.as_ref().unwrap()));
105 }
106 if let Some(body) = &error.bedrock_response_body {
107 return Some(body.clone());
108 }
109 None
110}
111
112fn is_non_empty_object(value: Option<&Value>) -> bool {
113 match value {
114 Some(Value::Object(map)) => !map.is_empty(),
115 _ => false,
116 }
117}
118
119pub fn format_provider_error(norm: &NormalizedProviderError, prefix: Option<&str>) -> String {
120 if norm.message_carries_body || norm.status.is_none() || norm.body.is_none() {
121 if let (Some(prefix), Some(status)) = (prefix, norm.status) {
122 return format!("{prefix} ({status}): {}", norm.message);
123 }
124 return norm.message.clone();
125 }
126 if let Some(prefix) = prefix {
127 format!(
128 "{prefix} ({}): {}",
129 norm.status.unwrap_or(0),
130 norm.body.as_deref().unwrap_or("")
131 )
132 } else {
133 format!("{}: {}", norm.status.unwrap_or(0), norm.body.as_deref().unwrap_or(""))
134 }
135}
136
137pub fn truncate_error_text(text: &str, max_chars: usize) -> String {
138 if text.len() <= max_chars {
139 return text.to_string();
140 }
141 format!("{}... [truncated {} chars]", &text[..max_chars], text.len() - max_chars)
142}
143
144pub fn safe_json_stringify(value: &Value) -> String {
145 serde_json::to_string(value).unwrap_or_else(|_| value.to_string())
146}
147
148pub async fn error_body_from_response(response: reqwest::Response) -> String {
149 let status = response.status();
150 let text = response.text().await.unwrap_or_default();
151 if text.trim().is_empty() {
152 format!("{status}")
153 } else {
154 truncate_error_text(&text, MAX_PROVIDER_ERROR_BODY_CHARS)
155 }
156}