Skip to content

fix: use model_group instead of model for per-model budget cache keys… - #25381

Closed
jk-f5 wants to merge 1 commit into
BerriAI:mainfrom
jk-f5:fix/model-budget-key-mismatch
Closed

fix: use model_group instead of model for per-model budget cache keys…#25381
jk-f5 wants to merge 1 commit into
BerriAI:mainfrom
jk-f5:fix/model-budget-key-mismatch

Conversation

@jk-f5

@jk-f5 jk-f5 commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Fixes per-model budget write path using deployment name instead of public model name for cache keys, causing budget enforcement to silently fail when deployments have provider prefixes.

Pre-Submission checklist

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Type

🐛 Bug Fix

Changes

Problem

The write path in async_log_success_event uses standard_logging_payload["model"] (the deployment name, e.g. "deepseek/deepseek-chat") to build cache keys. The read path in is_key_within_model_budget uses request_data["model"] (the public model name, e.g. "deepseek-chat") to build cache keys. These never match when the deployment name includes a provider prefix.

For example, a model group "deepseek-chat" backed by deployments "deepseek/deepseek-chat" and "openrouter/deepseek/deepseek-chat":

Write path (after deepseek/deepseek-chat call):
  cache key = virtual_key_spend:{hash}:deepseek/deepseek-chat:1d

Write path (after openrouter/deepseek/deepseek-chat call):
  cache key = virtual_key_spend:{hash}:openrouter/deepseek/deepseek-chat:1d

Read path (budget check):
  cache key = virtual_key_spend:{hash}:deepseek-chat:1d  -> miss

Spend is written to keys the read path never looks up. The budget is never enforced. Additionally, spend from different deployments in the same model group is tracked in separate counters instead of being aggregated, so even if the keys were found, the per-deployment totals would each be lower than the actual aggregate spend.

The read path does have a stripped-prefix fallback (try model, then strip prefix and retry), but it strips on the read side — it can remove a prefix from the lookup key, not add one to match a prefixed cache entry. When the write wrote deepseek/deepseek-chat and the read looks for deepseek-chat, stripping deepseek-chat yields deepseek-chat again (no / to strip). The fallback goes the wrong direction.

Fix

One-line change: prefer standard_logging_payload["model_group"] over ["model"] when building the model name used for cache keys and budget config lookups on the write path. Falls back to ["model"] when model_group is not set (e.g. direct API calls without the router).

model_group is the public router name set by metadata["model_group"] in the standard logging payload. It matches exactly how budget configs are keyed and how the read path looks up spend.

Performance impact

None. This changes which dict key is read from the standard logging payload. No additional computation, no additional I/O.

… on write path

The write path in async_log_success_event used
standard_logging_payload["model"] (the deployment name, e.g.
"deepseek/deepseek-chat") to build cache keys.  The read path in
is_key_within_model_budget uses request_data["model"] (the public
model name, e.g. "deepseek-chat") to build cache keys.  These never
match when the deployment name includes a provider prefix.

For example, a model group "deepseek-chat" backed by deployments
"deepseek/deepseek-chat" and "openrouter/deepseek/deepseek-chat":

  Write path (after deepseek/deepseek-chat call):
    cache key = virtual_key_spend:{hash}:deepseek/deepseek-chat:1d

  Write path (after openrouter/deepseek/deepseek-chat call):
    cache key = virtual_key_spend:{hash}:openrouter/deepseek/deepseek-chat:1d

  Read path (budget check):
    cache key = virtual_key_spend:{hash}:deepseek-chat:1d  -> miss

Spend is written to keys the read path never looks up.  The budget
is never enforced, and spend from different deployments in the same
group is tracked in separate counters instead of being aggregated.

The fix: prefer standard_logging_payload["model_group"] (the public
router name that matches how budgets are configured) over ["model"],
falling back to ["model"] when model_group is not set.  This aligns
the write path cache keys with the read path and correctly aggregates
spend across all deployments in a model group.

Includes 9 new tests covering model_group usage with single-slash and
multi-slash deployment names, fallback behavior, cache key alignment
between write and read paths, cross-deployment spend aggregation, and
an end-to-end write-then-read round trip.
@vercel

vercel Bot commented Apr 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Apr 8, 2026 11:44pm

Request Review

@codspeed-hq

codspeed-hq Bot commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing jk-f5:fix/model-budget-key-mismatch (80104f9) with main (072d410)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a cache key mismatch in the per-model budget limiter's write path. The write path was using standard_logging_payload["model"] (the deployment name, e.g. "deepseek/deepseek-chat") to build cache keys, while the read path used the public model/group name (e.g. "deepseek-chat"), causing silent budget enforcement failures. The fix prefers model_group over model on the write path, matching how budget configs are keyed and how the read path looks up spend.

Confidence Score: 5/5

Safe to merge — targeted one-line fix with comprehensive mock-only tests that directly verify the corrected cache key alignment.

The fix is minimal and correct: model_group or model on the write path aligns cache keys with the read path. model_group is a defined Optional[str] field on StandardLoggingPayload, the fallback to model preserves behaviour for direct (non-router) calls, and all 8 new tests are mock-only unit tests with no network calls. No P0/P1 findings were identified.

No files require special attention.

Vulnerabilities

No security concerns identified. The change only affects which dict key is read from an existing payload to construct an in-memory cache key; no auth, input validation, or data exposure concerns are introduced.

Important Files Changed

Filename Overview
litellm/proxy/hooks/model_max_budget_limiter.py One-line targeted fix: prefer model_group over model when building the cache key on the write path, aligning it with the read path's key format.
tests/test_litellm/proxy/hooks/test_model_budget_write_path_key_mismatch.py New test file with 8 mock-only unit tests covering: model_group preference, multi-slash deployments, end-user spend keys, model fallback, cache key alignment, spend aggregation, and an end-to-end write-then-read scenario.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Request completes] --> B[async_log_success_event]
    B --> C{model_group set?}
    C -->|Yes| D[Use model_group as cache key name]
    C -->|No| E[Use model as cache key name]
    D --> F[Write path and read path use same key]
    E --> G[Write path and read path may differ]
    F --> H[Budget correctly enforced]
    G --> I[Budget silently skipped]
    style H fill:#90EE90
    style I fill:#FFB6C1
Loading

Reviews (1): Last reviewed commit: "fix: use model_group instead of model fo..." | Re-trigger Greptile

@codecov

codecov Bot commented Apr 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@krrish-berri-2

Copy link
Copy Markdown
Contributor

Closing in favor of #25549

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