Merge pull request #25346 from BerriAI/litellm_Sameerlite/responses-bridge-optin

feat(responses): add use_chat_completions_api flag for openai/ models with custom api_base
This commit is contained in:
Sameer Kankute 2026-04-24 20:55:22 +05:30 committed by GitHub
commit 1720903bda
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 634 additions and 85 deletions

View File

@ -1505,6 +1505,84 @@ 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.
Use either of these to force the `/responses``/chat/completions` bridge:
1. **`use_chat_completions_api: true`** — makes it explicit that LiteLLM will call the providers chat-completions API.
2. **`openai/chat_completions/<model_name>`** — same pattern as `responses/` on chat completions: the model id encodes the routing choice.
#### Python SDK Usage
```python showLineNumbers title="Force bridge for custom openai/ endpoint (flag)"
import litellm
response = litellm.responses(
model="openai/my-custom-model",
input="Hello!",
api_base="http://localhost:8080",
api_key="fake-key",
use_chat_completions_api=True,
)
print(response)
```
Or encode it in the model id:
```python showLineNumbers title="Force bridge via openai/chat_completions/ model prefix"
import litellm
response = litellm.responses(
model="openai/chat_completions/my-custom-model",
input="Hello!",
api_base="http://localhost:8080",
api_key="fake-key",
)
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_chat_completions_api: true
```
Alternatively set `model: openai/chat_completions/my-custom-model` instead of the flag.
**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

