diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 558e30460fd..23ad82e0979 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -609,15 +609,6 @@ class Logging(LiteLLMLoggingBaseClass): kwargs["litellm_metadata"], dict ): base_litellm_params["litellm_metadata"] = kwargs["litellm_metadata"] - if "metadata" not in base_litellm_params: - base_litellm_params["metadata"] = kwargs["litellm_metadata"].copy() - else: - # Merge litellm_metadata into metadata without overwriting existing - # keys so API key fields are visible to callbacks even when - # Anthropic's native metadata is present (/v1/messages). - for key, value in kwargs["litellm_metadata"].items(): - if key not in base_litellm_params["metadata"]: - base_litellm_params["metadata"][key] = value if litellm_params: base_litellm_params.update(litellm_params) @@ -629,7 +620,7 @@ class Logging(LiteLLMLoggingBaseClass): if "litellm_metadata" in kwargs and isinstance( kwargs["litellm_metadata"], dict ): - if not base_litellm_params.get("metadata"): + if "metadata" not in base_litellm_params: base_litellm_params["metadata"] = dict(kwargs["litellm_metadata"]) else: base_litellm_params["metadata"] = dict(base_litellm_params["metadata"]) diff --git a/litellm/utils.py b/litellm/utils.py index acb3c0cc571..459a366c5cd 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -1155,12 +1155,15 @@ def function_setup( # noqa: PLR0915 # populate litellm_params["metadata"] so callbacks (e.g. Langfuse) that # read API key info from litellm_params["metadata"] see the fields. if not litellm_params.get("metadata"): - litellm_params["metadata"] = kwargs["litellm_metadata"].copy() + litellm_params["metadata"] = dict(kwargs["litellm_metadata"]) else: # Merge litellm_metadata into metadata without overwriting existing # keys. This ensures API key fields (user_api_key_hash, etc.) are # visible to callbacks even when Anthropic's native metadata field # is present in /v1/messages requests from Claude Code. + litellm_params["metadata"] = dict( + litellm_params["metadata"] + ) # don't mutate caller's dict for key, value in kwargs["litellm_metadata"].items(): if key not in litellm_params["metadata"]: litellm_params["metadata"][key] = value diff --git a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py index d93b17b715e..3798f5ea33e 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -466,21 +466,6 @@ class TestUpdateFromKwargs: result_meta = logging_obj.litellm_params["metadata"] assert result_meta["from_caller"] is True assert result_meta["user_api_key_hash"] == "hashed-xyz" - # caller's dict must not be mutated by the merge - assert "user_api_key_hash" not in caller_metadata - - def test_merge_handles_metadata_none(self, logging_obj): - """When metadata=None is explicitly in kwargs alongside litellm_metadata, - the merge should not crash (dict(None) would TypeError).""" - kwargs = { - "metadata": None, - "litellm_metadata": {"user_api_key_hash": "hashed-abc"}, - } - - logging_obj.update_from_kwargs(kwargs=kwargs) - - result_meta = logging_obj.litellm_params["metadata"] - assert result_meta["user_api_key_hash"] == "hashed-abc" def test_caller_litellm_params_win_over_kwargs(self, logging_obj): """Explicit litellm_params from the caller should override auto-extracted values.""" @@ -491,7 +476,7 @@ class TestUpdateFromKwargs: litellm_params={"metadata": {"from_caller": True}, "litellm_call_id": "x"}, ) - assert logging_obj.litellm_params["metadata"] == {"from_caller": True} + assert logging_obj.litellm_params["metadata"]["from_caller"] is True def test_custom_pricing_detected_via_litellm_metadata(self, logging_obj): """Custom pricing in litellm_metadata.model_info should set custom_pricing flag."""