feat(daemon): add Anthropic rate limit quota to cc statusline#213
feat(daemon): add Anthropic rate limit quota to cc statusline#213
Conversation
Display 5-hour and 7-day session quota utilization from the Anthropic OAuth usage API in the cc statusline output. The daemon fetches and caches this data (10-minute TTL) using the same lazy-fetch background refresh pattern as git branch and today cost. OAuth token is read fresh from macOS Keychain on each fetch; non-macOS platforms gracefully omit these fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Summary of ChangesHello @AnnatarHe, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a significant enhancement by integrating Anthropic API rate limit utilization directly into the Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request adds a useful feature to display Anthropic API rate limit utilization in the cc statusline. The implementation is well-structured, with caching, asynchronous background updates, and graceful degradation for non-macOS platforms. The code is generally high quality. My review includes a few suggestions to improve maintainability and testability by replacing magic numbers with constants, using a native library for keychain access, and improving the test strategy for the API fetching logic. Overall, great work on this feature.
commands/cc_statusline.go
Outdated
| case maxUtil >= 80: | ||
| return color.Red.Sprint(text) | ||
| case maxUtil >= 50: | ||
| return color.Yellow.Sprint(text) |
There was a problem hiding this comment.
The utilization thresholds 80 and 50 are used as magic numbers. To improve readability and maintainability, consider defining them as named constants at the package level. This makes their purpose clear and simplifies future updates.
For example:
const (
quotaHighUtilizationThreshold = 80.0
quotaMediumUtilizationThreshold = 50.0
)| return "", nil | ||
| } | ||
|
|
||
| out, err := exec.Command("security", "find-generic-password", "-s", "Claude Code-credentials", "-w").Output() |
There was a problem hiding this comment.
|
|
||
| // fetchAnthropicUsage calls the Anthropic OAuth usage API and returns rate limit data. | ||
| func fetchAnthropicUsage(ctx context.Context, token string) (*AnthropicRateLimitData, error) { | ||
| req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.anthropic.com/api/oauth/usage", nil) |
There was a problem hiding this comment.
| func TestFetchAnthropicUsage_Success(t *testing.T) { | ||
| server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| assert.Equal(t, "GET", r.Method) | ||
| assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization")) | ||
| assert.Equal(t, "oauth-2025-04-20", r.Header.Get("anthropic-beta")) | ||
|
|
||
| resp := anthropicUsageResponse{ | ||
| FiveHour: anthropicUsageBucket{ | ||
| Utilization: 0.45, | ||
| ResetsAt: "2025-01-15T12:00:00Z", | ||
| }, | ||
| SevenDay: anthropicUsageBucket{ | ||
| Utilization: 0.23, | ||
| ResetsAt: "2025-01-20T00:00:00Z", | ||
| }, | ||
| } | ||
| w.Header().Set("Content-Type", "application/json") | ||
| json.NewEncoder(w).Encode(resp) | ||
| })) | ||
| defer server.Close() | ||
|
|
||
| // We need to test with the real function but override the URL. | ||
| // Since fetchAnthropicUsage uses a hardcoded URL, we test the parsing logic | ||
| // by calling the test server directly. | ||
| req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, nil) | ||
| assert.NoError(t, err) | ||
| req.Header.Set("Authorization", "Bearer test-token") | ||
| req.Header.Set("anthropic-beta", "oauth-2025-04-20") | ||
|
|
||
| resp, err := http.DefaultClient.Do(req) | ||
| assert.NoError(t, err) | ||
| defer resp.Body.Close() | ||
|
|
||
| var usage anthropicUsageResponse | ||
| err = json.NewDecoder(resp.Body).Decode(&usage) | ||
| assert.NoError(t, err) | ||
|
|
||
| assert.Equal(t, 0.45, usage.FiveHour.Utilization) | ||
| assert.Equal(t, "2025-01-15T12:00:00Z", usage.FiveHour.ResetsAt) | ||
| assert.Equal(t, 0.23, usage.SevenDay.Utilization) | ||
| assert.Equal(t, "2025-01-20T00:00:00Z", usage.SevenDay.ResetsAt) | ||
| } |
There was a problem hiding this comment.
This test is named TestFetchAnthropicUsage_Success, but it doesn't call the fetchAnthropicUsage function. Because the URL is hardcoded in fetchAnthropicUsage, this test re-implements the HTTP request logic instead of testing the actual production function.
To enable proper unit testing, consider refactoring fetchAnthropicUsage to accept the URL as a parameter. This would allow you to pass the test server's URL during testing.
Example:
// In anthropic_ratelimit.go
func fetchAnthropicUsage(ctx context.Context, token string, url string) (*AnthropicRateLimitData, error) { /* ... */ }
// In test
data, err := fetchAnthropicUsage(context.Background(), "test-token", server.URL)
// assertions...
Codecov Report❌ Patch coverage is
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 1 file with indirect coverage changes 🚀 New features to boost your workflow:
|
Wrap the quota section (🚦) in OSC 8 hyperlink escape sequences so it becomes a clickable link to https://claude.ai/settings/usage in terminals that support it (iTerm2, Kitty, WezTerm). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| // Fetch immediately on start | ||
| s.fetchActiveRanges(context.Background()) | ||
| s.fetchGitInfo() | ||
| go s.fetchRateLimit(context.Background()) |
There was a problem hiding this comment.
🟡 Untracked goroutines in fetchRateLimit can cause race conditions and goroutine leaks
The fetchRateLimit function is spawned as a detached goroutine (go s.fetchRateLimit(context.Background())) but is not tracked by the wg WaitGroup.
Root Cause and Impact
At daemon/cc_info_timer.go:159 and daemon/cc_info_timer.go:173, fetchRateLimit is spawned without being added to the WaitGroup:
go s.fetchRateLimit(context.Background())However, when Stop() is called at line 109, it only waits for the timerLoop goroutine:
s.wg.Wait()This creates two problems:
-
Race condition: After
Stop()clears the rate limit cache (lines 143-147), a still-runningfetchRateLimitgoroutine can write back to the cache (lines 406-409), leaving stale data in a "stopped" service. -
Goroutine leak: If the HTTP request in
fetchAnthropicUsageis slow (up to 5 second timeout atdaemon/anthropic_ratelimit.go:85), the goroutine continues running afterStop()returns, potentially accessing a service that the caller believes is fully stopped.
Impact: The race condition could cause inconsistent state if the timer is restarted, and the goroutine leak wastes resources.
Prompt for agents
To fix this issue, you need to track the fetchRateLimit goroutines in the WaitGroup. Modify the code as follows:
1. In timerLoop() at lines 159 and 173, wrap the goroutine call to track it:
- Add s.wg.Add(1) before spawning the goroutine
- Modify fetchRateLimit to call s.wg.Done() when it completes (using defer)
2. Alternatively, consider using a context with cancellation so that when Stop() is called, any in-flight HTTP requests can be cancelled. Pass a cancellable context derived from a service-level context instead of context.Background().
Example fix for lines 159 and 173:
s.wg.Add(1)
go func() {
defer s.wg.Done()
s.fetchRateLimit(context.Background())
}()
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
cc statuslineoutput (🚦 5h:45% 7d:23%)Statusline Output Order
Files Changed
daemon/anthropic_ratelimit.go— Keychain token retrieval, Anthropic API call, typesdaemon/anthropic_ratelimit_test.go— API parsing, keychain JSON, cache testsdaemon/cc_info_timer.go— Rate limit cache + async fetch in timer loopdaemon/socket.go— Rate limit fields inCCInfoResponsecommands/cc_statusline.go— Quota display with color coding (green/yellow/red)commands/cc_statusline_test.go— Quota format, propagation, and color testsdaemon/cc_info_handler_test.go— Rate limit response field testsTest plan
go build ./...compiles cleanlygo vet ./daemon/... ./commands/...passesgo test ./daemon/...)cc statuslineshows quota data🚦 -display)🤖 Generated with Claude Code