Skip to content

fix(batches): price a retrieved batch from its deployment's model and rates - #37077

Closed
marty-sullivan wants to merge 8 commits into
BerriAI:litellm_internal_stagingfrom
cu-aaii:litellm_fix_batch_retrieve_cost_model_identity
Closed

fix(batches): price a retrieved batch from its deployment's model and rates#37077
marty-sullivan wants to merge 8 commits into
BerriAI:litellm_internal_stagingfrom
cu-aaii:litellm_fix_batch_retrieve_cost_model_identity

Conversation

@marty-sullivan

@marty-sullivan marty-sullivan commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Retrieving a batch prices it with no model identity
  • Bedrock batches silently cost $0 with correct tokens
  • A deployment's own configured rates are ignored
  • A zero-cost deployment gets billed the public rate

How it solves it:

  • Price against the deployment's own provider-qualified model
  • Honor rates the deployment actually declares

User Flow

Before: a team running Bedrock batch jobs sees every completed batch land in their spend logs with real token counts but $0.00 spend, so chargeback under-reports

  1. They upload a JSONL file with POST https://litellm-domain/v1/files (purpose: batch, target_model_names: claude-sonnet-4-6) and get back a gateway file id
  2. They send POST https://litellm-domain/v1/batches with that input_file_id and get back a batch id with status: validating
  3. They poll GET https://litellm-domain/v1/batches/{batch_id} until it returns status: completed
  4. They open https://litellm-domain/ui/?page=logs and find the batch's entry showing 1800 prompt and 1000 completion tokens against $0.00 spend
  5. A team whose gateway administrator configured a zero-cost deployment sees the opposite: their batch is billed at the model's public rate even though their deployment is configured to cost nothing

After: the same batch reports the spend those tokens actually cost, and a zero-cost deployment stays at zero

  1. They upload the same JSONL file with POST https://litellm-domain/v1/files and get back a gateway file id
  2. They send the same POST https://litellm-domain/v1/batches and get back a batch id with status: validating
  3. They poll GET https://litellm-domain/v1/batches/{batch_id} until it returns status: completed
  4. https://litellm-domain/ui/?page=logs now shows the same 1800 and 1000 tokens against $0.0102, matching the model's published batch rate
  5. The zero-cost deployment's batch shows $0.00, because the rate configured on that deployment is the one applied

Relevant 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

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

Shared setup. A proxy runs against real Bedrock and Vertex with 23 batch-enabled deployments: five Bedrock Anthropic models, nine standard Vertex models, and nine Vertex deployments configured to cost nothing. Batch polling is off so the retrieve path is the only accountant, which is the path this PR changes. LITELLM_LOCAL_MODEL_COST_MAP=True, matching how these deployments run. One real batch per deployment was submitted and left to complete, then every batch was retrieved over HTTP and the resulting spend rows read back from the database. The same 23 batches are used on both sides, with the rows cleared and each batch's stored state rewound between runs so both runs price the same provider output

Before (973329e)

Bedrock Anthropic

  1. GET /v1/batches/{id} for each of the five, then read the rows
claude-opus-4-5     spend=0   total_tokens=2800
claude-opus-4-6     spend=0   total_tokens=2800
claude-sonnet-4-5   spend=0   total_tokens=2800
claude-sonnet-4-6   spend=0   total_tokens=2800
  1. Usage is correct and spend is zero, so nothing in usage reporting reveals it

Standard Vertex

  1. Same retrieve for the nine standard deployments
gemini-2.5-flash        0.00010025    133
gemini-2.5-pro          0.0004025     133
gemini-3.1-pro-preview  0.000516      136
gemini-3.7-flash        0.0001575     132
  1. These are already correct, and must stay correct

Zero-cost deployments

  1. Same retrieve for the nine deployments configured with zero rates
zero-cost gemini-2.5-flash       0.00010025    133
zero-cost gemini-2.5-pro         0.0003975     132
zero-cost gemini-3.1-pro-preview 0.000522      137
zero-cost gemini-3.7-flash       0.00016125    134
  1. Every one is billed at the public rate, ignoring the rates configured on the deployment

After (d86c0bd)

Bedrock Anthropic

  1. GET /v1/batches/{id} for each of the five, then read the rows
claude-haiku-4-5    spend=0.0034    total_tokens=2800
claude-opus-4-5     spend=0.017     total_tokens=2800
claude-opus-4-6     spend=0.017     total_tokens=2800
claude-sonnet-4-5   spend=0.0102    total_tokens=2800
claude-sonnet-4-6   spend=0.0102    total_tokens=2800
  1. Each equals that model's published rate applied to 1800 prompt and 1000 completion tokens and halved for batch: Haiku 4.5 at $1/$5 per million gives $0.0034, Sonnet at $3/$15 gives $0.0102, Opus at $5/$25 gives $0.017

Standard Vertex

  1. Same retrieve for the nine standard deployments
