usage

package
v0.9.2 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package usage reads GET /api/oauth/usage and normalizes it into a shape the auto-switch engine can rank on.

The one thing this package exists to get right is that every number it carries is TRI-STATE. A window that could not be read is not an empty account, and a reset that was not reported is not "resets now". cswap got this wrong once and a single expired token parked its engine on the account that reset last, so nothing here returns a bare float64.

The other thing is units. The BODY reports utilization as a percent (0-100) with an ISO-8601 resets_at; the anthropic-ratelimit-unified-* response HEADERS report the same quantity as a fraction (0-1) with resets_at in epoch seconds. Claude Code converts the header form to the body form with `n.utilization*100` and `new Date(n.resets_at*1000).toISOString()` (function seedUtilization in the 2.1.239 bundle), and its own schema documents the body side as "Percentage of the window used, 0-100". This package stores the BODY form, and WindowFromHeader is the only place the other form is allowed to exist. Mixing the two makes a 92%-consumed account read as 0.92% and the engine never switches.

Index

Constants

View Source
const (
	// ServeTTL is the poll policy's serveTTL: a reading younger than this is
	// served from the cache with no fetch, `--refresh` included.
	//
	// It is an alias rather than a second spelling. The policy lives in
	// internal/pollpolicy; a cache that carried its own copy of the number
	// would be one edit away from serving readings the scheduler thinks are
	// already stale.
	ServeTTL = pollpolicy.ServeTTL

	CacheFileName = "usage.json"
)
View Source
const (
	// ProbeRetryAfter is the CEILING of the backoff ladder: the rate an account
	// that warm-ups demonstrably cannot wake settles at.
	//
	// It is no longer a retry interval. It used to be one — the only gate there
	// was — and that is what made the mechanism defeat itself: a five-hour
	// window goes cold five hours after it was started, and a flat six-hour gate
	// then refuses to restart it for another hour. The clock was cold for an
	// hour of every cycle, and it was cold in the hour right after the rollover,
	// which is the hour where starting it is worth the most. Measured on a live
	// fleet before this changed: cycles of 21805 s and 22213 s carrying 3805 s
	// and 4213 s of cold clock, about 4.2-4.6 hours per account per day.
	//
	// Six hours survives as the ceiling because it is what an account nothing
	// can wake used to cost, and the ladder must never be worse than what it
	// replaced: an account stuck at the cap is attempted about four times a day,
	// the same as before.
	ProbeRetryAfter = 6 * time.Hour

	// ProbePollDelay is how long after a probe the poll that reads what it woke
	// is scheduled for.
	//
	// Deliberately NOT in internal/pollpolicy: every number there is the usage
	// endpoint's own budget or the congestion response to it, and this is a wait
	// for a DIFFERENT service to have finished processing a turn. Polling at
	// once instead would spend the usage budget on top of the inference budget
	// the probe just spent, for a reading that is not there yet.
	ProbePollDelay = 60 * time.Second

	// ProbeWakeMargin is how far AFTER a window's own rollover the poll that
	// finds it cold is aimed.
	//
	// A margin rather than the instant itself, because resets_at is not exact:
	// consecutive readings of one unchanged window disagreed by up to 0.9 s, and
	// drifts of -0.58 s and +0.87 s were both observed. Aiming at R itself would
	// land before the rollover on about half of those and buy a reading that
	// still says warm, which costs a whole poll interval.
	//
	// Sixty seconds and not more: this is dead clock by construction — the
	// window is cold for every second of it — so the margin is the smallest one
	// the jitter cannot beat, not a comfortable one.
	ProbeWakeMargin = 60 * time.Second

	// ProbeConfirmAfter is how long a warm-up is given to show up in a reading
	// before the reading is allowed to call it ineffective.
	//
	// It is ten minutes against a measured turn-to-reset lag of 61-62 s, and the
	// order of magnitude is the point. That lag sits directly ON the 60 s
	// ProbePollDelay: the poll a warm-up schedules for itself arrives at the
	// moment the endpoint is still deciding, so judging on THAT reading would
	// score working warm-ups as failures, walk the ladder to its six-hour cap,
	// and re-create by hand the very cold hour this design exists to remove. A
	// strike costs hours; the measurement it rests on is one minute; the
	// deadline sits far above both.
	//
	// The consequence is a rule, not a suggestion: the +60 s confirm poll may
	// CLEAR a streak and may never add one.
	ProbeConfirmAfter = 10 * time.Minute

	// ProbeSettleGap is the floor the ladder starts at — the interval in which
	// no verdict can exist yet, so no verdict can pace anything.
	//
	// It is a backstop against a 1 Hz loop and NOT the pacer. The pacer is the
	// window's own rollover: an account whose warm-up worked is not eligible
	// again until its clock actually runs down, which no timer here expresses.
	ProbeSettleGap = 15 * time.Minute
)
View Source
const (
	ScopeModel   = "model"
	ScopeSurface = "surface"
)

The two scopes Claude Code models a weekly_scoped entry under. Its cached-usage schema types `scope` as {model?: {display_name}, surface?: {display_name}} and names no third key, which is where these two come from. That schema is a zod passthrough at every level and `kind` is a plain string rather than an enum, so it is Claude Code's read model and not a closed server contract: a third scope key would survive its parse, and ScopedWindows here would drop the entry for want of a name it recognizes.

Claude Code's own projection is NARROWER than ScopedWindows rather than the same rule. It filters `n.kind === "weekly_scoped" && n.scope?.model`, then intersects the model display name with the tengu_usage_overage_included_models allowlist, and never reads the surface at all — an entry scoped only to a surface is dropped there and kept here. That projection feeds a usage dialog; which caps to DRAW is a different question from which caps a session is subject to, so following it exactly would lose windows that can bind.

They are exported so no caller outside this package has to spell either again.

View Source
const (

	// BetaHeader is the anthropic-beta value Claude Code's OAuth request path
	// attaches to this call (`NI` in the 2.1.239 bundle). It is set by Claude
	// Code's own code, not by axios beneath it, so ccdad matches it.
	BetaHeader = "oauth-2025-04-20"
)

Variables

View Source
var ErrForbidden = errors.New("the usage endpoint refused this credential")

ErrForbidden is a 403, and it is deliberately NOT ErrUnauthorized.

Claude Code's retry wrapper refreshes and retries a 401 unconditionally, but it retries a 403 only when the caller opts in with `also403Revoked` AND the body says the token was revoked — and the usage call opts into neither, so a 403 there is rethrown on the first response and becomes "unavailable". Folding the two together would have ccdad refresh a perfectly good token against an organization that has withdrawn access, forever.

View Source
var ErrNoUsageFields = errors.New("the usage endpoint returned a body with no usage fields")

ErrNoUsageFields is a 200 whose body carries none of the eight keys a usage response is made of. Claude Code calls this an in-band error and falls back to its cached seed rather than reading six unknown windows out of it, because a body that parses is not a body that answered the question.

View Source
var ErrRateLimited = errors.New("the usage endpoint reported a rate limit in the response body")

ErrRateLimited is a rate limit delivered inside a 200, as {"error":{"type":"rate_limit_error"}}. It is separated from ErrNoUsageFields because the poll policy backs off for one and not the other.

View Source
var ErrUnauthorized = errors.New("the usage endpoint rejected the token")

ErrUnauthorized is a 401: the access token was not accepted.

It is deliberately this package's own sentinel and not identity's: they are different endpoints, and a caller that fetches usage checks the error the usage client returns. What it means here is narrower than "the account is dead" — Claude Code answers a 401 on this call by refreshing the token and retrying once — so a caller must read this as "refresh and try again", never as "quarantine the account".

View Source
var ErrUnknownScope = errors.New("the window is scoped to a key this build does not name")

ErrUnknownScope marks the one refusal that is an OPT-IN rather than a rejection: a well-formed scoped name filed under a scope key this build does not name.

It is a refusal because no reading this build takes produces that window, so ValidWindowName cannot answer nil and let a caller believe the name is live. It is distinguishable because a caller that OFFERS the opt-in still has to refuse an outright misspelling, and the two arrive here looking identical: nothing without a reading in hand can tell weekly_scoped:region:eu from weekly_scoped:modle:Fable. A caller that offers the opt-in says so; a caller that does not treats this as any other refusal.

The opt-in itself is a window_threshold entry naming the window. The config loader carries such an entry whatever its name is, so the opt-in works from a hand-written file today; this error is the shape a prompt needs to offer it.

Functions

func CachePath

func CachePath() (string, error)

CachePath is where the cache lives.

func ExpectedPct added in v0.3.0

func ExpectedPct(name WindowName, w Window, now time.Time) (float64, bool)

ExpectedPct is the share of a window that has elapsed, as a percent: given how long is left before this window resets, what utilization is on pace.

It carries NO suppression, and that is the whole difference between it and Pace. Pace is a verdict a person reads, and "ahead of pace" in the first hours after a reset is noise -- elapsed time is tiny there, so almost any usage divides out as far ahead, and a dashboard built on it cries wolf every Monday. This is a number a comparator holds a utilization against, and there the early reading is the useful one: an account that has spent a quarter of a window in its first twentieth should hand the next session to a peer, which costs nothing while a peer still has room.

It answers false for a window with no length on record, one that reported no reset, and one whose reset is further out than the window is long. A zero would read as "this window has just reset", which is the most generous answer there is, and it would be handed to a threshold.

PaceOf computes the same share for the windows it does answer for, and TestExpectedPctAgreesWithTheShareThatPaceReports pins the two together so the day either the cap or the window table moves, both move.

func IsWeekly added in v0.2.0

func IsWeekly(n WindowName) bool

IsWeekly reports whether a window's quota is the seven-day kind. It is a question about PERISHABILITY, not about length: the ranking asks it to find the quota consume-first should spend before it expires, and a five-hour rollover is not quota anyone can lose. Pace is no longer scoped to it — windowLength is the lookup that decides what can be paced.

Every SCOPED window is weekly by construction: ScopedWindows admits a limits[] entry only when its kind is weekly_scoped. It is exported because the ranking asks the same question of the same names, and two copies of this list would drift.

func RecordProbe added in v0.3.0

func RecordProbe(timeout time.Duration, uuid string, at time.Time, w WindowName, probeErr error) error

RecordProbe stamps a probe attempt against an account and schedules the poll that will read what the probe woke.

It is written twice for one probe, and the duplication is deliberate: an unattended caller stamps it BEFORE it starts a detached probe, because a probe that never starts must still consume the budget or an unstartable one is attempted on every cadence forever; and the probe itself stamps it again when it knows the outcome. Both go through the cache's own lock, and the second — the only writer that knows whether it worked — lands last.

Neither writer touches ColdStreaks. The verdict belongs to Judged and to the reading it runs on, which is what keeps the double stamp from advancing one streak twice: this function records that an attempt happened and nothing about whether it worked.

NextPollAt is NOT divided by the identity's size the way a cadence is. This is one poll rather than a rate, and the ordinary divided cadence resumes with the reading it takes.

func ValidWindowName added in v0.3.0

func ValidWindowName(n WindowName) error

ValidWindowName reports why n is not a window a threshold may be set on, and nil when it is one.

It returns an error rather than a bool because every caller has a user to tell, and the refusals are four different sentences. Only one of them is "that is not a window name". cinder_cove's name is perfectly real and only its rollover is not, so saying that would be a lie. A scoped name with no display half has the right SHAPE and nothing to attribute. And a scoped name under a key this build does not know is the one that wraps ErrUnknownScope, because the answer a caller wants there is "not yet" rather than "no".

It is deliberately narrower than WindowName.Scoped(), which tests only the weekly_scoped: prefix and therefore answers true for weekly_scoped: and weekly_scoped:region:. Neither is a name any reading can produce.

func WindowLength added in v0.7.0

func WindowLength(n WindowName) (time.Duration, bool)

