feat(budget): preserve flat-rate capacity when metered budget is exhausted - #14348
Merged
diegosouzapw merged 7 commits intoSep 24, 2026
Conversation
…exhausted The dollar budget is scoped by apiKeyId and is enforced in the api-key policy phase, which runs before any provider is resolved. That gate can answer "has this key spent its allowance?" but never "would this request spend any of it?", so once the allowance is gone the whole request is rejected, including when the provider that would have served it is a flat-rate subscription the allowance does not pay for. A mixed combo cannot fall back to that capacity either. Make budget state a candidate constraint instead of a pre-routing verdict. The chat path passes meteredBudget: "defer-to-candidate" to enforceApiKeyPolicy and re-applies the budget in handleSingleModelChat, after the provider is resolved and before a credential is acquired. That is the single dispatch funnel for the endpoint, so direct requests, combo targets and pipeline stages are all covered. The economic class comes from the classification the repository already has, isFlatRateProvider, so there is one source of truth and no provider id is hardcoded. A provider that is not classified flat-rate is metered, which keeps an unknown provider from becoming a spending bypass. The gate can only remove a candidate; it never nominates one. Ranking, health, quota and compatibility are untouched and normal routing still chooses among the eligible candidates. With the allowance spent and no eligible non-metered candidate, the request fails closed without any paid transport. Placing the gate before credential acquisition matters: the refusal never acquires an account, and the locally produced 429 never reaches the fallback loop that would read it as an upstream rate limit and cool a healthy connection. The refusal is tagged BUDGET_EXCEEDED, and the combo loop now classifies that alongside TOKEN_LIMIT_EXCEEDED as a local per-API-key policy breach, so it is never mistaken for an upstream refusal. Accounting follows the same classification. recordCost now receives the budget-consumable share of the estimate, which is zero for a flat-rate provider, so subscription traffic stops drawing down the metered allowance. Observability is unaffected: analytics recompute cost from the request log with their own flatRateAsZero option, so flat-rate traffic stays fully visible. No schema migration. Every other caller of enforceApiKeyPolicy keeps the default "enforce" behaviour unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner
|
This is a real gap and the fix is clean — reusing |
# Conflicts: # open-sse/handlers/chatCore.ts # src/shared/utils/apiKeyPolicy.ts
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…le-size ceilings Merging the current release tip left the metered-budget wiring 2 lines over the frozen chatCore.ts ceiling and 1 line over executeTargetAttempt.ts. The recordCost wrapper now relies on the contextual type from recordStreamingCost and the comments are condensed; behavior is unchanged. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
The deferred metered-budget branch and the defaulted options parameter pushed enforceApiKeyPolicy from 15 to 17 cyclomatic complexity, a new violation. The skip-when-deferred decision moves into validateBudgetUnlessDeferred; behavior is unchanged. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
diegosouzapw
merged commit Sep 24, 2026
a11eb0f
into
diegosouzapw:release/v3.8.51
10 of 16 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Dollar-budget enforcement runs in the api-key policy phase, before any provider is resolved, and the budget itself is scoped only by
apiKeyId. That gate can answer "has this key spent its allowance?" but never "would this request spend any of it?".So once the allowance is exhausted the request is rejected outright — including when the provider that would have served it is a flat-rate subscription or coding plan that the metered allowance does not pay for. A combo mixing metered and flat-rate targets cannot fall back to the flat-rate one either: nothing gets as far as candidate evaluation.
Impact
recordCostinchatCore), so subscription usage draws down an allowance it never spends against.Existing capability reused
The repository already classifies provider economics explicitly, in
src/lib/usage/flatRateProviders.ts(isFlatRateProvider). This patch reuses that single source of truth rather than introducing a second one, and hardcodes no provider id.A provider that is not classified flat-rate is metered. An unknown or unclassified provider is therefore metered by construction, so it can never become a spending bypass.
Design
Budget state becomes an eligibility constraint during candidate evaluation instead of a pre-routing verdict.
enforceApiKeyPolicygainsmeteredBudget?: "enforce" | "defer-to-candidate". The default is"enforce", so all ~27 existing callers are behaviourally unchanged. Only the chat path opts in.handleSingleModelChatre-applies the budget per resolved candidate. That is the single dispatch funnel for this endpoint — direct requests, combo targets and pipeline stages all pass through it.BUDGET_EXCEEDED, and the combo loop now classifies that alongsideTOKEN_LIMIT_EXCEEDEDas a local per-API-key policy breach, so it is never mistaken for an upstream refusal.The budget can only remove a candidate. It never nominates one, never reorders, and never selects a provider. Ranking, health, quota, capability and combo behaviour are untouched — normal routing remains authoritative over which eligible candidate is used.
Fail-closed behaviour
With the allowance exhausted and no eligible non-metered candidate, the request fails without any paid transport — no upstream call is made, on the direct path or through a combo. Verified by asserting that zero upstream dispatches were recorded, not just by the response status.
Accounting
OBSERVABILITY != BUDGET CONSUMPTION.recordCostnow receives the budget-consumable share of the estimate, which is zero for a flat-rate provider. Telemetry is untouched: analytics recompute cost from the request log with their ownflatRateAsZerooption (lib/usage/costCalculator,lib/usage/usageStats), so flat-rate traffic stays fully visible in usage and cost surfaces. No analytics behaviour was changed to make a budget test pass.Compatibility
apiKeyId.Tests
18 new tests, all passing.
tests/unit/metered-budget-must-not-disable-flat-rate.test.ts— 9/9. Eligibility per economic class, unknown-provider conservatism, accounting split, recovery when the allowance returns, and the local-vs-upstream classification reaching the combo exhaustion sets.tests/integration/combo-matrix/metered-budget-eligibility.test.ts— 9/9. Drives the real chat pipeline and asserts which upstream was actually dispatched: metered target excluded, flat-rate target selected by normal routing, no metered transport, fail-closed with metered-only and with an unhealthy flat-rate candidate, and the provider connection left clean after a refusal (read from the connection row, because a behavioural retry can pass while a connection is quietly cooling).Mutation proof — each control was removed in turn and the relevant tests went red; every mutation was reverted:
Other checks
typecheck:core— clean.no-unused-varscounts matchconfig/quality/eslint-suppressions.jsonexactly).tests/integration/combo-matrix/*— 34/36.The 4 remaining failures are pre-existing. Each was confirmed by reverting this patch in the same tree and re-running: they fail identically on the unmodified base, with the same assertion signature (for example
402 !== 200in the emergency-fallback budget cases). No assertion was rewritten.A full-suite run is not claimed: the two attempted full sweeps were aborted by local disk exhaustion (
ENOSPC) and their output is not reported as evidence.Runtime validation scope
The behaviour was exercised through OmniRoute's real routing pipeline using the integration harness, with isolated database state and recorded upstream dispatch — not through mocked routing decisions.
It was not deployed to, or validated against, a live production OmniRoute instance. No production instance was modified or used for budget-exhaustion testing.
🤖 Generated with Claude Code