From aa32e4cc8861eb6134391949373ebae6049311ff Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:37:18 -0700 Subject: [PATCH] fix(logging): stop the spend metadata merge from walking a dict the logging thread writes to (#46044) --- litellm/litellm_core_utils/core_helpers.py | 4 +- .../litellm_core_utils/test_core_helpers.py | 55 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/litellm/litellm_core_utils/core_helpers.py b/litellm/litellm_core_utils/core_helpers.py index cc1f6fef019..e706bcadef8 100644 --- a/litellm/litellm_core_utils/core_helpers.py +++ b/litellm/litellm_core_utils/core_helpers.py @@ -318,7 +318,7 @@ def remove_items_at_indices(items: _PoppableByIndex | None, indices: Iterable[in items.pop(index) -def add_missing_spend_metadata_to_litellm_metadata(litellm_metadata: dict, metadata: dict) -> dict: +def add_missing_spend_metadata_to_litellm_metadata(litellm_metadata: dict, metadata: Mapping[str, object]) -> dict: """ Helper to get litellm metadata for spend tracking @@ -326,7 +326,7 @@ def add_missing_spend_metadata_to_litellm_metadata(litellm_metadata: dict, metad and user_api_key values are in 'metadata'. """ potential_spend_tracking_metadata_substring: Final = "user_api_key" - for key, value in metadata.items(): + for key, value in dict(metadata).items(): if potential_spend_tracking_metadata_substring in key: litellm_metadata[key] = value return litellm_metadata diff --git a/tests/unit/litellm_core_utils/test_core_helpers.py b/tests/unit/litellm_core_utils/test_core_helpers.py index 9a7f96000e6..c3a2f032651 100644 --- a/tests/unit/litellm_core_utils/test_core_helpers.py +++ b/tests/unit/litellm_core_utils/test_core_helpers.py @@ -1,6 +1,8 @@ """Tests for litellm_core_utils.core_helpers module.""" import logging +import threading +from collections.abc import Iterator, Mapping from typing import Final import httpx @@ -13,6 +15,7 @@ from litellm.litellm_core_utils.core_helpers import ( budget_reservation_from_metadata, drop_params_env_flag, drop_params_flag, + get_litellm_metadata_from_kwargs, get_parent_otel_span_from_kwargs, get_or_create_metadata_bucket, get_provider_response_headers_from_hidden_params, @@ -587,3 +590,55 @@ class TestProviderResponseHeadersInHiddenParams: assert get_provider_response_headers_from_hidden_params(sibling) is None assert "additional_headers" not in sibling._hidden_params + + +def test_spend_metadata_merge_survives_the_logging_thread_inserting_into_the_request_metadata() -> None: + metadata: Final[dict[str, object]] = {} + + def logging_thread_records_hidden_params() -> None: + metadata["hidden_params"] = {"response_cost": 0.1} + + class KeyCheckedWhileTheLoggingThreadRuns(str): + def __contains__(self, key: str) -> bool: + writer: Final = threading.Thread(target=logging_thread_records_hidden_params) + writer.start() + writer.join() + return super().__contains__(key) + + metadata[KeyCheckedWhileTheLoggingThreadRuns("user_api_key_alias")] = "alias" + metadata["user_api_key_hash"] = "hash" + metadata["requester_ip_address"] = "10.0.0.1" + litellm_metadata: Final[dict[str, object]] = {"user_api_key_alias": "stale", "model_group": "claude"} + + merged: Final = get_litellm_metadata_from_kwargs( + {"litellm_params": {"metadata": metadata, "litellm_metadata": litellm_metadata}} + ) + + assert "hidden_params" in metadata + assert merged == {"user_api_key_alias": "alias", "user_api_key_hash": "hash", "model_group": "claude"} + + +def test_spend_metadata_merge_reads_request_metadata_that_is_a_read_only_mapping() -> None: + class ReadOnlyMetadata(Mapping[str, object]): + def __init__(self, values: dict[str, object]) -> None: + self._values: Final = values + + def __getitem__(self, key: str) -> object: + return self._values[key] + + def __iter__(self) -> Iterator[str]: + return iter(self._values) + + def __len__(self) -> int: + return len(self._values) + + merged: Final = get_litellm_metadata_from_kwargs( + { + "litellm_params": { + "metadata": ReadOnlyMetadata({"user_api_key_hash": "hash", "requester_ip_address": "10.0.0.1"}), + "litellm_metadata": {"model_group": "claude"}, + } + } + ) + + assert merged == {"user_api_key_hash": "hash", "model_group": "claude"}