From 50cc298dccad03f750ed4aede3cb7438691b59d8 Mon Sep 17 00:00:00 2001 From: Souravrajvi0 <144546710+Souravrajvi0@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:19:16 +0000 Subject: [PATCH 1/4] fix(responses): stop internal metadata reaching strict upstreams When proxy-internal fields leaked into the provider-facing metadata parameter (including empty {}), strict OpenAI-compatible /v1/responses backends returned HTTP 400. Only forward caller metadata: prefer litellm_metadata.requester_metadata on proxy routes and drop metadata entirely when no caller-supplied values exist. Fixes BerriAI/litellm#35780 --- litellm/responses/utils.py | 24 +++++++-- .../responses/test_responses_utils.py | 53 +++++++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index db2e515609c..c6912ed977f 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -231,14 +231,30 @@ class ResponsesAPIRequestUtils: ) non_default_params["previous_response_id"] = decoded_previous_response_id - if "metadata" in non_default_params: - from litellm.utils import add_openai_metadata + litellm_metadata = params.get("litellm_metadata") + if not isinstance(litellm_metadata, dict): + litellm_metadata = {} - converted_metadata: Final = add_openai_metadata(non_default_params["metadata"]) - if converted_metadata is not None: + requester_metadata = litellm_metadata.get("requester_metadata") + metadata_in_params = non_default_params.get("metadata") + is_proxy_internal_metadata = any(key.startswith("user_api_key_") for key in litellm_metadata) + + metadata_source: dict | None = None + if isinstance(requester_metadata, dict): + metadata_source = requester_metadata + elif not is_proxy_internal_metadata and isinstance(metadata_in_params, dict): + metadata_source = metadata_in_params + + if metadata_source is not None: + from litellm.utils import get_requester_metadata + + converted_metadata: Final = get_requester_metadata(metadata_source) + if converted_metadata: non_default_params["metadata"] = converted_metadata else: non_default_params.pop("metadata", None) + else: + non_default_params.pop("metadata", None) return cast(ResponsesAPIOptionalRequestParams, non_default_params) diff --git a/tests/test_litellm/responses/test_responses_utils.py b/tests/test_litellm/responses/test_responses_utils.py index 0141cf5d96a..fa8c351d184 100644 --- a/tests/test_litellm/responses/test_responses_utils.py +++ b/tests/test_litellm/responses/test_responses_utils.py @@ -132,6 +132,59 @@ class TestResponsesAPIRequestUtils: assert result["max_output_tokens"] == 100 assert result["prompt"] == {"id": "pmpt_456"} + def test_get_requested_response_api_optional_param_strips_empty_metadata(self): + """Regression #35780: empty metadata dict must not reach upstream.""" + result = ResponsesAPIRequestUtils.get_requested_response_api_optional_param( + {"temperature": 0.7, "metadata": {}} + ) + assert "metadata" not in result + + def test_get_requested_response_api_optional_param_strips_internal_metadata(self): + """Regression #35780: proxy/router internal metadata must not reach upstream.""" + result = ResponsesAPIRequestUtils.get_requested_response_api_optional_param( + { + "temperature": 0.7, + "metadata": { + "model_group": "test-model", + "deployment": "openai/gpt-4o", + "model_info": {"id": "dep-1"}, + "user_api_key_user_id": "user-1", + }, + "litellm_metadata": { + "user_api_key_team_id": "team-1", + "tags": ["internal-tag"], + }, + } + ) + assert "metadata" not in result + + def test_get_requested_response_api_optional_param_preserves_caller_metadata(self): + """Caller-supplied string metadata should still be forwarded.""" + result = ResponsesAPIRequestUtils.get_requested_response_api_optional_param( + { + "temperature": 0.7, + "metadata": {"customer_id": "cust-123", "campaign": "spring"}, + } + ) + assert result["metadata"] == {"customer_id": "cust-123", "campaign": "spring"} + + def test_get_requested_response_api_optional_param_uses_requester_metadata(self): + """Proxy snapshots caller metadata under litellm_metadata.requester_metadata.""" + result = ResponsesAPIRequestUtils.get_requested_response_api_optional_param( + { + "temperature": 0.7, + "metadata": { + "model_group": "test-model", + "user_api_key_user_id": "user-1", + }, + "litellm_metadata": { + "requester_metadata": {"customer_id": "cust-456"}, + "user_api_key_team_id": "team-1", + }, + } + ) + assert result["metadata"] == {"customer_id": "cust-456"} + def test_decode_previous_response_id_to_original_previous_response_id(self): """Test decoding a LiteLLM encoded previous_response_id to the original previous_response_id""" # Setup From 154dd813b1eb906e1fbb797555e54a46251cf5e9 Mon Sep 17 00:00:00 2001 From: Souravrajvi0 <144546710+Souravrajvi0@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:34:30 +0000 Subject: [PATCH 2/4] fix(responses): avoid double-unwrapping caller metadata Use add_openai_metadata on the resolved metadata source instead of get_requester_metadata, which treated caller-owned requester_metadata keys as another proxy envelope and dropped sibling fields. Build the final optional-params dict without mutating metadata via pop. --- litellm/responses/utils.py | 33 +++++++++---------- .../responses/test_responses_utils.py | 28 ++++++++++++++++ 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index c6912ed977f..4d060836e3b 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -231,32 +231,31 @@ class ResponsesAPIRequestUtils: ) non_default_params["previous_response_id"] = decoded_previous_response_id - litellm_metadata = params.get("litellm_metadata") - if not isinstance(litellm_metadata, dict): - litellm_metadata = {} + litellm_metadata_raw = params.get("litellm_metadata") + litellm_metadata: Final = litellm_metadata_raw if isinstance(litellm_metadata_raw, dict) else {} - requester_metadata = litellm_metadata.get("requester_metadata") + proxy_requester_metadata = litellm_metadata.get("requester_metadata") metadata_in_params = non_default_params.get("metadata") - is_proxy_internal_metadata = any(key.startswith("user_api_key_") for key in litellm_metadata) + is_proxy_internal_metadata: Final = any(key.startswith("user_api_key_") for key in litellm_metadata) metadata_source: dict | None = None - if isinstance(requester_metadata, dict): - metadata_source = requester_metadata + if isinstance(proxy_requester_metadata, dict): + metadata_source = proxy_requester_metadata elif not is_proxy_internal_metadata and isinstance(metadata_in_params, dict): metadata_source = metadata_in_params - if metadata_source is not None: - from litellm.utils import get_requester_metadata + from litellm.utils import add_openai_metadata - converted_metadata: Final = get_requester_metadata(metadata_source) - if converted_metadata: - non_default_params["metadata"] = converted_metadata - else: - non_default_params.pop("metadata", None) - else: - non_default_params.pop("metadata", None) + converted_metadata: Final = ( + add_openai_metadata(metadata_source) if metadata_source is not None else None + ) + non_default_params_with_metadata: Final = ( + {**non_default_params, "metadata": converted_metadata} + if converted_metadata + else {key: value for key, value in non_default_params.items() if key != "metadata"} + ) - return cast(ResponsesAPIOptionalRequestParams, non_default_params) + return cast(ResponsesAPIOptionalRequestParams, non_default_params_with_metadata) # fmt: off @overload diff --git a/tests/test_litellm/responses/test_responses_utils.py b/tests/test_litellm/responses/test_responses_utils.py index fa8c351d184..8f2724688ad 100644 --- a/tests/test_litellm/responses/test_responses_utils.py +++ b/tests/test_litellm/responses/test_responses_utils.py @@ -185,6 +185,34 @@ class TestResponsesAPIRequestUtils: ) assert result["metadata"] == {"customer_id": "cust-456"} + def test_get_requested_response_api_optional_param_preserves_sibling_metadata_keys(self): + result = ResponsesAPIRequestUtils.get_requested_response_api_optional_param( + { + "temperature": 0.7, + "metadata": { + "customer_id": "cust-123", + "requester_metadata": {"nested": "value"}, + }, + } + ) + assert result["metadata"] == {"customer_id": "cust-123"} + + def test_get_requested_response_api_optional_param_proxy_snapshot_no_double_unwrap(self): + result = ResponsesAPIRequestUtils.get_requested_response_api_optional_param( + { + "temperature": 0.7, + "metadata": {"model_group": "test-model"}, + "litellm_metadata": { + "requester_metadata": { + "customer_id": "cust-789", + "requester_metadata": {"nested": "value"}, + }, + "user_api_key_team_id": "team-1", + }, + } + ) + assert result["metadata"] == {"customer_id": "cust-789"} + def test_decode_previous_response_id_to_original_previous_response_id(self): """Test decoding a LiteLLM encoded previous_response_id to the original previous_response_id""" # Setup From 237b3072a1cf269754baa2dff59447bb91086384 Mon Sep 17 00:00:00 2001 From: Souravrajvi0 <144546710+Souravrajvi0@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:38:38 +0000 Subject: [PATCH 3/4] style(responses): ruff format metadata resolution block --- litellm/responses/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index 4d060836e3b..1ac77270eea 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -246,9 +246,7 @@ class ResponsesAPIRequestUtils: from litellm.utils import add_openai_metadata - converted_metadata: Final = ( - add_openai_metadata(metadata_source) if metadata_source is not None else None - ) + converted_metadata: Final = add_openai_metadata(metadata_source) if metadata_source is not None else None non_default_params_with_metadata: Final = ( {**non_default_params, "metadata": converted_metadata} if converted_metadata From 813a2714fdfaaa8434acecd6dab692a14c5272d5 Mon Sep 17 00:00:00 2001 From: Souravrajvi0 <144546710+Souravrajvi0@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:52:15 +0000 Subject: [PATCH 4/4] fix(responses): satisfy type-discipline gate for metadata block Use Final assignments and a single metadata_source expression so LIT001 and LIT010 budgets are not exceeded. --- litellm/responses/utils.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index 1ac77270eea..6e1024f09ca 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -231,18 +231,20 @@ class ResponsesAPIRequestUtils: ) non_default_params["previous_response_id"] = decoded_previous_response_id - litellm_metadata_raw = params.get("litellm_metadata") + litellm_metadata_raw: Final = params.get("litellm_metadata") litellm_metadata: Final = litellm_metadata_raw if isinstance(litellm_metadata_raw, dict) else {} - proxy_requester_metadata = litellm_metadata.get("requester_metadata") - metadata_in_params = non_default_params.get("metadata") + proxy_requester_metadata: Final = litellm_metadata.get("requester_metadata") + metadata_in_params: Final = non_default_params.get("metadata") is_proxy_internal_metadata: Final = any(key.startswith("user_api_key_") for key in litellm_metadata) - metadata_source: dict | None = None - if isinstance(proxy_requester_metadata, dict): - metadata_source = proxy_requester_metadata - elif not is_proxy_internal_metadata and isinstance(metadata_in_params, dict): - metadata_source = metadata_in_params + metadata_source: Final = ( + proxy_requester_metadata + if isinstance(proxy_requester_metadata, dict) + else ( + metadata_in_params if not is_proxy_internal_metadata and isinstance(metadata_in_params, dict) else None + ) + ) from litellm.utils import add_openai_metadata