fix(cost): skip a chat usage.cost that already holds litellm's stamped bill and Responses API objects

This commit is contained in:
devin-ai-integration[bot] 2026-10-29 19:57:08 +00:00
parent 55bfef86d9
commit 0d0877c73a
3 changed files with 33 additions and 9 deletions

View file

@ -1021,16 +1021,23 @@ OWN_REPORTED_COST_READER_PROVIDERS: Final = frozenset({"xai", "perplexity", "ope
def _carries_litellm_bill(response: object) -> bool:
"""A response the stream builder already priced carries litellm's own bill in ``usage.cost`` too."""
hidden: Final[object] = getattr(response, "_hidden_params", None)
stamped: Final = hidden.get("response_cost") if isinstance(hidden, Mapping) else None
hidden: Final = as_str_mapping(getattr(response, "_hidden_params", None))
stamped: Final[object] = None if hidden is None else hidden.get("response_cost")
return isinstance(stamped, (int, float)) and not isinstance(stamped, bool)
def billed_usage_cost(response: object, custom_llm_provider: str | None) -> float | None:
"""The charge an OpenAI-compatible upstream reports in ``usage.cost``, billed unless the provider has its own
reader or the number is litellm's own bill written back onto the usage."""
if custom_llm_provider in OWN_REPORTED_COST_READER_PROVIDERS or _carries_litellm_bill(response):
"""The charge an OpenAI-compatible upstream reports in a chat completion's ``usage.cost``. Skipped once the stream
builder stamped litellm's own bill onto the response (its ``usage.cost`` then holds that bill), and for Responses
API objects, whose terminal ``usage.cost`` litellm also rewrites and whose upstream figure is billed through
``set_response_cost_in_hidden_params``."""
from litellm.types.utils import ModelResponse
if (
custom_llm_provider in OWN_REPORTED_COST_READER_PROVIDERS
or not isinstance(response, ModelResponse)
or _carries_litellm_bill(response)
):
return None
cost: Final = provider_reported_usage_cost(_usage_field(response, "usage"), custom_llm_provider)
return cost if cost is not None and math.isfinite(cost) and cost >= 0 else None

View file

@ -803,3 +803,20 @@ class TestReceiveStampSurvivesASecondMetadataBucket:
}
assert get_litellm_metadata_from_kwargs(kwargs)["litellm_received_at"] is own
def test_billed_usage_cost_bills_any_chat_upstream_without_its_own_reader() -> None:
from types import SimpleNamespace
from litellm.litellm_core_utils.core_helpers import billed_usage_cost
from litellm.types.utils import ModelResponse, Usage
usage = Usage(prompt_tokens=100, completion_tokens=20, total_tokens=120, cost=0.037)
chat = ModelResponse(usage=usage)
assert billed_usage_cost(chat, "openai") == 0.037
assert billed_usage_cost(chat, "openrouter") == 0.037
assert billed_usage_cost(chat, "xai") is None
assert billed_usage_cost(SimpleNamespace(usage=usage), "openai") is None
stamped = ModelResponse(usage=usage)
stamped._hidden_params["response_cost"] = 0.037
assert billed_usage_cost(stamped, "openai") is None

View file

@ -2659,7 +2659,7 @@ def test_perplexity_streaming_dict_cost_bills_through_its_own_calculator():
) == pytest.approx(0.00503)
def test_openai_compatible_streaming_cost_is_priced_from_the_cost_map():
def test_openai_compatible_streaming_reported_cost_is_billed_instead_of_the_cost_map():
import litellm
from litellm.cost_calculator import (
get_response_cost_from_hidden_params,
@ -2681,7 +2681,7 @@ def test_openai_compatible_streaming_cost_is_priced_from_the_cost_map():
id="chatcmpl-openai-compatible",
model=model,
choices=[],
usage=Usage(completion_tokens=5, prompt_tokens=10, total_tokens=15, cost=3_144_000),
usage=Usage(completion_tokens=5, prompt_tokens=10, total_tokens=15, cost=0.037),
)
CustomStreamWrapper._propagate_usage_cost_to_hidden_params(complete_response, "openai")
@ -2693,7 +2693,7 @@ def test_openai_compatible_streaming_cost_is_priced_from_the_cost_map():
custom_llm_provider="openai",
call_type="completion",
optional_params={},
) == pytest.approx(2e-5)
) == pytest.approx(0.037)
def test_xai_streaming_reported_cost_still_takes_the_margin(monkeypatch):