Add Codex API rate limit monitoring and notifications#261
Conversation
Add support for fetching Codex rate limit usage data from the OpenAI API (~/.codex/auth.json) alongside existing Anthropic usage tracking. The daemon now loads Codex auth credentials, fetches usage every 10 minutes (same cache TTL as Anthropic), and sends the data to the server via POST /api/v1/codex-usage. https://claude.ai/code/session_01FPxeHEA8w2cC136GFpi3to
There was a problem hiding this comment.
Code Review
This pull request introduces Codex rate limit tracking to the CCInfoTimerService. It adds functionality to load local authentication, fetch usage data from the OpenAI API, cache the results, and report them to a central server. The review feedback suggests clearing the lastError field when the timer stops to prevent stale error displays and recommends parallelizing the Anthropic and Codex rate limit fetches to improve efficiency and robustness against timeouts.
| s.codexRateLimitCache.mu.Lock() | ||
| s.codexRateLimitCache.usage = nil | ||
| s.codexRateLimitCache.fetchedAt = time.Time{} | ||
| s.codexRateLimitCache.lastAttemptAt = time.Time{} | ||
| s.codexRateLimitCache.mu.Unlock() |
There was a problem hiding this comment.
When stopping the timer due to inactivity, the lastError field in the Codex rate limit cache should be cleared along with other fields. This ensures that stale error messages aren't displayed when the service resumes activity and a new fetch is pending.
| s.codexRateLimitCache.mu.Lock() | |
| s.codexRateLimitCache.usage = nil | |
| s.codexRateLimitCache.fetchedAt = time.Time{} | |
| s.codexRateLimitCache.lastAttemptAt = time.Time{} | |
| s.codexRateLimitCache.mu.Unlock() | |
| s.codexRateLimitCache.mu.Lock() | |
| s.codexRateLimitCache.usage = nil | |
| s.codexRateLimitCache.fetchedAt = time.Time{} | |
| s.codexRateLimitCache.lastAttemptAt = time.Time{} | |
| s.codexRateLimitCache.lastError = "" | |
| s.codexRateLimitCache.mu.Unlock() |
| s.fetchRateLimit(ctx) | ||
| s.fetchCodexRateLimit(ctx) |
There was a problem hiding this comment.
The Anthropic and Codex rate limit fetches are executed sequentially within a shared 10-second timeout. If the first fetch takes a significant amount of time or times out, the second fetch may be delayed or skipped entirely due to context expiration. Since these fetches are independent, running them in parallel would be more efficient and robust. This also applies to the fetch logic inside the ticker loop (lines 206-207).
var wg sync.WaitGroup
wg.Add(2)
go func() { defer wg.Done(); s.fetchRateLimit(ctx) }()
go func() { defer wg.Done(); s.fetchCodexRateLimit(ctx) }()
wg.Wait()
Codecov Report❌ Patch coverage is
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 2 files with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 16df58bdd1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if auth.TokenData == nil || auth.TokenData.AccessToken == "" { | ||
| return nil, fmt.Errorf("no access token found in codex auth") | ||
| } |
There was a problem hiding this comment.
Support API-key auth mode in loadCodexAuth
This loader always errors unless tokenData.accessToken is present, even though the parsed schema includes authMode and apiKey. When ~/.codex/auth.json is in API-key mode (no tokenData), Codex usage fetching will always return auth and the daemon never sends Codex usage to /api/v1/codex-usage for those users.
Useful? React with 👍 / 👎.
| ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | ||
| defer cancel() | ||
| s.fetchRateLimit(ctx) | ||
| s.fetchCodexRateLimit(ctx) |
There was a problem hiding this comment.
Decouple Codex fetch timeout from Anthropic fetch
The periodic loop runs Anthropic and Codex fetches sequentially under a single 10-second context. If the Anthropic request stalls or consumes most of that budget, fetchCodexRateLimit starts with an already-expired (or near-expired) context and fails; because lastAttemptAt is still updated, Codex retries are suppressed for the full cache TTL. This makes Codex monitoring unreliable during Anthropic latency/outage windows.
Useful? React with 👍 / 👎.
| s.fetchRateLimit(ctx) | ||
| s.fetchCodexRateLimit(ctx) |
There was a problem hiding this comment.
🟡 Shared 10-second context timeout insufficient for two sequential network calls
Both fetchRateLimit(ctx) and fetchCodexRateLimit(ctx) are called sequentially within the same goroutine sharing a single 10-second context timeout (daemon/cc_info_timer.go:180-183 and daemon/cc_info_timer.go:204-207). Each function makes an HTTP call with a 5-second client timeout, so in the worst case both calls together need ~10 seconds just for HTTP, leaving zero margin for other operations (macOS Keychain exec.Command in anthropic_ratelimit.go:76, file I/O in codex_ratelimit.go:68). If fetchRateLimit consumes most of the context budget, fetchCodexRateLimit will fail with a context deadline exceeded error. Critically, fetchCodexRateLimit records lastAttemptAt before the API call (daemon/cc_info_timer.go:582-584), so a context-induced failure triggers the 10-minute TTL backoff (codexUsageCacheTTL), preventing retry and leaving codex rate limit data stale for up to 10 minutes.
Prompt for agents
The fetchRateLimit and fetchCodexRateLimit calls are sequential inside a single goroutine sharing one 10-second context. Each has an internal 5-second HTTP client timeout, so in the worst case, the budget is exactly consumed by the first call, leaving nothing for the second. This is made worse because fetchCodexRateLimit records lastAttemptAt before attempting the fetch, so a context-deadline failure triggers a 10-minute TTL backoff.
Approach 1: Give each fetch its own independent context with a 10-second timeout. Replace the single shared context with two separate context.WithTimeout calls, one for fetchRateLimit and one for fetchCodexRateLimit.
Approach 2: Increase the shared context timeout to 20 seconds to accommodate both calls.
Approach 3: Run both fetches concurrently (each with its own context) inside the goroutine, since they are independent operations. This would also improve latency.
The same pattern appears twice: once in the immediate-fetch block at lines 175-184 and once in the ticker loop at lines 199-208. Both should be updated.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
This PR adds support for monitoring OpenAI Codex API rate limits and sending usage data to the ShellTime server for scheduling push notifications when rate limits reset.
Key Changes
New file
daemon/codex_ratelimit.go: Implements Codex rate limit monitoring with:loadCodexAuth(): Reads authentication credentials from~/.codex/auth.jsonfetchCodexUsage(): Calls the Codex usage API to retrieve current rate limit datashortenCodexAPIError(): Converts API errors to short status strings for displayUpdated
daemon/cc_info_timer.go:codexRateLimitCachefield toCCInfoTimerServicefor caching rate limit datafetchCodexRateLimit(): Periodically fetches Codex rate limits with 10-minute TTL cachingsendCodexUsageToServer(): Sends rate limit data to ShellTime server via/api/v1/codex-usageendpointGetCachedCodexRateLimit()andGetCachedCodexRateLimitError()accessors for retrieving cached dataImplementation Details
~/.codex/auth.jsonlocation used by OpenAI CLI toolshttps://claude.ai/code/session_01FPxeHEA8w2cC136GFpi3to