mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(logging): degrade instead of dropping success callbacks when result normalization fails
This commit is contained in:
parent
c274cf321c
commit
f332f780b6
2 changed files with 93 additions and 9 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue