feat(token_rate_limit): add inject action type for soft-limit tiers - #1297
abdallahsamabd wants to merge 1 commit into
Conversation
9b3d1d1 to
d5679f9
Compare
d5679f9 to
08d9194
Compare
praxis-bot
left a comment
There was a problem hiding this comment.
PR Review
Solid implementation. The usage_after threading through both algorithm backends (in-process and Valkey Lua) is correct, and the config-time validation (ascending capacities, deny-is-last, deny-capacity-matches-algorithm) is thorough. One issue in the example config.
| # Header-only enforcement: never blocks, just signals. | ||
| - name: team-beta | ||
| match: | ||
| headers: |
There was a problem hiding this comment.
[Medium] This comment says "Header-only enforcement: never blocks, just signals" but the config below includes type: deny at capacity 50000 (line 101), which will hard-reject with 429 when the budget is exhausted. Either remove the deny tier to make this a genuine inject-only example (the algorithm's own capacity still provides a hard ceiling), or update the comment to say something like "Same capacity with a single soft tier."
There was a problem hiding this comment.
removed the deny tier from team-beta's example config
0c14322 to
33d1041
Compare
| saw_deny: &mut bool, | ||
| ) -> Result<CompiledAction, FilterError> { | ||
| match tier.action.action_type { | ||
| ActionType::Inject => { |
There was a problem hiding this comment.
Should we reject inject tiers whose capacity is above the algorithm capacity? Anything that would push usage past capacity gets a 429, so an admitted request never gets that far. With capacity: 50000 and an inject tier at 60000, the config loads fine but the tier never fires. Failing at load time seems better than nobody noticing.
There was a problem hiding this comment.
Done, Reject inject tiers whose capacity exceeds the algorithm capacity
| # Tiers must have strictly ascending capacity values. The deny tier (if | ||
| # present) must be last, and its capacity must equal the algorithm's | ||
| # `capacity`. Inject-only tiers (no deny) enable soft, header-only | ||
| # enforcement where usage is tracked but never hard-blocked. |
There was a problem hiding this comment.
"usage is tracked but never hard-blocked" isn't quite right. Without a deny tier, the algorithm capacity still returns 429. Someone copying this for header-only enforcement will be surprised. Maybe something like "inject-only tiers add headers below the algorithm capacity; requests over capacity are still rejected".
There was a problem hiding this comment.
Done, Fix the header comment about inject-only tiers
| action: | ||
| type: deny | ||
|
|
||
| # Header-only enforcement: never blocks, just signals. |
There was a problem hiding this comment.
Same thing here. "never blocks" contradicts the next line, which says capacity is still a hard ceiling. I'd drop "never blocks".
| "openai/responses/responses-routing.yaml", | ||
| "openai/responses/web-search-chat-completions-fixture.yaml", | ||
| "prompt-enrichment.yaml", | ||
| "token-rate-limit-soft-tiers.yaml", |
There was a problem hiding this comment.
Any reason this one is in SKIP? token-rate-limit.yaml has a functional test, and AGENTS.md asks for one on new example configs. Even a basic test that loads the config and checks that the tier header shows up once usage crosses a threshold would catch config drift.
There was a problem hiding this comment.
Maybe because the feature is not fully implemented ?
There was a problem hiding this comment.
Done, Remove from SKIP list and add an integration test
33d1041 to
0f25c24
Compare
|
@abdallahsamabd fix the lint please |
Signed-off-by: Abdallah Samara <abdallahsamabd@gmail.com>
0f25c24 to
9ed9aeb
Compare
feat(token_rate_limit): add inject action type for soft-limit tiers (S1)
Closes #881
Summary
Implements the S1 milestone from the
00121_token-rate-limitingproposal: graduated enforcement tiers with aninjectaction type that adds headers to the upstream request without blocking, enabling notify-before-block soft limits.What changed
Configuration (
config.rs):TierConfig,ActionConfig, andActionType(inject/deny) structsRuleConfiggains an optionaltiers: Vec<TierConfig>field (backward compatible)Backend interface (
backend.rs):BackendReserve::Admittednow carriesusage_after: u64— the total committed tokens in the budget after a reservation is placedRESERVE_SCRIPT,TOKEN_BUCKET_RESERVE_SCRIPT) updated to compute and returnusage_afteras a 4th element; Rust parsing updated accordinglyLedgers (
ledger.rs,token_bucket_ledger.rs):Reservationstructs in both algorithms gainusage_aftermax_usageacross all budgets (settled + active + estimate)capacity - remaining_tokensafter decrementFilter logic (
mod.rs):CompiledTier,CompiledActioncompile_tiers(),validate_tier_ordering(),compile_tier_action()— enforce ascending capacities, deny-is-last, deny-capacity-matches-algorithm, inject-has-headers, valid HTTP header namesevaluate_tiers()— walks breached tiers lowest-to-highest, pushes headers ontoctx.request_headers_to_set; higher tiers' headers naturally override lower tiers' for the same header namepraxis_ai_token_rate_limit_soft_tier_activations_total{rule, capacity}Tests (
tests.rs):Example config:
examples/configs/token-rate-limit-soft-tiers.yamldemonstrating graduated enforcement for two teamsDesign decisions
usage_afteris computed inside the ledger/Lua script (not reconstructed in the filter) so tier evaluation sees the exact same value the admission decision used — no TOCTOU gap(HeaderName, HeaderValue)pairs; zero per-request parsing overheadOption<Vec<TierConfig>>(not a default empty vec) so existing configs withouttiers:parse identically to beforeevaluate_tiersearly-breaks at the first unbreached tier (ascending sort) for O(breached) not O(all)Example YAML
As hourly usage crosses 80k →
warningheader. Crosses 95k →degradedheader (overrideswarningfor same name) + fairness ID. Hits 100k → hard 429.