From 4b4b4a79b9216d7cd808f4bb277ac8223359dd52 Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Sun, 3 May 2026 02:04:05 +0000 Subject: [PATCH] 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) --- litellm/proxy/litellm_pre_call_utils.py | 10 +++++-- .../proxy/test_pricing_field_strip.py | 29 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index ac32d18aa8..7e0054b33f 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -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 diff --git a/tests/test_litellm/proxy/test_pricing_field_strip.py b/tests/test_litellm/proxy/test_pricing_field_strip.py index 881a44e93c..b73504b896 100644 --- a/tests/test_litellm/proxy/test_pricing_field_strip.py +++ b/tests/test_litellm/proxy/test_pricing_field_strip.py @@ -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 = {