Skip to content

fix(quota): match model conditions against the requested parent as well as the dispatched target - #957

Merged
jarvis9443 merged 3 commits into
mainfrom
fix/policy-model-condition-pair
Aug 12, 2026
Merged

fix(quota): match model conditions against the requested parent as well as the dispatched target#957
jarvis9443 merged 3 commits into
mainfrom
fix/policy-model-condition-pair

Conversation

@jarvis9443

@jarvis9443 jarvis9443 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Problem

A conditional rate-limit policy whose model condition names a routing group — the dashboard offers every model kind as a condition value — never fires. The request gate defers model-property policies to the per-target gate, and the per-target condition input carried only the dispatched member's identity, so the group's UUID was compared nowhere. The same held for model_name conditions naming the group's alias, and for ensemble/semantic parent entries. The policy saves, projects, and silently never matches.

Fixes api7/AISIX-Cloud#1267

Change

The per-target condition input now carries the {dispatched target, requested parent} pair for the two model dimensions (ConditionInput.routing_parent_model / _model_name):

  • the pair is the request's model-identity SET: positive operators (==/in/~~/~*) are raw-true when either identity satisfies them; ~= is raw-true only when both differ (the two identities are distinct strings, so an ∃ reading of ~= would be vacuously true on every routed request), preserving a ~= b!(a == b); negate flips the combined result, so !(model in [<group>]) excludes requests addressed to the group;
  • reservation phases and bucket values are unchanged: model-property policies still defer to the per-target gate, group_by: [model] still splits per concrete target. One counting nuance that predates this PR and is unchanged by it: window counts are consumed per reserved attempt, so an attempt that fails upstream after reserving still consumes shared-bucket quota during failover — the docs PR states this explicitly;
  • classic scope: model rows already matched parent ids at the request gate; the conditional form now agrees with them instead of contradicting them;
  • member-referencing conditions keep matching through the group (the AISIX-Cloud#1087 principle), pinned by a regression case.

Wired through every group-capable dispatch loop — chat (both streaming and non-streaming), /v1/messages, /v1/responses, /v1/messages/count_tokens — and the ensemble panel/judge reservations via the shared reserve_model_only path; semantic routing shares the chat loop. On an ensemble, a parent-referencing condition reserves per sub-call (panel members + judge); the parent-level per-request cap remains the entry's own inline rate_limit.

This also brings the condition semantics in line with how mainstream gateways key requested-model limits: caller-facing limits are keyed on the model name the client sent (the group alias), while per-deployment limits follow the concrete target — both are now expressible.

Behavior changes

  1. Group/ensemble/semantic entry ids and aliases in model/model_name conditions start matching (previously dead config).
  2. Negated conditions referencing a parent entry flip from "matches all via-parent traffic" to "excludes via-parent traffic" — the previous behavior was an artifact of the missing comparison, not a documented semantic.
  3. On routed dispatches, model ~= X / model_name ~= X now requires both the target and the requested parent to differ from X (previously only the target was compared).

Tests

  • Core unit tests: pair matching, negation flip, ~= ∀-semantics with the ~= ≡ !(==) equivalence pinned across all three value positions, member-through-parent, no parent leak into identity dimensions.
  • Quota phase tests: group-referencing policy defers at the gate and matches per target with member-split buckets; group_by [model] buckets stay on the target id.
  • New e2e (group-model-condition-ratelimit-e2e.test.ts, real binary + etcd + mock upstream), 7 cases: the reported scenario (team ∧ group-id, per-member split, 429 with policy attribution), group-alias model_name, negation exclusion, member-id failover + shared bucket with the direct alias, and /v1/messages + streaming-chat (real SSE fixture) + /v1/responses siblings. 6 of 7 fail on main (the member-id regression guard passes, as it should); all 7 pass with the fix. The existing multidim suite — including the case pinning "the group parent never burns a per-target bucket" — is unchanged and green.
  • Full workspace tests and the entire e2e suite pass locally.

Generated schemas/resources/rate_limit_policy.schema.json picks up the new dimension descriptions. The cp-admin.yaml description update and user-facing docs (EN/ZH) ship in the paired CP/docs PRs.

🤖 Generated with Claude Code

…ll as the dispatched target

A conditional rate-limit policy whose model condition names a routing
group (or whose model_name names the group's alias) never fired: the
request gate defers model-property policies to the per-target gate,
where the condition input carried only the dispatched member's identity
— the parent's id was compared nowhere.

The per-target condition input now carries the {dispatched target,
requested parent} pair for the model dimensions. A leaf is raw-true
when either identity satisfies its operator, and negate flips the
combined result — so a group-referencing condition selects exactly the
requests addressed to that group, and a negated one excludes them
instead of (absurdly) matching all of them. Reservation phases and
bucket values are unchanged: group_by [model] still splits per concrete
target, and classic scope:model rows (which already matched parent ids
at the request gate) now agree with the conditional form.

Wired through every group-capable dispatch loop (chat streaming +
non-streaming, /v1/messages, /v1/responses, /v1/messages/count_tokens)
and the ensemble panel/judge reservations; semantic routing shares the
chat loop. On an ensemble, a parent-referencing condition reserves per
sub-call (panel members + judge) — the parent-level per-request cap
remains the entry's own inline rate_limit.

Fixes api7/AISIX-Cloud#1267
Copilot AI balanced review requested due to automatic review settings August 12, 2026 09:01
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Model policies now match dispatched targets and addressed routing parents. Routing-parent identity flows through routed and ensemble quota reservations. Tests cover direct dispatch, routing groups, negation, failover, aliases, target bucketing, and multiple request endpoints.

Changes

Routing-parent policy matching

Layer / File(s) Summary
Condition evaluation contract
crates/aisix-core/src/models/policy_conditions.rs, schemas/resources/rate_limit_policy.schema.json
ConditionInput now carries optional parent model ID and name values. Model conditions combine target and parent matches. Identity and provider dimensions remain target-only.
Quota reservation propagation
crates/aisix-proxy/...
RoutingParent replaces the routing boolean. Quota reservations receive parent identity for routed targets and ensemble members. Direct requests pass no parent context.
Routing policy validation
crates/aisix-proxy/src/quota.rs, tests/e2e/src/cases/group-model-condition-ratelimit-e2e.test.ts
Unit and end-to-end tests cover parent matching, negation, target bucketing, failover, aliases, direct member dispatch, and chat, streaming, messages, and responses requests.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ChatRequest
  participant QuotaReservation
  participant PolicyEvaluator
  participant TargetModel
  Client->>ChatRequest: submit request for routing parent
  ChatRequest->>QuotaReservation: reserve routed target with RoutingParent
  QuotaReservation->>PolicyEvaluator: evaluate target and parent conditions
  PolicyEvaluator->>QuotaReservation: return policy match and target bucket
  QuotaReservation->>TargetModel: continue dispatch or reject
Loading

Possibly related PRs

  • api7/aisix#783: Changes the same routing-target quota reservation flow.
  • api7/aisix#886: Changes the conditional policy evaluation and quota-routing paths.
  • api7/aisix#944: Changes overlapping policy reservation logic in crates/aisix-proxy/src/quota.rs.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
E2e Test Quality Review ⚠️ Warning The E2E suite covers routing-group chat, messages, and responses, but the PR changes separate ensemble panel/judge and count_tokens quota paths without E2E coverage. Add real etcd plus spawned-binary E2E cases for conditional parent model/model_name limits on ensemble panel/judge and /v1/messages/count_tokens, including 429 and policy attribution assertions.
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement parent model-group matching, quota enforcement, and coverage required by issue [#1267].
Out of Scope Changes check ✅ Passed The routing, quota, schema, and test changes directly support parent-condition matching and preserve related behavior.
Security Check ✅ Passed Diff adds only rate-limit matching and routing context. No production secret exposure, plaintext credential storage, authorization, ownership, TLS, shared-resource, or secret-reference issue was in...
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main quota fix: matching model conditions against both the requested parent and dispatched target.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/policy-model-condition-pair

Comment @coderabbitai help to get the list of available commands.

Copilot AI 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.

Pull request overview

Updates conditional quota matching so routed requests consider both the requested parent model and dispatched target.

Changes:

  • Adds parent-aware model condition evaluation across dispatch paths.
  • Preserves concrete-target bucket grouping.
  • Adds unit, schema, and E2E coverage.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
crates/aisix-core/src/models/policy_conditions.rs Adds parent-aware condition evaluation.
crates/aisix-proxy/src/quota.rs Threads parent identity into quota matching.
crates/aisix-proxy/src/chat.rs Wires chat, semantic, and ensemble dispatch.
crates/aisix-proxy/src/messages.rs Wires Messages dispatch.
crates/aisix-proxy/src/responses.rs Wires Responses dispatch.
crates/aisix-proxy/src/count_tokens.rs Wires token-count dispatch.
crates/aisix-proxy/src/ensemble.rs Propagates ensemble parent identity.
crates/aisix-proxy/src/lib.rs Updates quota test callers.
schemas/resources/rate_limit_policy.schema.json Updates generated dimension descriptions.
tests/e2e/src/cases/group-model-condition-ratelimit-e2e.test.ts Adds routing-group quota scenarios.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/e2e/src/cases/group-model-condition-ratelimit-e2e.test.ts
Comment thread crates/aisix-core/src/models/policy_conditions.rs Outdated

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/aisix-core/src/models/policy_conditions.rs (1)

475-524: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Combine routed Ne checks with AND.

ConditionOperator::Ne is valid for Model and ModelName. The current OR combination makes model ~= <group-id> true for every request routed through that group because the target differs from the group. Apply Ne across the pair as target != value && parent != value, and add regression tests for both target and parent matches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aisix-core/src/models/policy_conditions.rs` around lines 475 - 524,
Update the routed evaluation logic before applying negation so
ConditionOperator::Ne requires both the target and optional routing parent to
differ from the value, while existing operators retain their OR behavior. Use
the visible leaf evaluation flow and add regression coverage for Model/ModelName
cases where either the target or parent matches the excluded value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/aisix-core/src/models/policy_conditions.rs`:
- Around line 475-524: Update the routed evaluation logic before applying
negation so ConditionOperator::Ne requires both the target and optional routing
parent to differ from the value, while existing operators retain their OR
behavior. Use the visible leaf evaluation flow and add regression coverage for
Model/ModelName cases where either the target or parent matches the excluded
value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 05b8de19-1b0f-4624-88e1-d9a536be35fe

📥 Commits

Reviewing files that changed from the base of the PR and between e14b693 and 68f9a64.

📒 Files selected for processing (10)
  • crates/aisix-core/src/models/policy_conditions.rs
  • crates/aisix-proxy/src/chat.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/ensemble.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/quota.rs
  • crates/aisix-proxy/src/responses.rs
  • schemas/resources/rate_limit_policy.schema.json
  • tests/e2e/src/cases/group-model-condition-ratelimit-e2e.test.ts

The two identities in the {target, parent} pair are distinct strings,
so combining ~= disjunctively made it vacuously true on every routed
request and broke the a ~= b == !(a == b) equivalence the operator
vocabulary (and the dashboard's operator normalization) relies on.
Positive operators keep the exists-reading; ~= now requires every
identity to differ.

Also extends the e2e to the streaming-chat and /v1/responses loops
per review.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/e2e/src/cases/group-model-condition-ratelimit-e2e.test.ts`:
- Around line 438-465: Restore the combined setup guard in the streaming test at
tests/e2e/src/cases/group-model-condition-ratelimit-e2e.test.ts lines 438-465 by
including seed and otlp alongside etcdReachable and app. Apply the same guard
update to the responses test at lines 467-489, preserving ctx.skip() and the
early return.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aa90475e-d213-4670-95fc-5167cedfe96e

📥 Commits

Reviewing files that changed from the base of the PR and between 68f9a64 and a1ea94f.

📒 Files selected for processing (2)
  • crates/aisix-core/src/models/policy_conditions.rs
  • tests/e2e/src/cases/group-model-condition-ratelimit-e2e.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/aisix-core/src/models/policy_conditions.rs

Comment thread tests/e2e/src/cases/group-model-condition-ratelimit-e2e.test.ts
The mock answers SSE or JSON per fixture, so the streaming case gets
its own member + group and asserts the event-stream content type —
the reservation now provably rides the streaming loop, not a JSON
fallback.
@jarvis9443
jarvis9443 merged commit a1f62fd into main Aug 12, 2026
14 checks passed
@jarvis9443
jarvis9443 deleted the fix/policy-model-condition-pair branch August 12, 2026 09:39
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.

2 participants