cmx_core/
error_summary.rs1use anyhow::Error;
2
3const MAX_LEN: usize = 120;
4const WRAPPER_PREFIXES: &[&str] = &["LLM analysis failed:", "LLM gateway error:"];
5
6pub fn summarize_gateway_error(error: &Error) -> String {
9 let flattened = collapse_whitespace(&format!("{error:#}"));
10 let without_wrappers = strip_wrappers(&flattened);
11 let without_body =
12 trim_suffix_at_any(without_wrappers, &[" - {", " - [", " caused by: ", " Caused by: "]);
13
14 let summary = if let Some(openai) = provider_phrase(without_body, "OpenAI API error:") {
15 openai
16 } else if looks_like_ollama_unreachable(without_body) {
17 "Ollama unreachable at localhost:11434".to_string()
18 } else {
19 without_body.to_string()
20 };
21
22 truncate(&summary)
23}
24
25fn collapse_whitespace(input: &str) -> String {
26 input.split_whitespace().collect::<Vec<_>>().join(" ")
27}
28
29fn strip_wrappers(mut text: &str) -> &str {
30 loop {
31 let mut stripped = false;
32 for prefix in WRAPPER_PREFIXES {
33 if let Some(rest) = text.strip_prefix(prefix) {
34 text = rest.trim_start();
35 stripped = true;
36 break;
37 }
38 }
39 if !stripped {
40 return text;
41 }
42 }
43}
44
45fn trim_suffix_at_any<'a>(text: &'a str, delimiters: &[&str]) -> &'a str {
46 let cutoff = delimiters.iter().filter_map(|d| text.find(d)).min().unwrap_or(text.len());
47 text[..cutoff].trim_end_matches([' ', ':'])
48}
49
50fn provider_phrase(text: &str, marker: &str) -> Option<String> {
51 let start = text.find(marker)?;
52 Some(text[start..].trim().to_string())
53}
54
55fn looks_like_ollama_unreachable(text: &str) -> bool {
56 let lower = text.to_ascii_lowercase();
57 (lower.contains("localhost:11434") || lower.contains("ollama"))
58 && (lower.contains("connection refused")
59 || lower.contains("failed to connect")
60 || lower.contains("error sending request")
61 || lower.contains("tcp connect error"))
62}
63
64fn truncate(text: &str) -> String {
65 if text.chars().count() <= MAX_LEN {
66 text.to_string()
67 } else {
68 let head: String = text.chars().take(MAX_LEN).collect();
69 format!("{head}…")
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::summarize_gateway_error;
76
77 #[test]
78 fn summarize_gateway_error_keeps_short_message() {
79 assert_eq!(summarize_gateway_error(&anyhow::anyhow!("short error")), "short error");
80 }
81
82 #[test]
83 fn summarize_gateway_error_strips_openai_json_body() {
84 let error = anyhow::anyhow!(
85 "LLM analysis failed: LLM gateway error: OpenAI API error: 401 Unauthorized - {{ \
86 \"error\": {{ \"message\": \"You didn't provide an API key\", \"type\": \
87 \"invalid_request_error\" }} }}"
88 );
89
90 let summary = summarize_gateway_error(&error);
91 assert_eq!(summary, "OpenAI API error: 401 Unauthorized");
92 assert!(!summary.contains('{'));
93 assert!(!summary.contains('\n'));
94 assert!(!summary.contains("\"error\""));
95 }
96}