Skip to content

Commit 0520d5c

Browse files
Michael-RZ-BerriMichael Riad ZakyMichael Riad Zaky
authored
[Fix] Unify cost calc in success_handler dict and typed branches (#26629)
* Unify cost calc in success_handler dict and typed branches * Trim verbose comments and docstrings --------- Co-authored-by: Michael Riad Zaky <michaelr@Mac.localdomain> Co-authored-by: Michael Riad Zaky <michaelr@Michaels-MacBook-Air.local>
1 parent 21ed389 commit 0520d5c

2 files changed

Lines changed: 142 additions & 13 deletions

File tree

litellm/litellm_core_utils/litellm_logging.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,6 +1467,8 @@ def _response_cost_calculator(
14671467
LiteLLMRealtimeStreamLoggingObject,
14681468
OpenAIModerationResponse,
14691469
"SearchResponse",
1470+
dict,
1471+
list,
14701472
],
14711473
cache_hit: Optional[bool] = None,
14721474
litellm_model_name: Optional[str] = None,
@@ -1744,6 +1746,7 @@ def _process_hidden_params_and_response_cost(
17441746
start_time,
17451747
end_time,
17461748
):
1749+
"""Resolve hidden params, compute response cost, and emit the standard logging payload."""
17471750
hidden_params = getattr(logging_result, "_hidden_params", {})
17481751
if hidden_params:
17491752
if self.model_call_details.get("litellm_params") is not None:
@@ -1877,24 +1880,12 @@ def _success_handler_helper_fn(
18771880
):
18781881
if self._is_recognized_call_type_for_logging(
18791882
logging_result=logging_result
1880-
):
1883+
) or isinstance(logging_result, (dict, list)):
18811884
self._process_hidden_params_and_response_cost(
18821885
logging_result=logging_result,
18831886
start_time=start_time,
18841887
end_time=end_time,
18851888
)
1886-
elif isinstance(result, dict) or isinstance(result, list):
1887-
self.model_call_details["standard_logging_object"] = (
1888-
self._build_standard_logging_payload(
1889-
result, start_time, end_time
1890-
)
1891-
)
1892-
if (
1893-
standard_logging_payload := self.model_call_details.get(
1894-
"standard_logging_object"
1895-
)
1896-
) is not None:
1897-
emit_standard_logging_payload(standard_logging_payload)
18981889
elif standard_logging_object is not None:
18991890
self.model_call_details["standard_logging_object"] = (
19001891
standard_logging_object

tests/test_litellm/litellm_core_utils/test_litellm_logging.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2534,3 +2534,141 @@ def test_get_standard_logging_object_payload_includes_litellm_call_id(logging_ob
25342534

25352535
assert payload is not None
25362536
assert payload["litellm_call_id"] == call_id
2537+
2538+
2539+
def _make_dict_logging_obj():
2540+
"""Build a Logging instance configured for a non-streaming dict result."""
2541+
obj = LitellmLogging(
2542+
model="claude-haiku-4-5@20251001",
2543+
messages=[{"role": "user", "content": "hi"}],
2544+
stream=False,
2545+
call_type="acompletion",
2546+
litellm_call_id="test-call-id",
2547+
start_time=time.time(),
2548+
function_id="test-fn",
2549+
)
2550+
obj.model_call_details = {
2551+
"model": "claude-haiku-4-5@20251001",
2552+
"custom_llm_provider": "vertex_ai",
2553+
"litellm_params": {"metadata": {}},
2554+
"response_cost": None,
2555+
}
2556+
return obj
2557+
2558+
2559+
def test_success_handler_computes_cost_for_dict_response():
2560+
"""Non-streaming dict responses run through the cost calculator."""
2561+
logging_obj = _make_dict_logging_obj()
2562+
expected_cost = 0.42
2563+
with (
2564+
patch.object(
2565+
logging_obj,
2566+
"_response_cost_calculator",
2567+
return_value=expected_cost,
2568+
) as mock_calc,
2569+
patch.object(
2570+
logging_obj,
2571+
"_build_standard_logging_payload",
2572+
return_value={"response_cost": expected_cost},
2573+
),
2574+
patch(
2575+
"litellm.litellm_core_utils.litellm_logging.emit_standard_logging_payload"
2576+
),
2577+
patch.object(
2578+
logging_obj,
2579+
"_is_recognized_call_type_for_logging",
2580+
return_value=False,
2581+
),
2582+
patch.object(
2583+
logging_obj,
2584+
"_transform_usage_objects",
2585+
side_effect=lambda result: result,
2586+
),
2587+
):
2588+
logging_obj.success_handler(
2589+
result={"id": "msg_1"},
2590+
start_time=time.time(),
2591+
end_time=time.time(),
2592+
)
2593+
mock_calc.assert_called_once()
2594+
assert logging_obj.model_call_details["response_cost"] == expected_cost
2595+
2596+
2597+
def test_success_handler_preserves_precomputed_cost_for_dict_response():
2598+
"""Precomputed response_cost on model_call_details must not be overwritten."""
2599+
logging_obj = _make_dict_logging_obj()
2600+
precomputed_cost = 1.23
2601+
logging_obj.model_call_details["response_cost"] = precomputed_cost
2602+
with (
2603+
patch.object(
2604+
logging_obj,
2605+
"_response_cost_calculator",
2606+
return_value=9.99,
2607+
) as mock_calc,
2608+
patch.object(
2609+
logging_obj,
2610+
"_build_standard_logging_payload",
2611+
return_value={"response_cost": precomputed_cost},
2612+
),
2613+
patch(
2614+
"litellm.litellm_core_utils.litellm_logging.emit_standard_logging_payload"
2615+
),
2616+
patch.object(
2617+
logging_obj,
2618+
"_is_recognized_call_type_for_logging",
2619+
return_value=False,
2620+
),
2621+
patch.object(
2622+
logging_obj,
2623+
"_transform_usage_objects",
2624+
side_effect=lambda result: result,
2625+
),
2626+
):
2627+
logging_obj.success_handler(
2628+
result={"id": "msg_2"},
2629+
start_time=time.time(),
2630+
end_time=time.time(),
2631+
)
2632+
mock_calc.assert_not_called()
2633+
assert logging_obj.model_call_details["response_cost"] == precomputed_cost
2634+
2635+
2636+
def test_success_handler_unified_helper_runs_for_typed_results():
2637+
"""Recognized typed responses still flow through the unified helper."""
2638+
logging_obj = _make_dict_logging_obj()
2639+
expected_cost = 0.10
2640+
typed_result = MagicMock()
2641+
typed_result._hidden_params = {}
2642+
2643+
with (
2644+
patch.object(
2645+
logging_obj,
2646+
"_response_cost_calculator",
2647+
return_value=expected_cost,
2648+
) as mock_calc,
2649+
patch.object(
2650+
logging_obj,
2651+
"_build_standard_logging_payload",
2652+
return_value={"response_cost": expected_cost},
2653+
),
2654+
patch(
2655+
"litellm.litellm_core_utils.litellm_logging.emit_standard_logging_payload"
2656+
),
2657+
patch.object(
2658+
logging_obj,
2659+
"_is_recognized_call_type_for_logging",
2660+
return_value=True,
2661+
),
2662+
patch.object(
2663+
logging_obj,
2664+
"_transform_usage_objects",
2665+
side_effect=lambda result: result,
2666+
),
2667+
):
2668+
logging_obj.success_handler(
2669+
result=typed_result,
2670+
start_time=time.time(),
2671+
end_time=time.time(),
2672+
)
2673+
mock_calc.assert_called_once()
2674+
assert logging_obj.model_call_details["response_cost"] == expected_cost

0 commit comments

Comments
 (0)