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:
1 parent
428e2df40a
commit
873447ed17
4 files changed
+17
-5
No files matched your search
+13
-3
@@ -10,6 +10,8 @@ use std::time::{Duration, Instant};
|
|||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct ProviderStatus {
|
pub struct ProviderStatus {
|
||||||
pub up: bool,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
pub latency_ms: u64,
|
pub latency_ms: u64,
|
||||||
@@ -40,17 +42,25 @@ pub async fn check_all(app: &App) {
|
|||||||
async move {
|
async move {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let res = tokio::time::timeout(Duration::from_secs(15), provider.list_models()).await;
|
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(Ok(_)) => None,
|
||||||
Ok(Err(e)) => Some(e.to_string().chars().take(300).collect()),
|
Ok(Err(e)) => Some(e.to_string().chars().take(300).collect()),
|
||||||
Err(_) => Some("timed out".to_string()),
|
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 {
|
for (name, st) in futures::future::join_all(checks).await {
|
||||||
if !st.up {
|
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);
|
found.insert(name, st);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -372,7 +372,7 @@ function cleanModel(m: ModelRow) {
|
|||||||
return out;
|
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 }) {
|
function ProviderEditor({ name, p, reload, status }: { name: string; p: Json; reload: () => void; status?: ProviderStatus }) {
|
||||||
const t = useT();
|
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()}
|
title={(status.error ?? "") + " · " + new Date(status.checked_at).toLocaleTimeString()}
|
||||||
>
|
>
|
||||||
<Icon name={status.up ? "check" : "warning"} size={12} />
|
<Icon name={status.up ? "check" : "warning"} size={12} />
|
||||||
{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")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="spacer" />
|
<span className="spacer" />
|
||||||
|
|||||||
@@ -616,6 +616,7 @@ export const de: Messages = {
|
|||||||
"settings.providerName": "Name (optional)",
|
"settings.providerName": "Name (optional)",
|
||||||
"settings.providerUp": "erreichbar · {ms} ms",
|
"settings.providerUp": "erreichbar · {ms} ms",
|
||||||
"settings.providerDown": "nicht erreichbar",
|
"settings.providerDown": "nicht erreichbar",
|
||||||
|
"settings.providerAuth": "API-Key fehlt oder ungültig",
|
||||||
"settings.providerCheck": "Provider prüfen",
|
"settings.providerCheck": "Provider prüfen",
|
||||||
"settings.providerChecking": "Prüfe…",
|
"settings.providerChecking": "Prüfe…",
|
||||||
"settings.reloadProviders": "Provider & Modelle neu laden",
|
"settings.reloadProviders": "Provider & Modelle neu laden",
|
||||||
|
|||||||
@@ -615,6 +615,7 @@ export const en = {
|
|||||||
"settings.providerName": "Name (optional)",
|
"settings.providerName": "Name (optional)",
|
||||||
"settings.providerUp": "reachable · {ms} ms",
|
"settings.providerUp": "reachable · {ms} ms",
|
||||||
"settings.providerDown": "unreachable",
|
"settings.providerDown": "unreachable",
|
||||||
|
"settings.providerAuth": "API key missing or rejected",
|
||||||
"settings.providerCheck": "Check providers",
|
"settings.providerCheck": "Check providers",
|
||||||
"settings.providerChecking": "Checking…",
|
"settings.providerChecking": "Checking…",
|
||||||
"settings.reloadProviders": "Reload providers & models",
|
"settings.reloadProviders": "Reload providers & models",
|
||||||
|
|||||||
Reference in new issue
Block a user