WindowLength is how long a window runs before it rolls over, and whether this release knows. It is the ONLY source of that figure outside this package: a caller that re-declares 18000 and 604800 owns a second copy of a rule that lives here, and the two copies drift the first time a window's length moves.

A wrapper rather than a rename, so the export is purely additive: windowLength keeps both of its in-package call sites -- PaceOf here and ExpectedPct in expected.go -- and the long note on it, which is about why cinder_cove is absent from the table and why this is not IsWeekly with a length bolted on, stays where it explains something rather than becoming the public contract.

cinder_cove still answers false, and that is the load-bearing half for an outside caller. Its resets_at is an expiry rather than a rollover, so a caller handed a plausible length for it would invent an endless series of grants that never arrive.

func WithCache

func WithCache(timeout time.Duration, fn func(*Cache) error) (err error)

WithCache runs fn against the cache under a cross-process lock and writes back what it changed. This is the only safe way to modify the cache.

An atomic rename alone is not enough here. The daemon writes one account's entry while `ccdad list --refresh` writes another, and both do a read-modify-write of the same document: without the lock the second rename silently drops the first one's entry. The lock is cclock's — the same mkdir-based advisory mutex ccdad already uses against Claude Code, with the same staleness recovery — rather than a second lock mechanism.

fn returning an error leaves the file exactly as it was: a poll that failed halfway must not persist half a reading.

Types

type Cache

type Cache struct {
	// contains filtered or unexported fields
}

Cache is the parsed usage cache.

func LoadCache

func LoadCache() (*Cache, error)

LoadCache reads the cache without taking a lock.

No lock is needed to read: every write is a rename, so a reader sees one whole version of the document or another, never a torn one. A file that is unreadable ANYWAY — hand-edited, truncated by a full disk, written by a future version — degrades to an empty cache, which reads as UNKNOWN for every account. It must never degrade to zero: an unread account is not an empty one, and cswap's version of this bug parked its engine permanently.

func (*Cache) Delete

func (c *Cache) Delete(uuid string)

Delete drops an account's reading.

func (*Cache) Get

func (c *Cache) Get(uuid string) (Entry, bool)

Get returns an account's cached reading.

func (*Cache) LoadError

func (c *Cache) LoadError() error

LoadError is why the cache came back empty when a file did exist. It is not fatal — a cache that cannot be read leaves every account UNKNOWN, which the engine already knows how to handle — but `ccdad doctor` should still be able to say so out loud rather than having the corruption stay invisible.

func (*Cache) MayFetch

func (c *Cache) MayFetch(uuid string, now time.Time) bool

MayFetch reports whether the endpoint may be called for this account, and it is the gate `--refresh` has to pass too. A reading younger than ServeTTL means no; anything else — no reading at all, an aged one, or one dated in the future by a clock that moved — means yes.

func (*Cache) Prune

func (c *Cache) Prune(accounts map[string]time.Time)

Prune drops readings that no longer belong to anyone.

accounts maps each managed account's uuid to when it was added. An entry whose uuid is absent belonged to an account that has been removed. An entry OLDER than its account's AddedAt belonged to a previous account at the same uuid — removed and added again — and letting that through would hand a fresh login the headroom its predecessor had already spent.

func (*Cache) Put

func (c *Cache) Put(uuid string, e Entry)

Put stores an account's reading.

type Client

type Client struct {
	HTTP    *http.Client
	BaseURL string
}

Client calls the usage endpoint.

func NewClient

func NewClient() *Client

NewClient returns a Client with a bounded timeout and the stdlib transport, so proxy environment variables keep working.

func (*Client) FetchUsage

func (c *Client) FetchUsage(ctx context.Context, accessToken string) (*Snapshot, error)

FetchUsage reads accessToken's account usage.

Errors name the HTTP status and the network cause, never the token and never the response body.

type Entry

type Entry struct {
	// Snapshot is the normalized reading. It is shared, not copied, so treat it
	// as read-only.
	Snapshot *Snapshot `json:"snapshot"`
	// FetchedAt is when the reading was taken.
	FetchedAt time.Time `json:"fetched_at"`
	// NextPollAt is when the scheduler intends to poll again.
	NextPollAt time.Time `json:"next_poll_at,omitempty"`
	Poll       PollState `json:"poll,omitempty"`
	// Probe is what the last probe of this account did. A probe spends the
	// account's own quota, so its schedule is persisted for the same reason
	// Poll's backoff is: a stamp that did not survive the process is a budget
	// that resets every time ccdad restarts.
	Probe ProbeState `json:"probe,omitempty"`
	// StandDownUntil is how long this account has yielded its share of the
	// identity's budget to the one a session is actually running against.
	//
	// It is a SEPARATE field from NextPollAt because it has a different writer:
	// another account's poll sets it, while NextPollAt is set by this account's
	// own. One field would mean whichever of the two goroutines finished last
	// erased the other's decision — including a 429's floor.
	StandDownUntil time.Time `json:"stand_down_until,omitempty"`
	// ServeTTL is how long this reading may be served before another poll is
	// worth a request. Zero means the package default: an entry written before
	// this field existed has no opinion, and no opinion must not read as "stale
	// immediately".
	ServeTTL time.Duration `json:"serve_ttl,omitempty"`
}

Entry is one account's cached reading.

func (Entry) Age

func (e Entry) Age(now time.Time) (time.Duration, bool)

Age is how old the reading is, and whether that could be worked out at all. An entry dated in the future is a clock that moved backwards, not a fresh reading, so it reports no age rather than a negative one.

func (Entry) Fresh

func (e Entry) Fresh(now time.Time) bool

Fresh reports whether this reading may be served without a fetch.

It is the flat ServeTTL and deliberately NOT ScheduledTTL. This is the gate on the HAND-HELD path — `ccdad list --refresh`, through MayFetch — where it is the only rate bound there is, since that path ignores nextPollAt on purpose. Serving the scheduler's shortened TTL here would let a scripted refresh reach the endpoint six times as often as before, against an allowance of 28-30 an hour, on exactly the account where a 429 is most expensive.

