fix(logging): degrade instead of dropping success callbacks when result normalization fails

This commit is contained in:
Devin AI 2026-07-29 15:44:36 +00:00
parent c274cf321c
commit f332f780b6
2 changed files with 93 additions and 9 deletions

View file

@ -1854,15 +1854,7 @@ class Logging(LiteLLMLoggingBaseClass):
self.model_call_details["end_time"] = end_time
self.model_call_details["cache_hit"] = cache_hit
if self.call_type == CallTypes.anthropic_messages.value:
result = self._handle_anthropic_messages_response_logging(result=result)
elif (
self.call_type == CallTypes.generate_content.value
or self.call_type == CallTypes.agenerate_content.value
):
result = self._handle_non_streaming_google_genai_generate_content_response_logging(result=result)
elif self.call_type == CallTypes.asend_message.value or self.call_type == CallTypes.send_message.value:
result = self._handle_a2a_response_logging(result=result)
result = self._normalize_result_for_call_type(result=result)
logging_result = self.normalize_logging_result(result=result)
@ -1910,6 +1902,36 @@ class Logging(LiteLLMLoggingBaseClass):
except Exception as e:
raise Exception(f"[Non-Blocking] LiteLLM.Success_Call Error: {str(e)}")
def _normalize_result_for_call_type(self, result: object) -> object:
"""
Convert a call-type-specific result into the shape the logging pipeline expects.
A failure here must never skip the callbacks: a streaming call has a single
fire-and-forget success event, so raising loses the spend record entirely with
no client-visible symptom. Falling back to the untransformed result keeps
callbacks running on a degraded payload.
"""
try:
if self.call_type == CallTypes.anthropic_messages.value:
return self._handle_anthropic_messages_response_logging(result=result)
if self.call_type in (
CallTypes.generate_content.value,
CallTypes.agenerate_content.value,
):
return self._handle_non_streaming_google_genai_generate_content_response_logging(result=result)
if self.call_type in (
CallTypes.asend_message.value,
CallTypes.send_message.value,
):
return self._handle_a2a_response_logging(result=result)
return result
except Exception as e: # noqa: BLE001 # any normalizer failure must degrade, never skip callbacks
verbose_logger.exception(
f"LiteLLM.LoggingError: failed to normalize {self.call_type} result of type "
f"{type(result).__name__} for logging; logging the raw result instead. Error: {e}"
)
return result
def _is_recognized_call_type_for_logging(
self,
logging_result: Any,

View file

@ -693,6 +693,68 @@ async def test_anthropic_messages_marks_litellm_params_async():
litellm.callbacks = original_callbacks
def _anthropic_messages_logging_obj_for_unnormalizable_result(stream: bool) -> LitellmLogging:
logging_obj = LitellmLogging(
model="gpt-4o",
messages=[{"role": "user", "content": "hi"}],
stream=stream,
call_type="anthropic_messages",
start_time=time.time(),
litellm_call_id="normalization-failure",
function_id="fn",
)
logging_obj.update_environment_variables(
model="gpt-4o",
user="",
optional_params={},
litellm_params={"api_base": "", "aanthropic_messages": True},
)
return logging_obj
def test_success_handler_helper_fn_falls_back_to_raw_result_on_normalization_failure():
"""A result the ``anthropic_messages`` normalizer cannot handle (e.g. the raw
Responses API payload the OpenAI bridge hands over) used to raise out of
``_success_handler_helper_fn`` before any callback ran, so a streaming call, which
dispatches exactly one fire-and-forget success event, lost its spend record with no
client-visible symptom. The helper now degrades to the untransformed result."""
logging_obj = _anthropic_messages_logging_obj_for_unnormalizable_result(stream=True)
unnormalizable_result = {"id": "resp_1", "object": "response", "output": []}
start_time, end_time, result = logging_obj._success_handler_helper_fn(result=unnormalizable_result)
assert result is unnormalizable_result
assert start_time is not None and end_time is not None
@pytest.mark.asyncio
async def test_async_success_handler_fires_callbacks_on_normalization_failure():
"""The async success path must still invoke callbacks when result normalization
fails, otherwise ``/v1/messages`` calls silently log nothing."""
import litellm
from litellm.integrations.custom_logger import CustomLogger
class Tracker(CustomLogger):
def __init__(self):
super().__init__()
self.calls = []
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
self.calls.append(response_obj)
tracker = Tracker()
logging_obj = _anthropic_messages_logging_obj_for_unnormalizable_result(stream=False)
original_async_callbacks = list(litellm._async_success_callback or [])
litellm._async_success_callback = [tracker]
try:
await logging_obj.async_success_handler(result={"id": "resp_1", "object": "response", "output": []})
finally:
litellm._async_success_callback = original_async_callbacks
assert len(tracker.calls) == 1
@pytest.mark.asyncio
async def test_agenerate_content_marks_litellm_params_async():
"""LIT-4475: the async ``agenerate_content`` entrypoint must plant