feat(codex): redeem banked usage-limit resets via /usage reset (#64280)

OpenAI lets ChatGPT-plan Codex users bank rate-limit reset credits, but
until now they could only be redeemed from the Codex CLI/app or the
website. This wires the same backend API into Hermes:

- /usage on the openai-codex provider now shows "You have N resets
  banked - use /usage reset to activate" (parsed from the
  rate_limit_reset_credits field the /usage endpoint already returns).
- New /usage reset subcommand (CLI + gateway) redeems one banked
  credit via POST .../rate-limit-reset-credits/consume with a UUID
  idempotency key, mirroring codex-rs backend-client semantics
  (PathStyle /wham vs /api/codex, ChatGPT-Account-Id header,
  reset/nothing_to_reset/no_credit/already_redeemed outcomes).
- Guard: redemption is refused while no rate-limit window is fully
  exhausted, since a banked reset restores the FULL 5h + weekly
  allowance and spending it early wastes it. /usage reset --force
  overrides. Zero banked credits and non-codex providers are refused
  with clear messages; nothing_to_reset reports the credit was NOT
  spent.
- i18n: new gateway.usage.unknown_subcommand / reset_wrong_provider
  keys across all 16 locales; docs updated (cli.md, messaging index).

Tested with unit tests plus a real-socket E2E against a local fake
Codex backend exercising redeem/guard/force and the /usage hint.
This commit is contained in:
Teknium 2026-07-14 03:23:19 -07:00 committed by GitHub
parent 8582f35d96
commit 89bd0fba90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 579 additions and 7 deletions

49
cli.py
View file

@ -8722,7 +8722,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
elif canonical == "compress":
self._manual_compress(cmd_original)
elif canonical == "usage":
self._show_usage()
self._handle_usage_command(cmd_original)
elif canonical == "credits":
self._show_credits()
elif canonical == "billing":
@ -9623,6 +9623,53 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
def _handle_usage_command(self, cmd_original: str):
"""Dispatch `/usage [reset [--force]]`.
Bare `/usage` keeps the classic display. `/usage reset` redeems one
banked Codex rate-limit reset credit (guarded: refuses when limits
aren't exhausted unless --force).
"""
parts = cmd_original.split()
args = [p.lower() for p in parts[1:]]
if args and args[0] == "reset":
self._usage_reset(force="--force" in args[1:])
return
if args:
print(f" Unknown /usage subcommand: {' '.join(parts[1:])}. Try /usage or /usage reset [--force].")
return
self._show_usage()
def _usage_reset(self, force: bool = False):
"""`/usage reset [--force]` — redeem one banked Codex reset credit."""
provider = (
(getattr(self.agent, "provider", None) if self.agent else None)
or getattr(self, "provider", None)
)
normalized = str(provider or "").strip().lower()
if normalized != "openai-codex":
print(" Banked usage resets are only available on the openai-codex provider.")
print(" Switch with `/model` or `hermes auth` first.")
return
base_url = (getattr(self.agent, "base_url", None) if self.agent else None) or getattr(self, "base_url", None)
api_key = (getattr(self.agent, "api_key", None) if self.agent else None) or getattr(self, "api_key", None)
from agent.account_usage import redeem_codex_reset_credit
print(" ⏳ Checking banked reset credits...")
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as _pool:
try:
result = _pool.submit(
redeem_codex_reset_credit,
base_url=base_url,
api_key=api_key,
force=force,
).result(timeout=45.0)
except concurrent.futures.TimeoutError:
print(" ❌ Timed out talking to the Codex backend — try again shortly.")
return
print(f" {result.message}")
def _show_usage(self):
"""Rate limits + session token usage (when a live agent exists) + Nous credits.