ratelimit

package
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 13, 2026 License: MIT Imports: 18 Imported by: 0

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

View Source
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

View Source
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 Clear

func Clear()

Clear wipes all stored snapshots (used by tests).

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

func Probe(apiKey, model string) error

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

func Record(model string, h http.Header)

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.

func (*AuthError) Error

func (e *AuthError) Error() string

func (*AuthError) Hint

func (e *AuthError) Hint() string

Hint returns the concrete next step for an operator. The dashboard renders this verbatim next to a link to the Claude Code login panel.

func (*AuthError) Unwrap

func (e *AuthError) Unwrap() error

Unwrap maps the classification onto a sentinel. "Nothing is configured" and "what is configured no longer works" call for different words in the UI, so they unwrap differently; every other case is a re-authentication.

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).

type Window

type Window struct {
	Limit     int64     `json:"limit"`
	Remaining int64     `json:"remaining"`
	Reset     time.Time `json:"reset,omitempty"`
}

Window is a single rate-limit dimension snapshot.

func (Window) PercentUsed

func (w Window) PercentUsed() int

PercentUsed returns 0..100.

func (Window) Used

func (w Window) Used() int64

Used returns the consumed amount within the window.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL