diff --git a/api/providers.py b/api/providers.py index be53358a..708759d9 100644 --- a/api/providers.py +++ b/api/providers.py @@ -388,6 +388,17 @@ def _entry_is_pool_exhausted(entry): return str(_entry_value(entry, "last_status") or "").strip().lower() == "exhausted" +def _entry_pool_exhausted_reason(entry): + code = _entry_value(entry, "last_error_code") + reset_at = _iso(_parse_dt(getattr(entry, "last_error_reset_at", None))) + reason = "Credential pool marked this credential exhausted" + if code: + reason += " after provider status " + code + if reset_at: + reason += "; retry after " + reset_at + return reason + "." + + def _fetch_codex_entry_snapshot(entry): access_token = _entry_value(entry, "runtime_api_key", "access_token") if not access_token: @@ -511,6 +522,17 @@ def _fetch_codex_account_usage_from_pool(): for index, entry in enumerate(entries, start=1): label = _safe_entry_label(entry, index) pool_exhausted = _entry_is_pool_exhausted(entry) + if pool_exhausted: + rows.append({ + "label": label, + "status": "exhausted", + "plan": None, + "windows": [], + "details": [], + "unavailable_reason": _entry_pool_exhausted_reason(entry), + "fetched_at": None, + }) + continue try: snapshot, did_query, reason = _fetch_codex_entry_snapshot(entry) if did_query: @@ -521,7 +543,7 @@ def _fetch_codex_account_usage_from_pool(): windows = _snapshot_windows_payload(snapshot) if snapshot is not None else [] details = _snapshot_details_payload(snapshot) if snapshot is not None else [] snapshot_available = _snapshot_available(snapshot) - status = "available" if snapshot_available and not pool_exhausted else "exhausted" if pool_exhausted else "unavailable" + status = "available" if snapshot_available else "unavailable" rows.append({ "label": label, "status": status, diff --git a/tests/test_provider_quota_status.py b/tests/test_provider_quota_status.py index 05ac5bd1..5ca97db8 100644 --- a/tests/test_provider_quota_status.py +++ b/tests/test_provider_quota_status.py @@ -373,25 +373,23 @@ def test_codex_account_usage_subprocess_reports_read_only_credential_pool(monkey assert entries_called == [True] assert [call["url"] for call in seen] == [ "https://chatgpt.com/backend-api/wham/usage", - "https://chatgpt.com/backend-api/wham/usage", ] - assert [call["timeout"] for call in seen] == [4.0, 4.0] + assert [call["timeout"] for call in seen] == [4.0] assert seen[0]["headers"]["authorization"] == f"Bearer {primary_token}" assert seen[0]["headers"]["chatgpt-account-id"] == "acct-primary" - assert seen[1]["headers"]["chatgpt-account-id"] == "acct-exhausted" assert snapshot["provider"] == "openai-codex" assert snapshot["source"] == "usage_api_pool" assert snapshot["windows"][0]["label"] == "Session" assert snapshot["windows"][0]["used_percent"] == 15 - assert snapshot["details"] == ["1/2 credentials available", "1 exhausted", "Plans: Pro, Plus"] + assert snapshot["details"] == ["1/2 credentials available", "1 exhausted", "Plans: Pro"] assert snapshot["available"] is True assert snapshot["pool"] == { "total_credentials": 2, - "queried_credentials": 2, + "queried_credentials": 1, "available_credentials": 1, "exhausted_credentials": 1, "failed_credentials": 0, - "plans": ["Pro", "Plus"], + "plans": ["Pro"], "next_reset_at": "2030-03-17T17:46:40Z", "best_remaining_by_window": [ { @@ -439,26 +437,11 @@ def test_codex_account_usage_subprocess_reports_read_only_credential_pool(monkey { "label": "Plus backup", "status": "exhausted", - "plan": "Plus", - "windows": [ - { - "label": "Session", - "used_percent": 95.0, - "remaining_percent": 5.0, - "reset_at": "2030-03-17T17:46:40Z", - "detail": None, - }, - { - "label": "Weekly", - "used_percent": 40.0, - "remaining_percent": 60.0, - "reset_at": "2030-03-24T12:30:00Z", - "detail": None, - }, - ], - "details": ["Credits balance: $12.50"], - "unavailable_reason": None, - "fetched_at": snapshot["pool"]["credentials"][1]["fetched_at"], + "plan": None, + "windows": [], + "details": [], + "unavailable_reason": "Credential pool marked this credential exhausted.", + "fetched_at": None, }, ], }