From 873447ed17dc5a9320285e9e3b472ad09cd2210b Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Mon, 9 Nov 2026 05:30:12 +0100 Subject: [PATCH] Provider status: tell rejected API keys apart from unreachable hosts A 404/405 on the model list counts as reachable (endpoint without a list). Co-Authored-By: Claude Opus 5.5 --- server/src/health.rs | 16 +++++++++++++--- web/src/components/Settings.tsx | 4 ++-- web/src/i18n/de.ts | 1 + web/src/i18n/en.ts | 1 + 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/server/src/health.rs b/server/src/health.rs index ce78708..ed7a8a0 100644 --- a/server/src/health.rs +++ b/server/src/health.rs @@ -10,6 +10,8 @@ use std::time::{Duration, Instant}; #[derive(Debug, Clone, Serialize)] pub struct ProviderStatus { pub up: bool, + /// "ok", "auth" (key missing/rejected) or "down" (host does not answer properly). + pub state: &'static str, #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, pub latency_ms: u64, @@ -40,17 +42,25 @@ pub async fn check_all(app: &App) { async move { let start = Instant::now(); let res = tokio::time::timeout(Duration::from_secs(15), provider.list_models()).await; - let error = match res { + let error: Option = match res { Ok(Ok(_)) => None, Ok(Err(e)) => Some(e.to_string().chars().take(300).collect()), Err(_) => Some("timed out".to_string()), }; - (name, ProviderStatus { up: error.is_none(), error, latency_ms: start.elapsed().as_millis() as u64, checked_at: chrono_now() }) + // The host answered but has no model list: reachable. + let error = error.filter(|e| !(e.contains("HTTP 404") || e.contains("HTTP 405"))); + let state = match &error { + None => "ok", + Some(e) if e.contains("HTTP 401") || e.contains("HTTP 403") => "auth", + Some(_) => "down", + }; + (name, ProviderStatus { up: error.is_none(), state, error, latency_ms: start.elapsed().as_millis() as u64, checked_at: chrono_now() }) } }); for (name, st) in futures::future::join_all(checks).await { if !st.up { - tracing::warn!(provider = %name, error = st.error.as_deref().unwrap_or(""), "provider unreachable"); + let msg = if st.state == "auth" { "provider rejected the API key" } else { "provider unreachable" }; + tracing::warn!(provider = %name, error = st.error.as_deref().unwrap_or(""), "{msg}"); } found.insert(name, st); } diff --git a/web/src/components/Settings.tsx b/web/src/components/Settings.tsx index e228305..d637e7b 100644 --- a/web/src/components/Settings.tsx +++ b/web/src/components/Settings.tsx @@ -372,7 +372,7 @@ function cleanModel(m: ModelRow) { return out; } -type ProviderStatus = { up: boolean; error?: string; latency_ms: number; checked_at: number }; +type ProviderStatus = { up: boolean; state?: "ok" | "auth" | "down"; error?: string; latency_ms: number; checked_at: number }; function ProviderEditor({ name, p, reload, status }: { name: string; p: Json; reload: () => void; status?: ProviderStatus }) { const t = useT(); @@ -445,7 +445,7 @@ function ProviderEditor({ name, p, reload, status }: { name: string; p: Json; re title={(status.error ?? "") + " · " + new Date(status.checked_at).toLocaleTimeString()} > - {status.up ? t("settings.providerUp", { ms: status.latency_ms }) : t("settings.providerDown")} + {status.up ? t("settings.providerUp", { ms: status.latency_ms }) : status.state === "auth" ? t("settings.providerAuth") : t("settings.providerDown")} )} diff --git a/web/src/i18n/de.ts b/web/src/i18n/de.ts index c3b998d..8040535 100644 --- a/web/src/i18n/de.ts +++ b/web/src/i18n/de.ts @@ -616,6 +616,7 @@ export const de: Messages = { "settings.providerName": "Name (optional)", "settings.providerUp": "erreichbar · {ms} ms", "settings.providerDown": "nicht erreichbar", + "settings.providerAuth": "API-Key fehlt oder ungültig", "settings.providerCheck": "Provider prüfen", "settings.providerChecking": "Prüfe…", "settings.reloadProviders": "Provider & Modelle neu laden", diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 9ab2385..740b4c6 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -615,6 +615,7 @@ export const en = { "settings.providerName": "Name (optional)", "settings.providerUp": "reachable · {ms} ms", "settings.providerDown": "unreachable", + "settings.providerAuth": "API key missing or rejected", "settings.providerCheck": "Check providers", "settings.providerChecking": "Checking…", "settings.reloadProviders": "Reload providers & models",