From 2c0e47ba7e177244fee032ec60129b61095752b9 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 25 Oct 2026 03:07:22 +0000 Subject: [PATCH] fix(cost): keep the provider's usage.cost behind a stamped bill and read it on every pricing pass stamp_billed_usage_cost records the provider's own usage.cost in hidden params before writing litellm's bill over it, and completion_cost prices from that recorded figure, so a second pass (uncached_price, MCP rounds) bills the same number as the first instead of re-billing the stamped total or losing the report on an unmapped model. cost_per_token reads the reported cost per provider, so OpenCode Go's balance-hit usage.cost of 0 no longer zeroes a row. --- litellm/cost_calculator.py | 24 +++++++++++++++------ litellm/litellm_core_utils/hidden_params.py | 21 +++++++++++------- tests/unit/test_main.py | 7 +++--- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 2eef729296c..d37bd7cc67e 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -25,7 +25,7 @@ from litellm.constants import ( from litellm.litellm_core_utils.core_helpers import as_str_mapping, provider_reported_usage_cost from litellm.litellm_core_utils.cost_discount import resolve_cost_discount from litellm.litellm_core_utils.decisions_pricing import decisions_pricing_fields, is_decisions_call_type -from litellm.litellm_core_utils.hidden_params import HIDDEN_PARAMS_ATTR, usage_cost_stamped_by_litellm +from litellm.litellm_core_utils.hidden_params import HIDDEN_PARAMS_ATTR, provider_stated_usage_cost from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import ( StandardBuiltInToolCostTracking, ) @@ -408,8 +408,8 @@ def _per_second_pricing_cost( return resolved_cost_per_second * seconds, 0.0 -def _billable_reported_cost(usage_object: Usage | None) -> float | None: - return _billable_header_cost(provider_reported_usage_cost(usage_object, None)) +def _billable_reported_cost(usage_object: Usage | None, custom_llm_provider: str | None) -> float | None: + return _billable_header_cost(provider_reported_usage_cost(usage_object, custom_llm_provider)) def _split_reported_cost(reported_cost: float, priced: tuple[float, float] | None) -> tuple[float, float]: @@ -465,7 +465,7 @@ def cost_per_token( Without a reported cost the token math is the bill. """ priced_by_caller: Final = custom_cost_per_token is not None or custom_cost_per_second is not None - reported_cost: Final = None if priced_by_caller else _billable_reported_cost(usage_object) + reported_cost: Final = None if priced_by_caller else _billable_reported_cost(usage_object, custom_llm_provider) rated: Final = partial( _cost_per_token_from_rates, model=model, @@ -1560,6 +1560,16 @@ def _store_cost_breakdown_in_logging_obj( # Don't fail the main cost calculation if breakdown storage fails +def _with_provider_stated_cost(completion_response: object, usage: Usage | None) -> Usage | None: + if usage is None: + return usage + stamped_cost: Final = getattr(usage, "cost", None) + stated_cost: Final = provider_stated_usage_cost(completion_response, stamped_cost) + if stated_cost is stamped_cost: + return usage + return usage.model_copy(update=MappingProxyType({"cost": stated_cost})) + + def _without_provider_stated_cost(usage: Usage | None) -> Usage | None: if usage is None or getattr(usage, "cost", None) is None: return usage @@ -1702,10 +1712,12 @@ def completion_cost( cache_creation_input_tokens: int | None = None cache_read_input_tokens: int | None = None audio_transcription_file_duration: float = 0.0 - provider_usage_object: Final = get_usage_object(completion_response=completion_response) + provider_usage_object: Final = _with_provider_stated_cost( + completion_response, get_usage_object(completion_response=completion_response) + ) cost_per_token_usage_object: Final[Usage | None] = ( _without_provider_stated_cost(provider_usage_object) - if custom_pricing or base_model is not None or usage_cost_stamped_by_litellm(completion_response) + if custom_pricing or base_model is not None else provider_usage_object ) rerank_billed_units: RerankBilledUnits | None = None diff --git a/litellm/litellm_core_utils/hidden_params.py b/litellm/litellm_core_utils/hidden_params.py index 30082f1af7f..ed493394f03 100644 --- a/litellm/litellm_core_utils/hidden_params.py +++ b/litellm/litellm_core_utils/hidden_params.py @@ -136,15 +136,20 @@ def get_or_create_hidden_params(obj: object) -> MutableMapping[str, object]: raise TypeError(f"unsupported hidden params storage: {type(hidden_params).__name__}") -USAGE_COST_STAMPED_BY_LITELLM: Final = "usage_cost_stamped_by_litellm" +USAGE_COST_BEFORE_LITELLM_STAMP: Final = "usage_cost_before_litellm_stamp" def stamp_billed_usage_cost(obj: object, usage: object, cost: float) -> None: - """usage.cost becomes what litellm billed, marked so the calculator never re-bills it as a provider's report.""" - setattr(usage, "cost", cost) - set_hidden_param(obj, USAGE_COST_STAMPED_BY_LITELLM, True) - - -def usage_cost_stamped_by_litellm(obj: object) -> bool: + """usage.cost becomes what litellm billed; the provider's own figure is kept so a later pricing pass bills the same.""" hidden_params: Final = get_hidden_params(obj) - return hidden_params is not None and hidden_params.get(USAGE_COST_STAMPED_BY_LITELLM) is True + if hidden_params is None or USAGE_COST_BEFORE_LITELLM_STAMP not in hidden_params: + set_hidden_param(obj, USAGE_COST_BEFORE_LITELLM_STAMP, getattr(usage, "cost", None)) + setattr(usage, "cost", cost) + + +def provider_stated_usage_cost(obj: object, usage_cost: object) -> object: + """What the provider itself put in usage.cost, looking past a bill litellm stamped there.""" + hidden_params: Final = get_hidden_params(obj) + if hidden_params is None or USAGE_COST_BEFORE_LITELLM_STAMP not in hidden_params: + return usage_cost + return hidden_params.get(USAGE_COST_BEFORE_LITELLM_STAMP) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 70b268bf2a2..1faf6dc12ad 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -5062,7 +5062,7 @@ def test_stream_chunk_builder_prices_alias_from_openai_sdk_usage_chunk(): assert response._hidden_params["response_cost"] == pytest.approx(0.000704) -def test_stream_chunk_builder_leaves_xai_reported_cost_to_the_calculator(monkeypatch: pytest.MonkeyPatch): +def test_stream_chunk_builder_stamps_the_margin_priced_bill_over_the_xai_reported_cost(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr(litellm, "cost_margin_config", {"xai": 0.5}) usage_chunk: Final = _stream_builder_text_chunk("grok-4", "") usage_chunk.usage = Usage(prompt_tokens=5, completion_tokens=2, total_tokens=7, cost=0.42) @@ -5078,9 +5078,10 @@ def test_stream_chunk_builder_leaves_xai_reported_cost_to_the_calculator(monkeyp ) assert response is not None - assert getattr(response.usage, "cost", None) == pytest.approx(0.42) - assert response._hidden_params.get("response_cost") is None + assert getattr(response.usage, "cost", None) == pytest.approx(0.63) + assert response._hidden_params.get("response_cost") == pytest.approx(0.63) assert logging_obj.response_cost_calculator(result=response) == pytest.approx(0.63) + assert logging_obj.response_cost_calculator(result=response, ignore_stamped_cost=True) == pytest.approx(0.63) def test_speech_mistral_dispatches_and_decodes_audio(respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch):