Skip to content

fix(budget): treat max_budget=0 as blocking spend, not unlimited - #43292

Closed
apex-mochen wants to merge 3 commits into
BerriAI:mainfrom
apex-mochen:fix/budget-zero-blocks-spend
Closed

apex-mochen wants to merge 3 commits into
BerriAI:mainfrom
apex-mochen:fix/budget-zero-blocks-spend

Conversation

@apex-mochen

@apex-mochen apex-mochen commented Sep 26, 2026 •

Copy link
Copy Markdown

Fixes #43214.

max_budget=0 is a legit config (admin wants to block all spend on a provider/deployment/tag), but the three guards used if config.max_budget and ..., so 0 was falsy and the comparison was skipped. Use is not None instead, matching the existing provider-null check a few lines above.

Signed-off-by: ding 2756823972@qq.com


Rebuilt on current main

The previous revision of this branch came from an older base and had budget_limiter.py
rewritten wholesale. Two consequences: the file's line endings were converted to CRLF, and —
more seriously — it reverted work that had since landed on main. It removed
_sum_increments_by_key, _redis_increment_queue_lock, _redis_increment_flush_lock and
_detached_increment_operations, and replaced the _push_increments_to_redis helper with
inline code. That is why a three-line fix showed up as +848/-937, and why enterprise-routing,
CodeQL and codecov/patch were failing on the branch.

The branch is now a single commit on f4308bc1 containing only the change:

litellm/router_strategy/budget_limiter.py            +3  -3
tests/unit/router_strategy/test_budget_limiter.py   +82  -0

Tests

  • test_zero_max_budget_blocks_spend[provider|deployment|tag] — a configured max_budget=0
    must not keep the deployment, for each of the three guards.
  • test_provider_budget_boundary_is_unchanged — 10.0 with spend 0.5 stays kept, 10.0 with
    spend 10.0 is blocked, so the fix cannot be mistaken for "block everything".

Verified locally with litellm's pinned toolchain:

  • pytest tests/unit/router_strategy/test_budget_limiter.py → 12 passed (5 new cases).
  • Control: reverting only the budget_limiter.py change makes all three zero-budget cases
    fail, so the tests pin the defect rather than describing it.
  • ruff check and ruff format --check with the pinned ruff==0.15.3 → clean.
  • Both files are LF and valid UTF-8.

Adjacent, deliberately not changed here

  • _has_budget_configured (budget_limiter.py:844) has the same falsy-zero shape, but changing
    it alone changes nothing: _init_deployment_budgets still requires budget_duration to be set
    (:890), so a deployment with max_budget=0 and no duration gets no budget config either way.
    What a budget without a period should mean is a decision, not a typo fix, so it is called out
    rather than bundled in.
  • A provider budget config whose max_budget is None makes _filter_out_deployments_above_budget
    continue (:240), which drops the deployment entirely rather than treating it as unlimited.
    I hit this while writing the boundary test; it is a separate issue and is not addressed here.

@greptile-apps

greptile-apps Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 2/5

[Medium risk] Changes budget limit logic for zero values.

The PR is not safe to merge until spend recording and Redis synchronization are restored.

Findings

  1. P1 Failed writes lose spend ▶
  2. P1 Sync overwrites fresh spend ▶
  3. P1 Missing provider aborts accounting ▶
  4. P2 Zero-budget fix lacks tests ▶

Summary

The PR makes zero-valued provider, deployment, and tag budgets block routing, but also changes success-event accounting and Redis spend synchronization.

  • The zero-budget comparisons implement the stated behavior when the corresponding budget is configured.
  • The unrelated accounting changes can lose or temporarily understate spend.

Reviews (1) · Last reviewed commit: "fix(budget): treat max_budget=0 as block..."

Comment on lines +96 to +100
async def _push_increments_to_redis(redis_cache: RedisCache, queued: list[RedisPipelineIncrementOperation]) -> None:
try:
await redis_cache.async_increment_pipeline(increment_list=queued)
except Exception as e:
log_redis_failure(verbose_router_logger, logging.ERROR, "Error syncing in-memory cache with Redis", e)

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.

P1 Failed writes lose spend When a Redis pipeline fails, this helper only logs the error after the spend increments have been removed from the queue. A transient Redis outage permanently loses those increments, so later routing decisions can use an understated budget. Keep failed batches available for retry.

Comment on lines +598 to +604
redis_values: Final = await self.dual_cache.redis_cache.async_batch_get_cache(key_list=cache_keys)

# Update in-memory cache with Redis values
if isinstance(redis_values, dict): # Check if redis_values is a dictionary
for key, value in redis_values.items():
if value is not None:
await self.dual_cache.in_memory_cache.async_set_cache(key=key, value=float(value))

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.

P1 Sync overwrites fresh spend The sync starts a Redis write without waiting for it, then replaces in-memory spend with a Redis read. If the read precedes the write, or a new charge arrives during the read, the replacement omits locally recorded spend. Until a later sync corrects it, a near-limit deployment can be admitted past its budget. Wait for the write and retain pending local increments when updating memory.

Comment on lines +419 to +421
custom_llm_provider: Final[str] = kwargs.get("litellm_params", {}).get("custom_llm_provider", None)
if custom_llm_provider is None:
raise ValueError("custom_llm_provider is required")

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.

P1 Missing provider aborts accounting On success callbacks where litellm_params omits custom_llm_provider, this now raises even when the standard logging payload identifies the provider. The exception occurs before provider, deployment, or tag spend is recorded, leaving those budgets undercounted. It also violates the repository directive to avoid backwards-incompatible changes without a user-controlled flag. Preserve the payload as a provider source and allow provider-independent accounting to continue.

Rule Used: What: avoid backwards-incompatible changes without user-controlled flags Why: This breaks current behaviour for users using existing functionality Example of BAD: this PR (#22164) introduced run_post_custom... (source)

budget_limit=config.max_budget,
)

if config.max_budget is not None and current_spend >= config.max_budget:

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.

P2 Zero-budget fix lacks tests The provider, deployment, and tag guards now change how zero limits behave, but no changed test checks that each one excludes a request before any spend. Add regression cases for all three so a future truthiness check cannot silently restore unlimited spending.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread litellm/router_strategy/budget_limiter.py Outdated
Comment thread litellm/router_strategy/budget_limiter.py Outdated
@veria-ai

veria-ai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 2 · PR risk: 0/10

Comment thread litellm/router_strategy/budget_limiter.py Fixed
Comment thread litellm/router_strategy/budget_limiter.py Fixed
Comment thread litellm/router_strategy/budget_limiter.py Fixed
@codecov

codecov Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing apex-mochen:fix/budget-zero-blocks-spend (d2cc411) with main (74cad08)

Open in CodSpeed

max_budget=0 was falsy, so the budget guard skipped and allowed
spend with a zero budget. Compare against None instead.

Signed-off-by: ding <2756823972@qq.com>
@apex-mochen
apex-mochen force-pushed the fix/budget-zero-blocks-spend branch from 95a8652 to 25c5b73 Compare September 27, 2026 13:57
Signed-off-by: apex-mochen <2756823972@qq.com>
@apex-mochen

Copy link
Copy Markdown
Author

Fixed the CRLF issue so the PR now contains only the intended semantic changes. I also covered the additional max_budget=0 path in budget_limiter.py and added regression tests for zero-budget provider/deployment/tag behavior. No unrelated accounting or Redis logic is changed.

@apex-mochen

Copy link
Copy Markdown
Author

misc / Run tests is the only non-green check on this PR, and main is failing the identical tests:

failing passed
this PR @ d2cc411a3a56 4 6819
main @ 74cad08997 (this PR's base commit) 4 6819

Same 4 tests on both sides:

misc / Run tests has been continuously red on main since 74cad08997 (2026-09-28T03:31:22) with no green run since (112 red / 180 green across the last 292 commits), so this run is not going to go green on a re-run. Flagging it so the inherited failure isn't read as a blocker on this diff.

@apex-mochen

Copy link
Copy Markdown
Author

Hi @lets-order-some-fries, same situation as #43293 — the only red check is misc / Run tests, which fails identically on main (evidence in the comment above). Could you take a look when you get a chance? Happy to adjust anything you'd like.

@apex-mochen apex-mochen closed this Oct 3, 2026
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.

RouterBudgetLimiting treats max_budget=0 as unlimited instead of blocking all spend (provider/deployment/tag)

2 participants