hermes - ✅(Solved) Fix /usage shows no Codex account quota; _read_codex_tokens() ignores credential_pool [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#15167Fetched 2026-04-25 06:24:04
View on GitHub
Comments
0
Participants
1
Timeline
7
Reactions
0
Participants
Timeline (top)
labeled ×6cross-referenced ×1

After authenticating via hermes auth add openai-codex --type oauth, the /usage slash command in the Telegram gateway never shows the ChatGPT subscription rate-limit window (5h / weekly). Tracing the call shows two separate gaps:

  1. _read_codex_tokens() only reads from the legacy auth.json -> providers["openai-codex"] slot. hermes auth add ... --type oauth actually writes to auth.json -> credential_pool["openai-codex"]. Result: every call site that depends on _read_codex_tokens() (including agent/account_usage.py::_fetch_codex_account_usage()) raises AuthError(code=codex_auth_missing) even though the credential exists.
  2. gateway/run.py::_handle_usage_command() only attempts the account-usage fetch when provider is resolved from a live agent or persisted billing row. When the agent is evicted from the cache and state.db has no billing_provider row for the session, the command falls through to the "Session Info" stub with the _(Detailed usage available after the first agent response)_ notice and never tries the auth pool.

Combined, this means a fresh-after-login user, or any user whose agent has been evicted, gets a stub message — the actual ChatGPT subscription quota is reachable but not surfaced.

Affected version: v0.11.0 (latest at time of report).

Error Message

provider = provider or persisted.get("billing_provider") base_url = base_url or persisted.get("billing_base_url")

  • When no provider could be resolved from the agent or session DB, try

  • known OAuth providers from the auth pool so /usage still surfaces

  • account quotas (e.g. ChatGPT codex subscription windows) for users

  • whose agent has been evicted from the cache.

  • if not provider:
  •    try:
  •        from hermes_cli.auth import read_credential_pool
  •        for candidate in ("openai-codex", "anthropic"):
  •            if read_credential_pool(candidate):
  •                provider = candidate
  •                break
  •    except Exception:
  •        pass
  • Fetch account usage off the event loop so slow provider APIs don't

    block the gateway. Failures are non-fatal -- account_lines stays [].

    account_lines: list[str] = [] if provider:

Root Cause

After authenticating via hermes auth add openai-codex --type oauth, the /usage slash command in the Telegram gateway never shows the ChatGPT subscription rate-limit window (5h / weekly). Tracing the call shows two separate gaps:

  1. _read_codex_tokens() only reads from the legacy auth.json -> providers["openai-codex"] slot. hermes auth add ... --type oauth actually writes to auth.json -> credential_pool["openai-codex"]. Result: every call site that depends on _read_codex_tokens() (including agent/account_usage.py::_fetch_codex_account_usage()) raises AuthError(code=codex_auth_missing) even though the credential exists.
  2. gateway/run.py::_handle_usage_command() only attempts the account-usage fetch when provider is resolved from a live agent or persisted billing row. When the agent is evicted from the cache and state.db has no billing_provider row for the session, the command falls through to the "Session Info" stub with the _(Detailed usage available after the first agent response)_ notice and never tries the auth pool.

Combined, this means a fresh-after-login user, or any user whose agent has been evicted, gets a stub message — the actual ChatGPT subscription quota is reachable but not surfaced.

Affected version: v0.11.0 (latest at time of report).

Fix Action

Fix / Workaround

Two minimal patches; together they make /usage show the Codex window unconditionally when an OAuth credential is present.

Verified behavior with both patches

  • A more principled fix might unify legacy providers and credential_pool reads behind a single accessor, since this skew likely affects other providers too (anthropic, openrouter, qwen) that have both legacy and pooled write paths. The patch above is the minimal change to unblock the user-facing /usage regression for Codex; happy to expand if maintainers prefer.
  • Patch 2 only handles the no-agent fallback. The "agent cached but agent.provider is unset" case may also exist; not encountered in this repro.

PR fix notes

PR #15173: fix(auth/gateway): surface Codex OAuth credential from credential_pool to /usage (#15167)

Description (problem / solution / changelog)

What does this PR do?

Fixes #15167. When a user runs `hermes auth add openai-codex --type oauth`, the OAuth tokens land in `auth.json -> credential_pool["openai-codex"]` — the newer pooled slot — not the legacy `providers["openai-codex"]` slot. Two gaps then conspired to make `/usage` in the gateway surface nothing, even with a valid OAuth credential on disk:

Gap 1 — `_read_codex_tokens` only reads the legacy slot. Every downstream Codex consumer (`agent/account_usage.py::_fetch_codex_account_usage`, refresh paths) funnels through this reader. A fresh OAuth add bypasses the legacy slot, so the reader raised `AuthError(code=codex_auth_missing)` despite the credential existing, and `fetch_account_usage` silently returned `None`.

Gap 2 — `/usage` handler only probed live/cached agent + session DB. A fresh-after-login user whose agent has been evicted (or never instantiated yet) has neither a live `agent.provider` nor a `billing_provider` row on the session DB. The handler fell through to the "Session Info" stub and never even attempted the account-usage call.

Net user-visible effect: `/usage` shows the stub message (`(Detailed usage available after the first agent response)`) instead of the ChatGPT subscription window (5h / weekly) even though the credential is valid.

Fix

Two surgical patches that land together:

1. `hermes_cli/auth.py::_read_codex_tokens` — fall back to credential_pool

When the legacy slot is empty, pick the first pool entry with `auth_type == "oauth"` and a non-empty `access_token`, then project it onto the same `{"tokens": {...}, "last_refresh": ..., "base_url": ...}` shape the legacy slot returns. The rest of the module stays unaware of where the credential came from.

Invariants preserved:

  • Legacy slot still wins when both slots are populated (backward-compat for any hybrid state left by tooling mid-migration)
  • Non-OAuth pool entries (`auth_type: api_key` and friends) are skipped — they lack `refresh_token` and would crash the refresh flow
  • Malformed OAuth entries (missing `access_token`) fall through to the existing `codex_auth_missing` error path rather than being silently returned

2. `gateway/run.py::_handle_usage_command` — probe credential_pool when no provider resolved

After the existing `live agent → cached agent → session DB billing row` resolution chain, probe the auth `credential_pool` for a small allow-list of account-usage-capable providers when `provider` is still unset. First hit wins. Probe errors are swallowed — strictly best-effort; the handler always returns a string rather than propagating a `RuntimeError` from a corrupted auth.json.

The allow-list is a new module-level `_USAGE_ACCOUNT_PROVIDERS = ("openai-codex", "anthropic")` constant so future providers with account-usage APIs can be added in one obvious place instead of sprinkled inline.

Verified behavior after fix

``` 📈 Account limits Provider: openai-codex (Plus) Session: 82% remaining (18% used) • resets in 22m Weekly: 91% remaining (9% used) • resets in 4d 5h ```

Appears even when no agent is in cache.

Test coverage (9 new tests, all green locally)

`tests/hermes_cli/test_auth_codex_provider.py` — 6 cases:

  • `test_read_codex_tokens_falls_back_to_credential_pool_oauth_entry` — the core fix: OAuth in pool, legacy empty → pool wins
  • `test_read_codex_tokens_prefers_legacy_slot_over_pool` — hybrid state: both populated → legacy still wins (backward-compat guard)
  • `test_read_codex_tokens_skips_non_oauth_pool_entries` — `auth_type: api_key` in pool doesn't get treated as a Codex OAuth token
  • `test_read_codex_tokens_skips_pool_entries_missing_access_token` — malformed entries don't silently pass through
  • `test_read_codex_tokens_picks_first_valid_oauth_entry_when_multiple` — deterministic order when pool has several OAuth entries
  • `test_read_codex_tokens_empty_pool_raises_auth_error` — both slots empty still raises, user gets the "run hermes auth" guidance

`tests/gateway/test_usage_command.py` — 3 cases:

  • `test_usage_command_probes_credential_pool_when_no_agent_and_no_billing_row` — the #15167 repro end-to-end
  • `test_usage_command_stops_probing_pool_on_first_hit` — iteration short-circuits on first provider with a stored credential
  • `test_usage_command_pool_probe_errors_fall_through_gracefully` — handler swallows `RuntimeError` from a corrupted auth.json and still returns a string

Related Issue

Fixes #15167

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✅ Tests (adding or improving test coverage)

Test plan

  • `tests/hermes_cli/test_auth_codex_provider.py` — 22 passed (16 pre-existing + 6 new)
  • `tests/gateway/test_usage_command.py` — my 3 new tests pass; the pre-existing tests that fail locally (`requests` missing in my venv, unrelated to this PR) are green on clean `origin/main` per CI history
  • No behavioural change when the legacy `providers` slot is populated — hybrid-state backward-compat explicitly tested
  • The `_USAGE_ACCOUNT_PROVIDERS` tuple is a single-line extension point — adding anthropic/openrouter OAuth probe in future is a 1-char edit

Not in scope (explicit follow-up candidates per the reporter's own notes)

  • Unifying the legacy `providers` and `credential_pool` reads behind a single accessor — probably worthwhile across anthropic/openrouter/qwen too since they have the same legacy-vs-pool skew, but a much bigger surface than what this user-facing regression needs to unblock.
  • The "agent cached but `agent.provider` is unset" case — not encountered in the reported repro; separate change if it turns out to be real.

Changed files

  • gateway/run.py (modified, +27/-0)
  • hermes_cli/auth.py (modified, +52/-1)
  • tests/gateway/test_usage_command.py (modified, +158/-0)
  • tests/hermes_cli/test_auth_codex_provider.py (modified, +180/-0)

Code Example

# 1. Fresh install
hermes setup
hermes auth add openai-codex --type oauth   # complete OAuth device flow
hermes gateway install && hermes gateway start

# 2. Send /usage in Telegram (no prior chat turn, OR after agent eviction)
#    Output:
#      📊 Session Info
#      Messages: N
#      Estimated context: ~M tokens
#      _(Detailed usage available after the first agent response)_

# 3. Verify token IS present in the pool but not legacy slot
python -c "import json; a=json.load(open('~/.hermes/auth.json'.replace('~', '/Users/me')));
print('legacy:', a['providers'].get('openai-codex'));
print('pool len:', len(a['credential_pool'].get('openai-codex') or []))"
# legacy: None
# pool len: 1

# 4. Direct call confirms the lookup gap
python -c "from agent.account_usage import fetch_account_usage; print(fetch_account_usage('openai-codex'))"
# None  (silently — internally raises codex_auth_missing AuthError)

---

@@ def _read_codex_tokens(*, _lock: bool = True) -> Dict[str, Any]:
     state = _load_provider_state(auth_store, "openai-codex")
     if not state:
+        # `hermes auth add --type oauth` writes to credential_pool, not the
+        # legacy providers slot. Fall back to the pool so all codex consumers
+        # (account-usage fetch, token refresh, etc.) see the OAuth credential.
+        pool_entries = (auth_store.get("credential_pool") or {}).get("openai-codex") or []
+        oauth_entry = next(
+            (e for e in pool_entries
+             if isinstance(e, dict) and e.get("auth_type") == "oauth" and e.get("access_token")),
+            None,
+        )
+        if oauth_entry:
+            state = {
+                "tokens": {
+                    "access_token": oauth_entry.get("access_token"),
+                    "refresh_token": oauth_entry.get("refresh_token"),
+                    "id_token": oauth_entry.get("id_token"),
+                    "account_id": oauth_entry.get("account_id"),
+                },
+                "last_refresh": oauth_entry.get("last_refresh"),
+                "base_url": oauth_entry.get("base_url"),
+            }
+    if not state:
         raise AuthError(
             "No Codex credentials stored. Run `hermes auth` to authenticate.",
             ...
         )

---

provider = provider or persisted.get("billing_provider")
         base_url = base_url or persisted.get("billing_base_url")

+    # When no provider could be resolved from the agent or session DB, try
+    # known OAuth providers from the auth pool so /usage still surfaces
+    # account quotas (e.g. ChatGPT codex subscription windows) for users
+    # whose agent has been evicted from the cache.
+    if not provider:
+        try:
+            from hermes_cli.auth import read_credential_pool
+            for candidate in ("openai-codex", "anthropic"):
+                if read_credential_pool(candidate):
+                    provider = candidate
+                    break
+        except Exception:
+            pass
+
     # Fetch account usage off the event loop so slow provider APIs don't
     # block the gateway. Failures are non-fatal -- account_lines stays [].
     account_lines: list[str] = []
     if provider:

---

📈 Account limits
Provider: openai-codex (Plus)
Session: 82% remaining (18% used) • resets in 22m
Weekly:  91% remaining (9% used)  • resets in 4d 5h
RAW_BUFFERClick to expand / collapse

/usage shows no Codex account quota; _read_codex_tokens() ignores credential_pool

Summary

After authenticating via hermes auth add openai-codex --type oauth, the /usage slash command in the Telegram gateway never shows the ChatGPT subscription rate-limit window (5h / weekly). Tracing the call shows two separate gaps:

  1. _read_codex_tokens() only reads from the legacy auth.json -> providers["openai-codex"] slot. hermes auth add ... --type oauth actually writes to auth.json -> credential_pool["openai-codex"]. Result: every call site that depends on _read_codex_tokens() (including agent/account_usage.py::_fetch_codex_account_usage()) raises AuthError(code=codex_auth_missing) even though the credential exists.
  2. gateway/run.py::_handle_usage_command() only attempts the account-usage fetch when provider is resolved from a live agent or persisted billing row. When the agent is evicted from the cache and state.db has no billing_provider row for the session, the command falls through to the "Session Info" stub with the _(Detailed usage available after the first agent response)_ notice and never tries the auth pool.

Combined, this means a fresh-after-login user, or any user whose agent has been evicted, gets a stub message — the actual ChatGPT subscription quota is reachable but not surfaced.

Affected version: v0.11.0 (latest at time of report).

Reproduction

# 1. Fresh install
hermes setup
hermes auth add openai-codex --type oauth   # complete OAuth device flow
hermes gateway install && hermes gateway start

# 2. Send /usage in Telegram (no prior chat turn, OR after agent eviction)
#    Output:
#      📊 Session Info
#      Messages: N
#      Estimated context: ~M tokens
#      _(Detailed usage available after the first agent response)_

# 3. Verify token IS present in the pool but not legacy slot
python -c "import json; a=json.load(open('~/.hermes/auth.json'.replace('~', '/Users/me')));
print('legacy:', a['providers'].get('openai-codex'));
print('pool len:', len(a['credential_pool'].get('openai-codex') or []))"
# legacy: None
# pool len: 1

# 4. Direct call confirms the lookup gap
python -c "from agent.account_usage import fetch_account_usage; print(fetch_account_usage('openai-codex'))"
# None  (silently — internally raises codex_auth_missing AuthError)

Suggested fix

Two minimal patches; together they make /usage show the Codex window unconditionally when an OAuth credential is present.

1. hermes_cli/auth.py — fall back to credential_pool in _read_codex_tokens()

@@ def _read_codex_tokens(*, _lock: bool = True) -> Dict[str, Any]:
     state = _load_provider_state(auth_store, "openai-codex")
     if not state:
+        # `hermes auth add --type oauth` writes to credential_pool, not the
+        # legacy providers slot. Fall back to the pool so all codex consumers
+        # (account-usage fetch, token refresh, etc.) see the OAuth credential.
+        pool_entries = (auth_store.get("credential_pool") or {}).get("openai-codex") or []
+        oauth_entry = next(
+            (e for e in pool_entries
+             if isinstance(e, dict) and e.get("auth_type") == "oauth" and e.get("access_token")),
+            None,
+        )
+        if oauth_entry:
+            state = {
+                "tokens": {
+                    "access_token": oauth_entry.get("access_token"),
+                    "refresh_token": oauth_entry.get("refresh_token"),
+                    "id_token": oauth_entry.get("id_token"),
+                    "account_id": oauth_entry.get("account_id"),
+                },
+                "last_refresh": oauth_entry.get("last_refresh"),
+                "base_url": oauth_entry.get("base_url"),
+            }
+    if not state:
         raise AuthError(
             "No Codex credentials stored. Run `hermes auth` to authenticate.",
             ...
         )

2. gateway/run.py::_handle_usage_command — try auth pool when no provider resolved

         provider = provider or persisted.get("billing_provider")
         base_url = base_url or persisted.get("billing_base_url")

+    # When no provider could be resolved from the agent or session DB, try
+    # known OAuth providers from the auth pool so /usage still surfaces
+    # account quotas (e.g. ChatGPT codex subscription windows) for users
+    # whose agent has been evicted from the cache.
+    if not provider:
+        try:
+            from hermes_cli.auth import read_credential_pool
+            for candidate in ("openai-codex", "anthropic"):
+                if read_credential_pool(candidate):
+                    provider = candidate
+                    break
+        except Exception:
+            pass
+
     # Fetch account usage off the event loop so slow provider APIs don't
     # block the gateway. Failures are non-fatal -- account_lines stays [].
     account_lines: list[str] = []
     if provider:

Verified behavior with both patches

📈 Account limits
Provider: openai-codex (Plus)
Session: 82% remaining (18% used) • resets in 22m
Weekly:  91% remaining (9% used)  • resets in 4d 5h

Output appears even when no agent is in cache.

Notes / open questions

  • A more principled fix might unify legacy providers and credential_pool reads behind a single accessor, since this skew likely affects other providers too (anthropic, openrouter, qwen) that have both legacy and pooled write paths. The patch above is the minimal change to unblock the user-facing /usage regression for Codex; happy to expand if maintainers prefer.
  • Patch 2 only handles the no-agent fallback. The "agent cached but agent.provider is unset" case may also exist; not encountered in this repro.

Happy to send a PR with the above two patches plus tests if useful.

extent analysis

TL;DR

The most likely fix involves updating the _read_codex_tokens() function to fall back to the credential_pool when the legacy providers slot is empty, and modifying the _handle_usage_command() function to attempt to resolve the provider from the auth pool when no provider is resolved from the agent or session DB.

Guidance

  • Update the _read_codex_tokens() function to check the credential_pool for OAuth credentials when the legacy providers slot is empty.
  • Modify the _handle_usage_command() function to attempt to resolve the provider from the auth pool when no provider is resolved from the agent or session DB.
  • Consider unifying the legacy providers and credential_pool reads behind a single accessor to address potential issues with other providers.
  • Test the changes to ensure they resolve the issue and do not introduce new problems.

Example

The provided diff patches demonstrate the necessary changes:

# In hermes_cli/auth.py
+        pool_entries = (auth_store.get("credential_pool") or {}).get("openai-codex") or []
+        oauth_entry = next(
+            (e for e in pool_entries
+             if isinstance(e, dict) and e.get("auth_type") == "oauth" and e.get("access_token")),
+            None,
+        )
+        if oauth_entry:
+            state = {
+                "tokens": {
+                    "access_token": oauth_entry.get("access_token"),
+                    "refresh_token": oauth_entry.get("refresh_token"),
+                    "id_token": oauth_entry.get("id_token"),
+                    "account_id": oauth_entry.get("account_id"),
+                },
+                "last_refresh": oauth_entry.get("last_refresh"),
+                "base_url": oauth_entry.get("base_url"),
+            }

# In gateway/run.py
+    if not provider:
+        try:
+            from hermes_cli.auth import

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING