diff --git a/litellm/litellm_core_utils/safe_json_dumps.py b/litellm/litellm_core_utils/safe_json_dumps.py index ea26d549326..77b238ba8aa 100644 --- a/litellm/litellm_core_utils/safe_json_dumps.py +++ b/litellm/litellm_core_utils/safe_json_dumps.py @@ -94,6 +94,7 @@ def safe_json_structure( return model_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/unit/litellm_core_utils/test_safe_json_dumps.py b/tests/unit/litellm_core_utils/test_safe_json_dumps.py index 740ce4f433a..1524dbc297d 100644 --- a/tests/unit/litellm_core_utils/test_safe_json_dumps.py +++ b/tests/unit/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