From c01a3bc8ca7f9054065a313922fc0cf2be7b0f9d Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:53:31 -0700 Subject: [PATCH] fix(passthrough): bill an OpenAI websocket session at the price its own turns came to The handler already worked out what each turn cost from the frames the provider sent back, but the logging layer prices whatever result it is handed and a single result can only name one model, so it threw that away and recomputed the whole connection at the session model's rate. A connection that ran a mini model and then a reasoning model was billed at the mini rate over both turns' tokens, roughly a third of what it really cost. The result now carries the settled price on the header the cost calculator reads before it recomputes, so what the handler works out is what lands in the spend row. The price is set after the response is built because the constructor's hidden_params argument never survives: it stores the private attribute before pydantic initializes it, and pydantic then resets it to the default --- ...i_websocket_passthrough_logging_handler.py | 24 ++++++++++----- ...i_websocket_passthrough_logging_handler.py | 30 +++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_websocket_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_websocket_passthrough_logging_handler.py index c7df3259ce2..d3e145f8751 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_websocket_passthrough_logging_handler.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_websocket_passthrough_logging_handler.py @@ -23,6 +23,7 @@ _SESSION_CREATED: Final = "session.created" _REALTIME_RESPONSE_DONE: Final = "response.done" _RESPONSES_COMPLETED: Final = "response.completed" _MODEL_NAMING_EVENTS: Final = frozenset({_SESSION_CREATED, _REALTIME_RESPONSE_DONE, _RESPONSES_COMPLETED}) +_RESPONSE_COST_HEADER: Final = "llm_provider-x-litellm-response-cost" class _NamedModel(BaseModel): @@ -148,6 +149,21 @@ def _responses_cost(usage: Usage, model: str) -> float: return 0.0 +def _priced_result(model: str, billing: _Billing, start_time: datetime) -> ModelResponse: + result: Final = ModelResponse( + id=f"openai-websocket-{start_time.timestamp()}", + object="chat.completion", + created=int(start_time.timestamp()), + model=model, + usage=billing.usage, + ) + hidden: Final = { # mutable-ok: response_cost_calculator writes optional_params into this dict + "additional_headers": MappingProxyType({_RESPONSE_COST_HEADER: billing.cost}) + } + result._hidden_params = hidden # pyright: ignore[reportPrivateUsage] # the one settled-cost channel + return result + + class OpenAIWebsocketPassthroughLoggingHandler: def openai_websocket_passthrough_handler( self, @@ -181,13 +197,7 @@ class OpenAIWebsocketPassthroughLoggingHandler: logging_obj.model_call_details.update(MappingProxyType({"response_cost": billing.cost})) priced: Final[PassThroughEndpointLoggingTypedDict] = { - "result": ModelResponse( - id=f"openai-websocket-{start_time.timestamp()}", - object="chat.completion", - created=int(start_time.timestamp()), - model=model, - usage=billing.usage, - ), + "result": _priced_result(model, billing, start_time), "kwargs": { **kwargs, "model": model, diff --git a/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_openai_websocket_passthrough_logging_handler.py b/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_openai_websocket_passthrough_logging_handler.py index ca0268950f7..7aeb68df26d 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_openai_websocket_passthrough_logging_handler.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_openai_websocket_passthrough_logging_handler.py @@ -237,3 +237,33 @@ def test_a_responses_turn_without_readable_usage_is_not_billed(): assert handled["result"] is None assert handled["kwargs"] == {"model": RESPONSES_MODEL, "custom_llm_provider": "openai"} + + +def test_the_price_of_a_multi_model_connection_reaches_the_spend_row(): + """ + The logging layer prices whatever result it is handed, and one result can + only name one model, so a connection that ran two models has to carry the + price it already worked out or the cheaper model pays for both turns. + """ + handled = _handle( + [ + _responses_completed(RESPONSES_MODEL, input_tokens=13, output_tokens=5), + _responses_completed(REASONING_MODEL, input_tokens=20, output_tokens=7), + ], + url_route="/openai_passthrough/v1/responses", + ) + + logged_cost = litellm.response_cost_calculator( + response_object=handled["result"], + model=handled["kwargs"]["model"], + custom_llm_provider="openai", + call_type="completion", + optional_params={}, + ) + + session_prices = litellm.model_cost[RESPONSES_MODEL] + priced_on_the_session_model = ( + 33 * session_prices["input_cost_per_token"] + 12 * session_prices["output_cost_per_token"] + ) + assert logged_cost == pytest.approx(handled["kwargs"]["response_cost"]) + assert logged_cost > priced_on_the_session_model