v3 M2+: Codex live usage endpoint (fresh) with rollout fallback

Adopt the live ChatGPT usage API (found via github.com/rygel/AIUsageTracker, MIT)
so Codex % is fresh every poll instead of only as fresh as the last local
rollout write:

    GET https://chatgpt.com/backend-api/wham/usage
    Authorization: Bearer <access_token from $CODEX_HOME/auth.json tokens>
    ChatGPT-Account-Id: <account_id>
    -> rate_limit.primary_window / secondary_window {used_percent, reset_at}, plan_type

poll() now tries the live endpoint first and falls back to the rollout snapshot
when the token is missing/expired/offline (we read the current OAuth token but
don't refresh it). Live-first fixes the staleness of the rollout-only approach:
verified live s=1% (5h, resets 300m), w=0% (7d, resets 10080m), st="Plus" — a full
fresh window vs the old stale past-reset heuristic.

+4 tests (live preferred, limit_reached->limited, 401->rollout fallback, no-auth
->rollout). 126 passed, 2 Linux-only failures (baseline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wenil
2026-07-10 09:02:12 +03:00
co-authored by Claude Opus 4.8
parent 4eeb6e242b
commit d1cc4670b1
2 changed files with 166 additions and 12 deletions
+75
View File
@@ -10,6 +10,8 @@ import json
import os
import time
import httpx
from daemon.providers.openai_codex import OpenAICodexProvider
@@ -103,3 +105,76 @@ def test_skips_file_without_rate_limits(tmp_path):
def test_no_sessions_dir_reports_no_data(tmp_path):
st = _run(OpenAICodexProvider(codex_home=str(tmp_path)).poll())
assert st.ok is False and st.st == "no data"
# -- live ChatGPT usage endpoint (preferred over rollouts) --------------------
def _write_auth(home, access_token="tok", account_id="acct"):
home.mkdir(parents=True, exist_ok=True)
(home / "auth.json").write_text(
json.dumps({"tokens": {"access_token": access_token, "account_id": account_id}}),
encoding="utf-8")
def _live_resp(pri_pct, pri_reset, sec_pct, sec_reset, plan="plus", limit_reached=False):
return httpx.Response(200, json={
"plan_type": plan,
"rate_limit": {"allowed": True, "limit_reached": limit_reached,
"primary_window": {"used_percent": pri_pct, "reset_at": pri_reset},
"secondary_window": {"used_percent": sec_pct, "reset_at": sec_reset}},
})
def test_live_usage_preferred_over_rollout(tmp_path):
_write_auth(tmp_path)
# a rollout is also present, but the live endpoint must win
now = time.time()
_write_rollout(tmp_path, "a", [_rl(99.0, now + 600, 99.0, now + 600)])
seen = {}
def handler(req: httpx.Request) -> httpx.Response:
seen["auth"] = req.headers.get("authorization")
seen["acct"] = req.headers.get("chatgpt-account-id")
seen["url"] = str(req.url)
return _live_resp(5, now + 3600, 20, now + 7200)
p = OpenAICodexProvider(codex_home=str(tmp_path), transport=httpx.MockTransport(handler))
st = _run(p.poll())
assert st.ok is True and st.st == "Plus"
assert st.s == 5.0 and 58 <= st.sr <= 60
assert st.w == 20.0 and 118 <= st.wr <= 120
assert seen["auth"] == "Bearer tok" and seen["acct"] == "acct"
assert seen["url"] == "https://chatgpt.com/backend-api/wham/usage"
def test_live_limit_reached_is_limited(tmp_path):
_write_auth(tmp_path)
now = time.time()
def handler(req):
return _live_resp(100, now + 60, 50, now + 600, limit_reached=True)
p = OpenAICodexProvider(codex_home=str(tmp_path), transport=httpx.MockTransport(handler))
st = _run(p.poll())
assert st.st == "limited" and st.ok is True
def test_falls_back_to_rollout_on_token_expiry(tmp_path):
_write_auth(tmp_path)
now = time.time()
_write_rollout(tmp_path, "a", [_rl(64.0, now + 600, 0.0, now + 600)])
def handler(req):
return httpx.Response(401, json={"detail": "unauthorized"})
p = OpenAICodexProvider(codex_home=str(tmp_path), transport=httpx.MockTransport(handler))
st = _run(p.poll())
assert st.ok is True and st.s == 64.0 # from the rollout snapshot
def test_no_auth_uses_rollout_without_network(tmp_path):
now = time.time()
_write_rollout(tmp_path, "a", [_rl(30.0, now + 600, 0.0, now + 600)])
# no auth.json => live path skipped entirely; no transport needed
st = _run(OpenAICodexProvider(codex_home=str(tmp_path)).poll())
assert st.ok is True and st.s == 30.0