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
This commit is contained in:
Souravrajvi0 2026-08-10 15:19:16 +00:00
parent f6b9518ddb
commit 50cc298dcc
2 changed files with 73 additions and 4 deletions

View file

@ -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)

View file

@ -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