fix(quota): run input guardrails before the rate-limit reservation (#542) - #561

Merged
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce
Jun 8, 2026
Merged

fix(quota): run input guardrails before the rate-limit reservation (#542)#561
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce

Conversation

@moonming

@moonmingmoonming commented Jun 8, 2026

Copy link
Copy Markdown
Member

What

A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot. /v1/messages, /v1/responses, /v1/embeddings ran check_inputaftercrate::quota::enforce, and Reservation::drop only releases the concurrency permit — it never refunds the request counter. So a content-policy refusal counted against the caller's quota. /v1/chat/completions already runs guardrails before the reservation precisely to avoid this (its own code comment says so).

How

Hoist the resolve-chain + check_input block above quota::enforce on all three surfaces. (messages was pre-existing; responses + embeddings inherited the ordering from #541/#544.) The budget pre-check ordering is unchanged. Deliberately not fixed via a Reservation::drop refund — that would also refund slots on upstream 5xx/timeouts, a separate policy decision affecting every surface.

Test plan

blocked_request_does_not_consume_rate_limit_slot: API key capped at RPM=1, a blocking guardrail; a blocked request (422) followed by a benign one — the benign request still returns 200 (pre-fix it got 429 because the block burned the only slot). The existing per-surface input-block tests still pass (the guardrail blocks correctly in its new position). fmt + clippy clean; 408 aisix-proxy lib tests pass.

Surfaced by the independent pre-fix audit of the #719 follow-up batch. Refs #542, #719.

Summary by CodeRabbit

  • Improvements

    • Requests blocked by input guardrails and content policies are now evaluated before rate-limit reservation. This prevents blocked requests from consuming your quota, allowing a higher proportion of your rate limit to serve legitimate requests.
  • Tests

    • Added test to verify that content-blocked requests do not consume rate limit slots.

)
A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot: /v1/messages,
/v1/responses, /v1/embeddings ran check_input AFTER crate::quota::enforce, and
Reservation::drop only releases the concurrency permit — it never refunds the
request counter. So a content-policy refusal counted against the caller's
quota. /v1/chat/completions already runs guardrails BEFORE the reservation
specifically to avoid this.
Hoist the resolve-chain + check_input block above quota::enforce on all three
surfaces (messages pre-existing; responses + embeddings widened by #541/#544).
Budget pre-check ordering is unchanged. Not fixed via Reservation::drop refund
— that would also refund slots on upstream failures, a separate policy.
Test: blocked_request_does_not_consume_rate_limit_slot — RPM=1, a blocked
request then a benign one; the benign request still returns 200 (pre-fix it
got 429 because the block burned the slot). fmt + clippy clean; 408 lib tests
pass.
@coderabbitai

coderabbitaiBot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 38d6f1dd-47fe-4149-a3d3-c67e718af39b

📥 Commits

Reviewing files that changed from the base of the PR and between 963068f and f48ce10.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs

📝 Walkthrough

Walkthrough

Three request handlers reorder quota enforcement to occur after input guardrail checks, preventing content-policy blocks from consuming rate-limit slots. A test verifies blocked requests preserve the quota reservation.

Changes

Guardrail-before-quota reordering

Layer / File(s)Summary
Guardrail-before-quota reordering across handlers
crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs
Embeddings, messages, and responses handlers relocate ModelRateLimit construction and crate::quota::enforce() calls to occur after input guardrail checks resolve, preventing blocked requests from consuming RPM slots. Documentation updates explain the ordering requirement and issue references (#542, #719).
Rate-limit slot preservation test
crates/aisix-proxy/src/responses.rs
New test blocked_request_does_not_consume_rate_limit_slot verifies that with rpm=1, a guardrail-blocked /v1/responses request returns 422 and does not consume the quota slot, allowing a subsequent benign request to succeed with 200.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

@moonming
moonming merged commit 69c7685 into mainJun 8, 2026
8 checks passed
@moonming
moonming deleted the fix/issue-542-guardrail-before-enforce branch June 8, 2026 08:00
Sign up for freeto 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.

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(quota): run input guardrails before the rate-limit reservation (#542) - #561

Merged
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce
Jun 8, 2026
Merged

fix(quota): run input guardrails before the rate-limit reservation (#542)#561
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce

Conversation

@moonming

@moonmingmoonming commented Jun 8, 2026

Copy link
Copy Markdown
Member

What

A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot. /v1/messages, /v1/responses, /v1/embeddings ran check_inputaftercrate::quota::enforce, and Reservation::drop only releases the concurrency permit — it never refunds the request counter. So a content-policy refusal counted against the caller's quota. /v1/chat/completions already runs guardrails before the reservation precisely to avoid this (its own code comment says so).

How

Hoist the resolve-chain + check_input block above quota::enforce on all three surfaces. (messages was pre-existing; responses + embeddings inherited the ordering from #541/#544.) The budget pre-check ordering is unchanged. Deliberately not fixed via a Reservation::drop refund — that would also refund slots on upstream 5xx/timeouts, a separate policy decision affecting every surface.

Test plan

blocked_request_does_not_consume_rate_limit_slot: API key capped at RPM=1, a blocking guardrail; a blocked request (422) followed by a benign one — the benign request still returns 200 (pre-fix it got 429 because the block burned the only slot). The existing per-surface input-block tests still pass (the guardrail blocks correctly in its new position). fmt + clippy clean; 408 aisix-proxy lib tests pass.

Surfaced by the independent pre-fix audit of the #719 follow-up batch. Refs #542, #719.

Summary by CodeRabbit

  • Improvements

    • Requests blocked by input guardrails and content policies are now evaluated before rate-limit reservation. This prevents blocked requests from consuming your quota, allowing a higher proportion of your rate limit to serve legitimate requests.
  • Tests

    • Added test to verify that content-blocked requests do not consume rate limit slots.

)
A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot: /v1/messages,
/v1/responses, /v1/embeddings ran check_input AFTER crate::quota::enforce, and
Reservation::drop only releases the concurrency permit — it never refunds the
request counter. So a content-policy refusal counted against the caller's
quota. /v1/chat/completions already runs guardrails BEFORE the reservation
specifically to avoid this.
Hoist the resolve-chain + check_input block above quota::enforce on all three
surfaces (messages pre-existing; responses + embeddings widened by #541/#544).
Budget pre-check ordering is unchanged. Not fixed via Reservation::drop refund
— that would also refund slots on upstream failures, a separate policy.
Test: blocked_request_does_not_consume_rate_limit_slot — RPM=1, a blocked
request then a benign one; the benign request still returns 200 (pre-fix it
got 429 because the block burned the slot). fmt + clippy clean; 408 lib tests
pass.
@coderabbitai

coderabbitaiBot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 38d6f1dd-47fe-4149-a3d3-c67e718af39b

📥 Commits

Reviewing files that changed from the base of the PR and between 963068f and f48ce10.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs

📝 Walkthrough

Walkthrough

Three request handlers reorder quota enforcement to occur after input guardrail checks, preventing content-policy blocks from consuming rate-limit slots. A test verifies blocked requests preserve the quota reservation.

Changes

Guardrail-before-quota reordering

Layer / File(s)Summary
Guardrail-before-quota reordering across handlers
crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs
Embeddings, messages, and responses handlers relocate ModelRateLimit construction and crate::quota::enforce() calls to occur after input guardrail checks resolve, preventing blocked requests from consuming RPM slots. Documentation updates explain the ordering requirement and issue references (#542, #719).
Rate-limit slot preservation test
crates/aisix-proxy/src/responses.rs
New test blocked_request_does_not_consume_rate_limit_slot verifies that with rpm=1, a guardrail-blocked /v1/responses request returns 422 and does not consume the quota slot, allowing a subsequent benign request to succeed with 200.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

@moonming
moonming merged commit 69c7685 into mainJun 8, 2026
8 checks passed
@moonming
moonming deleted the fix/issue-542-guardrail-before-enforce branch June 8, 2026 08:00
Sign up for freeto 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.

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(quota): run input guardrails before the rate-limit reservation (#542) - #561

Merged
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce
Jun 8, 2026
Merged

fix(quota): run input guardrails before the rate-limit reservation (#542)#561
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce

Conversation

@moonming

@moonmingmoonming commented Jun 8, 2026

Copy link
Copy Markdown
Member

What

A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot. /v1/messages, /v1/responses, /v1/embeddings ran check_inputaftercrate::quota::enforce, and Reservation::drop only releases the concurrency permit — it never refunds the request counter. So a content-policy refusal counted against the caller's quota. /v1/chat/completions already runs guardrails before the reservation precisely to avoid this (its own code comment says so).

How

Hoist the resolve-chain + check_input block above quota::enforce on all three surfaces. (messages was pre-existing; responses + embeddings inherited the ordering from #541/#544.) The budget pre-check ordering is unchanged. Deliberately not fixed via a Reservation::drop refund — that would also refund slots on upstream 5xx/timeouts, a separate policy decision affecting every surface.

Test plan

blocked_request_does_not_consume_rate_limit_slot: API key capped at RPM=1, a blocking guardrail; a blocked request (422) followed by a benign one — the benign request still returns 200 (pre-fix it got 429 because the block burned the only slot). The existing per-surface input-block tests still pass (the guardrail blocks correctly in its new position). fmt + clippy clean; 408 aisix-proxy lib tests pass.

Surfaced by the independent pre-fix audit of the #719 follow-up batch. Refs #542, #719.

Summary by CodeRabbit

  • Improvements

    • Requests blocked by input guardrails and content policies are now evaluated before rate-limit reservation. This prevents blocked requests from consuming your quota, allowing a higher proportion of your rate limit to serve legitimate requests.
  • Tests

    • Added test to verify that content-blocked requests do not consume rate limit slots.

)
A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot: /v1/messages,
/v1/responses, /v1/embeddings ran check_input AFTER crate::quota::enforce, and
Reservation::drop only releases the concurrency permit — it never refunds the
request counter. So a content-policy refusal counted against the caller's
quota. /v1/chat/completions already runs guardrails BEFORE the reservation
specifically to avoid this.
Hoist the resolve-chain + check_input block above quota::enforce on all three
surfaces (messages pre-existing; responses + embeddings widened by #541/#544).
Budget pre-check ordering is unchanged. Not fixed via Reservation::drop refund
— that would also refund slots on upstream failures, a separate policy.
Test: blocked_request_does_not_consume_rate_limit_slot — RPM=1, a blocked
request then a benign one; the benign request still returns 200 (pre-fix it
got 429 because the block burned the slot). fmt + clippy clean; 408 lib tests
pass.
@coderabbitai

coderabbitaiBot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 38d6f1dd-47fe-4149-a3d3-c67e718af39b

📥 Commits

Reviewing files that changed from the base of the PR and between 963068f and f48ce10.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs

📝 Walkthrough

Walkthrough

Three request handlers reorder quota enforcement to occur after input guardrail checks, preventing content-policy blocks from consuming rate-limit slots. A test verifies blocked requests preserve the quota reservation.

Changes

Guardrail-before-quota reordering

Layer / File(s)Summary
Guardrail-before-quota reordering across handlers
crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs
Embeddings, messages, and responses handlers relocate ModelRateLimit construction and crate::quota::enforce() calls to occur after input guardrail checks resolve, preventing blocked requests from consuming RPM slots. Documentation updates explain the ordering requirement and issue references (#542, #719).
Rate-limit slot preservation test
crates/aisix-proxy/src/responses.rs
New test blocked_request_does_not_consume_rate_limit_slot verifies that with rpm=1, a guardrail-blocked /v1/responses request returns 422 and does not consume the quota slot, allowing a subsequent benign request to succeed with 200.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

@moonming
moonming merged commit 69c7685 into mainJun 8, 2026
8 checks passed
@moonming
moonming deleted the fix/issue-542-guardrail-before-enforce branch June 8, 2026 08:00
Sign up for freeto 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.

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(quota): run input guardrails before the rate-limit reservation (#542) - #561

Merged
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce
Jun 8, 2026
Merged

fix(quota): run input guardrails before the rate-limit reservation (#542)#561
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce

Conversation

@moonming

@moonmingmoonming commented Jun 8, 2026

Copy link
Copy Markdown
Member

What

A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot. /v1/messages, /v1/responses, /v1/embeddings ran check_inputaftercrate::quota::enforce, and Reservation::drop only releases the concurrency permit — it never refunds the request counter. So a content-policy refusal counted against the caller's quota. /v1/chat/completions already runs guardrails before the reservation precisely to avoid this (its own code comment says so).

How

Hoist the resolve-chain + check_input block above quota::enforce on all three surfaces. (messages was pre-existing; responses + embeddings inherited the ordering from #541/#544.) The budget pre-check ordering is unchanged. Deliberately not fixed via a Reservation::drop refund — that would also refund slots on upstream 5xx/timeouts, a separate policy decision affecting every surface.

Test plan

blocked_request_does_not_consume_rate_limit_slot: API key capped at RPM=1, a blocking guardrail; a blocked request (422) followed by a benign one — the benign request still returns 200 (pre-fix it got 429 because the block burned the only slot). The existing per-surface input-block tests still pass (the guardrail blocks correctly in its new position). fmt + clippy clean; 408 aisix-proxy lib tests pass.

Surfaced by the independent pre-fix audit of the #719 follow-up batch. Refs #542, #719.

Summary by CodeRabbit

  • Improvements

    • Requests blocked by input guardrails and content policies are now evaluated before rate-limit reservation. This prevents blocked requests from consuming your quota, allowing a higher proportion of your rate limit to serve legitimate requests.
  • Tests

    • Added test to verify that content-blocked requests do not consume rate limit slots.

)
A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot: /v1/messages,
/v1/responses, /v1/embeddings ran check_input AFTER crate::quota::enforce, and
Reservation::drop only releases the concurrency permit — it never refunds the
request counter. So a content-policy refusal counted against the caller's
quota. /v1/chat/completions already runs guardrails BEFORE the reservation
specifically to avoid this.
Hoist the resolve-chain + check_input block above quota::enforce on all three
surfaces (messages pre-existing; responses + embeddings widened by #541/#544).
Budget pre-check ordering is unchanged. Not fixed via Reservation::drop refund
— that would also refund slots on upstream failures, a separate policy.
Test: blocked_request_does_not_consume_rate_limit_slot — RPM=1, a blocked
request then a benign one; the benign request still returns 200 (pre-fix it
got 429 because the block burned the slot). fmt + clippy clean; 408 lib tests
pass.
@coderabbitai

coderabbitaiBot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 38d6f1dd-47fe-4149-a3d3-c67e718af39b

📥 Commits

Reviewing files that changed from the base of the PR and between 963068f and f48ce10.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs

📝 Walkthrough

Walkthrough

Three request handlers reorder quota enforcement to occur after input guardrail checks, preventing content-policy blocks from consuming rate-limit slots. A test verifies blocked requests preserve the quota reservation.

Changes

Guardrail-before-quota reordering

Layer / File(s)Summary
Guardrail-before-quota reordering across handlers
crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs
Embeddings, messages, and responses handlers relocate ModelRateLimit construction and crate::quota::enforce() calls to occur after input guardrail checks resolve, preventing blocked requests from consuming RPM slots. Documentation updates explain the ordering requirement and issue references (#542, #719).
Rate-limit slot preservation test
crates/aisix-proxy/src/responses.rs
New test blocked_request_does_not_consume_rate_limit_slot verifies that with rpm=1, a guardrail-blocked /v1/responses request returns 422 and does not consume the quota slot, allowing a subsequent benign request to succeed with 200.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

@moonming
moonming merged commit 69c7685 into mainJun 8, 2026
8 checks passed
@moonming
moonming deleted the fix/issue-542-guardrail-before-enforce branch June 8, 2026 08:00
Sign up for freeto 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.

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(quota): run input guardrails before the rate-limit reservation (#542) - #561

Merged
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce
Jun 8, 2026
Merged

fix(quota): run input guardrails before the rate-limit reservation (#542)#561
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce

Conversation

@moonming

@moonmingmoonming commented Jun 8, 2026

Copy link
Copy Markdown
Member

What

A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot. /v1/messages, /v1/responses, /v1/embeddings ran check_inputaftercrate::quota::enforce, and Reservation::drop only releases the concurrency permit — it never refunds the request counter. So a content-policy refusal counted against the caller's quota. /v1/chat/completions already runs guardrails before the reservation precisely to avoid this (its own code comment says so).

How

Hoist the resolve-chain + check_input block above quota::enforce on all three surfaces. (messages was pre-existing; responses + embeddings inherited the ordering from #541/#544.) The budget pre-check ordering is unchanged. Deliberately not fixed via a Reservation::drop refund — that would also refund slots on upstream 5xx/timeouts, a separate policy decision affecting every surface.

Test plan

blocked_request_does_not_consume_rate_limit_slot: API key capped at RPM=1, a blocking guardrail; a blocked request (422) followed by a benign one — the benign request still returns 200 (pre-fix it got 429 because the block burned the only slot). The existing per-surface input-block tests still pass (the guardrail blocks correctly in its new position). fmt + clippy clean; 408 aisix-proxy lib tests pass.

Surfaced by the independent pre-fix audit of the #719 follow-up batch. Refs #542, #719.

Summary by CodeRabbit

  • Improvements

    • Requests blocked by input guardrails and content policies are now evaluated before rate-limit reservation. This prevents blocked requests from consuming your quota, allowing a higher proportion of your rate limit to serve legitimate requests.
  • Tests

    • Added test to verify that content-blocked requests do not consume rate limit slots.

)
A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot: /v1/messages,
/v1/responses, /v1/embeddings ran check_input AFTER crate::quota::enforce, and
Reservation::drop only releases the concurrency permit — it never refunds the
request counter. So a content-policy refusal counted against the caller's
quota. /v1/chat/completions already runs guardrails BEFORE the reservation
specifically to avoid this.
Hoist the resolve-chain + check_input block above quota::enforce on all three
surfaces (messages pre-existing; responses + embeddings widened by #541/#544).
Budget pre-check ordering is unchanged. Not fixed via Reservation::drop refund
— that would also refund slots on upstream failures, a separate policy.
Test: blocked_request_does_not_consume_rate_limit_slot — RPM=1, a blocked
request then a benign one; the benign request still returns 200 (pre-fix it
got 429 because the block burned the slot). fmt + clippy clean; 408 lib tests
pass.
@coderabbitai

coderabbitaiBot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 38d6f1dd-47fe-4149-a3d3-c67e718af39b

📥 Commits

Reviewing files that changed from the base of the PR and between 963068f and f48ce10.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs

📝 Walkthrough

Walkthrough

Three request handlers reorder quota enforcement to occur after input guardrail checks, preventing content-policy blocks from consuming rate-limit slots. A test verifies blocked requests preserve the quota reservation.

Changes

Guardrail-before-quota reordering

Layer / File(s)Summary
Guardrail-before-quota reordering across handlers
crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs
Embeddings, messages, and responses handlers relocate ModelRateLimit construction and crate::quota::enforce() calls to occur after input guardrail checks resolve, preventing blocked requests from consuming RPM slots. Documentation updates explain the ordering requirement and issue references (#542, #719).
Rate-limit slot preservation test
crates/aisix-proxy/src/responses.rs
New test blocked_request_does_not_consume_rate_limit_slot verifies that with rpm=1, a guardrail-blocked /v1/responses request returns 422 and does not consume the quota slot, allowing a subsequent benign request to succeed with 200.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

@moonming
moonming merged commit 69c7685 into mainJun 8, 2026
8 checks passed
@moonming
moonming deleted the fix/issue-542-guardrail-before-enforce branch June 8, 2026 08:00
Sign up for freeto 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.

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(quota): run input guardrails before the rate-limit reservation (#542) - #561

Merged
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce
Jun 8, 2026
Merged

fix(quota): run input guardrails before the rate-limit reservation (#542)#561
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce

Conversation

@moonming

@moonmingmoonming commented Jun 8, 2026

Copy link
Copy Markdown
Member

What

A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot. /v1/messages, /v1/responses, /v1/embeddings ran check_inputaftercrate::quota::enforce, and Reservation::drop only releases the concurrency permit — it never refunds the request counter. So a content-policy refusal counted against the caller's quota. /v1/chat/completions already runs guardrails before the reservation precisely to avoid this (its own code comment says so).

How

Hoist the resolve-chain + check_input block above quota::enforce on all three surfaces. (messages was pre-existing; responses + embeddings inherited the ordering from #541/#544.) The budget pre-check ordering is unchanged. Deliberately not fixed via a Reservation::drop refund — that would also refund slots on upstream 5xx/timeouts, a separate policy decision affecting every surface.

Test plan

blocked_request_does_not_consume_rate_limit_slot: API key capped at RPM=1, a blocking guardrail; a blocked request (422) followed by a benign one — the benign request still returns 200 (pre-fix it got 429 because the block burned the only slot). The existing per-surface input-block tests still pass (the guardrail blocks correctly in its new position). fmt + clippy clean; 408 aisix-proxy lib tests pass.

Surfaced by the independent pre-fix audit of the #719 follow-up batch. Refs #542, #719.

Summary by CodeRabbit

  • Improvements

    • Requests blocked by input guardrails and content policies are now evaluated before rate-limit reservation. This prevents blocked requests from consuming your quota, allowing a higher proportion of your rate limit to serve legitimate requests.
  • Tests

    • Added test to verify that content-blocked requests do not consume rate limit slots.

)
A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot: /v1/messages,
/v1/responses, /v1/embeddings ran check_input AFTER crate::quota::enforce, and
Reservation::drop only releases the concurrency permit — it never refunds the
request counter. So a content-policy refusal counted against the caller's
quota. /v1/chat/completions already runs guardrails BEFORE the reservation
specifically to avoid this.
Hoist the resolve-chain + check_input block above quota::enforce on all three
surfaces (messages pre-existing; responses + embeddings widened by #541/#544).
Budget pre-check ordering is unchanged. Not fixed via Reservation::drop refund
— that would also refund slots on upstream failures, a separate policy.
Test: blocked_request_does_not_consume_rate_limit_slot — RPM=1, a blocked
request then a benign one; the benign request still returns 200 (pre-fix it
got 429 because the block burned the slot). fmt + clippy clean; 408 lib tests
pass.
@coderabbitai

coderabbitaiBot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 38d6f1dd-47fe-4149-a3d3-c67e718af39b

📥 Commits

Reviewing files that changed from the base of the PR and between 963068f and f48ce10.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs

📝 Walkthrough

Walkthrough

Three request handlers reorder quota enforcement to occur after input guardrail checks, preventing content-policy blocks from consuming rate-limit slots. A test verifies blocked requests preserve the quota reservation.

Changes

Guardrail-before-quota reordering

Layer / File(s)Summary
Guardrail-before-quota reordering across handlers
crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs
Embeddings, messages, and responses handlers relocate ModelRateLimit construction and crate::quota::enforce() calls to occur after input guardrail checks resolve, preventing blocked requests from consuming RPM slots. Documentation updates explain the ordering requirement and issue references (#542, #719).
Rate-limit slot preservation test
crates/aisix-proxy/src/responses.rs
New test blocked_request_does_not_consume_rate_limit_slot verifies that with rpm=1, a guardrail-blocked /v1/responses request returns 422 and does not consume the quota slot, allowing a subsequent benign request to succeed with 200.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

@moonming
moonming merged commit 69c7685 into mainJun 8, 2026
8 checks passed
@moonming
moonming deleted the fix/issue-542-guardrail-before-enforce branch June 8, 2026 08:00
Sign up for freeto 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.

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(quota): run input guardrails before the rate-limit reservation (#542) - #561

Merged
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce
Jun 8, 2026
Merged

fix(quota): run input guardrails before the rate-limit reservation (#542)#561
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce

Conversation

@moonming

@moonmingmoonming commented Jun 8, 2026

Copy link
Copy Markdown
Member

What

A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot. /v1/messages, /v1/responses, /v1/embeddings ran check_inputaftercrate::quota::enforce, and Reservation::drop only releases the concurrency permit — it never refunds the request counter. So a content-policy refusal counted against the caller's quota. /v1/chat/completions already runs guardrails before the reservation precisely to avoid this (its own code comment says so).

How

Hoist the resolve-chain + check_input block above quota::enforce on all three surfaces. (messages was pre-existing; responses + embeddings inherited the ordering from #541/#544.) The budget pre-check ordering is unchanged. Deliberately not fixed via a Reservation::drop refund — that would also refund slots on upstream 5xx/timeouts, a separate policy decision affecting every surface.

Test plan

blocked_request_does_not_consume_rate_limit_slot: API key capped at RPM=1, a blocking guardrail; a blocked request (422) followed by a benign one — the benign request still returns 200 (pre-fix it got 429 because the block burned the only slot). The existing per-surface input-block tests still pass (the guardrail blocks correctly in its new position). fmt + clippy clean; 408 aisix-proxy lib tests pass.

Surfaced by the independent pre-fix audit of the #719 follow-up batch. Refs #542, #719.

Summary by CodeRabbit

  • Improvements

    • Requests blocked by input guardrails and content policies are now evaluated before rate-limit reservation. This prevents blocked requests from consuming your quota, allowing a higher proportion of your rate limit to serve legitimate requests.
  • Tests

    • Added test to verify that content-blocked requests do not consume rate limit slots.

)
A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot: /v1/messages,
/v1/responses, /v1/embeddings ran check_input AFTER crate::quota::enforce, and
Reservation::drop only releases the concurrency permit — it never refunds the
request counter. So a content-policy refusal counted against the caller's
quota. /v1/chat/completions already runs guardrails BEFORE the reservation
specifically to avoid this.
Hoist the resolve-chain + check_input block above quota::enforce on all three
surfaces (messages pre-existing; responses + embeddings widened by #541/#544).
Budget pre-check ordering is unchanged. Not fixed via Reservation::drop refund
— that would also refund slots on upstream failures, a separate policy.
Test: blocked_request_does_not_consume_rate_limit_slot — RPM=1, a blocked
request then a benign one; the benign request still returns 200 (pre-fix it
got 429 because the block burned the slot). fmt + clippy clean; 408 lib tests
pass.
@coderabbitai

coderabbitaiBot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 38d6f1dd-47fe-4149-a3d3-c67e718af39b

📥 Commits

Reviewing files that changed from the base of the PR and between 963068f and f48ce10.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs

📝 Walkthrough

Walkthrough

Three request handlers reorder quota enforcement to occur after input guardrail checks, preventing content-policy blocks from consuming rate-limit slots. A test verifies blocked requests preserve the quota reservation.

Changes

Guardrail-before-quota reordering

Layer / File(s)Summary
Guardrail-before-quota reordering across handlers
crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs
Embeddings, messages, and responses handlers relocate ModelRateLimit construction and crate::quota::enforce() calls to occur after input guardrail checks resolve, preventing blocked requests from consuming RPM slots. Documentation updates explain the ordering requirement and issue references (#542, #719).
Rate-limit slot preservation test
crates/aisix-proxy/src/responses.rs
New test blocked_request_does_not_consume_rate_limit_slot verifies that with rpm=1, a guardrail-blocked /v1/responses request returns 422 and does not consume the quota slot, allowing a subsequent benign request to succeed with 200.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

@moonming
moonming merged commit 69c7685 into mainJun 8, 2026
8 checks passed
@moonming
moonming deleted the fix/issue-542-guardrail-before-enforce branch June 8, 2026 08:00
Sign up for freeto 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.

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(quota): run input guardrails before the rate-limit reservation (#542) - #561

Merged
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce
Jun 8, 2026
Merged

fix(quota): run input guardrails before the rate-limit reservation (#542)#561
moonming merged 1 commit into
mainfrom
fix/issue-542-guardrail-before-enforce

Conversation

@moonming

@moonmingmoonming commented Jun 8, 2026

Copy link
Copy Markdown
Member

What

A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot. /v1/messages, /v1/responses, /v1/embeddings ran check_inputaftercrate::quota::enforce, and Reservation::drop only releases the concurrency permit — it never refunds the request counter. So a content-policy refusal counted against the caller's quota. /v1/chat/completions already runs guardrails before the reservation precisely to avoid this (its own code comment says so).

How

Hoist the resolve-chain + check_input block above quota::enforce on all three surfaces. (messages was pre-existing; responses + embeddings inherited the ordering from #541/#544.) The budget pre-check ordering is unchanged. Deliberately not fixed via a Reservation::drop refund — that would also refund slots on upstream 5xx/timeouts, a separate policy decision affecting every surface.

Test plan

blocked_request_does_not_consume_rate_limit_slot: API key capped at RPM=1, a blocking guardrail; a blocked request (422) followed by a benign one — the benign request still returns 200 (pre-fix it got 429 because the block burned the only slot). The existing per-surface input-block tests still pass (the guardrail blocks correctly in its new position). fmt + clippy clean; 408 aisix-proxy lib tests pass.

Surfaced by the independent pre-fix audit of the #719 follow-up batch. Refs #542, #719.

Summary by CodeRabbit

  • Improvements

    • Requests blocked by input guardrails and content policies are now evaluated before rate-limit reservation. This prevents blocked requests from consuming your quota, allowing a higher proportion of your rate limit to serve legitimate requests.
  • Tests

    • Added test to verify that content-blocked requests do not consume rate limit slots.

)
A guardrail-blocked request burned an RPM/RPD/RPS/RPH slot: /v1/messages,
/v1/responses, /v1/embeddings ran check_input AFTER crate::quota::enforce, and
Reservation::drop only releases the concurrency permit — it never refunds the
request counter. So a content-policy refusal counted against the caller's
quota. /v1/chat/completions already runs guardrails BEFORE the reservation
specifically to avoid this.
Hoist the resolve-chain + check_input block above quota::enforce on all three
surfaces (messages pre-existing; responses + embeddings widened by #541/#544).
Budget pre-check ordering is unchanged. Not fixed via Reservation::drop refund
— that would also refund slots on upstream failures, a separate policy.
Test: blocked_request_does_not_consume_rate_limit_slot — RPM=1, a blocked
request then a benign one; the benign request still returns 200 (pre-fix it
got 429 because the block burned the slot). fmt + clippy clean; 408 lib tests
pass.
@coderabbitai

coderabbitaiBot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 38d6f1dd-47fe-4149-a3d3-c67e718af39b

📥 Commits

Reviewing files that changed from the base of the PR and between 963068f and f48ce10.

📒 Files selected for processing (3)
  • crates/aisix-proxy/src/embeddings.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-proxy/src/responses.rs

📝 Walkthrough

Walkthrough

Three request handlers reorder quota enforcement to occur after input guardrail checks, preventing content-policy blocks from consuming rate-limit slots. A test verifies blocked requests preserve the quota reservation.

Changes

Guardrail-before-quota reordering

Layer / File(s)Summary
Guardrail-before-quota reordering across handlers
crates/aisix-proxy/src/embeddings.rs, crates/aisix-proxy/src/messages.rs, crates/aisix-proxy/src/responses.rs
Embeddings, messages, and responses handlers relocate ModelRateLimit construction and crate::quota::enforce() calls to occur after input guardrail checks resolve, preventing blocked requests from consuming RPM slots. Documentation updates explain the ordering requirement and issue references (#542, #719).
Rate-limit slot preservation test
crates/aisix-proxy/src/responses.rs
New test blocked_request_does_not_consume_rate_limit_slot verifies that with rpm=1, a guardrail-blocked /v1/responses request returns 422 and does not consume the quota slot, allowing a subsequent benign request to succeed with 200.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

@moonming
moonming merged commit 69c7685 into mainJun 8, 2026
8 checks passed
@moonming
moonming deleted the fix/issue-542-guardrail-before-enforce branch June 8, 2026 08:00
Sign up for freeto 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.

1 participant

@moonming