fix(logging): backfill streaming hidden response cost (#26606)

* fix(logging): backfill streaming hidden response cost

Made-with: Cursor

* fix(logging): avoid mutating streaming hidden params

Backfill calculated streaming response cost into logging payload copies so OTEL spans expose hidden_params.response_cost without mutating the response object.

Made-with: Cursor

* fix black formatting

Apply the repo-pinned Black 24.10.0 formatting expected by CI.

Made-with: Cursor

* fix(types): allow numeric hidden response cost

Allow standard logging hidden params to carry numeric response_cost values, matching LiteLLM's calculated cost payloads.

Made-with: Cursor

* refactor(logging): simplify hidden response cost backfill

Clean up metadata initialization and reuse the raw response cost when deciding whether to backfill hidden params.

Made-with: Cursor
This commit is contained in:
milan-berri 2026-04-28 18:41:20 +03:00 committed by GitHub
parent 10aed9e981
commit 52fb23a512
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 123 additions and 13 deletions

View file

@ -1725,12 +1725,18 @@ class Logging(LiteLLMLoggingBaseClass):
return
if self.model_call_details.get("litellm_params") is None:
return
self.model_call_details["litellm_params"].setdefault("metadata", {})
if self.model_call_details["litellm_params"]["metadata"] is None:
self.model_call_details["litellm_params"]["metadata"] = {}
self.model_call_details["litellm_params"]["metadata"]["hidden_params"] = (
getattr(logging_result, "_hidden_params", {})
)
metadata_hidden_params = hidden_params.copy()
response_cost = self.model_call_details.get("response_cost")
if (
metadata_hidden_params.get("response_cost") is None
and response_cost is not None
):
metadata_hidden_params["response_cost"] = response_cost
litellm_params = self.model_call_details["litellm_params"]
metadata = litellm_params.get("metadata") or {}
litellm_params["metadata"] = metadata
metadata["hidden_params"] = metadata_hidden_params
def _process_hidden_params_and_response_cost(
self,
@ -5438,11 +5444,6 @@ def get_standard_logging_object_payload(
completion_start_time_float=completion_start_time_float,
stream=kwargs.get("stream", False),
)
# clean up litellm hidden params
clean_hidden_params = StandardLoggingPayloadSetup.get_hidden_params(
hidden_params
)
# clean up litellm metadata
clean_metadata = StandardLoggingPayloadSetup.get_standard_logging_metadata(
metadata=metadata,
@ -5476,6 +5477,18 @@ def get_standard_logging_object_payload(
## Get model cost information ##
base_model = _get_base_model_from_metadata(model_call_details=kwargs)
custom_pricing = use_custom_pricing_for_model(litellm_params=litellm_params)
raw_response_cost = kwargs.get("response_cost")
response_cost: float = raw_response_cost or 0.0
# clean up litellm hidden params
clean_hidden_params = StandardLoggingPayloadSetup.get_hidden_params(
hidden_params
)
if (
clean_hidden_params["response_cost"] is None
and raw_response_cost is not None
):
clean_hidden_params["response_cost"] = response_cost
model_cost_information = StandardLoggingPayloadSetup.get_model_cost_information(
base_model=base_model,
@ -5484,7 +5497,6 @@ def get_standard_logging_object_payload(
init_response_obj=init_response_obj,
api_base=litellm_params.get("api_base"),
)
response_cost: float = kwargs.get("response_cost", 0) or 0.0
error_information = StandardLoggingPayloadSetup.get_error_information(
original_exception=original_exception,

View file

@ -2659,7 +2659,7 @@ class StandardLoggingHiddenParams(TypedDict):
] # id of the model in the router, separates multiple models with the same name but different credentials
cache_key: Optional[str]
api_base: Optional[str]
response_cost: Optional[str]
response_cost: Optional[Union[str, float]]
litellm_overhead_time_ms: Optional[float]
additional_headers: Optional[StandardLoggingAdditionalHeaders]
batch_models: Optional[List[str]]

View file

@ -2337,6 +2337,104 @@ def test_merge_hidden_params_from_response_into_metadata_populates_metadata():
assert meta["hidden_params"]["model_id"] == "mid-test"
def test_merge_hidden_params_from_response_into_metadata_backfills_response_cost():
"""Streaming metadata should include the already-calculated response cost."""
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
logging_obj = LiteLLMLoggingObj(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "hi"}],
stream=True,
call_type="acompletion",
start_time=time.time(),
litellm_call_id="merge-hp-cost-test",
function_id="merge-hp-cost-fn",
)
logging_obj.model_call_details = {
"litellm_params": {"metadata": {}},
"response_cost": 0.002,
}
class _Resp:
_hidden_params = {"response_cost": None, "model_id": "mid-test"}
response = _Resp()
logging_obj._merge_hidden_params_from_response_into_metadata(response)
meta = logging_obj.model_call_details["litellm_params"]["metadata"]
assert meta["hidden_params"]["response_cost"] == 0.002
assert meta["hidden_params"]["model_id"] == "mid-test"
assert response._hidden_params["response_cost"] is None
def test_standard_logging_hidden_params_backfills_response_cost_without_mutating_response():
"""Streaming standard logging payload should expose the calculated response cost."""
from datetime import datetime
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.types.utils import Usage
logging_obj = LiteLLMLoggingObj(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "hi"}],
stream=True,
call_type="acompletion",
start_time=time.time(),
litellm_call_id="standard-hp-cost-test",
function_id="standard-hp-cost-fn",
)
logging_obj.model_call_details = {
"litellm_params": {"metadata": {}, "proxy_server_request": {}},
"litellm_call_id": "standard-hp-cost-test",
"call_type": "acompletion",
"stream": True,
"model": "gpt-4o-mini",
"custom_llm_provider": "openai",
"optional_params": {"stream": True},
"response_cost": 0.002,
}
response = ModelResponse(
id="standard-hp-cost-response",
model="gpt-4o-mini",
usage=Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15),
)
response._hidden_params = {"response_cost": None, "model_id": "mid-test"}
payload = logging_obj._build_standard_logging_payload(
response, datetime.now(), datetime.now()
)
assert payload is not None
assert payload["hidden_params"]["response_cost"] == 0.002
assert response._hidden_params["response_cost"] is None
def test_merge_hidden_params_from_response_into_metadata_preserves_response_cost():
"""Do not overwrite provider-supplied response cost when it already exists."""
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
logging_obj = LiteLLMLoggingObj(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "hi"}],
stream=True,
call_type="acompletion",
start_time=time.time(),
litellm_call_id="merge-hp-preserve-cost-test",
function_id="merge-hp-preserve-cost-fn",
)
logging_obj.model_call_details = {
"litellm_params": {"metadata": {}},
"response_cost": 0.002,
}
class _Resp:
_hidden_params = {"response_cost": 0.001, "model_id": "mid-test"}
logging_obj._merge_hidden_params_from_response_into_metadata(_Resp())
meta = logging_obj.model_call_details["litellm_params"]["metadata"]
assert meta["hidden_params"]["response_cost"] == 0.001
assert meta["hidden_params"]["model_id"] == "mid-test"
def test_merge_hidden_params_from_response_into_metadata_no_op_when_empty():
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj