fix(proxy): move pricing strip below the litellm_metadata JSON-string parse

The strip ran before the proxy parses ``litellm_metadata`` from a JSON
string into a dict (a path used by multipart/form-data and ``extra_body``
callers), so ``isinstance(metadata, dict)`` was False and ``model_info``
survived the strip. Move the call to the same post-parse position the
``user_api_key_*`` strip already uses for the same reason. Adds a
regression test exercising the JSON-string ``litellm_metadata`` path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
user 2026-05-03 02:04:05 +00:00
parent 25671105e2
commit 4b4b4a79b9
No known key found for this signature in database
2 changed files with 37 additions and 2 deletions

View File

@ -1164,8 +1164,6 @@ async def add_litellm_data_to_request( # noqa: PLR0915
if _allow_client_mock_response and _internal_key in _CLIENT_MOCK_CONTROL_FIELDS:
continue
data.pop(_internal_key, None)
if not _key_or_team_allows_client_pricing_override(user_api_key_dict):
_strip_client_pricing_overrides(data)
# Strip spoofable auth metadata from user-supplied metadata dict
_user_metadata = data.get("metadata")
if isinstance(_user_metadata, dict):
@ -1367,6 +1365,14 @@ async def add_litellm_data_to_request( # noqa: PLR0915
]:
_user_meta.pop(_k, None)
# Strip pricing overrides AFTER the litellm_metadata string-to-dict parse
# above, for the same reason as the user_api_key_* strip — JSON-string
# metadata (sent via multipart/form-data or extra_body) wouldn't be a
# dict yet at the earlier strip point and the isinstance(dict) guard
# would silently skip the field.
if not _key_or_team_allows_client_pricing_override(user_api_key_dict):
_strip_client_pricing_overrides(data)
# Strip caller-supplied routing/budget tags unless the admin has opted
# this key or team in via metadata.allow_client_tags=True. Tags drive
# tag-based routing and tag budget attribution — accepting them from

View File

@ -231,6 +231,35 @@ async def test_add_litellm_data_to_request_skips_strip_with_key_opt_in():
assert updated["metadata"]["model_info"] == {"output_cost_per_token": 0.0002}
@pytest.mark.asyncio
async def test_add_litellm_data_to_request_strips_json_string_litellm_metadata():
"""``litellm_metadata`` may arrive as a JSON-encoded string (multipart/
form-data or ``extra_body``). The strip has to run after the proxy parses
it into a dict; otherwise the ``isinstance(dict)`` guard skips the field
and ``model_info`` survives the strip via the string path.
"""
import json
data = {
"model": "gpt-4",
"messages": [{"role": "user", "content": "hi"}],
"litellm_metadata": json.dumps({"model_info": {"input_cost_per_token": 0.0}}),
}
updated = await add_litellm_data_to_request(
data=data,
request=_make_request_mock(),
user_api_key_dict=_user_api_key_auth(),
proxy_config=MagicMock(),
general_settings={},
version="test-version",
)
parsed_metadata = updated.get("litellm_metadata")
assert isinstance(parsed_metadata, dict)
assert "model_info" not in parsed_metadata
@pytest.mark.asyncio
async def test_add_litellm_data_to_request_skips_strip_with_team_opt_in():
data = {