1use std::fmt;
4
5const QUOTED: usize = 240;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ProviderError {
15 Unreachable {
17 url: String,
19 reason: String,
21 },
22 Refused {
24 url: String,
26 status: u16,
28 said: String,
30 },
31 Stopped {
33 said: String,
35 },
36 Unreadable {
38 what: String,
40 },
41}
42
43impl fmt::Display for ProviderError {
44 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 Self::Unreachable { url, reason } => write!(formatter, "{url}: {reason}"),
47 Self::Refused { url, status, said } => write!(formatter, "{url}: {status} {said}"),
48 Self::Stopped { said } => formatter.write_str(said),
49 Self::Unreadable { what } => formatter.write_str(what),
50 }
51 }
52}
53
54impl ProviderError {
55 #[must_use]
59 pub fn without(self, secret: &str) -> Self {
60 let hide = |text: String| if secret.is_empty() { text } else { text.replace(secret, "…") };
61 match self {
62 Self::Unreachable { url, reason } => Self::Unreachable { url: hide(url), reason: hide(reason) },
63 Self::Refused { url, status, said } => Self::Refused { url: hide(url), status, said: hide(said) },
64 Self::Stopped { said } => Self::Stopped { said: hide(said) },
65 Self::Unreadable { what } => Self::Unreadable { what: hide(what) },
66 }
67 }
68}
69
70pub(crate) fn complaint(body: &str) -> String {
77 let Ok(value) = serde_json::from_str::<serde_json::Value>(body) else { return shorten(body) };
78 let error = value.get("error");
79 let said = [
80 value.get("metadata").and_then(|metadata| metadata.get("raw")),
81 error.and_then(|error| error.get("metadata")).and_then(|metadata| metadata.get("raw")),
82 error.and_then(|error| error.get("message")),
83 error.filter(|error| error.is_string()),
84 value.get("message"),
85 ]
86 .into_iter()
87 .flatten()
88 .find_map(|said| said.as_str().filter(|said| !said.trim().is_empty()))
89 .unwrap_or(body);
90 shorten(said)
91}
92
93fn shorten(said: &str) -> String {
95 let trimmed = said.trim();
96 match trimmed.char_indices().nth(QUOTED) {
97 Some((end, _)) => format!("{}…", &trimmed[..end]),
98 None => trimmed.to_owned(),
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn the_sentence_is_taken_out_of_the_shapes_providers_use() {
108 let anthropic = r#"{"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}"#;
109 assert_eq!(complaint(anthropic), "invalid x-api-key");
110 assert_eq!(complaint(r#"{"error":"model not found"}"#), "model not found");
111 assert_eq!(complaint(r#"{"message":"rate limited"}"#), "rate limited");
112 assert_eq!(complaint(" Bad Gateway \n"), "Bad Gateway");
113 }
114
115 #[test]
116 fn a_long_page_is_cut_with_an_ellipsis() {
117 let page = "x".repeat(1000);
118 let said = complaint(&page);
119 assert_eq!(said.chars().count(), QUOTED + 1);
120 assert!(said.ends_with('…'));
121 }
122}