mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
fix(cost): apply off-peak rates on the tiered-pricing path
Tiered pricing resolves its own base rates and returns early, before the off-peak swap ran, so a model carrying both tiered_pricing and off_peak_pricing billed the tier rate around the clock. Route every base-cost path through one helper so the window applies wherever the rates came from, and say plainly in the docstring that an off-peak rate replaces the rate it lands on rather than discounting it
This commit is contained in:
parent
d302301a4e
commit
4f174ffdd1
2 changed files with 73 additions and 22 deletions
|
|
@ -337,9 +337,10 @@ def _apply_off_peak_pricing(
|
|||
cache_read_cost: float,
|
||||
) -> tuple[float, float, float]:
|
||||
"""Swap in off-peak per-token rates when the current UTC time is inside one of the model's
|
||||
off_peak_pricing windows. Applied after threshold pricing so the discount is honored rather
|
||||
than overwritten when a model combines off-peak and above-threshold rates. Any rate left
|
||||
unset in off_peak_pricing falls back to the standard rate.
|
||||
off_peak_pricing windows. An off-peak rate replaces the rate that would otherwise apply
|
||||
rather than discounting it, so a model that also has tiered or above-threshold pricing bills
|
||||
the flat off-peak rate for the whole request while the window is open. Any rate left unset in
|
||||
off_peak_pricing falls back to the standard rate.
|
||||
"""
|
||||
off_peak: Final = model_info.get("off_peak_pricing")
|
||||
if not off_peak:
|
||||
|
|
@ -354,6 +355,22 @@ def _apply_off_peak_pricing(
|
|||
)
|
||||
|
||||
|
||||
def _apply_off_peak_to_base_costs(
|
||||
model_info: ModelInfo,
|
||||
current_time: datetime | None,
|
||||
base_costs: tuple[float, float, float, float, float],
|
||||
) -> tuple[float, float, float, float, float]:
|
||||
"""Apply off-peak rates to an already-resolved set of base costs, whichever pricing path
|
||||
produced them. Cache-creation rates are passed through untouched, since off_peak_pricing
|
||||
has no field for them.
|
||||
"""
|
||||
prompt, completion, cache_creation, cache_creation_above_1hr, cache_read = base_costs
|
||||
off_peak_prompt, off_peak_completion, off_peak_cache_read = _apply_off_peak_pricing(
|
||||
model_info, current_time, prompt, completion, cache_read
|
||||
)
|
||||
return (off_peak_prompt, off_peak_completion, cache_creation, cache_creation_above_1hr, off_peak_cache_read)
|
||||
|
||||
|
||||
def _get_token_base_cost(
|
||||
model_info: ModelInfo,
|
||||
usage: Usage,
|
||||
|
|
@ -376,7 +393,7 @@ def _get_token_base_cost(
|
|||
"""
|
||||
tiered_base_costs: Final = _get_tiered_base_costs(model_info=model_info, usage=usage)
|
||||
if tiered_base_costs is not None:
|
||||
return tiered_base_costs
|
||||
return _apply_off_peak_to_base_costs(model_info, current_time, tiered_base_costs)
|
||||
|
||||
# Get service tier aware cost keys
|
||||
input_cost_key: Final = _get_service_tier_cost_key("input_cost_per_token", service_tier)
|
||||
|
|
@ -410,15 +427,16 @@ def _get_token_base_cost(
|
|||
k for k in model_info if k.startswith("input_cost_per_token_above_") and not k.endswith(_SERVICE_TIER_SUFFIXES)
|
||||
]
|
||||
if not threshold_keys:
|
||||
off_peak_prompt_cost, off_peak_completion_cost, off_peak_cache_read_cost = _apply_off_peak_pricing(
|
||||
model_info, current_time, prompt_base_cost, completion_base_cost, cache_read_cost
|
||||
)
|
||||
return (
|
||||
off_peak_prompt_cost,
|
||||
off_peak_completion_cost,
|
||||
cache_creation_cost,
|
||||
cache_creation_cost_above_1hr,
|
||||
off_peak_cache_read_cost,
|
||||
return _apply_off_peak_to_base_costs(
|
||||
model_info,
|
||||
current_time,
|
||||
(
|
||||
prompt_base_cost,
|
||||
completion_base_cost,
|
||||
cache_creation_cost,
|
||||
cache_creation_cost_above_1hr,
|
||||
cache_read_cost,
|
||||
),
|
||||
)
|
||||
|
||||
# Only sort the threshold keys (typically 1-2 keys instead of 66+)
|
||||
|
|
@ -519,15 +537,16 @@ def _get_token_base_cost(
|
|||
except Exception:
|
||||
continue
|
||||
|
||||
discounted_prompt_cost, discounted_completion_cost, discounted_cache_read_cost = _apply_off_peak_pricing(
|
||||
model_info, current_time, prompt_base_cost, completion_base_cost, cache_read_cost
|
||||
)
|
||||
return (
|
||||
discounted_prompt_cost,
|
||||
discounted_completion_cost,
|
||||
cache_creation_cost,
|
||||
cache_creation_cost_above_1hr,
|
||||
discounted_cache_read_cost,
|
||||
return _apply_off_peak_to_base_costs(
|
||||
model_info,
|
||||
current_time,
|
||||
(
|
||||
prompt_base_cost,
|
||||
completion_base_cost,
|
||||
cache_creation_cost,
|
||||
cache_creation_cost_above_1hr,
|
||||
cache_read_cost,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -574,6 +574,38 @@ def test_get_model_info_propagates_off_peak_fields():
|
|||
assert info["off_peak_pricing"] == off_peak_pricing
|
||||
|
||||
|
||||
def test_get_token_base_cost_off_peak_wins_over_tiered_pricing():
|
||||
"""Tiered pricing resolves base rates on its own path and returns early, so off-peak has to
|
||||
be applied there too or a model carrying both would silently bill the tier rate all day."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
model_name = "litellm-test-off-peak-tiered"
|
||||
litellm.register_model(
|
||||
{
|
||||
model_name: {
|
||||
"litellm_provider": "openai",
|
||||
"mode": "chat",
|
||||
"tiered_pricing": [
|
||||
{"range": [0, 128000], "input_cost_per_token": 3e-6, "output_cost_per_token": 6e-6},
|
||||
],
|
||||
"off_peak_pricing": {
|
||||
"hours_utc": "16:30-00:30",
|
||||
"input_cost_per_token": 5e-7,
|
||||
"output_cost_per_token": 1e-6,
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
info = litellm.get_model_info(model=model_name)
|
||||
usage = Usage(prompt_tokens=1_000, completion_tokens=100, total_tokens=1_100)
|
||||
|
||||
inside = _get_token_base_cost(info, usage, current_time=datetime(2026, 1, 1, 18, 0, tzinfo=timezone.utc))
|
||||
assert inside[:2] == (5e-7, 1e-6)
|
||||
|
||||
outside = _get_token_base_cost(info, usage, current_time=datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc))
|
||||
assert outside[:2] == (3e-6, 6e-6)
|
||||
|
||||
|
||||
def test_generic_cost_per_token_gpt54_above_272k_tokens(_local_model_cost_map):
|
||||
"""GPT-5.4/5.4-pro: prompts >272K input tokens priced at 2x input, 1.5x output."""
|
||||
model = "gpt-5.4"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue