From 104695e467ab3f8bb8bd43cf4830c4d4a5462a7a Mon Sep 17 00:00:00 2001 From: emme1t <149944796+emme1t@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:38:56 -0700 Subject: [PATCH 1/3] fix(logging): ignore non-list metadata tags --- litellm/litellm_core_utils/litellm_logging.py | 20 ++++--- .../test_litellm_logging.py | 52 +++++++++++++++++++ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index d75d35e4531..dc1bcfda47d 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -7098,19 +7098,17 @@ class StandardLoggingPayloadSetup: litellm_metadata: Final = cast( # cast-ok: request metadata is caller-provided Mapping[str, object], litellm_params.get("litellm_metadata") or {} ) - if metadata.get("tags", []): - request_tags = cast(list[str], metadata.get("tags", [])).copy() # cast-ok: tags are caller-provided - elif litellm_metadata.get("tags", []): - request_tags = cast(list[str], litellm_metadata.get("tags", [])).copy() # cast-ok: tags are caller-provided - else: - request_tags = [] + request_tags: Final = next( + ( + cast(list[str], tags) # cast-ok: preserve existing caller-provided list elements + for tag_metadata in (metadata, litellm_metadata) + if isinstance(tags := tag_metadata.get("tags"), list) and tags + ), + (), + ) user_agent_tags: Final = StandardLoggingPayloadSetup._get_user_agent_tags(proxy_server_request) additional_header_tags: Final = StandardLoggingPayloadSetup._get_extra_header_tags(proxy_server_request) - if user_agent_tags is not None: - request_tags.extend(user_agent_tags) - if additional_header_tags is not None: - request_tags.extend(additional_header_tags) - return request_tags + return [*request_tags, *(user_agent_tags or ()), *(additional_header_tags or ())] _get_request_tags = get_request_tags diff --git a/tests/unit/litellm_core_utils/test_litellm_logging.py b/tests/unit/litellm_core_utils/test_litellm_logging.py index bbfae2229ce..d898fc81ea6 100644 --- a/tests/unit/litellm_core_utils/test_litellm_logging.py +++ b/tests/unit/litellm_core_utils/test_litellm_logging.py @@ -3409,6 +3409,58 @@ def test_get_request_tags_preserves_non_string_metadata_tags(): assert 7 in tags +@pytest.mark.parametrize("metadata_key", ["metadata", "litellm_metadata"]) +@pytest.mark.parametrize("invalid_tags", ["my-tag", 7, True, {"tag": "value"}, ("my-tag",), None, ""]) +def test_standard_logging_payload_ignores_non_list_tags( + logging_obj: LitellmLogging, metadata_key: str, invalid_tags: object +) -> None: + timestamp: Final = datetime.datetime(2026, 1, 1) + payload: Final = get_standard_logging_object_payload( + kwargs={ + "litellm_call_id": "invalid-tag-request", + "model": "test-model", + "messages": [], + "litellm_params": { + metadata_key: {"tags": invalid_tags}, + "proxy_server_request": {"headers": {"user-agent": "litellm/1.0.0"}}, + }, + }, + init_response_obj={}, + start_time=timestamp, + end_time=timestamp, + logging_obj=logging_obj, + status="success", + ) + + assert payload is not None + assert payload["litellm_call_id"] == "invalid-tag-request" + assert payload["request_tags"] == ["User-Agent: litellm", "User-Agent: litellm/1.0.0"] + + +@pytest.mark.parametrize("invalid_tags", ["my-tag", 7, {"tag": "value"}, ("my-tag",), [], None]) +def test_get_request_tags_uses_valid_fallback_tags(invalid_tags: object) -> None: + fallback_tags: Final = ["fallback-tag"] + tags: Final = StandardLoggingPayloadSetup.get_request_tags( + litellm_params={ + "metadata": {"tags": invalid_tags}, + "litellm_metadata": {"tags": fallback_tags}, + }, + proxy_server_request={"headers": {"user-agent": "litellm/1.0.0"}}, + ) + + assert tags == ["fallback-tag", "User-Agent: litellm", "User-Agent: litellm/1.0.0"] + assert fallback_tags == ["fallback-tag"] + + +def test_get_request_tags_skips_unused_malformed_metadata() -> None: + tags: Final = StandardLoggingPayloadSetup.get_request_tags( + litellm_params={"metadata": {"tags": ["primary-tag"]}, "litellm_metadata": "unused"}, + proxy_server_request={}, + ) + + assert tags == ["primary-tag"] + + def test_get_request_tags_from_metadata_and_litellm_metadata(): """ Test that _get_request_tags correctly picks tags from both 'metadata' and 'litellm_metadata'. From a92c39bef697d110ce3ed910ad88a9598161508f Mon Sep 17 00:00:00 2001 From: emme1t <149944796+emme1t@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:31:53 -0700 Subject: [PATCH 2/3] fix(logging): preserve scalar tags for background settlement --- litellm/interactions/background_cost_polling.py | 7 ++++++- tests/unit/interactions/test_background_cost_polling.py | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/litellm/interactions/background_cost_polling.py b/litellm/interactions/background_cost_polling.py index 275eb17787e..26e8b616db5 100644 --- a/litellm/interactions/background_cost_polling.py +++ b/litellm/interactions/background_cost_polling.py @@ -307,8 +307,13 @@ def _spend_metadata(logging_obj: "LiteLLMLoggingObj") -> Mapping[str, object]: rebuilt logging object has no request headers to derive them from. """ litellm_params: Final = _OBJECT_DICT.validate_python(logging_obj.model_call_details.get("litellm_params") or {}) + metadata: Final = _OBJECT_MAPPING.validate_python( + get_litellm_metadata_from_kwargs(kwargs=logging_obj.model_call_details) + ) + if metadata.get("tags") and not isinstance(metadata.get("tags"), list): + return metadata return { - **_OBJECT_MAPPING.validate_python(get_litellm_metadata_from_kwargs(kwargs=logging_obj.model_call_details)), + **metadata, **_request_tags(litellm_params), } diff --git a/tests/unit/interactions/test_background_cost_polling.py b/tests/unit/interactions/test_background_cost_polling.py index d55994dfb19..994aeca83e7 100644 --- a/tests/unit/interactions/test_background_cost_polling.py +++ b/tests/unit/interactions/test_background_cost_polling.py @@ -484,13 +484,14 @@ async def test_schedule_registers_an_agent_only_create_that_names_no_model(): @pytest.mark.asyncio -async def test_schedule_registers_a_create_whose_tags_are_a_string_and_keeps_them_as_sent(): +@pytest.mark.parametrize("metadata_field", ["metadata", "litellm_metadata"]) +async def test_schedule_registers_a_create_whose_tags_are_a_string_and_keeps_them_as_sent(metadata_field): """ The regression: storing the create's context read string tags as a list and raised, so a create sent with a tags string answered with an error after the provider had already started the interaction. """ - logging_obj = _logging_obj(litellm_params={"metadata": _create_metadata(tags="team-a,nightly-batch")}) + logging_obj = _logging_obj(litellm_params={metadata_field: _create_metadata(tags="team-a,nightly-batch")}) store = InMemoryBackgroundSettlementStore() task = await maybe_schedule_background_interaction_cost_polling( From 601b38462ed5df738518985b6a4ada368ea70ccd Mon Sep 17 00:00:00 2001 From: emme1t <149944796+emme1t@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:06:25 -0700 Subject: [PATCH 3/3] fix(logging): retain lazy fallback tag extraction --- litellm/litellm_core_utils/litellm_logging.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 639003c9a6a..0a239a9d34c 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -7519,9 +7519,9 @@ class StandardLoggingPayloadSetup: ) request_tags: Final = next( ( - cast(list[str], tags) # cast-ok: tags are caller-provided - for tags in (metadata.get("tags"), litellm_metadata.get("tags")) - if isinstance(tags, list) and tags + cast(list[str], tags) # cast-ok: preserve existing caller-provided list elements + for tag_metadata in (metadata, litellm_metadata) + if isinstance(tags := tag_metadata.get("tags"), list) and tags ), (), )