mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-14 04:09:09 +00:00
fix(logging): dedupe success logging for non-streaming anthropic_messages
The /v1/messages (anthropic_messages) route is always dispatched through the async wrapper, which logs via async_success_handler. But unlike acompletion, anthropic_messages sets no async flag in litellm_params, so _is_sync_litellm_request returns True in the sync success_handler and it re-dispatches every CustomLogger. The result for a single non-streaming request was two litellm_request OTEL spans and double cost/success callbacks. Skip the sync CustomLogger pass for anthropic_messages, mirroring how pass_through is already handled, so logging runs once via the canonical async path -- matching acompletion behaviour.
This commit is contained in:
parent
13b590c8ec
commit
2776dec9cf
2 changed files with 59 additions and 1 deletions
|
|
@ -2308,7 +2308,17 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
isinstance(callback, CustomLogger)
|
||||
and is_sync_request
|
||||
and self.call_type
|
||||
!= CallTypes.pass_through.value # pass-through endpoints call async_log_success_event
|
||||
not in (
|
||||
# both endpoints are always dispatched through the
|
||||
# async wrapper, which already calls
|
||||
# async_log_success_event. anthropic_messages
|
||||
# (/v1/messages) sets no acompletion-style flag, so
|
||||
# is_sync_request is True here and the sync pass would
|
||||
# otherwise re-dispatch every CustomLogger, doubling
|
||||
# OTEL spans + cost/success callbacks.
|
||||
CallTypes.pass_through.value,
|
||||
CallTypes.anthropic_messages.value,
|
||||
)
|
||||
): # custom logger class
|
||||
if self.stream and complete_streaming_response is None:
|
||||
callback.log_stream_event(
|
||||
|
|
|
|||
|
|
@ -3690,3 +3690,51 @@ def test_set_cost_breakdown_stores_reasoning_cost():
|
|||
cost_for_built_in_tools_cost_usd_dollar=0.0,
|
||||
)
|
||||
assert "reasoning_cost" not in no_reasoning.cost_breakdown
|
||||
|
||||
|
||||
def test_success_handler_skips_sync_callbacks_for_anthropic_messages(logging_obj):
|
||||
"""anthropic_messages (/v1/messages) is always dispatched through the async
|
||||
wrapper, so the sync success_handler pass must not re-dispatch CustomLogger
|
||||
callbacks -- the async handler is the canonical path.
|
||||
|
||||
Regression for the duplicate ``litellm_request`` OTEL spans + double
|
||||
success/cost callbacks on non-streaming /v1/messages. Unlike acompletion,
|
||||
anthropic_messages sets no async flag in litellm_params, so
|
||||
_is_sync_litellm_request is True and the sync pass would otherwise fire.
|
||||
"""
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
|
||||
class DummyLogger(CustomLogger):
|
||||
pass
|
||||
|
||||
logging_obj.stream = False
|
||||
logging_obj.call_type = "anthropic_messages"
|
||||
logging_obj.model_call_details["litellm_params"] = {}
|
||||
logging_obj.litellm_params = {}
|
||||
|
||||
dummy_logger = DummyLogger()
|
||||
dummy_logger.log_success_event = MagicMock()
|
||||
dummy_logger.log_stream_event = MagicMock()
|
||||
|
||||
model_response = ModelResponse(
|
||||
id="resp-123",
|
||||
model="gpt-4o-mini",
|
||||
choices=[
|
||||
{
|
||||
"message": {"role": "assistant", "content": "hello"},
|
||||
"finish_reason": "stop",
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
usage={"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
logging_obj,
|
||||
"get_combined_callback_list",
|
||||
return_value=[dummy_logger],
|
||||
):
|
||||
logging_obj.success_handler(result=model_response)
|
||||
|
||||
dummy_logger.log_success_event.assert_not_called()
|
||||
dummy_logger.log_stream_event.assert_not_called()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue