fix(interactions): price the settled background body against its own deployment

The poll fetches the terminal interaction through its own client call, which
stamps a response cost computed by a throwaway logging object holding none of
the original request's deployment context: no model_info, no router model_id,
no deployment litellm_params. Carrying that cost into the settlement event
billed custom-priced deployments at the wrong rate, and it also satisfied the
"already calculated" shortcut in _response_cost_calculator's caller, so the
settlement never repriced and never built a cost breakdown. The zeros stamped
by the usage-less create survived into the spend log row and the OTEL span.

Dropping the imported cost before re-emitting makes the settlement price the
settled body itself, against the deployment that served the create.
This commit is contained in:
mateo-berri 2026-08-22 16:06:52 -07:00
parent 8b566a7f0a
commit b460254428
2 changed files with 56 additions and 0 deletions

View file

@ -2202,7 +2202,20 @@ class Logging(LiteLLMLoggingBaseClass):
``in_progress`` response (no usage, so no cost was tracked); clearing
the dedup flags lets the completed result flow through cost calculation
and spend tracking exactly once, spanning create to completion.
The poll fetched this body through its own client call, which priced it
against a throwaway logging object holding none of this request's
deployment context: no ``model_info``, no router ``model_id``, no
deployment ``litellm_params``. Keeping that price would bill a
custom-priced deployment at the wrong rate, and it would also satisfy
the "already calculated" shortcut and skip repricing here, leaving the
cost breakdown at the zeros the usage-less create stamped and writing
those zeros to the spend log. Dropping it makes this event price the
settled body itself, against the deployment that served the create.
"""
settled_hidden_params: Final = getattr(result, "_hidden_params", None)
if isinstance(settled_hidden_params, dict):
settled_hidden_params.pop("response_cost", None)
self._reset_success_emission_dedupe()
await self.async_success_handler(result=result)

View file

@ -4644,6 +4644,49 @@ async def test_background_interaction_completion_rebills_after_in_progress_succe
assert logging_obj.model_call_details["standard_logging_object"]["total_tokens"] == 175
@pytest.mark.asyncio
async def test_background_interaction_completion_prices_the_settled_body_itself():
"""
The poll fetches the settled body through its own client call, which
prices it against a throwaway logging object holding none of this
request's deployment context. Adopting that price would bill a
custom-priced deployment at the wrong rate, and it would also satisfy the
"already calculated" shortcut and skip repricing, leaving the breakdown at
the zeros the usage-less create stamped and writing those to the spend log.
"""
import datetime as dt
from litellm.types.interactions import InteractionsAPIResponse
logging_obj = _interactions_logging_obj(stream=False)
in_progress = InteractionsAPIResponse(id="interactions/abc", model="gemini-2.5-flash", status="in_progress")
await logging_obj.async_success_handler(
result=in_progress,
start_time=dt.datetime.now(),
end_time=dt.datetime.now(),
)
completed = InteractionsAPIResponse(
id="interactions/abc",
model="gemini-2.5-flash",
status="completed",
steps=[],
usage=dict(INTERACTIONS_USAGE_BLOCK),
)
completed._hidden_params = {"response_cost": 99.0}
await logging_obj.async_log_background_interaction_completion(result=completed)
response_cost = logging_obj.model_call_details["response_cost"]
assert response_cost != 99.0
assert response_cost > 0
cost_breakdown = logging_obj.model_call_details["standard_logging_object"]["cost_breakdown"]
assert cost_breakdown["total_cost"] == response_cost
assert cost_breakdown["input_cost"] > 0
assert cost_breakdown["output_cost"] > 0
@pytest.mark.asyncio
async def test_background_interaction_completion_lets_otel_emit_the_cost_span():
"""