Files
hermes-agent/tests/hermes_cli/test_model_options_account_usage.py
Brooklyn Nicholson 38d19b92e8 feat(desktop): a progress chip warns before a subscription hits its usage wall
The provider heading's status slot now shows a fuel gauge for the
tightest usage window: hidden while there's room, "17% left · resets
3:00 PM" at 20% or less (amber from 10%), with a bar along its bottom
edge for what's left, then the existing "Limited until" once the login
is out. Hovering lists every window (5-hour, weekly, per-family).

model.options attaches `usage: { windows }` from a per-profile cache in
agent/account_usage_cache.py and refreshes it in the background, so the
picker never waits on a usage API. Every fetch_account_usage call lands
in that cache, including the one session.usage runs after each turn, so
the chip is at most a turn or two minutes behind. Covers the built-in
fetchers (Anthropic, Codex, OpenRouter) and any provider plugin's usage
hook.

Both pickers now render one ProviderStatusChip instead of separate
limit badges.
2026-10-05 12:21:38 -05:00

52 lines
2.2 KiB
Python

"""The model picker shows subscription usage before the wall, without ever waiting on a usage API.
Usage windows come from the provider's API, so ``model.options`` must read them from cache and
refresh in the background: a slow or hung usage endpoint can't be allowed to stall the picker.
"""
import threading
import time
from datetime import datetime, timedelta, timezone
from agent import account_usage
from agent.account_usage import AccountUsageSnapshot, AccountUsageWindow
from agent.account_usage_cache import cached_account_usage
from hermes_cli.inventory import build_model_options_payload, load_picker_context
def _snapshot(used: float) -> AccountUsageSnapshot:
reset = datetime.now(timezone.utc) + timedelta(hours=2)
return AccountUsageSnapshot(
provider="openrouter", source="test", fetched_at=datetime.now(timezone.utc),
windows=(AccountUsageWindow(label="Current session", used_percent=used, reset_at=reset),))
def test_picker_reads_cached_usage_and_refreshes_without_waiting(monkeypatch):
monkeypatch.setenv("OPENROUTER_API_KEY", "test-key-123")
release, called = threading.Event(), threading.Event()
answers = iter([_snapshot(92.0)])
def slow_usage_api(_base_url, _api_key):
called.set()
release.wait(10)
return next(answers)
monkeypatch.setitem(account_usage._USAGE_FETCHERS, "openrouter", slow_usage_api)
ctx = load_picker_context()
started = time.monotonic()
first = next(r for r in build_model_options_payload(ctx)["providers"] if r["slug"] == "openrouter")
assert time.monotonic() - started < 5, "the picker waited on the usage API"
assert "usage" not in first, "nothing cached yet, so nothing to show"
assert called.wait(5), "opening the picker must start a background usage refresh"
release.set()
deadline = time.monotonic() + 5
while cached_account_usage("openrouter") is None and time.monotonic() < deadline:
time.sleep(0.05)
row = next(r for r in build_model_options_payload(ctx)["providers"] if r["slug"] == "openrouter")
(window,) = row["usage"]["windows"]
assert window["label"] == "Current session" and window["used_percent"] == 92.0
assert datetime.fromisoformat(window["resets_at"]) > datetime.now(timezone.utc)