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 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-09 05:30:12 +01:00
1 parent 428e2df40a
commit 873447ed17
4 files changed
+17 -5

No files matched your search

+13 -3
View File
@@ -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<String>,
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<String> = 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);
}