func (Entry) FreshWithin added in v0.3.0

func (e Entry) FreshWithin(now time.Time, ttl time.Duration) bool

FreshWithin reports whether this reading is younger than ttl. An entry dated in the future is a clock that moved backwards rather than a fresh reading, so it is fresh under no ttl at all.

func (Entry) MayProbe added in v0.3.0

func (e Entry) MayProbe(now time.Time, w WindowName, rollover time.Time, hasRollover bool) bool

MayProbe answers for this entry's own probe state.

func (Entry) PollAt added in v0.3.0

func (e Entry) PollAt(live bool) time.Time

PollAt is when the scheduler intends to poll this account next: the schedule its own last poll earned, or a stand-down another account's poll wrote, whichever is LATER. Both are real, and neither cancels the other.

live exempts the account Claude Code is logged in as, and that exemption is what makes a stand-down safe to persist at all. A stand-down is written for the accounts that do not matter right now; a switch changes which account that is, and holding the newly live one to a stand-down written for its predecessor would blind the engine on the only account a session can be cut off on, for as long as half an hour.

func (Entry) ScheduledTTL added in v0.3.0

func (e Entry) ScheduledTTL() time.Duration

ScheduledTTL is the TTL that gates the next scheduled poll of this reading.

It is the flat ServeTTL, and a SHORTER persisted value is ignored. The field still exists and still round-trips, because the file is written by one version of ccdad and read by another and an unknown-shaped row must not become an error — but the danger band used to write 30 s here, and every cache on every machine that has been in the band is carrying those rows right now. A build that merely stopped WRITING the short TTL would keep honouring the one already on disk until that account's next successful poll, which is precisely the poll the short TTL is letting through too early.

A LONGER persisted value is honoured, because a future ccdad that slows a reading down is telling this one something it does not know, and the safe direction for an unknown is the slower one.

type ExtraUsage

type ExtraUsage struct {
	Present bool
	State   ExtraUsageState
	// DisabledReason is why the org or seat blocked overage. It is the string
	// the gate's notification names, so it is kept verbatim.
	DisabledReason string
	Currency       string
	// contains filtered or unexported fields
}

ExtraUsage is the credit axis: extra_usage from the response body.

func ExtraUsageFor

func ExtraUsageFor(in ExtraUsageInput) ExtraUsage

ExtraUsageFor builds a present ExtraUsage from already-normalized values.

func (ExtraUsage) AmountString added in v0.4.0

func (e ExtraUsage) AmountString(major float64) string

AmountString renders a MAJOR-UNIT figure — used, limit, or their difference — the way this account's own currency writes amounts: two decimals, except the zero-decimal currencies (see zeroDecimalCurrencies), which never had a minor unit to round to. It carries no currency code of its own, so a caller printing several figures side by side names the currency once.

func (ExtraUsage) CurrencyCode added in v0.4.0

func (e ExtraUsage) CurrencyCode() string

CurrencyCode is the ISO code AmountString's figures are in, defaulting to USD for the same reason majorUnits does: an unreported currency is Claude Code's own default (`tse.currency ?? "USD"`), not evidence of a two-decimal one only.

func (ExtraUsage) MonthlyLimit

func (e ExtraUsage) MonthlyLimit() (float64, bool)

MonthlyLimit is the account's own spend cap IN MAJOR UNITS — dollars for USD — and whether one was reported. A null limit means unlimited, which the credit gate reads as "no account cap" and falls back to the configured ceiling; it does not mean a cap of zero.

func (ExtraUsage) Percent

func (e ExtraUsage) Percent() (float64, bool)

Percent is extra_usage.utilization, a percent of 0-100. Unlike the two money figures it is already a percent on the wire and is not converted.

func (ExtraUsage) UsedCredits

func (e ExtraUsage) UsedCredits() (float64, bool)

UsedCredits is the money already spent, in major units, and whether it could be read at all. The credit gate refuses to switch when this is unknown: fail closed on money.

type ExtraUsageInput

type ExtraUsageInput struct {
	State          ExtraUsageState
	DisabledReason string
	Currency       string
	MonthlyLimit   *float64
	UsedCredits    *float64
	Utilization    *float64
}

ExtraUsageInput is what ExtraUsageFor takes, for the same reason NewWindow exists: the zero ExtraUsage stays the absent one, so a present-but-empty reading needs a constructor.

It takes every field the type carries, so there is no part of an ExtraUsage that only a parsed response can express -- an unconstructible field is a field nothing downstream can test against.

MonthlyLimit and UsedCredits are WIRE amounts, in the currency's minor unit, because that is what this type stores and what its JSON codec writes back; the accessors convert, see majorUnits. Utilization is already a percent and is not converted. An empty Currency reads as a two-decimal one.

type ExtraUsageState

type ExtraUsageState uint8

ExtraUsageState is how an account's overage credits stand. Claude Code reads four states, not two, and the difference between them decides money: Blocked is an org or seat policy refusal (org_spend_cap_reached, out_of_credits, seat_tier_zero_credit_limit and friends), which is not the same as an account that simply has overage switched off, and neither is the same as not knowing.

const (
	// ExtraUsageUnknown is the zero value deliberately: an unread account must
	// not present as one with credit room.
	ExtraUsageUnknown ExtraUsageState = iota
	ExtraUsageEnabled
	ExtraUsageDisabled
	ExtraUsageBlocked
)

func ParseExtraUsageState

func ParseExtraUsageState(name string) ExtraUsageState

ParseExtraUsageState is String's inverse, for reading a persisted state back.

An unrecognized name — a file written before the field existed, a typo, or a state a future release adds — reads as unknown for the same reason Classify's default leans the way it does: unknown is the side that does not spend.

func (ExtraUsageState) String

func (s ExtraUsageState) String() string

type Limit

