mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-25 05:54:53 +00:00
fix(pricing): omit provisional stream response-cost with fixed margin
update_response_metadata stamped response_cost_calculator onto live stream wrappers before usage existed. With a fixed cost_margin that priced a zero base as only fixed_amount, so x-litellm-response-cost advertised 0.001 while spend logs billed the full margined cost. Leave response_cost unset for CustomStreamWrapper / BaseResponsesAPIStreamingIterator until the stream is assembled. Fixes #46364
This commit is contained in:
parent
e4dc696af8
commit
c065ee5cb6
2 changed files with 110 additions and 6 deletions
|
|
@ -101,6 +101,18 @@ def response_timing_metrics(
|
|||
return {"_response_ms": total_response_time_ms, "litellm_overhead_time_ms": overhead_ms}
|
||||
|
||||
|
||||
def _is_live_stream_wrapper(result: object) -> bool:
|
||||
"""True for wrappers returned before the stream has usage / final cost.
|
||||
|
||||
Imports are local to avoid the utils <-> streaming_handler cycle this
|
||||
module sits inside (same pattern as ``utils._is_converted_stream_result``).
|
||||
"""
|
||||
from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper
|
||||
from litellm.responses.streaming_iterator import BaseResponsesAPIStreamingIterator
|
||||
|
||||
return isinstance(result, (CustomStreamWrapper, BaseResponsesAPIStreamingIterator))
|
||||
|
||||
|
||||
class ResponseMetadata:
|
||||
"""
|
||||
Handles setting and managing `_hidden_params`, `response_time_ms`, and `litellm_overhead_time_ms` for LiteLLM responses
|
||||
|
|
@ -132,16 +144,25 @@ class ResponseMetadata:
|
|||
## ADD OTHER HIDDEN PARAMS
|
||||
model_info: Final = kwargs.get("model_info", {}) or {}
|
||||
model_id: Final = model_info.get("id", None)
|
||||
new_params: Final = {
|
||||
"litellm_call_id": getattr(logging_obj, "litellm_call_id", None),
|
||||
"api_base": get_api_base(model=model or "", optional_params=kwargs),
|
||||
"model_id": model_id,
|
||||
"response_cost": logging_obj.response_cost_calculator(
|
||||
# Live stream wrappers have no usage yet. Pricing them here applies a
|
||||
# fixed cost_margin to a zero base and the proxy then advertises that
|
||||
# provisional value as x-litellm-response-cost (#46364). Leave cost
|
||||
# unset until the stream is assembled for spend logging.
|
||||
response_cost: Final = (
|
||||
None
|
||||
if _is_live_stream_wrapper(self.result)
|
||||
else logging_obj.response_cost_calculator(
|
||||
result=self.result,
|
||||
litellm_model_name=model,
|
||||
router_model_id=model_id,
|
||||
cache_hit=cache_hit,
|
||||
),
|
||||
)
|
||||
)
|
||||
new_params: Final = {
|
||||
"litellm_call_id": getattr(logging_obj, "litellm_call_id", None),
|
||||
"api_base": get_api_base(model=model or "", optional_params=kwargs),
|
||||
"model_id": model_id,
|
||||
"response_cost": response_cost,
|
||||
"additional_headers": process_response_headers(
|
||||
self._get_additional_headers_from_hidden_params() or {},
|
||||
preserve_litellm_internal_headers=True,
|
||||
|
|
|
|||
|
|
@ -649,3 +649,86 @@ def test_update_response_metadata_prices_per_second_deployment_from_its_stamped_
|
|||
|
||||
assert result._response_ms == pytest.approx(2000)
|
||||
assert result._hidden_params["response_cost"] == pytest.approx(0.02 * 2)
|
||||
|
||||
|
||||
class TestLiveStreamSkipsProvisionalResponseCost:
|
||||
"""Regression for #46364: a fixed cost_margin must not price a live stream
|
||||
before usage exists, or the proxy advertises only fixed_amount as
|
||||
x-litellm-response-cost while spend logs bill the full margined cost."""
|
||||
|
||||
def _stream_like_result(self):
|
||||
from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper
|
||||
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.model_call_details = {
|
||||
"litellm_params": {"model_info": {"id": "deployment-1"}, "metadata": {}},
|
||||
"custom_llm_provider": "openai",
|
||||
}
|
||||
logging_obj.stream_options = None
|
||||
logging_obj.messages = [{"role": "user", "content": "hi"}]
|
||||
return CustomStreamWrapper(
|
||||
completion_stream=iter([]),
|
||||
model="gpt-4o-mini",
|
||||
logging_obj=logging_obj,
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
|
||||
def test_update_response_metadata_leaves_stream_response_cost_unset(self):
|
||||
result = self._stream_like_result()
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.model_call_details = {"llm_api_duration_ms": 50.0}
|
||||
logging_obj.caching_details = None
|
||||
logging_obj.litellm_call_id = "call-stream-margin"
|
||||
logging_obj.response_cost_calculator = MagicMock(return_value=0.001)
|
||||
|
||||
update_response_metadata(
|
||||
result=result,
|
||||
logging_obj=logging_obj,
|
||||
model="gpt-4o-mini",
|
||||
kwargs={"model_info": {"id": "deployment-1"}},
|
||||
start_time=datetime.datetime(2025, 1, 1, 0, 0, 0),
|
||||
end_time=datetime.datetime(2025, 1, 1, 0, 0, 0, 100000),
|
||||
)
|
||||
|
||||
logging_obj.response_cost_calculator.assert_not_called()
|
||||
assert result._hidden_params.get("response_cost") is None
|
||||
|
||||
def test_stream_headers_omit_provisional_fixed_margin_cost(self, monkeypatch):
|
||||
"""After the fix, streaming headers must not advertise fixed_amount alone."""
|
||||
monkeypatch.setattr(
|
||||
litellm,
|
||||
"cost_margin_config",
|
||||
{"openai": {"percentage": 0.1, "fixed_amount": 0.001}},
|
||||
)
|
||||
result = self._stream_like_result()
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.model_call_details = {
|
||||
"llm_api_duration_ms": 50.0,
|
||||
"litellm_params": {},
|
||||
"custom_llm_provider": "openai",
|
||||
}
|
||||
logging_obj.caching_details = None
|
||||
logging_obj.litellm_call_id = "call-stream-margin"
|
||||
logging_obj.cost_breakdown = None
|
||||
logging_obj.call_type = "acompletion"
|
||||
# Pre-fix behavior: calculator prices a zero-usage stream as fixed_amount.
|
||||
logging_obj.response_cost_calculator = MagicMock(return_value=0.001)
|
||||
|
||||
update_response_metadata(
|
||||
result=result,
|
||||
logging_obj=logging_obj,
|
||||
model="gpt-4o-mini",
|
||||
kwargs={"model_info": {"id": "deployment-1"}},
|
||||
start_time=datetime.datetime(2025, 1, 1, 0, 0, 0),
|
||||
end_time=datetime.datetime(2025, 1, 1, 0, 0, 0, 100000),
|
||||
)
|
||||
|
||||
processing = ProxyBaseLLMRequestProcessing(data={})
|
||||
headers = processing._stream_response_headers(
|
||||
hidden_params=result._hidden_params,
|
||||
user_api_key_dict=UserAPIKeyAuth(api_key="sk-test"),
|
||||
logging_obj=logging_obj,
|
||||
version="test",
|
||||
callback_headers={},
|
||||
)
|
||||
assert "x-litellm-response-cost" not in headers
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue