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