type Limit struct {
	Kind  string
	Group string

	ModelDisplayName   string
	SurfaceDisplayName string
	// OtherScopes is every scope key this build does not name, by key, with the
	// scope's display name for a value. Claude Code types the scope object as
	// {model?, surface?} and names no third key, but every level of that schema
	// is a passthrough, so a key added server-side is legal wire. Dropping one at
	// the decode would make a weekly cap the session is subject to invisible
	// rather than merely unranked, and the ranking would spend against quota it
	// could not see.
	OtherScopes map[string]string
	// contains filtered or unexported fields
}

Limit is one entry of the limits[] array: a per-model or per-surface weekly window the server reports alongside the fixed six.

func LimitFor added in v0.2.0

func LimitFor(in LimitInput) Limit

LimitFor builds a Limit from already-normalized values.

func (Limit) Percent

func (l Limit) Percent() (float64, bool)

Percent is this limit's utilization as a percent of 0-100, and whether it was reported. It is the same quantity a Window carries under `utilization`, in the same unit: Claude Code's own projection of a limits[] entry is `{utilization: n.percent, resets_at: n.resets_at}`.

The schema writes `percent` as a plain non-null number, so on the WIRE this value's tri-state is the presence of the entry rather than the nullability of the field. It is still read tri-state here, for two reasons. A body that omits the key unmarshals to 0 in Go, which reads as "0% used" — the one direction that makes a spent account look fresh, and this is a value the ranking takes a minimum over, so a fresh-looking entry is invisible rather than loud. And Claude Code null-guards the value it derives from percent on both of its own paths (`a.utilization === null` in formatRateLimits, `?? null` in the model_scoped projection), so the null case is real one projection downstream.

func (Limit) Reset

func (l Limit) Reset() (time.Time, bool)

Reset is when this limit rolls over, and whether it was reported.

type LimitInput added in v0.2.0

type LimitInput struct {
	Kind    string
	Group   string
	Model   string
	Surface string
	// OtherScopes is the scope keys this build does not name; see
	// Limit.OtherScopes.
	OtherScopes map[string]string

	Percent  *float64
	ResetsAt *time.Time
}

LimitInput is what LimitFor takes, for the same reason NewWindow exists: the tri-state fields are unexported, so a reading that did not come from a parsed response needs a constructor. Percent is already a percent of 0-100 and is not converted.

type NamedWindow

type NamedWindow struct {
	Name WindowName
	Window
}

NamedWindow pairs a window with the key it arrived under, so a caller that ranks windows can name the one that binds.

type Pace

type Pace struct {
	Reason PaceReason
	// ExpectedPct is the share of the window that has elapsed, as a percent.
	ExpectedPct float64
	// ActualPct is the window's reported utilization, as a percent.
	ActualPct float64
	// AheadOfPace is ActualPct > ExpectedPct.
	AheadOfPace bool
	// contains filtered or unexported fields
}

Pace is how one window's consumption compares with the time elapsed in it.

It deliberately carries no projection fields. projectedExhaustionAt and willLastToReset stay out of every human-facing view, and the way to make that stick is to keep them off the struct a renderer ranges over — see Projection.

func PaceOf

func PaceOf(name WindowName, w Window, now time.Time) Pace

PaceOf measures one window against the clock.

func (Pace) OK

func (p Pace) OK() bool

OK reports whether this reading says anything at all.

func (Pace) Projection

func (p Pace) Projection() (Projection, bool)

Projection is the extrapolation, and whether there was one to make. It reports nothing when pace itself is suppressed: the numbers from a window's first seventh are exactly the ones too noisy to extrapolate from.

type PaceReason

type PaceReason uint8

PaceReason says why a pace reading is or is not available. Every non-OK value means "say nothing", never "say zero".

const (
	PaceOK PaceReason = iota
	// PaceNoUtilization: the window reported no utilization to compare.
	PaceNoUtilization
	// PaceNoReset: no resets_at, so there is no window start to measure from.
	// Taking the zero time here would put the start in 1970 and report
	// effectively infinite overage on every account.
	PaceNoReset
	// PaceNoWindowLength: ccdad has no length for this window, so there is no
	// elapsed share to compute. cinder_cove is the case — its resets_at is an
	// expiry — along with any window name a later release adds.
	PaceNoWindowLength
	// PaceTooEarly: less than a seventh of the window has run.
	PaceTooEarly
	// PaceWindowNotStarted: the reset is further out than the window is long,
	// so either the local clock or the endpoint's is wrong. There is no elapsed
	// time to divide by.
	PaceWindowNotStarted
)

func (PaceReason) String

func (r PaceReason) String() string

type PollState

type PollState struct {
	// Interval is the cadence currently in force, after any AIMD increase.
	Interval time.Duration `json:"interval,omitempty"`
	// LastRateLimited is when a 429 was last seen. The zero time means never,
	// which is not the same as "an hour ago".
	LastRateLimited time.Time `json:"last_rate_limited,omitempty"`
	// LastBindingPct is the previous sample's binding utilization, and
	// HasLastBinding whether there was one. The poll policy detects movement by
	// comparing against it, so it is persisted for the same reason the backoff
	// is: a restarted daemon with no baseline sees no movement, and one that
	// treated "no baseline" as movement would drop the whole fleet to the urgent
	// cadence on every start.
	LastBindingPct float64 `json:"last_binding_pct,omitempty"`
	HasLastBinding bool    `json:"has_last_binding,omitempty"`
}

PollState is the poll policy's per-account state, persisted so that restarting ccdad does not reset a backoff that a 429 earned. The policy owns what these mean; the cache only carries them across a process boundary.

type ProbeState added in v0.3.0

type ProbeState struct {
	// LastAttemptAt is when a probe was last ATTEMPTED, whatever came of it.
	// The zero time means never.
	LastAttemptAt time.Time `json:"last_attempt_at,omitempty"`
	// LastError is why the last attempt failed, kept because it is the only
	// record of a probe that ran detached and reported to nobody.
	//
	// It is a REPORT and never a gate. The exit code cannot tell a turn that was
	// billed and then failed from one that never authenticated — both are
	// "claude exited 1" — so nothing schedules on it. Judged is what schedules,
	// and it reads the window instead.
	LastError string `json:"last_error,omitempty"`
	// Window is the window the last attempt aimed at. Judged needs it: the
	// verdict is "did THAT window start its clock", and a probe of a model
	// scoped weekly is not answered by the five-hour window waking.
	//
	// Empty means an entry written before this field existed. Judged treats that
	// as inconclusive rather than as a failure — a strike costs hours, and an
	// upgrade is not evidence about an account.
	Window WindowName `json:"window,omitempty"`
	// ColdStreaks counts, per window, how many consecutive attempts aimed at
	// that window a later reading judged to have woken nothing.
	//
	// Per WINDOW and not one counter, and this is load-bearing rather than
	// tidy. probeModel cannot express a model VERSION, so a weekly cap scoped to
	// a build `--model opus` no longer resolves to is a window warm-ups can
	// never wake; with a single counter, every five-hour rollover would retarget
	// it and reset that hopeless window's ladder to the bottom rung, and the
	// account would spend roughly three turns per five-hour cycle on it forever.
	// Held per window, the hopeless one climbs to the cap and stays there while
	// the five-hour window keeps its own clean record.
	ColdStreaks map[WindowName]int `json:"cold_streaks,omitempty"`
}

ProbeState is what the last warm-up of an account did.

It lives in the usage cache rather than beside the quarantine in the engine state, and the placement is a decision rather than convenience. Three things settle it. This is the schedule of an attempt to take a READING, which is what NextPollAt and PollState in this same entry already are. Cache.Prune drops an entry dated before its account's AddedAt, so an account removed and added again at the same uuid gets a fresh probe budget — where strategy.State.Prune only drops uuids that are gone, and would hand a new login its predecessor's backoff. And the engine state is written only when the engine actually moves or quarantines, on purpose, so that an engine-rate writer is not put behind a poller-rate lock; a probe stamp is a poller-rate event — it is followed a minute later by a poll writing this very entry — and putting it there would be that same mistake in the other direction.

func (ProbeState) Judged added in v0.5.0

func (p ProbeState) Judged(prevFetchedAt time.Time, snap *Snapshot, now time.Time) ProbeState

Judged is this probe state after a reading taken at now, given the reading FetchedAt of the entry the reading is replacing.

This is the verdict, and it is taken from the WINDOW rather than from the child's exit status. The exit code is not the question: a probe can exit 1 having already spent its turn, and one can exit 0 having spent it against a different window than the one asked for — the second case is why the gate this replaces counted every attempt rather than only the failures. What can be observed is whether the window's clock is running, and a reading is the only thing that can say so.

Three outcomes:

  • the aimed window reports a reset in the FUTURE — its clock is running, so the streak is cleared. This runs whether or not an attempt is outstanding, which is what lets warmth from ANY source clear a standing streak: a human using the account is as good an answer as a warm-up, and an account nobody warmed for a day must not carry yesterday's ladder into tonight.
  • an attempt IS outstanding, ProbeConfirmAfter has passed, and the window still reports no future reset — the attempt woke nothing, so the streak advances one rung.
  • anything else — no reading, no aimed window, or too soon to say — is inconclusive and changes nothing.

"Outstanding" is derived from the two timestamps rather than from a flag: an attempt is outstanding while the reading being replaced is not NEWER than the attempt. A persisted boolean would be set by the child process and cleared by the daemon, which is exactly the shape that goes stale when one of the two dies; two timestamps written by the same lock cannot disagree. It also makes the verdict fire at most once per attempt for free, because the very commit that judges an attempt writes the FetchedAt that ends it — so the daemon's pre-spawn stamp and the child's outcome stamp cannot advance one streak twice.

func (ProbeState) MayProbe added in v0.5.0

func (p ProbeState) MayProbe(now time.Time, w WindowName, rollover time.Time, hasRollover bool) bool

MayProbe reports whether a warm-up of w may be attempted for this account at now. rollover is the reset instant the last reading reported for w, and hasRollover whether it reported one at all — strategy.ColdWindow answers both.

There are two arms because there are two ways for a clock to be stopped, and they want opposite gates.

A window whose reading carries a rollover that has PASSED is a clock that ran down. The gate for it is not an interval at all: one attempt per rollover, spelled as "the last attempt predates this rollover". That is a structural bound rather than a tuned one — however wrong every schedule around it goes, this arm cannot spend more than one turn per five-hour window — and it is what lets the warm-up land on the same tick as the poll that discovered the window cold instead of waiting a further poll interval for a timer to agree.

Everything else — a window nothing has ever spent against, and any window whose attempts are being judged ineffective — is on the ladder. The ladder is a backstop against retrying a broken errand at the tick loop's 1 Hz, and it is keyed on this window's own streak so that one unwakeable window cannot pace another.

The streak arm outranks the rollover arm deliberately. An account whose warm-ups wake nothing would otherwise get a free attempt at every rollover forever, which is the flat gate's failure mode with extra steps.

LastAttemptAt is per ACCOUNT while the streak is per window, and that asymmetry is right rather than an oversight. A turn is a turn: whatever --model it was spent against, it starts the five-hour clock, so an attempt aimed at a weekly cap has already bought this rollover's warm-up and the account must not be charged a second one. What is per window is the JUDGEMENT — whether the window that turn aimed at actually woke — because that is a property of the window and not of the account. It lives on ProbeState rather than on Entry so that the ranking package, which carries the probe state on a Candidate and never the whole cache row, asks the same question through the same code. Entry.MayProbe is the delegate.

func (ProbeState) NextAttemptAt added in v0.5.0

func (p ProbeState) NextAttemptAt(w WindowName) time.Time

NextAttemptAt is the earliest instant a warm-up of w may be attempted, on the backoff arm alone. It is the reporting half of MayProbe's second arm, and it is what `ccdad probe` and `ccdad hover status` print rather than deriving a second copy of the ladder.

func (ProbeState) Strikes added in v0.5.0

func (p ProbeState) Strikes(w WindowName) int

Strikes is how many consecutive attempts at one window have woken nothing.

type Projection