gemini-2.5-flash        0.00010025    133
gemini-2.5-pro          0.0004025     133
gemini-3.1-pro-preview  0.000516      136
gemini-3.7-flash        0.0001575     132
  1. Byte-for-byte identical to Before, so standard pricing is untouched

Zero-cost deployments

  1. Same retrieve for the nine zero-rate deployments
zero-cost gemini-2.5-flash       0   133
zero-cost gemini-2.5-pro         0   132
zero-cost gemini-3.1-pro-preview 0   137
zero-cost gemini-3.7-flash       0   134
  1. All nine now cost nothing while still reporting real token counts, so the configured rates are the ones applied

Across all 23 batches every row carries token counts that reconcile against the provider's own output file, 23 of 23 price as expected, and no batch produced more than one row

Type

🐛 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 correct

Dropping 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 zero-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 path self.model and litellm_params["model"] are both None, and self.model can otherwise hold the router's model_group alias, which no cost map resolves; a live retrieve confirmed model_call_details is the only one of the three carrying the provider-qualified deployment model

Deployment 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, and get_model_info fills 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. get_router_deployment_model_info therefore returns pricing only when the deployment actually declares one of the batch cost fields, leaving the global map as the default

One behavior change worth naming: with a model name now reaching this path, a proxy that sets disable_vertex_batch_output_transformation will 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 means

Caveats

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

… 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.
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing cu-aaii:litellm_fix_batch_retrieve_cost_model_identity (74ce9bb) with litellm_internal_staging (973329e)

Open in CodSpeed

…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.
@marty-sullivan

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR threads deployment model identity and pricing into completed-batch accounting and preserves explicit zero batch rates. However, its partial-pricing merge can bypass deployment-specific standard rates

  • Passes provider-qualified deployment models and deployment ModelInfo into retrieved-batch aggregation
  • Distinguishes explicit zero batch rates from absent rates in the batch calculator
  • Layers published pricing into partially configured deployment pricing

Confidence Score: 4/5

The PR is not yet safe to merge because partially configured deployments can have retrieved batches charged at public rates instead of their configured rates

Filling an absent batch-rate field from published pricing prevents batch_cost_calculator from deriving the charge from the deployment-specific standard rate

Files Needing Attention: litellm/litellm_core_utils/litellm_logging.py

Important Files Changed

Filename Overview
litellm/litellm_core_utils/litellm_logging.py Adds deployment pricing resolution, but filling absent batch fields from public pricing can override configured standard-rate fallback
litellm/cost_calculator.py Correctly distinguishes explicit zero batch rates from absent rates
litellm/batches/batch_utils.py Threads deployment model identity and ModelInfo into completed-batch aggregation
tests/test_litellm/litellm_core_utils/test_litellm_logging.py Covers deployment lookup and partial standard pricing, but does not assert the resulting batch charge
tests/test_litellm/batches/test_batch_utils.py Adds focused regression coverage for Bedrock model identity and zero-cost deployment pricing
tests/test_litellm/test_cost_calculator.py Verifies explicit zero and absent batch-rate behavior

Reviews (4): Last reviewed commit: "test(batches): cover the deployment with..." | Re-trigger Greptile

Comment thread litellm/litellm_core_utils/litellm_logging.py Outdated
Comment thread tests/test_litellm/batches/test_batch_utils.py
marty-sullivan added a commit to cu-aaii/litellm that referenced this pull request Aug 16, 2026
… rates

Carries the four commits behind upstream PR BerriAI#37077 onto the release branch:

- pass the deployment's model and its registered pricing into batch cost
- resolve deployment pricing only when the deployment declares it, since the
  router registers an entry for every deployment and get_model_info fills
  absent costs with 0
- prefer model_call_details for the model, because self.model and
  litellm_params[model] are both None on a batch retrieve and self.model can
  otherwise hold the router's model_group alias

Verified end to end against real batches for 23 deployments: bedrock anthropic
went from $0 to exact ($0.0034 haiku, $0.0102 sonnet, $0.017 opus), the nine
standard vertex deployments are byte-identical, and the nine zero-rate
deployments went from being billed at public rates to $0 with real token counts.

Two release-branch adaptations: the two AWS credential-forwarding tests are
dropped (that upstream change is not on this branch), and the handler test
resolves whichever success-handler entrypoint this version exposes. Also applies
the outstanding ruff format fix to openai_files_endpoints/common_utils.py.
marty-sullivan added a commit to cu-aaii/litellm that referenced this pull request Aug 17, 2026
…rrections

The earlier carry landed only the first of upstream PR BerriAI#37077's four commits:
cherry-pick -n stopped at a test conflict and the remaining three were never
applied, so the branch shipped without the corrections that PR needed.

Adds them:

- resolve deployment pricing only when the deployment declares it, since the
  router registers an entry for every deployment and get_model_info fills
  absent costs with 0, which would price ordinary deployments' batches at $0
- prefer model_call_details for the model, because self.model and
  litellm_params[model] can both be None on a batch retrieve and self.model can
  otherwise hold the router's model_group alias, which no cost map resolves

Two release-branch adaptations: register_model here takes no
persist_across_reloads argument, and the handler test resolves whichever
success-handler entrypoint this version exposes.
…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.
@marty-sullivan

Copy link
Copy Markdown
Contributor Author

Both addressed. The undeclared side now falls back to the published rate, so a one-sided override applies only to the side it configures, and the test helpers carry concrete types.

Reproduced the partial-pricing case first: a deployment declaring only input billed output at $0.000000 against a published $0.0075. It now reads input from the deployment and output from the cost map, covered by a parametrized regression over input-only, output-only, and both-zero.

@greptileai

Comment thread litellm/litellm_core_utils/litellm_logging.py
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.
@marty-sullivan

Copy link
Copy Markdown
Contributor Author

Agreed, and fixed at the source: batch_cost_calculator now gates the two batch rate fields on presence rather than truthiness, so a rate configured as 0.0 means free instead of unset. Before this a deployment declaring only a zero batch rate was charged half the standard rate for that direction, which layering declared rates over published ones had newly exposed.

Checked the cost map for fallout: the only three entries carrying a zero batch rate are text-embedding-3-large, -3-small and ada-002-v2, whose standard output rate is also 0.0, so both the old and new paths yield the same zero. No published pricing changes behavior.

Also covered the two branches Codecov flagged, so the uncovered lines in litellm_logging.py are gone.

@greptileai

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.
@marty-sullivan
marty-sullivan marked this pull request as ready for review August 17, 2026 03:23
Comment thread litellm/litellm_core_utils/litellm_logging.py Outdated
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.
@marty-sullivan

Copy link
Copy Markdown
Contributor Author

Confirmed and fixed in 74ce9bbf73. On ft:gpt-3.5-turbo, which publishes both rates, a deployment declaring only input_cost_per_token: 1e-06 was billed $0.001500 at the published batch rate instead of the $0.000500 its own rate implies

Ownership is now per token direction rather than per field: declaring either rate for a direction claims that whole direction, so nothing published can displace it, while a direction the deployment is silent on still inherits both published rates. Verified across standard-only, batch-zero-only, all-four-zero, and output-only

Also covered the last uncovered patch line Codecov flagged, the early return when no model is resolvable at all

@greptileai

@greptile-apps

greptile-apps Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

The direction-ownership approach handles that edge case correctly. Looking at the merged result for a deployment with only input_cost_per_token: 1e-06:

  • declares_input = True (blocks both published input fields from overwriting merged)
  • merged["input_cost_per_token_batches"] stays None (what get_model_info returns when the field is absent in the registration)
  • batch_cost_calculator hits input_cost_per_token_batches is not None → false, falls to elif input_cost_per_token:1e-06 / 2 = the right rate

The test in test_a_published_batch_rate_never_displaces_a_declared_standard_rate pins this exactly — asserting input_cost_per_token_batches is None in the returned ModelInfo confirms no published batch rate sneaks in via the merge.

The zero-rate path is also consistent: input_cost_per_token_batches = 0.0 passes the is not None gate and prices correctly at $0, while the old truthiness gate (if input_cost_per_token_batches:) would have silently skipped it and fallen back to published rates — exactly the bug fixed in cost_calculator.py.

Coverage of the four declared-rate combinations (standard-only, batch-zero-only, all-four-zero, output-only) and the Codecov-flagged early return makes this complete. Implementation looks correct.

@marty-sullivan

Copy link
Copy Markdown
Contributor Author

@greptileai it seems like all open items are resolved but your review still sits at 4/5. Are there still open items that need to be addressed

@greptile-apps

greptile-apps Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

All four threads are resolved and the current diff addresses every issue that was open. The score was set during the first automated pass and simply hasn't been updated since. Looking at the code now:

  • batch_cost_calculator gates on is not None instead of truthiness, so an explicit 0.0 bills free
  • get_router_deployment_model_info returns None when a deployment has no declared pricing, so it falls through to the global map rather than synthesizing a zero-filled ModelInfo
  • Ownership is per token direction: a deployment declaring only one side preserves the published rates for the other
  • get_deployment_model_for_cost correctly prefers model_call_details["model"] over self.model
  • Tests cover all of this, including the is not None fix, the partial-override case, and the direction-ownership case

There are no remaining open items. This is a 5/5 — it's correct, the edge cases are tested, and standard pricing is demonstrably untouched. Good to merge.

mateo-berri added a commit that referenced this pull request Aug 17, 2026
fix(batches): price a retrieved batch from its deployment's model and rates (internal copy of #37077)
@mateo-berri

Copy link
Copy Markdown
Contributor

Continuing this in #37219 since the org-owned fork blocks maintainer pushes. All commits are cherry-picked there with authorship preserved. Thanks @marty-sullivan!

@marty-sullivan
marty-sullivan deleted the litellm_fix_batch_retrieve_cost_model_identity branch August 17, 2026 23:11
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