1use crate::provider::BrainError;
8
9pub fn humanize_brain_error(err: &BrainError, provider_label: &str, model: &str) -> String {
22 match err {
23 BrainError::RateLimit { retry_after } => humanize_rate_limit(provider_label, *retry_after),
24 BrainError::ServerError { status, body } => {
25 humanize_http_error(*status, body, provider_label, model)
26 }
27 BrainError::Timeout => {
28 format!(
29 "⏱️ {provider} a mis trop de temps à répondre. Réessaie ou change de provider (sparrow route set <provider>).",
30 provider = provider_label,
31 )
32 }
33 BrainError::Refusal(msg) => {
34 format!(
35 "🚫 {provider} a refusé la requête : {msg}\n→ Reformule ta demande ou change de modèle avec sparrow model --set <provider>:<model>.",
36 provider = provider_label,
37 msg = msg,
38 )
39 }
40 BrainError::Unknown(msg) => {
41 format!(
42 "❓ Erreur inconnue chez {provider} : {msg}\n→ Lance sparrow doctor pour un diagnostic complet.",
43 provider = provider_label,
44 msg = msg,
45 )
46 }
47 }
48}
49
50fn humanize_http_error(status: u16, body: &str, provider_label: &str, model: &str) -> String {
52 match status {
53 401 | 403 => humanize_unauthorized(provider_label),
54 404 => {
55 if body.contains("model") || body.contains("Model") {
56 humanize_model_not_found(provider_label, model)
57 } else {
58 format!(
59 "🔍 Ressource introuvable chez {provider}. Vérifie l'URL de base (sparrow config --edit).",
60 provider = provider_label,
61 )
62 }
63 }
64 429 => humanize_rate_limit(provider_label, None),
65 500..=599 => {
66 if body.to_lowercase().contains("overloaded")
67 || body.to_lowercase().contains("capacity")
68 {
69 format!(
70 "🏋️ {provider} est surchargé. Réessaie dans quelques minutes ou change de provider (sparrow route set <provider>).",
71 provider = provider_label,
72 )
73 } else {
74 format!(
75 "💥 Erreur serveur {provider} (HTTP {status}). C'est probablement temporaire — réessaie dans 30 secondes.\n→ Détails : {body}",
76 provider = provider_label,
77 status = status,
78 body = body,
79 )
80 }
81 }
82 _ => format!(
83 "⚠️ Erreur HTTP {status} chez {provider} : {body}\n→ Lance sparrow doctor pour un diagnostic.",
84 status = status,
85 provider = provider_label,
86 body = body,
87 ),
88 }
89}
90
91fn humanize_unauthorized(provider_label: &str) -> String {
93 let (url, signup_hint) = match provider_label.to_lowercase().as_str() {
94 "anthropic" => (
95 "https://console.anthropic.com/settings/keys",
96 " (payant, ~20$/mois de crédits offerts)",
97 ),
98 "nvidia nim" | "nvidia" => (
99 "https://build.nvidia.com/explore/discover",
100 " (gratuit — crée un compte NVIDIA Developer)",
101 ),
102 "openai" | "openai codex" => (
103 "https://platform.openai.com/api-keys",
104 " (payant, crédits gratuits à l'inscription)",
105 ),
106 "google gemini" | "gemini" => (
107 "https://aistudio.google.com/app/apikey",
108 " (gratuit jusqu'à 1500 requêtes/jour)",
109 ),
110 "groq" => (
111 "https://console.groq.com/keys",
112 " (gratuit — généreux tier gratuit)",
113 ),
114 "deepseek" => (
115 "https://platform.deepseek.com/api_keys",
116 " (très bon marché, ~0.27$/M tokens)",
117 ),
118 "openrouter" => (
119 "https://openrouter.ai/keys",
120 " (crédits gratuits à l'inscription)",
121 ),
122 "xai" | "xai (grok)" => ("https://console.x.ai/", " (payant)"),
123 _ => ("", ""),
124 };
125
126 if url.is_empty() {
127 format!(
128 "🔑 Ta clé API pour {provider} est invalide ou expirée.\n\
129 → Vérifie ta clé avec : sparrow auth list\n\
130 → Ajoutes-en une nouvelle : sparrow auth add {provider_lower}",
131 provider = provider_label,
132 provider_lower = provider_label.to_lowercase(),
133 )
134 } else {
135 format!(
136 "🔑 Ta clé API {provider} est invalide ou expirée.\n\
137 → Va sur {url} pour en créer une{signup}\n\
138 → Puis ajoute-la avec : sparrow auth add {provider_lower}\n\
139 → Ou exporte-la : export {env_var}=\"ta-clé\"",
140 provider = provider_label,
141 url = url,
142 signup = signup_hint,
143 provider_lower = provider_label.to_lowercase().replace(' ', "-"),
144 env_var = provider_env_var(provider_label),
145 )
146 }
147}
148
149fn humanize_rate_limit(provider_label: &str, retry_after: Option<u64>) -> String {
151 let wait = match retry_after {
152 Some(s) if s > 0 => format!("{s} secondes"),
153 _ => "quelques minutes".to_string(),
154 };
155 format!(
156 "⏳ T'as envoyé trop de requêtes à {provider}. Réessaie dans {wait}.\n\
157 → Astuce : utilise un modèle moins cher pour les tâches simples (sparrow route set nvidia).",
158 provider = provider_label,
159 wait = wait,
160 )
161}
162
163fn humanize_model_not_found(provider_label: &str, model: &str) -> String {
165 format!(
166 "🤷 Le modèle \"{model}\" n'existe pas chez {provider}.\n\
167 → Liste les modèles dispos : sparrow model --list\n\
168 → Change de modèle : sparrow model --set {provider_lower}:<model>",
169 model = model,
170 provider = provider_label,
171 provider_lower = provider_label.to_lowercase().replace(' ', "-"),
172 )
173}
174
175fn provider_env_var(provider_label: &str) -> String {
177 match provider_label.to_lowercase().as_str() {
178 "anthropic" => "ANTHROPIC_API_KEY".into(),
179 "nvidia nim" | "nvidia" => "NVIDIA_API_KEY".into(),
180 "openai" | "openai codex" => "OPENAI_API_KEY".into(),
181 "google gemini" | "gemini" => "GEMINI_API_KEY".into(),
182 "groq" => "GROQ_API_KEY".into(),
183 "deepseek" => "DEEPSEEK_API_KEY".into(),
184 "openrouter" => "OPENROUTER_API_KEY".into(),
185 "xai" | "xai (grok)" => "XAI_API_KEY".into(),
186 "huggingface" | "hugging face" => "HF_TOKEN".into(),
187 "nous" | "nous portal" => "NOUS_API_KEY".into(),
188 "novita" | "novitaai" => "NOVITA_API_KEY".into(),
189 "alibaba" | "alibaba cloud" => "DASHSCOPE_API_KEY".into(),
190 other => format!("{}_API_KEY", other.to_uppercase().replace([' ', '-'], "_")),
191 }
192}
193
194pub fn humanize_connection_error(provider_label: &str, error_msg: &str) -> String {
198 let lower = error_msg.to_lowercase();
199
200 if lower.contains("connection refused") || lower.contains("connect refused") {
201 format!(
202 "🔌 Impossible de contacter {provider}. Le serveur a refusé la connexion.\n\
203 → Vérifie l'URL de base : sparrow config --edit\n\
204 → Si t'utilises Ollama : ollama serve doit tourner.",
205 provider = provider_label,
206 )
207 } else if lower.contains("dns") || lower.contains("no address") || lower.contains("name") {
208 format!(
209 "🌐 Impossible de résoudre le nom d'hôte de {provider}. Check ta connexion internet ou DNS.",
210 provider = provider_label,
211 )
212 } else if lower.contains("timeout") || lower.contains("timed out") {
213 format!(
214 "⏱️ Timeout en contactant {provider}. Check ta connexion ou VPN.\n\
215 → Si le problème persiste, change de provider : sparrow route set nvidia",
216 provider = provider_label,
217 )
218 } else if lower.contains("ssl") || lower.contains("tls") || lower.contains("certificate") {
219 format!(
220 "🔒 Erreur SSL/TLS avec {provider}. Vérifie tes certificats ou désactive le VPN.\n\
221 → Détail : {error_msg}",
222 provider = provider_label,
223 error_msg = error_msg,
224 )
225 } else {
226 format!(
227 "📡 Problème de connexion vers {provider} : {error_msg}\n\
228 → Check ta connexion ou VPN.\n\
229 → Lance sparrow doctor pour un diagnostic complet.",
230 provider = provider_label,
231 error_msg = error_msg,
232 )
233 }
234}
235
236pub fn humanize_config_error(error_type: &str, context: &str) -> String {
238 match error_type {
239 "no_provider" => format!(
240 "⚙️ Aucun provider configuré.\n\
241 → Lance le setup : sparrow setup\n\
242 → Ou définis une variable d'environnement : export {ctx}_API_KEY=\"ta-clé\"\n\
243 → Providers gratuits dispos : NVIDIA (sparrow auth add nvidia), Groq, Gemini",
244 ctx = context.to_uppercase(),
245 ),
246 "no_model" => format!(
247 "⚙️ Aucun modèle défini pour le provider \"{ctx}\".\n\
248 → Liste les modèles : sparrow model --list\n\
249 → Définis un modèle : sparrow model --set {ctx}:<nom-du-modèle>",
250 ctx = context,
251 ),
252 "no_config_file" => format!(
253 "📄 Pas de fichier de config Sparrow trouvé.\n\
254 → Premier lancement ? Lance sparrow setup\n\
255 → Sinon, crée ~/.config/sparrow/config.toml manuellement",
256 ),
257 "config_parse_error" => format!(
258 "📄 Erreur de parsing dans le fichier de config.\n\
259 → Édite le fichier : sparrow config --edit\n\
260 → Détail de l'erreur : {ctx}\n\
261 → Format attendu : TOML valide (voir https://toml.io)",
262 ctx = context,
263 ),
264 "invalid_autonomy" => format!(
265 "⚙️ Niveau d'autonomie invalide : \"{ctx}\".\n\
266 → Valeurs acceptées : supervised, trusted, autonomous\n\
267 → Modifie : sparrow config --edit",
268 ctx = context,
269 ),
270 "invalid_sandbox" => format!(
271 "⚙️ Type de sandbox invalide : \"{ctx}\".\n\
272 → Valeurs acceptées : local, local-hardened, docker\n\
273 → Modifie : sparrow config --edit",
274 ctx = context,
275 ),
276 "budget_exceeded" => format!(
277 "💰 Budget dépassé !\n\
278 → Budget actuel : ${ctx}\n\
279 → Augmente le budget : sparrow config --edit (section [budget])\n\
280 → Ou utilise un provider gratuit : sparrow route set nvidia",
281 ctx = context,
282 ),
283 _ => format!(
284 "⚠️ Erreur de configuration : {ctx}\n\
285 → Lance sparrow doctor pour un diagnostic complet.\n\
286 → Édite la config : sparrow config --edit",
287 ctx = context,
288 ),
289 }
290}
291
292pub fn humanize_anyhow(err: &anyhow::Error, provider_label: &str, model: &str) -> String {
298 let msg = format!("{err:#}");
299
300 if let Some(status) = extract_http_status(&msg) {
302 return humanize_http_error(status, &msg, provider_label, model);
303 }
304
305 if msg.contains("Connection refused")
307 || msg.contains("connect")
308 || msg.contains("DNS")
309 || msg.contains("timeout")
310 || msg.contains("SSL")
311 || msg.contains("TLS")
312 {
313 return humanize_connection_error(provider_label, &msg);
314 }
315
316 if msg.contains("config") || msg.contains("toml") || msg.contains("parse") {
318 return humanize_config_error("config_parse_error", &msg);
319 }
320
321 format!(
323 "💥 Oups ! Une erreur est survenue : {msg}\n\
324 → Lance sparrow doctor pour un diagnostic.\n\
325 → Si le problème persiste, ouvre une issue : https://github.com/ucav/Sparrow/issues",
326 msg = msg,
327 )
328}
329
330fn extract_http_status(msg: &str) -> Option<u16> {
332 let re = regex::Regex::new(r"(?i)(?:HTTP\s+|status:\s*|^\s*)(\d{3})\b").ok()?;
334 re.captures(msg)
335 .and_then(|caps| caps.get(1))
336 .and_then(|m| m.as_str().parse::<u16>().ok())
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn test_unauthorized_anthropic() {
345 let msg = humanize_http_error(401, "", "Anthropic", "claude-sonnet-4-6");
346 assert!(msg.contains("invalide"));
347 assert!(msg.contains("console.anthropic.com"));
348 assert!(msg.contains("ANTHROPIC_API_KEY"));
349 }
350
351 #[test]
352 fn test_rate_limit() {
353 let msg = humanize_rate_limit("Groq", Some(30));
354 assert!(msg.contains("trop de requêtes"));
355 assert!(msg.contains("30 secondes"));
356 }
357
358 #[test]
359 fn test_model_not_found() {
360 let msg = humanize_model_not_found("NVIDIA NIM", "gpt-5-ultra");
361 assert!(msg.contains("n'existe pas"));
362 assert!(msg.contains("gpt-5-ultra"));
363 }
364
365 #[test]
366 fn test_connection_refused() {
367 let msg = humanize_connection_error("Ollama", "Connection refused (os error 111)");
368 assert!(msg.contains("Impossible de contacter"));
369 assert!(msg.contains("ollama serve"));
370 }
371
372 #[test]
373 fn test_config_no_provider() {
374 let msg = humanize_config_error("no_provider", "ANTHROPIC");
375 assert!(msg.contains("Aucun provider"));
376 }
377
378 #[test]
379 fn test_extract_http_status() {
380 assert_eq!(extract_http_status("HTTP 401 Unauthorized"), Some(401));
381 assert_eq!(
382 extract_http_status("status: 429 Too Many Requests"),
383 Some(429)
384 );
385 assert_eq!(extract_http_status("500 Internal Server Error"), Some(500));
386 assert_eq!(extract_http_status("no status here"), None);
387 }
388}