fix(gateway): /usage falls back to the configured provider when no route is known

With no live/cached agent and no persisted billing route (fresh session right
after `hermes auth add openai-codex`, or an evicted agent with an empty
transcript) /usage returned the bare stub and never tried the account-usage
fetch even though on-disk credentials could answer it. Resolve the provider
from `model.provider` in the gateway config, as /status already does; the
fetch stays fail-open. Complements the credential-pool-aware Codex resolver
that already covers the token-lookup half of this report.

Fixes #15167
This commit is contained in:
Teknium
2026-09-18 23:36:35 -07:00
parent 3ee389afdd
commit 58d1fa2252
2 changed files with 50 additions and 0 deletions

View File

@@ -73,6 +73,14 @@ HISTORY_UNREADABLE = ("⚠️ I can't read this conversation's history right now
"to start fresh.")
def _configured_provider() -> str:
"""``model.provider`` from the gateway config ("" when unset)."""
from gateway.run import _load_gateway_config
user_config = _load_gateway_config()
model_cfg = user_config.get("model", {}) if isinstance(user_config, dict) else {}
return _clean_str(model_cfg.get("provider")) if isinstance(model_cfg, dict) else ""
def _quiet_sync(call, default=None):
"""Sync twin of ``_quiet``."""
try:
@@ -573,6 +581,11 @@ class GatewayStatusCommandsMixin:
)
if not provider and getattr(self, "_session_db", None) is not None:
provider, base_url = await self._persisted_billing_route(source)
if not provider:
# Fresh or evicted session with no persisted route (e.g. /usage right after login):
# fall back to the configured provider, as /status does, so account limits such as
# Codex subscription windows still render from on-disk credentials (#15167).
provider = await _quiet(lambda: asyncio.to_thread(_configured_provider)) or None
if wants_reset:
if str(provider or "").strip().lower() != "openai-codex":
return t("gateway.usage.reset_wrong_provider")

View File

@@ -162,6 +162,43 @@ class TestUsageAccountSection:
assert "📊 **Session Info**" in result
assert "📈 **Account limits**" in result
@pytest.mark.asyncio
async def test_usage_command_falls_back_to_configured_provider_without_history(self, monkeypatch):
"""#15167: no agent, no persisted route, empty transcript -> still fetch account limits
for the configured provider instead of the bare "no data" stub."""
runner = _make_runner(SK)
runner._session_db = AsyncSessionDB(MagicMock())
runner._session_db._db.get_session.return_value = {}
runner._session_db._db.get_recent_session_model_route.return_value = None
session_entry = MagicMock()
session_entry.session_id = "sess-fresh"
runner.session_store.get_or_create_session.return_value = session_entry
runner.session_store.load_transcript.return_value = []
calls = []
async def _fake_to_thread(fn, *args, **kwargs):
calls.append({"fn": fn, "args": args, "kwargs": kwargs})
return fn(*args, **kwargs)
monkeypatch.setattr("gateway.run.asyncio.to_thread", _fake_to_thread)
monkeypatch.setattr("gateway.run._load_gateway_config", lambda: {"model": {"provider": "openai-codex"}})
monkeypatch.setattr(
"gateway.slash_commands_status.fetch_account_usage",
lambda provider, base_url=None, api_key=None: object(),
)
monkeypatch.setattr(
"gateway.slash_commands_status.render_account_usage_lines",
lambda snapshot, markdown=False: ["📈 **Account limits**", "Provider: openai-codex (Plus)",
"Weekly: 91% remaining (9% used)"],
)
monkeypatch.setattr("agent.account_usage.nous_credits_lines", lambda markdown=False: [])
result = await runner._handle_usage_command(MagicMock())
assert any(c["args"] == ("openai-codex",) for c in calls)
assert "📈 **Account limits**" in result and "Weekly: 91% remaining" in result
@pytest.mark.asyncio
async def test_usage_command_prefers_recent_persisted_route(self, monkeypatch):
runner = _make_runner(SK)