From 45a5d29f81cedafe465c5c0a8019863343b817cf Mon Sep 17 00:00:00 2001 From: Snehal58 Date: Mon, 6 Apr 2026 01:18:49 +0530 Subject: [PATCH] fix: drop invalid top-level params (strict) from OpenAI extra_body --- .../litellm_core_utils/llm_request_utils.py | 18 ++++ .../test_llm_request_utils.py | 100 ++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 tests/test_litellm/litellm_core_utils/test_llm_request_utils.py diff --git a/litellm/litellm_core_utils/llm_request_utils.py b/litellm/litellm_core_utils/llm_request_utils.py index dc70069ac5a..34df7e9a744 100644 --- a/litellm/litellm_core_utils/llm_request_utils.py +++ b/litellm/litellm_core_utils/llm_request_utils.py @@ -1,6 +1,12 @@ from typing import Dict, Optional import litellm +from litellm._logging import verbose_logger + +# Params that callers (e.g. LangChain) sometimes pass at the top level but +# that are NOT valid top-level OpenAI API fields. Forwarding them in +# extra_body causes OpenAI to return "400 Unrecognized request argument". +_OPENAI_INVALID_TOP_LEVEL_PARAMS = frozenset({"strict"}) def _ensure_extra_body_is_safe(extra_body: Optional[Dict]) -> Optional[Dict]: @@ -26,6 +32,18 @@ def _ensure_extra_body_is_safe(extra_body: Optional[Dict]) -> Optional[Dict]: # Langfuse TextPromptClients have .__dict__ attribute if _prompt is not None and hasattr(_prompt, "__dict__"): extra_body["metadata"]["prompt"] = _prompt.__dict__ + + # Drop params that are invalid at the OpenAI top-level request body. + # Some callers (e.g. LangChain) pass these at the completion() top level; + # they end up in extra_body and cause "400 Unrecognized request argument". + dropped = [k for k in _OPENAI_INVALID_TOP_LEVEL_PARAMS if k in extra_body] + for k in dropped: + extra_body.pop(k) + if dropped: + verbose_logger.debug( + "LiteLLM: dropped invalid top-level OpenAI params from extra_body: %s", + dropped, + ) return extra_body diff --git a/tests/test_litellm/litellm_core_utils/test_llm_request_utils.py b/tests/test_litellm/litellm_core_utils/test_llm_request_utils.py new file mode 100644 index 00000000000..07a8cda4724 --- /dev/null +++ b/tests/test_litellm/litellm_core_utils/test_llm_request_utils.py @@ -0,0 +1,100 @@ +""" +Tests for litellm_core_utils.llm_request_utils module. + +Covers _ensure_extra_body_is_safe, including the fix for top-level kwargs +(e.g. `strict=True`) that callers like LangChain pass to litellm.completion() +but that are not valid at the OpenAI top-level request body. +""" + +from litellm.litellm_core_utils.llm_request_utils import _ensure_extra_body_is_safe +from litellm.utils import get_optional_params + + +# --------------------------------------------------------------------------- +# _ensure_extra_body_is_safe +# --------------------------------------------------------------------------- + + +def test_ensure_extra_body_is_safe_strips_strict(): + """strict is not a valid top-level OpenAI param and must be removed.""" + result = _ensure_extra_body_is_safe({"strict": True, "some_custom_key": "value"}) + assert isinstance(result, dict) + body: dict = result + assert "strict" not in body + assert body.get("some_custom_key") == "value" + + +def test_ensure_extra_body_is_safe_none_passthrough(): + assert _ensure_extra_body_is_safe(None) is None + + +def test_ensure_extra_body_is_safe_non_dict_passthrough(): + sentinel = object() + assert _ensure_extra_body_is_safe(sentinel) is sentinel # type: ignore[arg-type] + + +def test_ensure_extra_body_is_safe_empty_dict(): + assert _ensure_extra_body_is_safe({}) == {} + + +def test_ensure_extra_body_is_safe_preserves_valid_keys(): + body = {"some_provider_param": 42, "another": "hello"} + result = _ensure_extra_body_is_safe(body) + assert result == {"some_provider_param": 42, "another": "hello"} + + +def test_ensure_extra_body_is_safe_strict_false_also_stripped(): + """strict=False is equally invalid at the top level.""" + result = _ensure_extra_body_is_safe({"strict": False}) + assert isinstance(result, dict) + body: dict = result + assert "strict" not in body + + +# --------------------------------------------------------------------------- +# get_optional_params — end-to-end: strict must not reach extra_body for OpenAI +# --------------------------------------------------------------------------- + + +def test_get_optional_params_openai_strict_not_in_extra_body(): + """ + Regression test for: litellm.BadRequestError: OpenAIException - Unrecognized + request argument supplied: strict + + When strict=True is passed as a top-level kwarg (LangChain-style), + it must NOT appear in extra_body for OpenAI models. + """ + params = get_optional_params( + model="gpt-4o", + custom_llm_provider="openai", + strict=True, + temperature=0.7, + ) + extra_body: dict = params.get("extra_body", {}) + assert "strict" not in extra_body, ( + "'strict' must not be forwarded in extra_body for OpenAI — " + "it causes a 400 'Unrecognized request argument' error." + ) + + +def test_get_optional_params_openai_strict_false_not_in_extra_body(): + params = get_optional_params( + model="gpt-4o", + custom_llm_provider="openai", + strict=False, + ) + extra_body: dict = params.get("extra_body", {}) + assert "strict" not in extra_body + + +def test_get_optional_params_openai_strict_does_not_block_other_extra_body_keys(): + """Unrecognized provider-extension keys other than strict still flow through.""" + params = get_optional_params( + model="gpt-4o", + custom_llm_provider="openai", + strict=True, + my_custom_provider_extension="foo", + ) + extra_body: dict = params.get("extra_body", {}) + assert "strict" not in extra_body + assert extra_body.get("my_custom_provider_extension") == "foo"