Skip to content

feat(token_rate_limit): add inject action type for soft-limit tiers - #1297

Open
abdallahsamabd wants to merge 1 commit into
praxis-proxy:mainfrom
abdallahsamabd:feat/limit881
Open

abdallahsamabd wants to merge 1 commit into
praxis-proxy:mainfrom
abdallahsamabd:feat/limit881

Conversation

@abdallahsamabd

@abdallahsamabd abdallahsamabd commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

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-limiting proposal: graduated enforcement tiers with an inject action type that adds headers to the upstream request without blocking, enabling notify-before-block soft limits.

What changed

Configuration (config.rs):

  • Added TierConfig, ActionConfig, and ActionType (inject/deny) structs
  • RuleConfig gains an optional tiers: Vec<TierConfig> field (backward compatible)

Backend interface (backend.rs):

  • BackendReserve::Admitted now carries usage_after: u64 — the total committed tokens in the budget after a reservation is placed
  • Both Valkey Lua scripts (RESERVE_SCRIPT, TOKEN_BUCKET_RESERVE_SCRIPT) updated to compute and return usage_after as a 4th element; Rust parsing updated accordingly

Ledgers (ledger.rs, token_bucket_ledger.rs):

  • Reservation structs in both algorithms gain usage_after
  • Sliding window: max_usage across all budgets (settled + active + estimate)
  • Token bucket: capacity - remaining_tokens after decrement

Filter logic (mod.rs):

  • New types: CompiledTier, CompiledAction
  • New config-time validation: compile_tiers(), validate_tier_ordering(), compile_tier_action() — enforce ascending capacities, deny-is-last, deny-capacity-matches-algorithm, inject-has-headers, valid HTTP header names
  • New request-time evaluation: evaluate_tiers() — walks breached tiers lowest-to-highest, pushes headers onto ctx.request_headers_to_set; higher tiers' headers naturally override lower tiers' for the same header name
  • New metric: praxis_ai_token_rate_limit_soft_tier_activations_total{rule, capacity}

Tests (tests.rs):

  • 19 new tests covering config validation (empty tiers, non-ascending capacities, deny-not-last, capacity mismatch, inject-without-headers, zero capacity, invalid headers, inject-only, backward compat) and admission behavior (header injection, below-threshold, highest-breached-wins, distinct headers, deny-still-rejects, inject-only-never-deny, token-bucket algorithm, body-path, match-condition scoping)

Example config:

  • New examples/configs/token-rate-limit-soft-tiers.yaml demonstrating graduated enforcement for two teams

Design decisions

  • usage_after is 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
  • Tiers are compiled once at startup into (HeaderName, HeaderValue) pairs; zero per-request parsing overhead
  • Option<Vec<TierConfig>> (not a default empty vec) so existing configs without tiers: parse identically to before
  • evaluate_tiers early-breaks at the first unbreached tier (ascending sort) for O(breached) not O(all)

Example YAML

tiers:
  - capacity: 80000
    action:
      type: inject
      headers:
        X-Token-Hour-Tier: warning
  - capacity: 95000
    action:
      type: inject
      headers:
        X-Token-Hour-Tier: degraded
        x-gateway-inference-fairness-id: "85"
  - capacity: 100000
    action:
      type: deny

As hourly usage crosses 80k → warning header. Crosses 95k → degraded header (overrides warning for same name) + fairness ID. Hits 100k → hard 429.

@abdallahsamabd
abdallahsamabd requested review from a team and cnuland September 22, 2026 12:30
@abdallahsamabd
abdallahsamabd marked this pull request as draft September 22, 2026 12:30
@abdallahsamabd
abdallahsamabd marked this pull request as ready for review September 22, 2026 13:15

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed the deny tier from team-beta's example config

@abdallahsamabd
abdallahsamabd force-pushed the feat/limit881 branch 4 times, most recently from 0c14322 to 33d1041 Compare September 23, 2026 07:14
saw_deny: &mut bool,
) -> Result<CompiledAction, FilterError> {
match tier.action.action_type {
ActionType::Inject => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@abdallahsamabd abdallahsamabd Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, Fix the header comment about inject-only tiers

action:
type: deny

# Header-only enforcement: never blocks, just signals.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here. "never blocks" contradicts the next line, which says capacity is still a hard ceiling. I'd drop "never blocks".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread xtask/src/lint_example_tests.rs Outdated
"openai/responses/responses-routing.yaml",
"openai/responses/web-search-chat-completions-fixture.yaml",
"prompt-enrichment.yaml",
"token-rate-limit-soft-tiers.yaml",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe because the feature is not fully implemented ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, Remove from SKIP list and add an integration test

@szedan-rh

Copy link
Copy Markdown
Contributor

@abdallahsamabd fix the lint please

Signed-off-by: Abdallah Samara <abdallahsamabd@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Soft limits: add inject action type for notify-without-block enforcement

4 participants