type Projection struct {
	// ExhaustionAt is when the window hits 100% at the current rate.
	ExhaustionAt time.Time
	// WillLastToReset is whether that lands at or after the reset.
	WillLastToReset bool
}

Projection is a linear extrapolation of the current burn.

It stays out of every HUMAN-FACING view: real usage is bursty, and a straight line through it is too rough to state as fact in a table a person reads. It is reachable only through Pace.Projection, so putting it in front of a person has to be a deliberate act.

type ScopedWindow added in v0.2.0

type ScopedWindow struct {
	NamedWindow
	Model   string
	Surface string
	// Scope is the scope KEY the entry was filed under and the name was built
	// from: ScopeModel, ScopeSurface, or — for a window that came back from
	// UnknownScopeWindows — a key this build does not name. It is what tells the
	// two apart once only the window is in hand.
	Scope string
}

ScopedWindow is one limits[] entry read as a window: a weekly cap the server scopes to one model or one surface.

Model and Surface are the scope's DISPLAY names, kept verbatim because that is the only handle the wire gives them — there is no stable identifier in the scope object. Both may be set; the entry is then named for its model, which is the half Claude Code's own filter requires (`n.scope?.model`).

type Snapshot

type Snapshot struct {
	FiveHour          Window
	SevenDay          Window
	SevenDayOAuthApps Window
	SevenDayOpus      Window
	SevenDaySonnet    Window
	// CinderCove is the one-time "Claude Code and Cowork credit" grant. Its
	// resets_at is an EXPIRY, not a recurring reset, which is why
	// RateLimitWindows deliberately leaves it out: ranking it as a window would
	// have the engine wait for a rollover that never comes.
	CinderCove Window

	ExtraUsage ExtraUsage
	Limits     []Limit
}

Snapshot is one reading of an account's usage.

func Parse

func Parse(body []byte) (*Snapshot, error)

Parse turns a usage response body into a Snapshot.

It refuses two kinds of body that a plain unmarshal would accept. A body that is not a JSON object is not a usage response at all; and an object carrying none of the eight known keys is what Claude Code calls an in-band error, which must not read as six unknown windows. A value that is present but unreadable — a null utilization, a resets_at that is not a timestamp — is unknown rather than an error, because refusing the whole response over one field would throw away the windows that were fine.

func (*Snapshot) AllWindows added in v0.2.0

func (s *Snapshot) AllWindows() []NamedWindow

AllWindows is every window an account can be ranked on: the five recurring ones and the scoped weekly windows. It is what a caller holding a binding window's NAME looks it up in — the ranking may narrow which of these bind for a given session, but it never binds on a window that is not in here.

func (*Snapshot) HasSubscriptionWindows

func (s *Snapshot) HasSubscriptionWindows() bool

HasSubscriptionWindows reports whether the account carried any of the recurring windows a plan is metered by.

It counts the model-specific weekly windows too, not just five_hour and seven_day. They are plan limits — an account only has an Opus or Sonnet weekly cap because a subscription gives it one — and an Opus-limited account whose five_hour and seven_day keys happen not to come back would otherwise classify as having no subscription evidence at all, which is the side of the credit gate that spends money.

cinder_cove is excluded for the opposite reason: it is a one-time credit grant, not a plan window, so it is evidence of the credit axis rather than of a subscription.

func (Snapshot) MarshalJSON

func (s Snapshot) MarshalJSON() ([]byte, error)

MarshalJSON writes the endpoint's own shape.

func (*Snapshot) Pace

func (s *Snapshot) Pace(now time.Time) map[WindowName]Pace

Pace measures every window the response actually carried and ccdad knows a length for, the scoped ones included: an account whose binding cap is a per-model weekly one would otherwise get no pace reading at all, which is the account the reading is most useful for. Windows that were absent, or that have nothing to say, are left out rather than reported as a zero reading.

func (*Snapshot) RateLimitWindows

func (s *Snapshot) RateLimitWindows() []NamedWindow

RateLimitWindows is the five recurring windows, in the schema's order. It excludes cinder_cove; see Snapshot.CinderCove for why.

func (*Snapshot) ResetFor added in v0.3.0

func (s *Snapshot) ResetFor(name WindowName) (time.Time, bool)

ResetFor is when one named window rolls over, and whether this reading named a time at all.

A window that reported no resets_at has never been spent against, which is not the same as one that resets now: the endpoint answers null until something is actually spent, and there is no reset, no pace and no projection until then. A window the response did not carry at all answers the same way, which is the honest reading — an absent window has no rollover either. A nil reading is the same answer again, and AllWindows already returns nothing for one.

func (*Snapshot) ScopedWindows added in v0.2.0

func (s *Snapshot) ScopedWindows() []ScopedWindow

ScopedWindows is the limits[] entries that can bind, in wire order.

An entry with no scope name is dropped rather than kept under an empty one. The scope object is nullable and BOTH of its halves are optional, so an entry naming neither is legal wire; it says a weekly cap exists without saying what it caps, which is nothing a ranking can attribute to a session.

The synthetic name carries the scope's own kind as well as its display name, so a model and a surface that share a display name stay two windows and a caller looking a binding window back up by name finds the one that bound. It is built by ScopedWindowName, which is the same builder ValidWindowName recognizes a name by, so nothing here can produce a name a caller holding only the name would reject.

func (*Snapshot) UnknownScopeWindows added in v0.3.0

func (s *Snapshot) UnknownScopeWindows() []ScopedWindow

UnknownScopeWindows is the limits[] entries this build cannot attribute: a weekly cap filed under a scope key it does not name, and under no key it does.

They are kept OUT of ScopedWindows, and therefore out of the set the engine binds on, for the reason an entry naming no scope at all has always been dropped — a cap ccdad cannot describe is not one it can tell a user it switched away for. But they are not discarded either: a user who knows what the scope means can set a threshold on the name and put it into the ranking, and nothing can be tuned that cannot first be seen.

