You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Per-caller rate limits and token quotas, a bounded admission queue with priority classes, and backpressure instead of unbounded memory growth when requests outpace providers.
Motivation
Once a daemon serves more than one consumer (multiple internal teams, an HTTP frontend, several scheduled jobs), fairness and overload behavior become operational requirements. The actor mailbox model already provides most of the machinery. This feature makes it configurable and observable.
Current state as of 0.35.0
Several pieces this issue assumed would be built from scratch now exist, but all of them are keyed by provider, never by caller. Nothing in the codebase carries a caller or tenant identity.
Already built:
Pre-flight spend enforcement.accounting::budget asks the accountant before every provider dispatch and refuses with a typed ActonAIErrorKind::BudgetExceeded. Scopes are BudgetScope::{Total, Provider}. Money is integer micro-USD, unpriced usage fails closed.
Provider rate limits.RateLimitConfig carries requests_per_minute and tokens_per_minute per provider.
A bounded queue. The provider actor queues when rate limited and rejects past max_queue_size (0 means unlimited), so the unbounded-growth half of the motivation is handled per provider.
Operator pause and drain.introspection::admission::AdmissionState is a global Running / Paused / Draining switch checked once at the top of a turn. Useful, but it is an operator kill switch rather than admission control.
Caller identity on requests. An API key or tenant label carried through the existing correlation-ID plumbing. Nothing exists today; this is the piece everything else hangs off.
Caller-scoped aggregation. Extend the accountant's aggregation keys and add a BudgetScope::Caller, so requests-per-window and tokens-per-window can be enforced per caller with the machinery already there.
Priority classes on the admission queue. The existing queue is FIFO and per provider. Classes belong to one owning actor at the front of the request path, not to each provider.
A typed rejection that distinguishes over-quota from overloaded. Budget denial is already typed; queue-full is not distinguished from it, and a caller cannot currently tell "you are over your quota" from "the system is saturated, retry".
Queue depth and rejection counters as metrics. Queue size appears today only as a tracing field on the provider actor, not as an exported counter or gauge.
Architecture notes
Admission decisions belong to one owning actor at the front of the request path, not checks scattered across providers. The provider-level queue stays where it is as a last line of defense.
Rejection is a normal, recorded outcome with a typed error (#[non_exhaustive] enum per existing error conventions), never a silent drop.
Defaults must be off/unlimited so single-user library consumers see zero behavior change, matching how budgets, audit, and checkpoints all default.
Summary
Per-caller rate limits and token quotas, a bounded admission queue with priority classes, and backpressure instead of unbounded memory growth when requests outpace providers.
Motivation
Once a daemon serves more than one consumer (multiple internal teams, an HTTP frontend, several scheduled jobs), fairness and overload behavior become operational requirements. The actor mailbox model already provides most of the machinery. This feature makes it configurable and observable.
Current state as of 0.35.0
Several pieces this issue assumed would be built from scratch now exist, but all of them are keyed by provider, never by caller. Nothing in the codebase carries a caller or tenant identity.
Already built:
accounting::budgetasks the accountant before every provider dispatch and refuses with a typedActonAIErrorKind::BudgetExceeded. Scopes areBudgetScope::{Total, Provider}. Money is integer micro-USD, unpriced usage fails closed.RateLimitConfigcarriesrequests_per_minuteandtokens_per_minuteper provider.max_queue_size(0means unlimited), so the unbounded-growth half of the motivation is handled per provider.introspection::admission::AdmissionStateis a globalRunning/Paused/Drainingswitch checked once at the top of a turn. Useful, but it is an operator kill switch rather than admission control.Remaining scope
BudgetScope::Caller, so requests-per-window and tokens-per-window can be enforced per caller with the machinery already there.Architecture notes
#[non_exhaustive]enum per existing error conventions), never a silent drop.