mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-02 07:13:02 +00:00
fix(openai): take the early Responses route decision on the request minus additional_drop_params
Filter the keys named in additional_drop_params out of the top level and extra_body before the hoisted bridge check so a dropped reasoning_effort cannot move a chat-only custom endpoint onto /responses Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
846907a3ff
commit
4ffb145435
4 changed files with 97 additions and 2 deletions
|
|
@ -1315,6 +1315,7 @@ from litellm.responses.bridge_check import ( # noqa: E402 # re-export at the o
|
|||
_resolve_openai_api_base as _resolve_openai_api_base, # pyright: ignore[reportPrivateUsage] # noqa: PLC0414 # re-export of moved helper
|
||||
)
|
||||
from litellm.responses.bridge_check import (
|
||||
bridge_check_request_params,
|
||||
bridge_reasoning_effort,
|
||||
keep_registered_mode,
|
||||
str_keyed_mapping,
|
||||
|
|
@ -6045,13 +6046,16 @@ def completion(
|
|||
if dynamic_api_key is not None:
|
||||
api_key = dynamic_api_key
|
||||
if responses_api_model_info.get("mode") != "responses":
|
||||
route_request_params: Final = bridge_check_request_params(routing_kwargs)
|
||||
responses_api_model_info, model = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
web_search_options=web_search_options,
|
||||
tools=tools,
|
||||
reasoning_effort=bridge_reasoning_effort(resolved_reasoning_effort, routing_kwargs.get("extra_body")),
|
||||
reasoning_summary=peek_reasoning_summary_aliases(routing_kwargs),
|
||||
reasoning_effort=bridge_reasoning_effort(
|
||||
resolved_reasoning_effort, route_request_params.get("extra_body")
|
||||
),
|
||||
reasoning_summary=peek_reasoning_summary_aliases(route_request_params),
|
||||
api_base=api_base,
|
||||
deployment_model_info=deployment_model_info,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -102,6 +102,24 @@ def str_keyed_mapping(value: object) -> Mapping[str, object] | None:
|
|||
)
|
||||
|
||||
|
||||
def bridge_check_request_params(request_params: Mapping[str, object]) -> Mapping[str, object]:
|
||||
"""The request as ``get_optional_params`` will hand it to the provider: every key named in
|
||||
``additional_drop_params`` removed at the top level and inside ``extra_body``, so the Responses route
|
||||
decision taken before that call reads the same effort and summary alias the parsed params will carry."""
|
||||
raw_dropped: Final = request_params.get("additional_drop_params")
|
||||
dropped_params: Final = (
|
||||
cast(Sequence[object], raw_dropped) # cast-ok: isinstance only narrows to Sequence[Unknown]
|
||||
if isinstance(raw_dropped, Sequence) and not isinstance(raw_dropped, str)
|
||||
else ()
|
||||
)
|
||||
dropped: Final = frozenset(param for param in dropped_params if isinstance(param, str))
|
||||
kept: Final = {key: value for key, value in request_params.items() if key not in dropped}
|
||||
extra_body: Final = str_keyed_mapping(kept.get("extra_body"))
|
||||
if extra_body is None:
|
||||
return kept
|
||||
return {**kept, "extra_body": {key: value for key, value in extra_body.items() if key not in dropped}}
|
||||
|
||||
|
||||
def bridge_reasoning_effort(
|
||||
reasoning_effort: str | Mapping[str, object] | None, extra_body: object
|
||||
) -> str | Mapping[str, object] | None:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import pytest
|
|||
from integration._support.client import Gateway
|
||||
from integration._support.provider import SharedProvider
|
||||
from integration.translation.case import TranslationTestCase
|
||||
from integration.translation.chat_completions.bases.openai import GPT_5_6_SOL_TEST_CASE as GPT_5_6_SOL_CHAT_TEST_CASE
|
||||
from integration.translation.chat_completions.bases.openai_responses import GPT_5_6_SOL_TEST_CASE
|
||||
from integration.translation.runner import assert_translation
|
||||
|
||||
|
|
@ -78,10 +79,47 @@ GPT_5_6_SOL_LATE_BRIDGE_WEB_SEARCH_TEST_CASE: Final = replace(
|
|||
expected_litellm_response={**GPT_5_6_SOL_TEST_CASE.expected_litellm_response, "model": "openai/gpt-5.6-sol"},
|
||||
)
|
||||
|
||||
# The rig's deployment has a custom api_base, which the bridge treats as a chat-only endpoint unless the
|
||||
# request names an effort. An effort the caller drops through additional_drop_params must not pick the
|
||||
# Responses route, so this request stays on /chat/completions exactly as before the early route decision
|
||||
GPT_5_6_SOL_DROPPED_EFFORT_STAYS_ON_CHAT_TEST_CASE: Final = replace(
|
||||
GPT_5_6_SOL_CHAT_TEST_CASE,
|
||||
scenario="dropped_extra_body_effort_stays_on_chat",
|
||||
litellm_request={
|
||||
**GPT_5_6_SOL_CHAT_TEST_CASE.litellm_request,
|
||||
"extra_body": {"reasoning_effort": "low"},
|
||||
"additional_drop_params": ["reasoning_effort"],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get weather",
|
||||
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
expected_provider_request={
|
||||
**GPT_5_6_SOL_CHAT_TEST_CASE.expected_provider_request,
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get weather",
|
||||
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
CASES: Final = (
|
||||
GPT_5_6_SOL_WEB_SEARCH_TEST_CASE,
|
||||
GPT_5_6_SOL_WEB_SEARCH_USER_LOCATION_TEST_CASE,
|
||||
GPT_5_6_SOL_LATE_BRIDGE_WEB_SEARCH_TEST_CASE,
|
||||
GPT_5_6_SOL_DROPPED_EFFORT_STAYS_ON_CHAT_TEST_CASE,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
35
tests/unit/responses/test_bridge_check.py
Normal file
35
tests/unit/responses/test_bridge_check.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from litellm.responses.bridge_check import bridge_check_request_params
|
||||
|
||||
|
||||
def test_a_dropped_effort_inside_extra_body_is_not_read_by_the_route_decision():
|
||||
request = {
|
||||
"extra_body": {"reasoning_effort": "low", "reasoningSummary": "auto"},
|
||||
"additional_drop_params": ["reasoning_effort"],
|
||||
"tools": [{"type": "function", "function": {"name": "get_weather"}}],
|
||||
}
|
||||
|
||||
params = bridge_check_request_params(request)
|
||||
|
||||
assert params["extra_body"] == {"reasoningSummary": "auto"}, params
|
||||
assert params["tools"] is request["tools"]
|
||||
|
||||
|
||||
def test_a_dropped_top_level_alias_is_removed_too():
|
||||
params = bridge_check_request_params({"reasoningSummary": "auto", "additional_drop_params": ["reasoningSummary"]})
|
||||
|
||||
assert "reasoningSummary" not in params, params
|
||||
|
||||
|
||||
def test_a_request_without_drops_is_returned_whole():
|
||||
request = {"extra_body": {"reasoning_effort": "low"}, "tools": []}
|
||||
|
||||
params = bridge_check_request_params(request)
|
||||
|
||||
assert params == request
|
||||
assert params["extra_body"] == {"reasoning_effort": "low"}
|
||||
|
||||
|
||||
def test_a_non_list_additional_drop_params_drops_nothing():
|
||||
request = {"extra_body": {"reasoning_effort": "low"}, "additional_drop_params": "reasoning_effort"}
|
||||
|
||||
assert bridge_check_request_params(request)["extra_body"] == {"reasoning_effort": "low"}
|
||||
Loading…
Add table
Reference in a new issue