Skip to content

fix(cost): honor an explicit zero tier rate instead of the fallback - #36697

Open
hsusul wants to merge 1 commit into
BerriAI:litellm_internal_stagingfrom
hsusul:fix-tiered-pricing-honor-zero-tier-rate
Open

fix(cost): honor an explicit zero tier rate instead of the fallback#36697
hsusul wants to merge 1 commit into
BerriAI:litellm_internal_stagingfrom
hsusul:fix-tiered-pricing-honor-zero-tier-rate

Conversation

@hsusul

@hsusul hsusul commented Aug 12, 2026

Copy link
Copy Markdown

TLDR

Problem this solves:

  • A tiered-pricing tier that prices cached reads or reasoning tokens at an explicit 0.0 is billed at the fallback rate instead of being free
  • calculate_tiered_cost and tier_rate resolved a tier's per-token cost with tier.get(cost_key) or tier.get(fallback_cost_key, 0), and the or short-circuits on a falsy 0.0, so a real zero price looks like a missing key and falls through to the fallback

How it solves it:

  • Add _resolve_tier_cost_per_token, which returns the primary rate whenever the key is present (including 0.0) and only reads the fallback when the key is absent (None)
  • Route all three sites (the in-range tier, the beyond-highest-tier overflow, and tier_rate) through that helper

User Flow

A user configures a Dashscope (or any tiered) model whose tier sets cache_read_input_token_cost: 0 while input_cost_per_token is nonzero, then sends a request whose prompt has cached tokens. Before this change every cached token is billed at input_cost_per_token; after it, the cached tokens are correctly billed at 0

Relevant issues

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

calculate_tiered_cost is a pure function, so the before/after is visible without a network call. A tier with free cache reads and a nonzero input rate:

from litellm.litellm_core_utils.llm_cost_calc.tiered_pricing import calculate_tiered_cost
tiers = [{"range": [0, 100000], "input_cost_per_token": 1e-6, "cache_read_input_token_cost": 0.0}]
calculate_tiered_cost(tokens=10000, tiered_pricing=tiers, cost_key="cache_read_input_token_cost", fallback_cost_key="input_cost_per_token")

Before: 0.01 (10000 cached tokens billed at the full input rate). After: 0.0

End-to-end QA a maintainer can run against a live proxy: register a Dashscope model whose tier carries cache_read_input_token_cost: 0, then

curl http://localhost:4000/v1/chat/completions -H "Authorization: Bearer $LITELLM_KEY" -H "Content-Type: application/json" -d '{"model":"<dashscope-tiered-model>","messages":[{"role":"user","content":"<long prompt reused to trigger a cache hit>"}]}'

run it twice so the second request reports cached prompt tokens, then confirm the logged prompt cost at http://localhost:4000/ui/?page=logs charges those cached tokens at 0 rather than the input rate

Type

🐛 Bug Fix

Changes

_resolve_tier_cost_per_token in litellm/litellm_core_utils/llm_cost_calc/tiered_pricing.py, used by calculate_tiered_cost (both the in-range and overflow sites) and tier_rate. This restores the behavior of #30749, which removed the same or short-circuit from the Dashscope calculator before the logic was extracted into this shared helper

Caveats (if any)

QA runbook

Covered by the new regression tests in tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tiered_pricing.py: an explicit 0.0 primary rate stays 0.0 at both the in-range and overflow sites and in tier_rate, while a missing key still falls back. The three zero-honoring tests fail on the current code and pass after the fix

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

A tier that prices cached reads or reasoning tokens at 0.0 was billed at
the fallback rate because calculate_tiered_cost and tier_rate resolved the
per-token cost with `tier.get(cost_key) or tier.get(fallback_cost_key, 0)`,
whose `or` short-circuits on a falsy 0.0. Distinguish a missing key from a
present zero so a free-cache-read or free-reasoning tier is billed at 0.

This restores the behavior of BerriAI#30749, which fixed the same short-circuit in
the dashscope calculator before the logic moved into this shared helper.
@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR changes tiered-pricing resolution so an explicit zero primary rate remains free while an absent primary rate still uses its fallback.

  • Adds a shared resolver used by in-range, overflow, and direct tier-rate calculations.
  • Adds regression coverage for explicit-zero and missing-primary behavior.

Confidence Score: 4/5

The pricing fix appears safe to merge, with only non-blocking repository-convention issues in the helper typing and test placement.

The new resolver consistently distinguishes explicit zero rates from missing values and the added tests cover in-range, overflow, direct-rate, and fallback behavior; the remaining findings concern maintainability conventions rather than incorrect runtime pricing.

Files Needing Attention: litellm/litellm_core_utils/llm_cost_calc/tiered_pricing.py; tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tiered_pricing.py

Important Files Changed

Filename Overview
litellm/litellm_core_utils/llm_cost_calc/tiered_pricing.py Correctly centralizes explicit-zero rate resolution, but the new helper uses a bare dict parameter contrary to repository typing guidance.
tests/test_litellm/litellm_core_utils/llm_cost_calc/test_tiered_pricing.py Adds focused local regression coverage, but creates a new test module rather than extending the repository’s existing mapped test file.

Reviews (1): Last reviewed commit: "fix(cost): honor an explicit zero tier r..." | Re-trigger Greptile

@@ -0,0 +1,72 @@
import os

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 Fragmented tier-pricing test coverage

This bug fix creates a new test module instead of extending the existing mapped test file, fragmenting tiered-pricing coverage and making the established test mapping harder to maintain and discover.

Context Used: CLAUDE.md (source)

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!



def _resolve_tier_cost_per_token(
tier: dict,

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 Coarse tier parameter type

The new helper declares tier as a bare dict, so static analysis cannot verify the tier key/value contract at this new abstraction boundary; repository guidance requires new function parameters to use strong types.

Context Used: CLAUDE.md (source)

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!

@codecov

codecov Bot commented Aug 12, 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 12, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing hsusul:fix-tiered-pricing-honor-zero-tier-rate (4da82eb) with litellm_internal_staging (3253598)

Open in CodSpeed

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.

1 participant