mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
fix(cost): stop non-string service_tier from silently dropping cost tracking (#30690)
completion_cost read service_tier straight from the request optional_params
and called service_tier.lower() on it, so a non-string value (dict/int/list,
reachable via allowed_openai_params/drop_params) raised AttributeError.
_response_cost_calculator swallowed that and returned response_cost=None, so
the request's cost was silently lost.
The isinstance guard alone is not enough: a surviving dict would crash again
downstream in _get_service_tier_cost_key, which also calls .lower(). A
request-level service_tier is only meaningful for pricing when it is a concrete
billable tier string, so coerce any non-string value to None and defer to the
tier the provider reports on the response usage, the same way "auto" already
does.
Adds a regression test driving a dict service_tier through completion_cost; it
raises AttributeError before the fix and prices at the served tier after.
(cherry picked from commit 43dadc5138)
This commit is contained in:
parent
9e30985290
commit
0dc6951af6
2 changed files with 60 additions and 4 deletions
|
|
@ -1227,10 +1227,14 @@ def completion_cost( # noqa: PLR0915
|
|||
if service_tier is None and optional_params is not None:
|
||||
service_tier = optional_params.get("service_tier")
|
||||
|
||||
# "auto" is a routing preference, not a billable tier: the provider picks
|
||||
# the tier and reports the one actually served on the response/usage, so
|
||||
# defer to that instead of pricing the request-level "auto" as standard
|
||||
if service_tier is not None and service_tier.lower() == ServiceTier.AUTO.value:
|
||||
# A request-level service_tier only prices the request when it is a
|
||||
# concrete billable tier string. "auto" is a routing preference and any
|
||||
# non-string value is not a billable tier, so defer to the tier the
|
||||
# provider reports on the response/usage instead of crashing or mispricing
|
||||
if (
|
||||
not isinstance(service_tier, str)
|
||||
or service_tier.lower() == ServiceTier.AUTO.value
|
||||
):
|
||||
service_tier = None
|
||||
|
||||
# Extract service_tier from completion_response if not provided
|
||||
|
|
|
|||
|
|
@ -2054,6 +2054,58 @@ def test_completion_cost_anthropic_auto_tier_uses_served_priority_rate():
|
|||
assert cost == pytest.approx(expected_priority)
|
||||
|
||||
|
||||
def test_completion_cost_non_string_service_tier_defers_to_served_tier():
|
||||
"""
|
||||
Regression: a non-string request-level ``service_tier`` (reachable via
|
||||
``allowed_openai_params``/``drop_params``) must not crash cost tracking.
|
||||
|
||||
Before the fix, ``completion_cost`` called ``service_tier.lower()`` on the
|
||||
request-level value, so a dict raised ``AttributeError``. ``_response_cost_calculator``
|
||||
swallowed it and reported ``response_cost=None``, silently dropping the cost.
|
||||
The non-string preference must be ignored so pricing defers to the tier the
|
||||
provider actually served on the response usage.
|
||||
"""
|
||||
from litellm import completion_cost
|
||||
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
|
||||
|
||||
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
|
||||
litellm.model_cost = litellm.get_model_cost_map(url="")
|
||||
|
||||
model = "claude-test-non-string-tier-cost-model"
|
||||
litellm.register_model(
|
||||
model_cost={
|
||||
model: {
|
||||
"input_cost_per_token": 3e-6,
|
||||
"output_cost_per_token": 15e-6,
|
||||
"input_cost_per_token_priority": 6e-6,
|
||||
"output_cost_per_token_priority": 30e-6,
|
||||
"litellm_provider": "anthropic",
|
||||
"max_tokens": 8192,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
usage = AnthropicConfig().calculate_usage(
|
||||
usage_object={
|
||||
"input_tokens": 1000,
|
||||
"output_tokens": 500,
|
||||
"service_tier": "priority",
|
||||
},
|
||||
reasoning_content=None,
|
||||
)
|
||||
response = ModelResponse(usage=usage, model=model)
|
||||
|
||||
cost = completion_cost(
|
||||
completion_response=response,
|
||||
model=model,
|
||||
custom_llm_provider="anthropic",
|
||||
optional_params={"service_tier": {"name": "auto"}},
|
||||
)
|
||||
|
||||
expected_priority = 1000 * 6e-6 + 500 * 30e-6
|
||||
assert cost == pytest.approx(expected_priority)
|
||||
|
||||
|
||||
def test_anthropic_cost_per_token_prices_cache_at_served_tier_with_multiplier():
|
||||
"""
|
||||
Regression for the cache/tier interaction in the Anthropic geo/speed path.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue