From e27698b3afbf9e1b0b2c2d35c5463c536af37a29 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Tue, 28 Jul 2026 14:52:39 +0000 Subject: [PATCH] Keep model lifecycle rows out of the request statistics A load, unload or download is recorded in the monitor but is not an HTTP call. It reads as running for as long as the load takes, so it was counted as an in-flight request with no client waiting, and a multi-minute download was folded into Avg latency and the error rate. The backend already excludes these rows from active_count for the same reason, so the page was also disagreeing with the number the API itself reports. Requests counted them too, so that is now the non-lifecycle count rather than the raw entry count. Also limit the API-reach sentence on the Hub settings page to GGUF models. The Hub opens that page for every downloaded model, but ModelConfigPage mirrors settings to the server only when target.isGguf, because auto-switch indexes GGUFs only, so a safetensors user was told the settings apply to an API request that cannot reach them. --- .../features/api-monitor/use-api-monitor.ts | 14 ++++++++- .../hub/catalog/hub-model-settings-view.tsx | 9 ++++-- tests/studio/test_model_picker_contracts.py | 31 +++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/studio/frontend/src/features/api-monitor/use-api-monitor.ts b/studio/frontend/src/features/api-monitor/use-api-monitor.ts index 3a40990120..0b613eb8c6 100644 --- a/studio/frontend/src/features/api-monitor/use-api-monitor.ts +++ b/studio/frontend/src/features/api-monitor/use-api-monitor.ts @@ -77,7 +77,19 @@ export function computeStats(entries: ApiMonitorEntry[]): MonitorStats { let generatedTokens = 0; let generatedDurationMs = 0; + let requests = 0; + for (const entry of entries) { + // A model load, unload or download is not an HTTP call. It shows as + // "running" for as long as the load takes, so counting it would report an + // in-flight request with no client waiting and fold a multi-minute download + // into "Avg latency". The backend already leaves these out of + // active_count for the same reason, so counting them here would also make + // the page disagree with the number the API itself reports. + if (entry.kind === "lifecycle") { + continue; + } + requests += 1; totalTokens += entryTokens(entry); if (entry.status === "running") { active += 1; @@ -106,7 +118,7 @@ export function computeStats(entries: ApiMonitorEntry[]): MonitorStats { const finished = completed + errors + cancelled; return { active, - total: entries.length, + total: requests, completed, errors, cancelled, diff --git a/studio/frontend/src/features/hub/catalog/hub-model-settings-view.tsx b/studio/frontend/src/features/hub/catalog/hub-model-settings-view.tsx index a293a0071a..30176a7d90 100644 --- a/studio/frontend/src/features/hub/catalog/hub-model-settings-view.tsx +++ b/studio/frontend/src/features/hub/catalog/hub-model-settings-view.tsx @@ -109,8 +109,13 @@ export function HubModelSettingsView({ />

- Saved settings apply everywhere this model loads, including when an - OpenAI-compatible API request asks for it. Turn on{" "} + {/* Only a GGUF is mirrored to the server, because API auto-switch + indexes GGUFs only, so promising the API case for anything + else describes a load that cannot happen. */} + {target.isGguf + ? "Saved settings apply everywhere this model loads, including when an OpenAI-compatible API request asks for it." + : "Saved settings apply everywhere Studio loads this model."}{" "} + Turn on{" "} Remember for this model {" "} diff --git a/tests/studio/test_model_picker_contracts.py b/tests/studio/test_model_picker_contracts.py index 71a3c7d659..f3dbd9c545 100644 --- a/tests/studio/test_model_picker_contracts.py +++ b/tests/studio/test_model_picker_contracts.py @@ -702,3 +702,34 @@ def test_backfill_compares_server_keys_by_normalized_identity(): assert 'key.lastIndexOf(":")' in src # Repo ids fold and POSIX paths do not, which is exactly what these do. assert "normalizeModelIdentity(" in src and "normalizeGgufVariantIdentity(" in src + + +def test_monitor_stats_exclude_model_lifecycle_rows(): + """A load, unload or download is recorded as a monitor entry but is not an + HTTP call. It reads as "running" for as long as the load takes, so counting + it reports an in-flight request with no client waiting and folds a + multi-minute download into "Avg latency". The backend already leaves these + out of active_count, so counting them here also makes the page disagree with + the number the API itself reports. + """ + src = " ".join(_read("features/api-monitor/use-api-monitor.ts").split()) + assert 'if (entry.kind === "lifecycle") { continue; }' in src + # "Requests" is a request count too, so it cannot stay entries.length. + assert "total: requests," in src + assert "total: entries.length" not in src + + backend = ( + WORKDIR / "studio" / "backend" / "core" / "inference" / "api_monitor.py" + ).read_text(encoding = "utf-8") + assert 'entry.kind != "lifecycle"' in backend, "the rule this mirrors" + + +def test_api_reach_copy_is_limited_to_gguf_models(): + """The Hub opens this page for every downloaded model, but ModelConfigPage + mirrors settings to the server only when target.isGguf, because API + auto-switch indexes GGUFs only. Telling a safetensors user the settings + apply to an API request describes a load that cannot happen. + """ + src = " ".join(_read("features/hub/catalog/hub-model-settings-view.tsx").split()) + assert "{target.isGguf ?" in src + assert "Saved settings apply everywhere Studio loads this model." in src