Attribute API-key traffic on the lifecycle rows it creates

The overlay opens on API-key traffic only, and record_lifecycle never set that
flag. Auto-switch and auto-download run before the endpoint opens its request
row, so a switch or a download that is refused never reaches api_monitor.start
and its lifecycle row is the whole trace of the request. The monitor therefore
stayed shut on exactly the failures automatic observability is for.

The row now carries the attribution: a load takes it from the request that drove
it, and auto-download passes it directly, since only an API request reaches that
path at all. A manual unload and an idle unload are not API traffic and stay
unattributed, so neither pops the overlay.
This commit is contained in:
danielhanchen 2026-07-28 22:04:57 +00:00
commit 384a55de43
4 changed files with 36 additions and 1 deletions

View file

@ -173,6 +173,7 @@ class ApiMonitor:
model: str,
reason: Optional[str] = None,
running: bool = False,
via_api_key: bool = False,
) -> str:
"""Record a model load/unload alongside the request traffic that caused it.
@ -199,6 +200,11 @@ class ApiMonitor:
event = event,
reason = reason,
shared = True,
# The overlay opens on API-key traffic only. A switch or download that
# is refused never reaches api_monitor.start, so this row is the whole
# trace of it, and without the attribution the monitor stayed shut on
# exactly the failures it exists to surface.
via_api_key = via_api_key,
)
with self._lock:
self._entries.appendleft(entry)

View file

@ -777,7 +777,8 @@ async def _dispatch(
return busy
monitor_id = api_monitor.record_lifecycle(
event = "download", model = label, reason = "api", running = True
# Only an API request reaches auto-download, hence reason "api".
event = "download", model = label, reason = "api", running = True, via_api_key = True
)
with _lock:
if _active is active:

View file

@ -5321,6 +5321,9 @@ async def _load_model_impl(
event = "load",
model = _lifecycle_model_label(request.model_path, request.gguf_variant),
running = True,
# Auto-switch loads run before the endpoint opens its request row, so a
# load that fails there leaves this as the only trace of API traffic.
via_api_key = _request_used_api_key(fastapi_request),
)
native_grant_backed = False

View file

@ -517,3 +517,28 @@ def test_hidden_shared_ids_do_not_outlive_their_entries():
for i in range(5):
monitor.record_lifecycle(event = "unload", model = f"org/M{i}")
assert not monitor._hidden_shared.get("alice")
def test_an_api_triggered_lifecycle_row_carries_the_attribution():
"""The overlay opens on API-key traffic only. An auto-switch or auto-download
that is refused never reaches api_monitor.start, so the lifecycle row is the
whole trace of that request; without the attribution the monitor stayed shut
on exactly the failures it exists to surface."""
monitor = ApiMonitor(max_entries = 5)
api_load = monitor.record_lifecycle(
event = "load", model = "org/Repo-GGUF", running = True, via_api_key = True
)
monitor.record_lifecycle(event = "unload", model = "org/Repo-GGUF", reason = "idle")
rows = {e["id"]: e for e in monitor.snapshot()}
assert rows[api_load]["via_api_key"] is True
# A background unload is not API traffic and must not pop the overlay.
idle = [e for e in rows.values() if e["event"] == "unload"]
assert idle and all(e["via_api_key"] is False for e in idle)
# The failure path keeps it: failing the row must not drop the attribution.
monitor.fail(api_load, error = "auto-switch refused")
after = {e["id"]: e for e in monitor.snapshot()}
assert after[api_load]["via_api_key"] is True
assert after[api_load]["status"] == "error"