fix: recursive pydantic issue (#19531)

This commit is contained in:
Harshit Jain 2026-01-23 09:26:41 +05:30 committed by GitHub
parent 06a749708d
commit 89ecdc405d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 94 additions and 9 deletions

View file

@ -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,

View file

@ -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

View file

@ -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