One ENTRY is one window even when it carries several unnamed scopes, because one entry is one cap; ranking it twice would double-count the same quota. The key is chosen in sorted order rather than in map order, so the same reading names the same window on every call — the ranking ties on the first window in order, and a tie that moved between calls would move the answer with it.

func (*Snapshot) UnmarshalJSON

func (s *Snapshot) UnmarshalJSON(data []byte) error

UnmarshalJSON reads a snapshot with the same parser a live response goes through, tri-state rules and eight-key probe included: a cache file that has been hand-edited into nonsense is refused for the same reasons a nonsense response is.

func (*Snapshot) UnnamableLimits added in v0.3.0

func (s *Snapshot) UnnamableLimits() int

UnnamableLimits is how many weekly_scoped entries produced no window at all: the wire said a weekly cap exists and gave nothing to name it by, so there is no key a threshold could be set on and no row a report could carry.

It is a count and not a list because there is nothing to list — the entries have no names, which is the whole of what is wrong with them.

Every part of dropping such an entry is deliberate. ScopedWindows has always left out a cap it cannot attribute, because a cap ccdad cannot describe is not one it can tell a user it switched away for. But deliberate and INVISIBLE are different things, and a weekly cap the ranking cannot see is the exact failure the rest of this file exists to prevent. A non-zero count is the operator's only sign that the wire is carrying quota this build has no handle on.

type StatusError

type StatusError struct {
	Status int
	// contains filtered or unexported fields
}

StatusError is a non-200 from the usage endpoint. It carries the status and the Retry-After the endpoint offered, and nothing else: the token is a live credential and the body is upstream text.

func (*StatusError) Error

func (e *StatusError) Error() string

func (*StatusError) RetryAfter

func (e *StatusError) RetryAfter() (time.Duration, bool)

RetryAfter is the wait the endpoint asked for, and whether it asked at all. The poll policy's AIMD backoff needs the difference: an absent header is not a zero wait.

func (*StatusError) Unwrap

func (e *StatusError) Unwrap() error

Unwrap reports the three conditions a caller acts on differently: a 401 means refresh and retry, a 403 means this credential is refused and refreshing will not change that, and a 429 means the shared per-identity budget is saturated and the poll policy must back off. Everything else is just a bad day upstream and unwraps to nothing.

type Window

type Window struct {
	Present bool
	// contains filtered or unexported fields
}

Window is one rate-limit window.

Present records that the response carried the key at all, which is a separate question from whether either field could be read: a freshly reset account reports {"utilization":null,"resets_at":null} and still HAS the window. Claude Code's own seed check is a truthiness test on the object, so a JSON null reads as absent here for the same reason.

func NewWindow

func NewWindow(pct *float64, resetsAt *time.Time) Window

NewWindow builds a present Window from already-normalized values: a percent of 0-100 and a reset time, either of which may be nil for "not reported". The zero Window is the absent one, so this is the only way to say "present, and here is what it said".

It exists because Window's tri-state fields are unexported — which is what stops a caller reading a zero out of an unknown — and without a constructor every package downstream would have to build its test readings out of JSON.

func WindowFromHeader

func WindowFromHeader(fraction *float64, resetsAtUnix *int64) Window

WindowFromHeader builds a Window from the anthropic-ratelimit-unified-* response headers, which use the OTHER representation: utilization is a 0-1 fraction and resets_at is an epoch second. This is the single conversion boundary; nothing else in the package may accept the header form.

Both halves are pointers, and a missing or non-finite half yields the ABSENT window rather than a half-filled one. That is Claude Code's own rule: `y9p` records a window only `if(o!==null&&i!==null)`, and its consumer re-checks both with Number.isFinite before trusting the record. Filling in the other half would mean a header that was never sent arriving as "0% used" or as a reset at the 1970 epoch — which reads as "already recovered" and puts the one account nobody could measure at the front of the recovery queue.

func (Window) Percent

func (w Window) Percent() (float64, bool)

Percent is the window's utilization as a percent of 0-100, and whether it was reported at all. It is never scaled: the body is already a percent.

A value that is not finite is not a reading. JSON cannot carry one, but a header parsed with Number()/ParseFloat can, and Claude Code's own consumer re-checks `Number.isFinite(l.utilization)` on exactly that path. Without this guard a NaN would report as KNOWN — and because every NaN comparison is false, it would then lose no comparison in the ranking and could hold first place while being the one account nobody could read.

func (Window) Reset

func (w Window) Reset() (time.Time, bool)

Reset is when the window rolls over, and whether it was reported at all. An unreported reset is unknown, never "now".

type WindowName

type WindowName string

WindowName identifies a window in the usage response.

const (
	WindowFiveHour          WindowName = "five_hour"
	WindowSevenDay          WindowName = "seven_day"
	WindowSevenDayOAuthApps WindowName = "seven_day_oauth_apps"
	WindowSevenDayOpus      WindowName = "seven_day_opus"
	WindowSevenDaySonnet    WindowName = "seven_day_sonnet"
	WindowCinderCove        WindowName = "cinder_cove"
)

The six windows the 2.1.239 schema names, in its own order.

func RateLimitWindowNames added in v0.3.0

func RateLimitWindowNames() []WindowName

RateLimitWindowNames is the five recurring windows by name, for a caller that has no reading to take them from — help text, a refusal, a settable-key list. It returns a copy so a caller cannot edit the table every answer comes from.

func ScopedWindowName added in v0.3.0

func ScopedWindowName(scope, display string) WindowName

ScopedWindowName is the name a scoped weekly cap is filed under. scope is ScopeModel or ScopeSurface, and display is the scope's DISPLAY name, verbatim, because that is the only handle the wire gives it — there is no stable identifier in the scope object.

The scope is in the name and not only the display half, so a model and a surface that share a display name stay two windows.

func (WindowName) Scoped added in v0.2.0

func (n WindowName) Scoped() bool

Scoped reports whether this name belongs to a limits[] entry rather than to one of the six keys the schema names. It is how a caller that has only a name tells a per-model or per-surface weekly window from a fixed one.

Jump to

Keyboard shortcuts

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