mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
fix: drop invalid top-level params (strict) from OpenAI extra_body
This commit is contained in:
parent
d251238bd7
commit
45a5d29f81
2 changed files with 118 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
100
tests/test_litellm/litellm_core_utils/test_llm_request_utils.py
Normal file
100
tests/test_litellm/litellm_core_utils/test_llm_request_utils.py
Normal file
|
|
@ -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"
|
||||
Loading…
Add table
Reference in a new issue