diff --git a/litellm/litellm_core_utils/safe_json_dumps.py b/litellm/litellm_core_utils/safe_json_dumps.py index 5b99e8cba98..6e30b364d8f 100644 --- a/litellm/litellm_core_utils/safe_json_dumps.py +++ b/litellm/litellm_core_utils/safe_json_dumps.py @@ -72,6 +72,7 @@ def safe_dumps( return result else: # Fall back to string conversion for non-serializable objects. + seen.remove(id(obj)) try: return _transform(key, strip_null_bytes(str(obj))) except Exception: diff --git a/tests/test_litellm/litellm_core_utils/test_safe_json_dumps.py b/tests/test_litellm/litellm_core_utils/test_safe_json_dumps.py index 30385ba758d..27d6e93b090 100644 --- a/tests/test_litellm/litellm_core_utils/test_safe_json_dumps.py +++ b/tests/test_litellm/litellm_core_utils/test_safe_json_dumps.py @@ -75,6 +75,27 @@ def test_unserializable_object(): assert result == "Unserializable Object" +def test_repeated_unserializable_sibling_is_not_circular(): + # The same non-serializable object appearing twice as siblings is not a + # circular reference and both occurrences must be serialized. + class Stringable: + def __str__(self): + return "value" + + obj = Stringable() + + assert json.loads(safe_dumps([obj, obj])) == ["value", "value"] + assert json.loads(safe_dumps({"a": obj, "b": obj})) == { + "a": "value", + "b": "value", + } + + # A genuine self-cycle must still be reported. + cycle = {} + cycle["self"] = cycle + assert json.loads(safe_dumps(cycle))["self"] == "CircularReference Detected" + + def test_non_standard_dict_keys(): try: # Test handling of dictionaries with non-standard keys