From b324599f0dad7be5583f3a04a7442d0a34214d92 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:28:05 +0000 Subject: [PATCH] fix(cost): mark usage.cost litellm wrote as its own bill and keep a breakdown for a reported cost --- litellm/cost_calculator.py | 8 ++++++ litellm/litellm_core_utils/core_helpers.py | 25 +++++++++++++------ .../streaming_chunk_builder_utils.py | 2 ++ litellm/responses/streaming_iterator.py | 3 ++- .../litellm_core_utils/test_core_helpers.py | 10 +++++--- .../llms/openai_like/test_kyma_provider.py | 8 +++--- 6 files changed, 40 insertions(+), 16 deletions(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 1550e6d280c..28cda2daa77 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -2334,6 +2334,14 @@ def response_cost_calculator( ) provider_response_cost: Final = get_response_cost_from_hidden_params(hidden_params) if provider_response_cost is not None and not custom_pricing: + _store_cost_breakdown_in_logging_obj( + litellm_logging_obj=litellm_logging_obj, + prompt_tokens_cost_usd_dollar=0.0, + completion_tokens_cost_usd_dollar=provider_response_cost, + cost_for_built_in_tools_cost_usd_dollar=0.0, + total_cost_usd_dollar=provider_response_cost, + original_cost=provider_response_cost, + ) return provider_response_cost response_cost = completion_cost( diff --git a/litellm/litellm_core_utils/core_helpers.py b/litellm/litellm_core_utils/core_helpers.py index 2966d8f4cfb..83e9de1378b 100644 --- a/litellm/litellm_core_utils/core_helpers.py +++ b/litellm/litellm_core_utils/core_helpers.py @@ -1020,17 +1020,28 @@ def chunk_usage(chunk: object) -> object: OWN_REPORTED_COST_READER_PROVIDERS: Final = frozenset({"xai", "perplexity", "opencode_go"}) -def _carries_litellm_bill(response: object) -> bool: +LITELLM_BILLED_USAGE_COST: Final = "litellm_billed_usage_cost" + + +def mark_usage_cost_as_litellm_bill(response: object, bill: float) -> None: + """Records litellm's bill as the response's ``response_cost`` and notes that ``usage.cost`` now holds that bill, + so ``billed_usage_cost`` does not read it back as the upstream's charge on a later pricing pass.""" + hidden: Final[object] = getattr(response, "_hidden_params", None) + if isinstance(hidden, dict): + cast(dict[str, object], hidden).update({"response_cost": bill, LITELLM_BILLED_USAGE_COST: bill}) + + +def _usage_cost_is_litellm_bill(response: object) -> bool: 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) + bill: Final[object] = None if hidden is None else hidden.get(LITELLM_BILLED_USAGE_COST) + usage_cost: Final[object] = getattr(_usage_field(response, "usage"), "cost", None) + return isinstance(bill, (int, float)) and not isinstance(bill, bool) and bill == usage_cost def billed_usage_cost(response: object, custom_llm_provider: str | None) -> float | None: - """The charge an OpenAI-compatible upstream reports in ``usage.cost``, skipped once litellm stamped its own bill - onto the response, because the stream builder and the Responses terminal event then rewrite ``usage.cost`` to - that bill.""" - if custom_llm_provider in OWN_REPORTED_COST_READER_PROVIDERS or _carries_litellm_bill(response): + """The charge an OpenAI-compatible upstream reports in ``usage.cost``, skipped for providers with their own reader + and once litellm rewrote ``usage.cost`` to its own bill (the stream builder and the Responses terminal event do).""" + if custom_llm_provider in OWN_REPORTED_COST_READER_PROVIDERS or _usage_cost_is_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 diff --git a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py index e308628fbdb..9aa7302b165 100644 --- a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py +++ b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py @@ -15,6 +15,7 @@ from litellm._logging import verbose_logger from litellm.litellm_core_utils.core_helpers import ( OWN_REPORTED_COST_READER_PROVIDERS, chunk_usage, + mark_usage_cost_as_litellm_bill, provider_reported_usage_cost, ) from litellm.litellm_core_utils.hidden_params import HIDDEN_PARAMS_ATTR, set_hidden_params @@ -1428,6 +1429,7 @@ def _stamp_streaming_usage_cost(usage: Usage, response: ModelResponse, logging_o return setattr(usage, "cost", computed_cost) _set_hidden_response_cost(response, float(computed_cost)) + mark_usage_cost_as_litellm_bill(response, float(computed_cost)) def _field_of(value: object, name: str) -> object: diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index 9b386a37870..9a5a73350fd 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -40,6 +40,7 @@ from litellm.constants import ( from litellm.exceptions import MidStreamFallbackError, RateLimitError from litellm.litellm_core_utils.asyncify import run_async_function from litellm.litellm_core_utils.core_helpers import ( + mark_usage_cost_as_litellm_bill, process_response_headers, provider_reported_usage_cost, set_response_cost_in_hidden_params, @@ -1870,7 +1871,7 @@ def stamp_responses_usage_cost( return if bill > 0 or isinstance(getattr(usage_obj, "cost", None), (int, float)): setattr(usage_obj, "cost", float(bill)) - response_obj.hidden_params = {**response_obj.hidden_params, "response_cost": float(bill)} + mark_usage_cost_as_litellm_bill(response_obj, float(bill)) def opening_item_payload(payload: Mapping[str, object]) -> Mapping[str, object]: diff --git a/tests/unit/litellm_core_utils/test_core_helpers.py b/tests/unit/litellm_core_utils/test_core_helpers.py index 275bd422c7a..6421aced622 100644 --- a/tests/unit/litellm_core_utils/test_core_helpers.py +++ b/tests/unit/litellm_core_utils/test_core_helpers.py @@ -808,7 +808,7 @@ class TestReceiveStampSurvivesASecondMetadataBucket: 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.litellm_core_utils.core_helpers import billed_usage_cost, mark_usage_cost_as_litellm_bill from litellm.types.utils import ModelResponse, Usage usage = Usage(prompt_tokens=100, completion_tokens=20, total_tokens=120, cost=0.037) @@ -817,6 +817,8 @@ def test_billed_usage_cost_bills_any_chat_upstream_without_its_own_reader() -> N assert billed_usage_cost(chat, "openrouter") == 0.037 assert billed_usage_cost(chat, "xai") is None assert billed_usage_cost(SimpleNamespace(usage=usage), "openai") == 0.037 - stamped = ModelResponse(usage=usage) - stamped._hidden_params["response_cost"] = 0.037 - assert billed_usage_cost(stamped, "openai") is None + priced_by_litellm = ModelResponse(usage=usage) + priced_by_litellm._hidden_params["response_cost"] = 0.037 + assert billed_usage_cost(priced_by_litellm, "openai") == 0.037 + mark_usage_cost_as_litellm_bill(priced_by_litellm, 0.037) + assert billed_usage_cost(priced_by_litellm, "openai") is None diff --git a/tests/unit/llms/openai_like/test_kyma_provider.py b/tests/unit/llms/openai_like/test_kyma_provider.py index 2b3740ca676..e834e0fc0e9 100644 --- a/tests/unit/llms/openai_like/test_kyma_provider.py +++ b/tests/unit/llms/openai_like/test_kyma_provider.py @@ -254,7 +254,7 @@ def test_interaction_on_kyma_is_billed_at_the_cost_kyma_reports(): @pytest.mark.parametrize("model", ("openrouter/openai/gpt-4o-mini", "deepseek/deepseek-chat")) -def test_interaction_on_another_provider_keeps_its_upstream_cost_out_of_the_body(model: str): +def test_interaction_on_another_openai_compatible_provider_is_billed_at_its_reported_cost(model: str): chat_usage: Final = {"prompt_tokens": 4, "completion_tokens": 3, "total_tokens": 7, "cost": 0.00721} responses_body: Final = { "id": "resp_upstream", @@ -278,7 +278,7 @@ def test_interaction_on_another_provider_keeps_its_upstream_cost_out_of_the_body upstream.route(path__regex=r".*/responses$").respond(200, json=responses_body) interaction: Final = litellm.interactions.create(model=model, input="ping", api_key="k") - assert "cost" not in interaction.model_dump(exclude_none=True)["usage"] + assert interaction.model_dump(exclude_none=True)["usage"]["cost"] == pytest.approx(0.00721) def test_sdk_completion_cost_prices_kyma_at_the_cost_kyma_reports(): @@ -410,8 +410,8 @@ def test_cost_headers_show_the_discount_and_margin_taken_from_the_cost_kyma_repo assert float(headers["x-litellm-response-cost-discount-amount"] or "nan") == pytest.approx(0.004) assert float(headers["x-litellm-response-cost-margin-amount"] or "nan") == pytest.approx(0.0004) assert float(headers["x-litellm-response-cost"] or "nan") == pytest.approx(0.0044) - assert headers["x-litellm-response-cost-input"] is None - assert headers["x-litellm-response-cost-output"] is None + assert float(headers["x-litellm-response-cost-input"] or "nan") == 0.0 + assert float(headers["x-litellm-response-cost-output"] or "nan") == pytest.approx(0.0044) @pytest.mark.parametrize("thinking", ({"type": "enabled", "budget_tokens": 1024}, {"type": "adaptive"}))