Documentation
¶
Overview ¶
Package ratelimit - claude_limits.go enforces per-project caps on the global Claude Code subscription utilization (5-hour and weekly windows).
Package ratelimit - claude_reauth.go classifies *why* the Claude Code subscription usage API cannot be reached, so a stalled caps panel says "re-authenticate" instead of going quietly blank.
Motivation (Task 20202): this hub's caps stopped updating for four days. The OAuth access token had expired and ~/.claude/.credentials.json carried an empty refreshToken (the shape `claude setup-token` writes), so the token resolver bailed out with a bare "no OAuth token available". Every consumer discarded that string: the UI handler dropped the error on the floor, the panel rendered four "not reported" rows, and the orchestrator's cap enforcement silently degraded to a no-op. Nothing anywhere said the words "log in again".
The fix is to make the failure a typed, classified, cached value that the API and the dashboard can both render, with a concrete recovery hint.
Package ratelimit - claude_refresh.go exposes this package's OAuth credential refresh to callers outside it.
Motivation (Task 20204): the `claude` CLI subprocesses spawned by pkg/provider/claudecode each manage their own OAuth refresh, entirely outside the single-flight locks maintained here. Claude.ai refresh tokens are single-use and rotate on every exchange, so when a parallel plan runs several tasks at once and the access token lapses mid-round, multiple CLI processes POST the same refresh token concurrently: one wins, the losers get invalid_grant and their task dies with "API Error: 401". The following task succeeds because by then the winner has written a fresh credential to disk — which is exactly the reported "sometimes 401, next task is fine" symptom.
A provider that hits a 401 therefore wants to re-read — and, only if the credential is genuinely stale, rotate — the token under *this* package's locks before retrying, rather than immediately re-sending the credential the server just rejected.
Package ratelimit - claude_usage.go fetches Claude Code subscription usage from the Anthropic OAuth usage API endpoint.
Package ratelimit captures and exposes Anthropic API rate-limit information from anthropic-ratelimit-* response headers.
On every Anthropic API call, callers invoke Record(model, headers). The package parses recognised headers (RPM, ITPM, OTPM, weekly/5h windows, priority tier) and stores a per-model snapshot in memory. Snapshots are also persisted to ~/.cloop/ratelimits.json so the Web UI can display the most recent values across cloop invocations.
Index ¶
- Constants
- Variables
- func Clear()
- func ClearUsageCache()
- func EnforceClaudeCodeLimits(cfg config.ClaudeCodeConfig, usage *ClaudeUsage) error
- func ForceCredentialRefresh() string
- func Probe(apiKey, model string) error
- func ProbeAll(apiKey string)
- func ReauthRequired() bool
- func Record(model string, h http.Header)
- func Snapshot() map[string]ModelLimits
- type AuthError
- type AuthProblem
- type ClaudeUsage
- type ClaudeUsageResponse
- type ExtraUsage
- type LimitViolation
- type ModelLimits
- type UsageDetail
- type UsageWindow
- type Window
Constants ¶
const MinUsageCacheTTL = time.Minute
MinUsageCacheTTL is the floor on how long a fetched ClaudeUsage snapshot is served without re-fetching. The OAuth usage API returns slowly-changing utilization percentages and is itself rate-limited, so callers that ask "may I make another claudecode call?" within this window receive the cached snapshot without a network round-trip. Callers may pass a longer TTL but anything shorter is rounded up to this value.
Variables ¶
var ( // ErrReauthRequired marks every failure that a human fixes by logging // into Claude Code again. ErrReauthRequired = errors.New("claude code re-authentication required") // ErrNoCredentials means no Claude Code credentials were found at all — // neither a usable credentials file nor CLAUDE_CODE_OAUTH_TOKEN. ErrNoCredentials = errors.New("no claude code credentials available") )
Sentinels for errors.Is, so callers never string-match. A transient failure resolves itself on the next refresh; these two do not, and the difference is exactly what the dashboard needs in order to say something useful.
Functions ¶
func ClearUsageCache ¶
func ClearUsageCache()
ClearUsageCache discards the in-memory ClaudeUsage snapshot so the next FetchOrCachedUsage call goes back to the OAuth usage API. Call this on reauthentication (login completes, logout) — the cached snapshot was tied to the previous identity's account and would otherwise be served as stale data for up to MinUsageCacheTTL.
func EnforceClaudeCodeLimits ¶
func EnforceClaudeCodeLimits(cfg config.ClaudeCodeConfig, usage *ClaudeUsage) error
EnforceClaudeCodeLimits returns a non-nil error when any of the per-project caps configured in cfg has been reached. Caller is responsible for fetching the usage snapshot (so that this check can be done without a network call when fresh cached data is available).
func ForceCredentialRefresh ¶
func ForceCredentialRefresh() string
ForceCredentialRefresh returns a currently-valid Claude Code OAuth access token, rotating the stored credential when it is past its recorded expiry.
It is safe to call concurrently and from multiple processes: the underlying exchange holds the process-wide oauthRefreshMu plus a cross-process flock, and re-checks freshness after taking them. When a peer already rotated the token while this caller was blocked, the peer's token is returned rather than spending the (now consumed) refresh token a second time — so a whole parallel round of 401s coalesces into at most one exchange.
Returns "" when no refresh is possible: no credentials file, no refresh token, or a failed exchange. Callers should treat that as "retry with whatever the CLI resolves for itself" rather than as a hard failure, since the credential may legitimately come from elsewhere — CLAUDE_CODE_OAUTH_TOKEN, or a long-lived `claude setup-token` credential that carries no refresh token at all.
func Probe ¶
Probe makes a lightweight Anthropic API call (1 token max) to capture rate-limit headers for the given model. This is used when the claudecode CLI provider is active and no headers are available from direct API calls. apiKey can be an API key or OAuth token (sk-ant-oat01-*).
func ProbeAll ¶
func ProbeAll(apiKey string)
ProbeAll probes rate limits for common Anthropic models.
func ReauthRequired ¶
func ReauthRequired() bool
ReauthRequired reports whether the last attempt failed in a way a human fixes by logging in again.
func Record ¶
Record extracts anthropic-ratelimit-* headers from h and stores them under model. Safe to call from concurrent provider goroutines. Headers absent from the response are not zeroed; the previous values for those dimensions are preserved.
func Snapshot ¶
func Snapshot() map[string]ModelLimits
Snapshot returns a deep-ish copy of the current per-model rate-limit map. Map keys are model names. Caller may freely read from the result without locking.
Types ¶
type AuthError ¶
type AuthError struct {
Problem AuthProblem
// Detail is the underlying cause, safe to show an operator. It never
// contains token material — only paths, statuses and server messages.
Detail string
}
AuthError is a classified authentication failure carrying an operator-facing recovery hint. It unwraps to ErrReauthRequired.
func AuthFailure ¶
func AuthFailure() *AuthError
AuthFailure reports the currently cached authentication failure, or nil when the last usage fetch authenticated successfully. The dashboard uses this to decide whether to show the re-authentication banner without triggering a fetch of its own.
type AuthProblem ¶
type AuthProblem string
AuthProblem classifies why the OAuth credential is unusable. It is serialized to the dashboard, so the values are stable identifiers.
const ( // AuthNoCredentials: no credentials file, no access token in it, and no // CLAUDE_CODE_OAUTH_TOKEN in the environment. AuthNoCredentials AuthProblem = "no_credentials" // AuthRefreshFailed: a refresh token existed but the exchange failed. // Usually means the single-use refresh token was already consumed. AuthRefreshFailed AuthProblem = "refresh_failed" // AuthRejected: a token was sent and the server answered 401. This is // the only classification backed by the server's own opinion. AuthRejected AuthProblem = "rejected" // AuthScopeInsufficient: the token authenticates but was not granted the // scope the usage endpoint requires (HTTP 403 oauth_scope_insufficient). // Retrying and refreshing are both useless — the grant itself is too // narrow, so only a fresh login with the right scopes fixes it. AuthScopeInsufficient AuthProblem = "scope_insufficient" )
type ClaudeUsage ¶
type ClaudeUsage struct {
FiveHour *UsageDetail `json:"five_hour,omitempty"`
SevenDay *UsageDetail `json:"seven_day,omitempty"`
SevenDayOpus *UsageDetail `json:"seven_day_opus,omitempty"`
SevenDaySonnet *UsageDetail `json:"seven_day_sonnet,omitempty"`
ExtraUsage *ExtraUsage `json:"extra_usage,omitempty"`
FetchedAt time.Time `json:"fetched_at"`
}
ClaudeUsage is the processed usage data exposed to the UI.
func FetchClaudeUsage ¶
func FetchClaudeUsage(token string) (*ClaudeUsage, error)
FetchClaudeUsage calls the Anthropic OAuth usage API to get subscription limits (5-hour window, weekly window, per-model breakdowns). The token should be a Claude Code OAuth access token (sk-ant-oat01-*).
func FetchOrCachedUsage ¶
func FetchOrCachedUsage(token string, ttl time.Duration) (*ClaudeUsage, error)
FetchOrCachedUsage returns the cached usage snapshot when it is fresher than max(ttl, MinUsageCacheTTL), otherwise re-fetches from the OAuth usage API. Concurrent callers coalesce on usageFetchMu so the API is hit once per refresh window even under contention. On fetch error, a previously cached snapshot (if any) is returned alongside the error so callers can fall back to stale data instead of failing open.
func GetCachedUsage ¶
func GetCachedUsage() *ClaudeUsage
GetCachedUsage returns the last fetched usage, or nil if not available. No freshness check — callers that need fresh data should use FetchOrCachedUsage instead.
type ClaudeUsageResponse ¶
type ClaudeUsageResponse struct {
FiveHour *UsageWindow `json:"five_hour"`
SevenDay *UsageWindow `json:"seven_day"`
SevenDayOpus *UsageWindow `json:"seven_day_opus"`
SevenDaySonnet *UsageWindow `json:"seven_day_sonnet"`
ExtraUsage *ExtraUsage `json:"extra_usage"`
Error *struct {
Message string `json:"message"`
} `json:"error,omitempty"`
}
ClaudeUsageResponse is the raw API response from /api/oauth/usage.
type ExtraUsage ¶
type ExtraUsage struct {
IsEnabled bool `json:"is_enabled"`
MonthlyLimit *float64 `json:"monthly_limit"`
UsedCredits *float64 `json:"used_credits"`
Utilization *float64 `json:"utilization"`
}
ExtraUsage represents extra/overflow usage info.
type LimitViolation ¶
type LimitViolation struct {
Window string // "weekly", "five_hour", "weekly_opus", "weekly_sonnet"
Utilization float64 // current global utilization 0-100
Cap float64 // configured per-project cap 0-100
}
LimitViolation describes a single tripped per-project claudecode cap.
func CheckClaudeCodeLimits ¶
func CheckClaudeCodeLimits(cfg config.ClaudeCodeConfig, usage *ClaudeUsage) []LimitViolation
CheckClaudeCodeLimits compares the configured per-project caps against a usage snapshot and returns the list of windows whose configured cap has been reached. Returns nil when no caps are tripped (or no caps configured).
usage may be nil — in that case CheckClaudeCodeLimits returns nil (no enforcement when usage data is unavailable).
func (LimitViolation) Error ¶
func (v LimitViolation) Error() string
type ModelLimits ¶
type ModelLimits struct {
Model string `json:"model"`
UpdatedAt time.Time `json:"updated_at"`
Tier string `json:"tier,omitempty"` // anthropic-priority-tier or similar
Requests Window `json:"requests"` // RPM
Tokens Window `json:"tokens"` // overall TPM (legacy, may be 0)
InputTokens Window `json:"input_tokens"` // ITPM
OutputTokens Window `json:"output_tokens"` // OTPM
Weekly Window `json:"weekly,omitempty"` // weekly window if present
FiveHour Window `json:"five_hour,omitempty"` // 5-hour rolling window
MonthlySpendUSD float64 `json:"monthly_spend_usd,omitempty"` // x-spend-limit-monthly if present
// Raw is the full set of anthropic-ratelimit-* headers as returned by the
// API, for debugging / forward-compatibility.
Raw map[string]string `json:"raw,omitempty"`
}
ModelLimits is the full rate-limit snapshot for one model.
type UsageDetail ¶
type UsageDetail struct {
Utilization float64 `json:"utilization"` // 0-100
ResetsAt time.Time `json:"resets_at"`
}
UsageDetail is a single limit window with parsed fields.
type UsageWindow ¶
type UsageWindow struct {
Utilization *float64 `json:"utilization"` // percentage 0-100, nil if not applicable
ResetsAt string `json:"resets_at"` // ISO 8601 timestamp
}
UsageWindow represents a single usage limit window (5-hour or 7-day).