@ -643,6 +643,29 @@ def _apply_prompt_management_to_responses_call(
return input, model, custom_llm_provider
# Opt-in via model id (mirrors the `responses/` prefix pattern on chat completions).
_OPENAI_CHAT_COMPLETIONS_RESPONSES_MODEL_PREFIX = "openai/chat_completions/"
def _normalize_openai_chat_completions_responses_model(model: str) -> tuple[str, bool]:
"""
Strip `openai/chat_completions/<name>` `openai/<name>` and return True when the
prefix was applied (same effect as use_chat_completions_api=True).
"""
if not model.startswith(_OPENAI_CHAT_COMPLETIONS_RESPONSES_MODEL_PREFIX):
return model, False
remainder = model[len(_OPENAI_CHAT_COMPLETIONS_RESPONSES_MODEL_PREFIX) :]
if not remainder:
return model, False
return f"openai/{remainder}", True
def _pop_use_chat_completions_api_kw(kwargs: Dict[str, Any]) -> bool:
"""Pop use_chat_completions_api; True when the chat-completions bridge is requested."""
use_cc = kwargs.pop("use_chat_completions_api", None)
return bool(use_cc)
def _resolve_model_provider_for_responses(
model: str,
custom_llm_provider: Optional[str],
@ -705,6 +728,175 @@ def _apply_managed_file_id_mapping(
return input, tools
def _responses_try_dispatch_mcp_gateway(
*,
tools: Optional[Iterable[ToolParam]],
input: Union[str, ResponseInputParam],
model: str,
include: Optional[List[ResponseIncludable]],
instructions: Optional[str],
max_output_tokens: Optional[int],
prompt: Optional[PromptObject],
metadata: Optional[Dict[str, Any]],
parallel_tool_calls: Optional[bool],
previous_response_id: Optional[str],
reasoning: Optional[Reasoning],
store: Optional[bool],
background: Optional[bool],
stream: Optional[bool],
temperature: Optional[float],
text: Any,
tool_choice: Optional[ToolChoice],
top_p: Optional[float],
truncation: Optional[Literal["auto", "disabled"]],
user: Optional[str],
extra_headers: Optional[Dict[str, Any]],
extra_query: Optional[Dict[str, Any]],
extra_body: Optional[Dict[str, Any]],
timeout: Optional[Union[float, httpx.Timeout]],
custom_llm_provider: Optional[str],
kwargs: Dict[str, Any],
_is_async: bool,
) -> Optional[Any]:
"""Return a response when MCP gateway handles the call; otherwise None."""
from litellm.responses.mcp.litellm_proxy_mcp_handler import (
LiteLLM_Proxy_MCP_Handler,
)
if not LiteLLM_Proxy_MCP_Handler._should_use_litellm_mcp_gateway(tools=tools):
return None
mcp_call_kwargs = {
"input": input,
"model": model,
"include": include,
"instructions": instructions,
"max_output_tokens": max_output_tokens,
"prompt": prompt,
"metadata": metadata,
"parallel_tool_calls": parallel_tool_calls,
"previous_response_id": previous_response_id,
"reasoning": reasoning,
"store": store,
"background": background,
"stream": stream,
"temperature": temperature,
"text": text,
"tool_choice": tool_choice,
"tools": tools,
"top_p": top_p,
"truncation": truncation,
"user": user,
"extra_headers": extra_headers,
"extra_query": extra_query,
"extra_body": extra_body,
"timeout": timeout,
"custom_llm_provider": custom_llm_provider,
**kwargs,
}
if _is_async:
return aresponses_api_with_mcp(**mcp_call_kwargs)
return run_async_function(aresponses_api_with_mcp, **mcp_call_kwargs)
def _responses_try_dispatch_emulated_file_search(
*,
tools: Optional[Iterable[ToolParam]],
input: Union[str, ResponseInputParam],
model: str,
responses_api_provider_config: Optional[BaseResponsesAPIConfig],
use_chat_completions_api: bool,
include: Optional[List[ResponseIncludable]],
instructions: Optional[str],
max_output_tokens: Optional[int],
prompt: Optional[PromptObject],
metadata: Optional[Dict[str, Any]],
parallel_tool_calls: Optional[bool],
previous_response_id: Optional[str],
reasoning: Optional[Reasoning],
store: Optional[bool],
background: Optional[bool],
stream: Optional[bool],
temperature: Optional[float],
text: Any,
tool_choice: Optional[ToolChoice],
top_p: Optional[float],
truncation: Optional[Literal["auto", "disabled"]],
user: Optional[str],
service_tier: Optional[str],
safety_identifier: Optional[str],
text_format: Optional[Union[Type[BaseModel], dict]],
allowed_openai_params: Optional[List[str]],
extra_headers: Optional[Dict[str, Any]],
extra_query: Optional[Dict[str, Any]],
extra_body: Optional[Dict[str, Any]],
timeout: Optional[Union[float, httpx.Timeout]],
custom_llm_provider: Optional[str],
kwargs: Dict[str, Any],
_is_async: bool,
) -> Optional[Any]:
"""Return a response when emulated file_search handles the call; otherwise None."""
if not _has_file_search_tool(tools) or not (
responses_api_provider_config is None
or use_chat_completions_api is True
or not responses_api_provider_config.supports_native_file_search()
):
return None
from litellm.responses.file_search.emulated_handler import (
aresponses_with_emulated_file_search,
)
_internal_skip = {"litellm_call_id", "aresponses"}
emulated_kwargs = {
"include": include,
"instructions": instructions,
"max_output_tokens": max_output_tokens,
"prompt": prompt,
"metadata": metadata,
"parallel_tool_calls": parallel_tool_calls,
"previous_response_id": previous_response_id,
"reasoning": reasoning,
"store": store,
"background": background,
"stream": stream,
"temperature": temperature,
"text": text,
"tool_choice": tool_choice,
"top_p": top_p,
"truncation": truncation,
"user": user,
"service_tier": service_tier,
"safety_identifier": safety_identifier,
"text_format": text_format,
"allowed_openai_params": allowed_openai_params,
"extra_headers": extra_headers,
"extra_query": extra_query,
"extra_body": extra_body,
"timeout": timeout,
"custom_llm_provider": custom_llm_provider,
**(
{
**(
{"use_chat_completions_api": True}
if use_chat_completions_api
else {}
),
**{k: v for k, v in kwargs.items() if k not in _internal_skip},
}
),
}
if _is_async:
return aresponses_with_emulated_file_search(
input=input, model=model, tools=tools, **emulated_kwargs
)
return run_async_function(
aresponses_with_emulated_file_search,
input=input,
model=model,
tools=tools,
**emulated_kwargs,
)
@client
def responses(
input: Union[str, ResponseInputParam],
@ -746,14 +938,12 @@ def responses(
Uses the synchronous HTTP handler to make requests.
"""
local_vars = locals()
from litellm.responses.mcp.litellm_proxy_mcp_handler import (
LiteLLM_Proxy_MCP_Handler,
)
try:
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_chat_completions_api = _pop_use_chat_completions_api_kw(kwargs)
# Convert text_format to text parameter if provided
text = ResponsesAPIRequestUtils.convert_text_format_to_text_param(
@ -776,6 +966,15 @@ def responses(
mock_response=litellm_params.mock_response
)
_stripped_model, _from_chat_completions_prefix = (
_normalize_openai_chat_completions_responses_model(model)
)
model = _stripped_model
local_vars["model"] = model
use_chat_completions_api = (
use_chat_completions_api or _from_chat_completions_prefix
)
model, custom_llm_provider = _resolve_model_provider_for_responses(
model=model,
custom_llm_provider=custom_llm_provider,
@ -808,38 +1007,37 @@ def responses(
#########################################################
# Native MCP Responses API
#########################################################
if LiteLLM_Proxy_MCP_Handler._should_use_litellm_mcp_gateway(tools=tools):
mcp_call_kwargs = {
"input": input,
"model": model,
"include": include,
"instructions": instructions,
"max_output_tokens": max_output_tokens,
"prompt": prompt,
"metadata": metadata,
"parallel_tool_calls": parallel_tool_calls,
"previous_response_id": previous_response_id,
"reasoning": reasoning,
"store": store,
"background": background,
"stream": stream,
"temperature": temperature,
"text": text,
"tool_choice": tool_choice,
"tools": tools,
"top_p": top_p,
"truncation": truncation,
"user": user,
"extra_headers": extra_headers,
"extra_query": extra_query,
"extra_body": extra_body,
"timeout": timeout,
"custom_llm_provider": custom_llm_provider,
**kwargs,
}
if _is_async:
return aresponses_api_with_mcp(**mcp_call_kwargs)
return run_async_function(aresponses_api_with_mcp, **mcp_call_kwargs)
_mcp_dispatch = _responses_try_dispatch_mcp_gateway(
tools=tools,
input=input,
model=model,
include=include,
instructions=instructions,
max_output_tokens=max_output_tokens,
prompt=prompt,
metadata=metadata,
parallel_tool_calls=parallel_tool_calls,
previous_response_id=previous_response_id,
reasoning=reasoning,
store=store,
background=background,
stream=stream,
temperature=temperature,
text=text,
tool_choice=tool_choice,
top_p=top_p,
truncation=truncation,
user=user,
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
custom_llm_provider=custom_llm_provider,
kwargs=kwargs,
_is_async=_is_async,
)
if _mcp_dispatch is not None:
return _mcp_dispatch
# get provider config
responses_api_provider_config: Optional[BaseResponsesAPIConfig]
@ -869,57 +1067,45 @@ def responses(
)
)
if _has_file_search_tool(tools) and (
responses_api_provider_config is None
or not responses_api_provider_config.supports_native_file_search()
):
from litellm.responses.file_search.emulated_handler import (
aresponses_with_emulated_file_search,
)
_file_search_dispatch = _responses_try_dispatch_emulated_file_search(
tools=tools,
input=input,
model=model,
responses_api_provider_config=responses_api_provider_config,
use_chat_completions_api=use_chat_completions_api,
include=include,
instructions=instructions,
max_output_tokens=max_output_tokens,
prompt=prompt,
metadata=metadata,
parallel_tool_calls=parallel_tool_calls,
previous_response_id=previous_response_id,
reasoning=reasoning,
store=store,
background=background,
stream=stream,
temperature=temperature,
text=text,
tool_choice=tool_choice,
top_p=top_p,
truncation=truncation,
user=user,
service_tier=service_tier,
safety_identifier=safety_identifier,
text_format=text_format,
allowed_openai_params=allowed_openai_params,
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
custom_llm_provider=custom_llm_provider,
kwargs=kwargs,
_is_async=_is_async,
)
if _file_search_dispatch is not None:
return _file_search_dispatch
_internal_skip = {"litellm_call_id", "aresponses"}
emulated_kwargs = {
"include": include,
"instructions": instructions,
"max_output_tokens": max_output_tokens,
"prompt": prompt,
"metadata": metadata,
"parallel_tool_calls": parallel_tool_calls,
"previous_response_id": previous_response_id,
"reasoning": reasoning,
"store": store,
"background": background,
"stream": stream,
"temperature": temperature,
"text": text,
"tool_choice": tool_choice,
"top_p": top_p,
"truncation": truncation,
"user": user,
"service_tier": service_tier,
"safety_identifier": safety_identifier,
"text_format": text_format,
"allowed_openai_params": allowed_openai_params,
"extra_headers": extra_headers,
"extra_query": extra_query,
"extra_body": extra_body,
"timeout": timeout,
"custom_llm_provider": custom_llm_provider,
**{k: v for k, v in kwargs.items() if k not in _internal_skip},
}
if _is_async:
return aresponses_with_emulated_file_search(
input=input, model=model, tools=tools, **emulated_kwargs
)
return run_async_function(
aresponses_with_emulated_file_search,
input=input,
model=model,
tools=tools,
**emulated_kwargs,
)
if responses_api_provider_config is None:
if responses_api_provider_config is None or use_chat_completions_api is True:
return litellm_completion_transformation_handler.response_api_handler(
model=model,
input=input,

View File

@ -201,6 +201,7 @@ class GenericLiteLLMParams(CredentialLiteLLMParams, CustomPricingLiteLLMParams):
budget_duration: Optional[str] = None
use_in_pass_through: Optional[bool] = False
use_litellm_proxy: Optional[bool] = False
use_chat_completions_api: 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
@ -327,6 +328,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 → CHAT COMPLETIONS BRIDGE ##
use_chat_completions_api: Optional[bool]
## UNIFIED PROJECT/REGION ##
region_name: Optional[str]
## VERTEX AI ##

View File

@ -0,0 +1,282 @@
"""
Tests for forcing the /responses /chat/completions bridge for `openai/` models
(via `use_chat_completions_api` or the `openai/chat_completions/<model>` model id).
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 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
from litellm.types.llms.openai import ResponseAPIUsage, ResponsesAPIResponse
class TestUseResponsesApiBridgeFlag:
"""Test that bridge opt-in forces the chat completions path."""
@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_use_chat_completions_api_true(
self, mock_get_config, mock_bridge_handler
):
"""When use_chat_completions_api=True, the bridge handler should be called."""
mock_get_config.return_value = litellm.OpenAIResponsesAPIConfig()
mock_bridge_handler.return_value = MagicMock()
litellm.responses(
model="openai/my-custom-model",
input="Hello",
use_chat_completions_api=True,
litellm_logging_obj=MagicMock(),
)
mock_bridge_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_bridge_used_when_model_uses_chat_completions_prefix(
self, mock_get_config, mock_bridge_handler
):
"""`openai/chat_completions/<name>` normalizes to `openai/<name>` and uses the bridge."""
mock_get_config.return_value = litellm.OpenAIResponsesAPIConfig()
mock_bridge_handler.return_value = MagicMock()
litellm.responses(
model="openai/chat_completions/my-custom-model",
input="Hello",
litellm_logging_obj=MagicMock(),
)
mock_bridge_handler.assert_called_once()
# Model string is provider-normalized after resolution; prefix only forces the bridge.
assert mock_bridge_handler.call_args.kwargs["model"].endswith("my-custom-model")
@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_chat_completions_api 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):
"""use_chat_completions_api should be popped and not passed 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_chat_completions_api=True,
litellm_logging_obj=MagicMock(),
)
call_kwargs = mock_bridge_handler.call_args
all_kwargs = call_kwargs.kwargs if call_kwargs.kwargs else {}
assert "use_chat_completions_api" 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()
@patch("litellm.responses.file_search.emulated_handler._call_aresponses")
@patch(
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
)
async def test_bridge_flag_forwarded_to_file_search_emulation(
self, mock_get_config, mock_call_aresponses
):
"""When use_chat_completions_api=True and file_search tool is present,
the flag should be forwarded to the inner aresponses call in the
file_search emulation path."""
# Setup: provider has native responses API support
mock_get_config.return_value = litellm.OpenAIResponsesAPIConfig()
# Mock the inner aresponses call to return a valid response
mock_response = ResponsesAPIResponse(
id="resp_123",
model="openai/my-custom-model",
created_at=1234567890,
output=[
{"type": "message", "content": [{"type": "text", "text": "Answer"}]}
],
usage=ResponseAPIUsage(
input_tokens=10, output_tokens=5, total_tokens=15
),
)
mock_call_aresponses.return_value = mock_response
await litellm.aresponses(
model="openai/my-custom-model",
input="Search for information",
tools=[{"type": "file_search"}],
use_chat_completions_api=True,
litellm_logging_obj=MagicMock(),
)
# Verify _call_aresponses was called with use_chat_completions_api=True
mock_call_aresponses.assert_called_once()
call_kwargs = mock_call_aresponses.call_args.kwargs
assert (
call_kwargs.get("use_chat_completions_api") is True
), "use_chat_completions_api should be forwarded to inner aresponses call"
@patch(
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler"
)
@patch("litellm.vector_stores.main.asearch")
@patch(
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
)
async def test_bridge_flag_prevents_native_responses_endpoint_call(
self, mock_get_config, mock_asearch, mock_bridge_handler
):
"""
Concrete failing scenario: native OpenAI responses config + bridge flag +
file_search emulation must still route inner calls through the bridge
(chat completions), not POST to api_base /v1/responses.
"""
mock_get_config.return_value = litellm.OpenAIResponsesAPIConfig()
mock_asearch.return_value = []
first_response = ResponsesAPIResponse(
id="resp_first",
model="openai/my-local-model",
created_at=1234567890,
output=[
{
"type": "function_call",
"name": "litellm_file_search",
"call_id": "call_123",
"arguments": '{"queries": ["test query"]}',
}
],
usage=ResponseAPIUsage(
input_tokens=10, output_tokens=5, total_tokens=15
),
)
second_response = ResponsesAPIResponse(
id="resp_second",
model="openai/my-local-model",
created_at=1234567891,
output=[
{
"type": "message",
"content": [{"type": "text", "text": "Final answer"}],
}
],
usage=ResponseAPIUsage(
input_tokens=20, output_tokens=10, total_tokens=30
),
)
mock_bridge_handler.side_effect = [first_response, second_response]
result = await litellm.aresponses(
model="openai/my-local-model",
input="Search for information",
tools=[
{
"type": "file_search",
"file_search": {"vector_store_ids": ["vs_123"]},
}
],
use_chat_completions_api=True,
api_base="http://localhost:8080/v1",
litellm_logging_obj=MagicMock(),
)
assert mock_bridge_handler.call_count == 2, (
"Bridge handler should be called twice: initial function-tool call "
"and follow-up with tool results"
)
for call in mock_bridge_handler.call_args_list:
all_kwargs = call.kwargs if call.kwargs else {}
assert "use_chat_completions_api" not in all_kwargs
assert result is not None
assert result.id is not None
@patch("litellm.responses.main.base_llm_http_handler.response_api_handler")
@patch("litellm.vector_stores.main.asearch")
@patch(
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config"
)
async def test_without_bridge_flag_uses_native_endpoint(
self, mock_get_config, mock_asearch, mock_native_handler
):
"""Without the bridge flag, openai/ with native config uses the native handler."""
mock_get_config.return_value = litellm.OpenAIResponsesAPIConfig()
mock_asearch.return_value = []
mock_native_handler.return_value = ResponsesAPIResponse(
id="resp_native",
model="openai/gpt-4o",
created_at=1234567890,
output=[
{
"type": "message",
"content": [{"type": "text", "text": "Native response"}],
}
],
usage=ResponseAPIUsage(
input_tokens=10, output_tokens=5, total_tokens=15
),
)
result = await litellm.aresponses(
model="openai/gpt-4o",
input="Hello",
litellm_logging_obj=MagicMock(),
)
mock_native_handler.assert_called_once()
assert result is not None