mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-09 00:26:01 +00:00
fix: Handle circular references in spend tracking metadata JSON serialization (#12643)
* fix: Handle circular references in spend tracking metadata JSON serialization - Fixes issue #12634 where circular references in metadata caused ValueError: Circular reference detected when logging spend data - Adds _safe_json_dumps() function that detects and handles circular references by replacing them with placeholder strings - Maintains full functionality for normal objects while preventing crashes from circular references - Adds comprehensive tests for circular reference handling - Critical fix for v1.74.3 stable release * fix: Replace bare except clauses with specific Exception handling - Fixes E722 linting errors in _safe_json_dumps function - Maintains same error handling behavior while following best practices - All tests continue to pass * refactor: Use existing safe_dumps utility instead of custom implementation - Replace custom _safe_json_dumps() with existing safe_dumps() from litellm_core_utils - Remove duplicate code and leverage existing circular reference handling - Update tests to use safe_dumps function - Maintains same functionality while reducing code duplication - All tests continue to pass
This commit is contained in:
@@ -12,6 +12,7 @@ import litellm
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.constants import REDACTED_BY_LITELM_STRING
|
||||
from litellm.litellm_core_utils.core_helpers import get_litellm_metadata_from_kwargs
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
from litellm.proxy._types import SpendLogsMetadata, SpendLogsPayload
|
||||
from litellm.proxy.utils import PrismaClient, hash_token
|
||||
from litellm.types.utils import (
|
||||
@@ -305,7 +306,7 @@ def get_logging_payload( # noqa: PLR0915
|
||||
model=kwargs.get("model", "") or "",
|
||||
user=metadata.get("user_api_key_user_id", "") or "",
|
||||
team_id=metadata.get("user_api_key_team_id", "") or "",
|
||||
metadata=json.dumps(clean_metadata),
|
||||
metadata=safe_dumps(clean_metadata),
|
||||
cache_key=cache_key,
|
||||
spend=kwargs.get("response_cost", 0),
|
||||
total_tokens=usage.get("total_tokens", standard_logging_total_tokens),
|
||||
|
||||
@@ -17,6 +17,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import litellm
|
||||
from litellm.constants import REDACTED_BY_LITELM_STRING
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
from litellm.proxy.spend_tracking.spend_tracking_utils import (
|
||||
_get_vector_store_request_for_spend_logs_payload,
|
||||
_sanitize_request_body_for_spend_logs_payload,
|
||||
@@ -175,3 +176,76 @@ def test_get_vector_store_request_for_spend_logs_payload_null_input(mock_should_
|
||||
mock_should_store.return_value = False
|
||||
result = _get_vector_store_request_for_spend_logs_payload(None)
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_safe_dumps_handles_circular_references():
|
||||
"""Test that safe_dumps can handle circular references without raising exceptions"""
|
||||
|
||||
# Create a circular reference
|
||||
obj1 = {"name": "obj1"}
|
||||
obj2 = {"name": "obj2", "ref": obj1}
|
||||
obj1["ref"] = obj2 # This creates a circular reference
|
||||
|
||||
# This should not raise an exception
|
||||
result = safe_dumps(obj1)
|
||||
|
||||
# Should be a valid JSON string
|
||||
assert isinstance(result, str)
|
||||
|
||||
# Should contain placeholder for circular reference
|
||||
assert "CircularReference Detected" in result
|
||||
|
||||
# Should be parseable as JSON
|
||||
parsed = json.loads(result)
|
||||
assert parsed["name"] == "obj1"
|
||||
assert parsed["ref"]["name"] == "obj2"
|
||||
|
||||
|
||||
def test_safe_dumps_normal_objects():
|
||||
"""Test that safe_dumps works correctly with normal objects"""
|
||||
|
||||
normal_obj = {
|
||||
"string": "test",
|
||||
"number": 42,
|
||||
"boolean": True,
|
||||
"null": None,
|
||||
"list": [1, 2, 3],
|
||||
"nested": {"key": "value"}
|
||||
}
|
||||
|
||||
result = safe_dumps(normal_obj)
|
||||
|
||||
# Should be a valid JSON string that can be parsed
|
||||
assert isinstance(result, str)
|
||||
parsed = json.loads(result)
|
||||
assert parsed == normal_obj
|
||||
|
||||
|
||||
def test_safe_dumps_complex_metadata_like_object():
|
||||
"""Test with a complex metadata-like object similar to what caused the issue"""
|
||||
|
||||
# Simulate a complex metadata object
|
||||
metadata = {
|
||||
"user_api_key": "test-key",
|
||||
"model": "gpt-4",
|
||||
"usage": {"total_tokens": 100},
|
||||
"mcp_tool_call_metadata": {
|
||||
"name": "test_tool",
|
||||
"arguments": {"param": "value"}
|
||||
}
|
||||
}
|
||||
|
||||
# Add a potential circular reference
|
||||
usage_detail = {"parent_metadata": metadata}
|
||||
metadata["usage"]["detail"] = usage_detail
|
||||
|
||||
# This should not raise an exception
|
||||
result = safe_dumps(metadata)
|
||||
|
||||
# Should be a valid JSON string
|
||||
assert isinstance(result, str)
|
||||
|
||||
# Should be parseable as JSON
|
||||
parsed = json.loads(result)
|
||||
assert parsed["user_api_key"] == "test-key"
|
||||
assert parsed["model"] == "gpt-4"
|
||||
|
||||
Reference in New Issue
Block a user