diff --git a/litellm/proxy/common_utils/callback_utils.py b/litellm/proxy/common_utils/callback_utils.py index ecec3f08dc4..0aa8d2291ef 100644 --- a/litellm/proxy/common_utils/callback_utils.py +++ b/litellm/proxy/common_utils/callback_utils.py @@ -750,11 +750,14 @@ def key_logging_entries(metadata: object) -> tuple[Mapping[str, object], ...] | entries: Final = decrypted_logging_entries(metadata) if entries is None: return None - return tuple( - resolved - for entry in object_sequence(entries) or () - if (resolved := _with_credential(string_keyed(entry))) is not None - ) + return tuple(resolved for entry in _logging_objects(entries) if (resolved := _with_credential(entry)) is not None) + + +def _logging_objects(entries: object) -> tuple[Mapping[str, object], ...]: + items: Final = () if entries in ("", {}) else object_sequence(entries) + if items is None or not all(isinstance(item, Mapping) for item in items): + raise TypeError(f"key metadata.logging must be a list of objects, got {type(entries).__name__}") + return tuple(string_keyed(item) for item in items) def _with_credential(entry: Mapping[str, object]) -> Mapping[str, object] | None: diff --git a/tests/unit/proxy/common_utils/test_callback_utils.py b/tests/unit/proxy/common_utils/test_callback_utils.py index 29e54b838d8..58b3c760db5 100644 --- a/tests/unit/proxy/common_utils/test_callback_utils.py +++ b/tests/unit/proxy/common_utils/test_callback_utils.py @@ -848,3 +848,35 @@ def test_key_logging_entries_drops_an_entry_whose_credential_is_not_defined(monk ] assert key_logging_entries({"logging": entries}) == (kept,) + + +@pytest.mark.parametrize( + "logging_value", + [ + pytest.param("abc", id="string"), + pytest.param({"callback_name": "arize"}, id="object"), + pytest.param(5, id="number"), + pytest.param(["x"], id="list-with-string"), + pytest.param([{"callback_name": "langfuse", "callback_vars": {}}, None], id="list-with-null"), + ], +) +def test_key_logging_entries_fails_on_non_object_logging_like_the_request_path_always_has( + logging_value: object, +) -> None: + with pytest.raises(TypeError): + key_logging_entries({"logging": logging_value}) + + +@pytest.mark.parametrize( + ("logging_value", "expected"), + [ + pytest.param("", (), id="empty-string"), + pytest.param({}, (), id="empty-object"), + pytest.param([], (), id="empty-list"), + pytest.param([{"bad": 1}], ({"bad": 1},), id="object-with-unknown-fields"), + ], +) +def test_key_logging_entries_passes_empty_and_object_entries_through( + logging_value: object, expected: tuple[dict[str, object], ...] +) -> None: + assert key_logging_entries({"logging": logging_value}) == expected