mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-15 09:12:18 +00:00
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
This commit is contained in:
parent
8a86bd4675
commit
c01a3bc8ca
2 changed files with 47 additions and 7 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue