fix(batches): price a retrieved batch from its deployment's model and rates (internal copy of #37077) - #37219
Conversation
… rates Retrieving a completed batch computed its cost with no model identity: neither the deployment's model nor its configured pricing reached the batch cost calculation. For bedrock that left the cost model falling back to the provider's own response model (e.g. "claude-sonnet-4-6"), which does not resolve under a bedrock provider, so the lookup missed and cost silently became $0 while usage stayed correct. Dropping the deployment's model info separately discarded any rates configured on that deployment, billing a zero-cost deployment at the public rate instead. Both are the same omission at the call site, so both are fixed by passing the logging object's own model and the pricing the router registered for the deployment.
…s it The router registers a model_info entry for every deployment, priced or not, and get_model_info fills absent costs with 0. Resolving deployment pricing through it therefore reported a free deployment for any ordinary one, which priced its batches at $0 while usage stayed correct: the same silent under-count this branch set out to remove, widened from bedrock to every provider. Caught by a live batch run, where four vertex batches that price correctly today came back at $0. The raw registration is now what decides: pricing is used only when the deployment actually declares one of the batch cost fields, so ordinary deployments fall back to the global cost map exactly as before. The earlier test missed this by using a deployment id that was never registered, where get_model_info does raise; a real deployment is always registered.
self.model can carry the router's model_group alias, which no cost map resolves, so a bedrock batch still priced at $0 after the model name started being passed. The deployment's own litellm_params model is used when present. Verified against the local (image-bound) cost map that dev and prod both force: alias 'claude-opus-4-5' prices $0.000000 while 'bedrock/global.anthropic.claude-opus-4-5-20251101-v1:0' prices $0.017000
On a batch retrieve both self.model and litellm_params[model] come back None, so the cost model fell through to the provider's own response model (an Anthropic id like claude-opus-4-5-20251101) which does not resolve under a bedrock provider, leaving bedrock batches at $0 with correct usage. model_call_details carries the deployment's provider-qualified model (bedrock/global.anthropic.claude-opus-4-5-20251101-v1:0), confirmed by instrumenting a live retrieve, so it is preferred with the previous two sources kept as fallbacks.
…nset Substituting a deployment's pricing wholesale billed the token direction it did not configure at zero: get_model_info fills an absent cost with 0, and any non-None pricing field suppressed the global fallback. A deployment declaring only input_cost_per_token therefore billed output at nothing. Each of the four batch cost fields now falls back to the model's published rate when the deployment leaves it unset, so a one-sided override applies to the side it configures and only that side. Adds a parametrized regression over input-only, output-only, and both-zero, plus coverage for a deployment whose model has no published entry. Annotates the new test helpers per the repo's type-coverage rule and drops the narrative banner comment from the batch tests.
batch_cost_calculator gated the batch rate fields on truthiness, so a deployment that configures input_cost_per_token_batches or its output twin as 0.0 was read as having configured nothing and that token direction fell through to half the standard rate. Layering declared rates over published ones made this reachable: a deployment declaring only a zero batch rate previously kept a fabricated zero on the standard field, which happened to bill nothing. The two batch fields are now gated on presence. Verified no cost-map entry changes behavior: the only three carrying a zero batch rate are embeddings, whose standard output rate is also 0.0, so both paths yield the same zero. Adds a parametrized regression over an explicit zero, an explicit non-zero, and unset, plus coverage for the deployment id get_model_info cannot resolve, which were the lines Codecov flagged.
Codecov's remaining uncovered patch line was the early return taken when no model is available to look a published entry up by, which leaves a deployment's own declared rates standing alone. Measuring the patch lines against the coverage report now leaves none uncovered.
Filling each cost field independently let a published batch rate outrank a standard rate the deployment configured itself: a deployment declaring only input_cost_per_token had its batches billed at the model's published batch price rather than half its own rate. Measured on a model that publishes both, that billed $0.001500 where the deployment's own rate meant $0.000500. Declaring either rate for a direction now claims that whole direction, so nothing published can displace it, and a direction the deployment is silent on still inherits both published rates.
Greptile SummaryThe PR corrects completed-batch accounting by carrying the deployment’s provider-qualified model and configured pricing into cost calculation.
Confidence Score: 5/5The PR appears safe to merge. The previously reported validation issue has been fixed, and no blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/litellm_core_utils/litellm_logging.py | Resolves deployment model identity and composes configured pricing with published rates on a copied model-info object. |
| litellm/cost_calculator.py | Distinguishes explicit zero batch rates from unset rates during input and output cost calculation. |
| litellm/batches/batch_utils.py | Threads deployment-level model information through completed-batch aggregation. |
| enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py | Aligns poller pricing with inline retrieval by using router-registered deployment pricing. |
| tests/test_litellm/test_cost_calculator.py | Replaces the previously reported disabled type suppressions with typed literals and adds explicit-zero pricing coverage. |
| tests/test_litellm/litellm_core_utils/test_litellm_logging.py | Covers deployment pricing resolution, one-sided overrides, cache immutability, and retrieve-path propagation. |
Reviews (2): Last reviewed commit: "test(cost): type the batch_cost_calculat..." | Re-trigger Greptile
…itellm_internal_copy_37077 # Conflicts: # tests/test_litellm/batches/test_batch_utils.py
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit e736b59. Configure here.
tin-berri
left a comment
There was a problem hiding this comment.
LGTM — solid fix for the batch-retrieve $0 pricing bug (missing model identity meant Bedrock cost-map lookups silently missed) and for custom deployment rates being ignored in favor of the public rate. Good catches on top of the original: avoiding the in-place mutation that would've poisoned get_model_info's lru-cached dict, per-direction rate ownership so a declared side never gets displaced by published rates, and extending the same fix to the enterprise cost poller. Live Bedrock QA matches expected cost to ~1e-16 relative error on both the standard and custom-rate deployments.
Internal copy of #37077 by @marty-sullivan (head 74ce9bb), whose org-owned fork blocks maintainer pushes. All 8 commits are cherry-picked with authorship preserved, plus follow-up commits from review and QA: a fix for an in-place mutation of
get_model_info's lru-cached dict, a merge absorbing #37078 and #37205 from staging, and a fix making the enterprise cost poller price from the same deployment rates as the retrieve pathTLDR
Problem this solves:
How it solves it:
User Flow
Before: a team running Bedrock batch jobs sees completed batches under-billed: $0.00 on gateways without background batch cost tracking, and a deployment's own configured rates ignored on gateways with it
purpose: batch,target_model_names: claude-haiku-4-5) and get back a gateway file idinput_file_idand get back a batch id withstatus: validatingstatus: completedAfter: the same batch reports the spend those tokens actually cost, at the rates the deployment declares
status: validatingstatus: completedRelevant issues
Follows #36876 and #37050, which made a batch cost row reach the spend logs at all. With rows now landing, their amounts became auditable, which is how this surfaced
Linear ticket
Resolves LIT-5666
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
Fresh end-to-end run against real Bedrock managed batches at the merged tip, with the enterprise cost poller active (
PROXY_BATCH_POLLING_INTERVAL=30), which is the production-realistic accountant. Before at the current staging base 3f4810b, After at 5a11fe1 (the tip differs from it only by a test-only commit). A broader 23-deployment run of the inline retrieve path (Bedrock + Vertex + zero-cost Vertex) is preserved in #37077's descriptionShared setup. Two deployments of
bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0:haiku-batch-stdwith no custom pricing andhaiku-batch-customdeclaringinput_cost_per_token_batches: 2.0e-06andoutput_cost_per_token_batches: 4.0e-06.LITELLM_LOCAL_MODEL_COST_MAP=True, fresh database per side. One real 100-record batch per deployment:Before (3f4810b)
aretrieve_batchrow per batchGET /v1/batches/{id}logsresponse_cost_failurewith "Model is None" (19 occurrences across the two batches), so a proxy without the enterprise poller books $0. The total blackout this causes on the older base, where batches got no priced row at all, is the 973329e run in fix(batches): price a retrieved batch from its deployment's model and rates #37077's descriptionAfter (5a11fe1)
us.profile rates halved), relative error 1.8e-16batch_ignore_default_loggingon the retrieve path), and the spend transactions carry theLiteLLM Proxy/CheckBatchCostuser agentType
🐛 Bug Fix
Changes
Retrieving a completed batch computed its cost with no model identity: the call that aggregates a batch's output passed neither the deployment's model nor its pricing
For Bedrock that mattered because the cost model then fell back to the provider's own response model, a bare name like
claude-opus-4-5-20251101, which does not resolve under a bedrock provider prefix. The lookup missed, and a missed lookup returns zero rather than raising, so cost became $0 while usage stayed correctDropping the deployment's model info mattered independently: rates configured on a deployment are registered by the router under that deployment's id, and never consulting them meant a deployment configured to cost nothing was billed from the global map instead. Standard deployments were unaffected, since their configured rates and the global map agree, which is why this only showed up on Bedrock and on custom-rate deployments
Both are fixed at the one call site, by passing the deployment's model and the pricing registered for it. Two details are worth calling out because the obvious implementations of each are wrong:
The model comes from
model_call_details. On this pathself.modelandlitellm_params["model"]are both None, andself.modelcan otherwise hold the router'smodel_groupalias, which no cost map resolves; a live retrieve confirmedmodel_call_detailsis the only one of the three carrying the provider-qualified deployment modelDeployment pricing is decided from the raw registration rather than
get_model_info. The router registers an entry for every deployment whether or not it declares pricing, andget_model_infofills absent costs with 0, so asking it cannot tell "configured as free" apart from "no pricing configured" and would have priced every ordinary deployment's batches at $0. The shared helper therefore returns pricing only when the deployment actually declares one of the batch cost fields, leaving the global map as the default. Ownership is per token direction: the side a deployment leaves unset keeps the published rates, and a declared side is never displaced by themOn top of the original PR, this copy merges the published rates onto a copy of
get_model_info's return value. That function serves every call from one lru-cached dict, so writing the merge into it poisoned every laterget_model_infolookup of the deployment id with the published model's rates for the life of the process; the new regression test fails on the in-place versionThis copy also extends the fix to the enterprise cost poller. #37050 hands accounting ownership to
CheckBatchCostwhenever it is active (poller_owns_accounting), but the poller priced fromdeployment_info.model_info.model_dump(), which never carrieslitellm_paramspricing, so a custom-rate deployment's batches would have been billed at the public rate on exactly the path that now owns them. The deployment-pricing merge is extracted to a module-leveldeployment_pricing_model_infoand the poller calls it with the same deployment id and provider-qualified model as the retrieve path; a regression test asserts the poller passes the declared batch rates through tocalculate_batch_cost_and_usageOne behavior change worth naming: with a model name now reaching this path, a proxy that sets
disable_vertex_batch_output_transformationwill take the Vertex-specific accounting branch on retrieve, which it previously could not reach. That branch exists for exactly that flag, so this makes the flag work on the retrieve path rather than changing what it meansCaveats
Final Attestation