Merge pull request #24783 from Sameerlite/Sameerlite/responses-bridge-optin

feat(responses): add use_responses_api_bridge flag for openai/ models with custom api_base
This commit is contained in:
Sameer Kankute 2026-04-02 18:25:29 +05:30 committed by GitHub
commit 8394167711
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 171 additions and 1 deletions

View file

@ -1505,6 +1505,64 @@ curl http://localhost:4000/v1/responses \
### Opt-in bridge for `openai/` models with custom `api_base`
If you're using an **OpenAI-compatible third-party provider** (e.g. llama.cpp, vLLM, LM Studio) via `openai/` prefix with a custom `api_base`, LiteLLM will normally forward `/responses` requests directly to that endpoint. If the provider only supports `/chat/completions`, the request will fail.
Set `use_responses_api_bridge: true` to force the `/responses``/chat/completions` bridge for these models.
#### Python SDK Usage
```python showLineNumbers title="Force bridge for custom openai/ endpoint"
import litellm
response = litellm.responses(
model="openai/my-custom-model",
input="Hello!",
api_base="http://localhost:8080",
api_key="fake-key",
use_responses_api_bridge=True,
)
print(response)
```
#### LiteLLM Proxy Usage
**Setup Config:**
```yaml showLineNumbers title="config.yaml — bridge for custom openai/ endpoint"
model_list:
- model_name: my-local-model
litellm_params:
model: openai/my-custom-model
api_base: http://localhost:8080/v1
api_key: fake-key
use_responses_api_bridge: true
```
**Start Proxy:**
```bash showLineNumbers title="Start LiteLLM Proxy"
litellm --config /path/to/config.yaml
# RUNNING on http://0.0.0.0:4000
```
**Make Request:**
```bash showLineNumbers title="Request via bridge"
curl http://localhost:4000/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-1234" \
-d '{
"model": "my-local-model",
"input": "Hello!"
}'
```
This is particularly useful when connecting clients that hardcode the `/responses` endpoint (e.g. OpenAI Codex CLI with `wire_api = "responses"`) to local or third-party OpenAI-compatible providers that only expose `/chat/completions`.
## Server-side compaction
For long-running conversations, you can enable **server-side compaction** so that when the rendered context size crosses a threshold, the server automatically runs compaction in-stream and emits a compaction item—no separate `POST /v1/responses/compact` call is required.

View file

@ -754,6 +754,7 @@ def responses(
litellm_logging_obj: LiteLLMLoggingObj = kwargs.get("litellm_logging_obj") # type: ignore
litellm_call_id: Optional[str] = kwargs.get("litellm_call_id", None)
_is_async = kwargs.pop("aresponses", False) is True
use_responses_api_bridge = kwargs.pop("use_responses_api_bridge", None)
# Convert text_format to text parameter if provided
text = ResponsesAPIRequestUtils.convert_text_format_to_text_param(
@ -871,6 +872,7 @@ def responses(
if _has_file_search_tool(tools) and (
responses_api_provider_config is None
or use_responses_api_bridge is True
or not responses_api_provider_config.supports_native_file_search()
):
from litellm.responses.file_search.emulated_handler import (
@ -919,7 +921,7 @@ def responses(
**emulated_kwargs,
)
if responses_api_provider_config is None:
if responses_api_provider_config is None or use_responses_api_bridge is True:
return litellm_completion_transformation_handler.response_api_handler(
model=model,
input=input,

View file

@ -199,6 +199,7 @@ class GenericLiteLLMParams(CredentialLiteLLMParams, CustomPricingLiteLLMParams):
budget_duration: Optional[str] = None
use_in_pass_through: Optional[bool] = False
use_litellm_proxy: Optional[bool] = False
use_responses_api_bridge: Optional[bool] = None
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
merge_reasoning_content_in_choices: Optional[bool] = False
model_info: Optional[Dict] = None
@ -318,6 +319,8 @@ class LiteLLMParamsTypedDict(TypedDict, total=False):
configurable_clientside_auth_params: CONFIGURABLE_CLIENTSIDE_AUTH_PARAMS # for allowing api base switching on finetuned models
## DROP PARAMS ##
drop_params: Optional[bool]
## RESPONSES API BRIDGE ##
use_responses_api_bridge: Optional[bool]
## UNIFIED PROJECT/REGION ##
region_name: Optional[str]
## VERTEX AI ##

View file

@ -0,0 +1,107 @@
"""
Tests for the `use_responses_api_bridge` flag that allows openai/ models
with custom api_base to opt-in to the /responses /chat/completions bridge.
"""
import os
import sys
from unittest.mock import MagicMock, patch
sys.path.insert(
0, os.path.abspath("../../..")
) # Adds the parent directory to the system path
import litellm
class TestUseResponsesApiBridgeFlag:
"""Test that use_responses_api_bridge forces the chat completions bridge."""
@patch(
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler"
)
@patch(
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
)
def test_bridge_used_when_flag_is_true(self, mock_get_config, mock_bridge_handler):
"""When use_responses_api_bridge=True, the bridge handler should be called
even though the provider (openai) has native responses API support."""
# Setup: provider config returns a non-None config (native support exists)
mock_get_config.return_value = litellm.OpenAIResponsesAPIConfig()
mock_bridge_handler.return_value = MagicMock()
litellm.responses(
model="openai/my-custom-model",
input="Hello",
use_responses_api_bridge=True,
litellm_logging_obj=MagicMock(),
)
mock_bridge_handler.assert_called_once()
@patch("litellm.responses.main.base_llm_http_handler.response_api_handler")
@patch(
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
)
def test_native_forwarding_when_flag_absent(
self, mock_get_config, mock_native_handler
):
"""When use_responses_api_bridge is not set, openai/ models should use
native responses API forwarding (existing behavior)."""
mock_get_config.return_value = litellm.OpenAIResponsesAPIConfig()
mock_native_handler.return_value = MagicMock()
litellm.responses(
model="openai/gpt-4o",
input="Hello",
litellm_logging_obj=MagicMock(),
)
mock_native_handler.assert_called_once()
@patch(
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler"
)
@patch(
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
)
def test_flag_does_not_leak_into_kwargs(self, mock_get_config, mock_bridge_handler):
"""The use_responses_api_bridge flag should be popped from kwargs and not
passed through to the bridge handler."""
mock_get_config.return_value = litellm.OpenAIResponsesAPIConfig()
mock_bridge_handler.return_value = MagicMock()
litellm.responses(
model="openai/my-custom-model",
input="Hello",
use_responses_api_bridge=True,
litellm_logging_obj=MagicMock(),
)
call_kwargs = mock_bridge_handler.call_args
# The flag should not appear in the kwargs passed to the bridge handler
all_kwargs = call_kwargs.kwargs if call_kwargs.kwargs else {}
assert "use_responses_api_bridge" not in all_kwargs
@patch(
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler"
)
@patch(
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
)
def test_bridge_used_when_provider_config_none(
self, mock_get_config, mock_bridge_handler
):
"""When the provider has no native responses API config (returns None),
the bridge should be used regardless of the flag (existing behavior)."""
mock_get_config.return_value = None
mock_bridge_handler.return_value = MagicMock()
litellm.responses(
model="anthropic/claude-3-haiku",
input="Hello",
litellm_logging_obj=MagicMock(),
)
mock_bridge_handler.assert_called_once()