diff --git a/gateway/slash_commands_status.py b/gateway/slash_commands_status.py index cc94a1807a..eb534963f8 100644 --- a/gateway/slash_commands_status.py +++ b/gateway/slash_commands_status.py @@ -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") diff --git a/tests/gateway/test_usage_command.py b/tests/gateway/test_usage_command.py index ae68e56fe3..d673d5dccb 100644 --- a/tests/gateway/test_usage_command.py +++ b/tests/gateway/test_usage_command.py @@ -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)