mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-17 23:51:30 +00:00
fix(responses): filter bridged kwargs like the native Responses path
A Responses request for a provider with a native Responses config that is served through the chat-completions bridge (use_chat_completions_api or the openai/chat_completions/ prefix) forwarded every raw kwarg, so a deployment-level chat_template_kwargs reached OpenAI chat completions and got a 400. The bridge now keeps only the keys a native dispatch would forward plus allowed_openai_params. Providers with no native Responses config keep the passthrough Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
7fd541efb9
commit
04c003c098
2 changed files with 126 additions and 3 deletions
|
|
@ -1,9 +1,10 @@
|
|||
import asyncio
|
||||
import contextvars
|
||||
from collections.abc import Coroutine, Generator, Iterable, Mapping
|
||||
from collections.abc import Coroutine, Generator, Iterable, Mapping, Sequence
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, NoReturn, Optional, TypeAlias, cast
|
||||
|
||||
import httpx
|
||||
|
|
@ -15,7 +16,7 @@ from litellm._logging import verbose_logger
|
|||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
LiteLLMResponsesTransformationHandler,
|
||||
)
|
||||
from litellm.constants import request_timeout
|
||||
from litellm.constants import DEFAULT_CHAT_COMPLETION_PARAM_VALUES, request_timeout
|
||||
from litellm.integrations.anthropic_cache_control_hook import CARRY_UNMATCHED_MESSAGE_POINTS
|
||||
from litellm.litellm_core_utils.asyncify import run_async_function
|
||||
from litellm.litellm_core_utils.core_helpers import normalize_drop_params
|
||||
|
|
@ -52,6 +53,7 @@ from litellm.llms.openai.data_residency import infer_openai_data_residency
|
|||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.responses.main import *
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.types.utils import all_litellm_params
|
||||
from litellm.utils import (
|
||||
ProviderConfigManager,
|
||||
client,
|
||||
|
|
@ -408,6 +410,26 @@ def _bridges_to_chat_completions(
|
|||
return responses_api_provider_config is None or use_chat_completions_api is True
|
||||
|
||||
|
||||
def _bridge_kwargs(
|
||||
kwargs: Mapping[str, object],
|
||||
responses_api_provider_config: BaseResponsesAPIConfig | None,
|
||||
allowed_openai_params: Sequence[str] | None,
|
||||
) -> Mapping[str, object]:
|
||||
"""Drop the provider-specific kwargs a native Responses dispatch would never forward, unless explicitly allowed."""
|
||||
if responses_api_provider_config is None:
|
||||
return kwargs
|
||||
forwarded_keys: Final = frozenset(
|
||||
(
|
||||
*litellm.OPENAI_CHAT_COMPLETION_PARAMS,
|
||||
*DEFAULT_CHAT_COMPLETION_PARAM_VALUES,
|
||||
*all_litellm_params,
|
||||
*GenericLiteLLMParams.model_fields,
|
||||
*(allowed_openai_params or ()),
|
||||
)
|
||||
)
|
||||
return MappingProxyType({key: value for key, value in kwargs.items() if key in forwarded_keys})
|
||||
|
||||
|
||||
_ResponsesCompatibilityFailure: TypeAlias = Literal["encrypted_task_unsupported"]
|
||||
|
||||
|
||||
|
|
@ -1281,6 +1303,7 @@ def responses(
|
|||
return _file_search_dispatch
|
||||
|
||||
if _bridges_to_chat_completions(responses_api_provider_config, use_chat_completions_api):
|
||||
bridge_kwargs: Final = _bridge_kwargs(kwargs, responses_api_provider_config, allowed_openai_params)
|
||||
return litellm_completion_transformation_handler.response_api_handler(
|
||||
model=model,
|
||||
input=input,
|
||||
|
|
@ -1292,7 +1315,7 @@ def responses(
|
|||
extra_body=extra_body,
|
||||
timeout=timeout if timeout is not None else request_timeout,
|
||||
allowed_openai_params=allowed_openai_params,
|
||||
**kwargs,
|
||||
**bridge_kwargs,
|
||||
)
|
||||
|
||||
# Get optional parameters for the responses API
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ Includes file_search emulation: the flag must be forwarded on inner aresponses
|
|||
calls so routed requests do not hit a custom api_base /v1/responses endpoint.
|
||||
"""
|
||||
|
||||
import json
|
||||
from importlib import import_module
|
||||
from typing import Final
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
import litellm
|
||||
from litellm.llms.custom_httpx.http_handler import HTTPHandler
|
||||
|
|
@ -189,6 +191,104 @@ class TestUseResponsesApiBridgeFlag:
|
|||
"reasoning_effort"
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("provider_config", "allowed_openai_params", "expected_chat_template_kwargs"),
|
||||
[
|
||||
pytest.param(litellm.OpenAIResponsesAPIConfig(), None, None, id="native-config-drops-unknown-param"),
|
||||
pytest.param(
|
||||
litellm.OpenAIResponsesAPIConfig(),
|
||||
["chat_template_kwargs"],
|
||||
{"thinking": True},
|
||||
id="native-config-keeps-allowed-param",
|
||||
),
|
||||
pytest.param(None, None, {"thinking": True}, id="no-native-config-keeps-passthrough"),
|
||||
],
|
||||
)
|
||||
@patch.object(import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config")
|
||||
def test_bridge_forwards_same_params_as_native_dispatch(
|
||||
self,
|
||||
mock_get_config,
|
||||
provider_config,
|
||||
allowed_openai_params,
|
||||
expected_chat_template_kwargs,
|
||||
respx_mock: respx.MockRouter,
|
||||
):
|
||||
"""A deployment-supplied provider-specific kwarg (``chat_template_kwargs``) reaches the
|
||||
provider through the bridge only when the native Responses path would forward it too:
|
||||
never for a provider with a native config, unless the caller allowed it explicitly, and
|
||||
always for a provider without one, whose only Responses path is the bridge."""
|
||||
mock_get_config.return_value = provider_config
|
||||
upstream: Final = respx_mock.post("https://api.openai.com/v1/chat/completions").mock(
|
||||
return_value=httpx.Response(
|
||||
status_code=200,
|
||||
json={
|
||||
"id": "chatcmpl-123",
|
||||
"object": "chat.completion",
|
||||
"created": 1677652288,
|
||||
"model": "my-custom-model",
|
||||
"choices": [
|
||||
{"index": 0, "message": {"role": "assistant", "content": "Answer"}, "finish_reason": "stop"}
|
||||
],
|
||||
"usage": {"prompt_tokens": 9, "completion_tokens": 1, "total_tokens": 10},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
response: Final = litellm.responses(
|
||||
model="openai/my-custom-model",
|
||||
input="Hello",
|
||||
use_chat_completions_api=True,
|
||||
allowed_openai_params=allowed_openai_params,
|
||||
chat_template_kwargs={"thinking": True},
|
||||
drop_params=True,
|
||||
api_key="fake-openai-api-key",
|
||||
num_retries=0,
|
||||
)
|
||||
|
||||
assert upstream.call_count == 1
|
||||
request_body: Final = json.loads(upstream.calls[0].request.read())
|
||||
assert request_body.get("chat_template_kwargs") == expected_chat_template_kwargs
|
||||
assert request_body["messages"] == [{"role": "user", "content": "Hello"}]
|
||||
assert response.output[0].content[0].text == "Answer"
|
||||
|
||||
def test_bridge_keeps_deployment_credentials_while_dropping_unknown_params(self, respx_mock: respx.MockRouter):
|
||||
"""Azure has a native Responses config, so its bridged request drops the unknown param but still
|
||||
authenticates with the deployment credential, which the native path reads from the same kwargs."""
|
||||
upstream: Final = respx_mock.post(
|
||||
"https://example-resource.openai.azure.com/openai/deployments/my-deployment/chat/completions",
|
||||
params={"api-version": "2024-10-21"},
|
||||
).mock(
|
||||
return_value=httpx.Response(
|
||||
status_code=200,
|
||||
json={
|
||||
"id": "chatcmpl-123",
|
||||
"object": "chat.completion",
|
||||
"created": 1677652288,
|
||||
"model": "my-deployment",
|
||||
"choices": [
|
||||
{"index": 0, "message": {"role": "assistant", "content": "Answer"}, "finish_reason": "stop"}
|
||||
],
|
||||
"usage": {"prompt_tokens": 9, "completion_tokens": 1, "total_tokens": 10},
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
litellm.responses(
|
||||
model="azure/my-deployment",
|
||||
input="Hello",
|
||||
use_chat_completions_api=True,
|
||||
api_base="https://example-resource.openai.azure.com",
|
||||
api_version="2024-10-21",
|
||||
azure_ad_token="fake-azure-ad-token",
|
||||
chat_template_kwargs={"thinking": True},
|
||||
num_retries=0,
|
||||
)
|
||||
|
||||
assert upstream.call_count == 1
|
||||
request: Final = upstream.calls[0].request
|
||||
assert request.headers["authorization"] == "Bearer fake-azure-ad-token"
|
||||
assert "chat_template_kwargs" not in json.loads(request.read())
|
||||
|
||||
@patch.object(import_module("litellm.responses.file_search.emulated_handler"), "_call_aresponses")
|
||||
@patch.object(
|
||||
import_module("litellm.responses.main").ProviderConfigManager, "get_provider_responses_api_config"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue