mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix: recursive pydantic issue (#19531)
This commit is contained in:
parent
06a749708d
commit
89ecdc405d
3 changed files with 94 additions and 9 deletions
|
|
@ -516,7 +516,9 @@ class CustomGuardrail(CustomLogger):
|
|||
from litellm.types.utils import GuardrailMode
|
||||
|
||||
# Use event_type if provided, otherwise fall back to self.event_hook
|
||||
guardrail_mode: Union[GuardrailEventHooks, GuardrailMode, List[GuardrailEventHooks]]
|
||||
guardrail_mode: Union[
|
||||
GuardrailEventHooks, GuardrailMode, List[GuardrailEventHooks]
|
||||
]
|
||||
if event_type is not None:
|
||||
guardrail_mode = event_type
|
||||
elif isinstance(self.event_hook, Mode):
|
||||
|
|
@ -524,11 +526,21 @@ class CustomGuardrail(CustomLogger):
|
|||
else:
|
||||
guardrail_mode = self.event_hook # type: ignore[assignment]
|
||||
|
||||
from litellm.litellm_core_utils.core_helpers import (
|
||||
filter_exceptions_from_params,
|
||||
)
|
||||
|
||||
# Sanitize the response to ensure it's JSON serializable and free of circular refs
|
||||
# This prevents RecursionErrors in downstream loggers (Langfuse, Datadog, etc.)
|
||||
clean_guardrail_response = filter_exceptions_from_params(
|
||||
guardrail_json_response
|
||||
)
|
||||
|
||||
slg = StandardLoggingGuardrailInformation(
|
||||
guardrail_name=self.guardrail_name,
|
||||
guardrail_provider=guardrail_provider,
|
||||
guardrail_mode=guardrail_mode,
|
||||
guardrail_response=guardrail_json_response,
|
||||
guardrail_response=clean_guardrail_response,
|
||||
guardrail_status=guardrail_status,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from litellm.constants import MAX_LANGFUSE_INITIALIZED_CLIENTS
|
|||
from litellm.litellm_core_utils.core_helpers import (
|
||||
safe_deep_copy,
|
||||
reconstruct_model_name,
|
||||
filter_exceptions_from_params,
|
||||
)
|
||||
from litellm.litellm_core_utils.redact_messages import redact_user_api_key_info
|
||||
from litellm.llms.custom_httpx.http_handler import _get_httpx_client
|
||||
|
|
@ -71,9 +72,8 @@ def _extract_cache_read_input_tokens(usage_obj) -> int:
|
|||
# Check prompt_tokens_details.cached_tokens (used by Gemini and other providers)
|
||||
if hasattr(usage_obj, "prompt_tokens_details"):
|
||||
prompt_tokens_details = getattr(usage_obj, "prompt_tokens_details", None)
|
||||
if (
|
||||
prompt_tokens_details is not None
|
||||
and hasattr(prompt_tokens_details, "cached_tokens")
|
||||
if prompt_tokens_details is not None and hasattr(
|
||||
prompt_tokens_details, "cached_tokens"
|
||||
):
|
||||
cached_tokens = getattr(prompt_tokens_details, "cached_tokens", None)
|
||||
if (
|
||||
|
|
@ -526,7 +526,6 @@ class LangFuseLogger:
|
|||
verbose_logger.debug("Langfuse Layer Logging - logging to langfuse v2")
|
||||
|
||||
try:
|
||||
metadata = metadata or {}
|
||||
standard_logging_object: Optional[StandardLoggingPayload] = cast(
|
||||
Optional[StandardLoggingPayload],
|
||||
kwargs.get("standard_logging_object", None),
|
||||
|
|
@ -692,9 +691,10 @@ class LangFuseLogger:
|
|||
|
||||
clean_metadata["litellm_response_cost"] = cost
|
||||
if standard_logging_object is not None:
|
||||
clean_metadata["hidden_params"] = standard_logging_object[
|
||||
"hidden_params"
|
||||
]
|
||||
hidden_params = standard_logging_object.get("hidden_params", {})
|
||||
clean_metadata["hidden_params"] = filter_exceptions_from_params(
|
||||
hidden_params
|
||||
)
|
||||
|
||||
if (
|
||||
litellm.langfuse_default_tags is not None
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
import pytest
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
from litellm.types.guardrails import GuardrailEventHooks
|
||||
import json
|
||||
|
||||
|
||||
class TestCustomGuardrailRecursion:
|
||||
"""
|
||||
Specific tests for the circular reference / RecursionError fix in logging.
|
||||
"""
|
||||
|
||||
def test_log_guardrail_information_handles_circular_references(self):
|
||||
"""
|
||||
Test that add_standard_logging method sanitizes input data containing circular references
|
||||
instead of crashing.
|
||||
|
||||
This reproduces the Langfuse crash scenario:
|
||||
Request -> Metadata -> GuardrailResponse -> DebugContext -> Request
|
||||
"""
|
||||
guardrail = CustomGuardrail(
|
||||
guardrail_name="recursion_test_guardrail",
|
||||
event_hook=GuardrailEventHooks.pre_call,
|
||||
)
|
||||
|
||||
# 1. Setup Circular Data
|
||||
request_data = {"user_id": "test_recursive_user"}
|
||||
metadata = {"session_id": "123"}
|
||||
request_data["metadata"] = metadata
|
||||
|
||||
# Create the danger: Guardrail Response holding a reference back to request_data
|
||||
dirty_response = {
|
||||
"flagged": False,
|
||||
"debug_context": request_data, # <--- ACCESS TO ROOT (Circular Ref)
|
||||
}
|
||||
|
||||
# 2. Invoke the logging method
|
||||
# If the fix is working, this will NOT raise RecursionError
|
||||
try:
|
||||
guardrail.add_standard_logging_guardrail_information_to_request_data(
|
||||
guardrail_json_response=dirty_response,
|
||||
request_data=request_data,
|
||||
guardrail_status="success",
|
||||
start_time=1.0,
|
||||
end_time=2.0,
|
||||
duration=1.0,
|
||||
masked_entity_count={},
|
||||
event_type=GuardrailEventHooks.pre_call,
|
||||
)
|
||||
except RecursionError:
|
||||
pytest.fail(
|
||||
"RecursionError raised! The cyclic reference sanitization failed."
|
||||
)
|
||||
|
||||
# 3. Verify the data stored is safe
|
||||
stored_info = request_data["metadata"][
|
||||
"standard_logging_guardrail_information"
|
||||
][0]
|
||||
stored_response = stored_info["guardrail_response"]
|
||||
|
||||
# Check that we can dump it to JSON without crashing (Ultimate proof)
|
||||
try:
|
||||
json.dumps(stored_response)
|
||||
except Exception as e:
|
||||
pytest.fail(f"Stored data is not JSON serializable: {e}")
|
||||
|
||||
# Check content - keys should be preserved but recursion broken
|
||||
assert "debug_context" in stored_response
|
||||
debug_context = stored_response["debug_context"]
|
||||
|
||||
# In a sanitized copy, the nested metadata should be a copy, not the original live dict
|
||||
assert debug_context["user_id"] == "test_recursive_user"
|
||||
# The 'metadata' inside 'debug_context' would be where recursion stops or is filtered
|
||||
assert "metadata" in debug_context
|
||||
Loading…
Add table
Reference in a new issue