fix(responses): strip the responses/ routing prefix on the Responses API path

openai/responses/<model> deployments reached OpenAI as the literal model id
responses/<model> on /v1/responses and on /v1/messages (which rides the
Responses API for the openai provider) and 400ed with model_not_found, while
/v1/chat/completions already stripped the prefix. Strip a leading responses/
right after provider resolution so every Responses API entrypoint (HTTP,
websocket, compaction, the /v1/messages adapter) sends the real model id
This commit is contained in:
mateo-berri 2026-08-18 13:28:22 -07:00
parent 6d32d4081d
commit 61625723a7
2 changed files with 73 additions and 8 deletions

View file

@ -640,6 +640,15 @@ def _pop_use_chat_completions_api_kw(kwargs: dict[str, object]) -> bool:
return bool(use_cc)
_RESPONSES_ROUTING_PREFIX: Final = "responses/"
def _strip_responses_routing_prefix(model: str) -> str:
if not model.startswith(_RESPONSES_ROUTING_PREFIX):
return model
return model[len(_RESPONSES_ROUTING_PREFIX) :]
def _resolve_model_provider_for_responses(
model: str,
custom_llm_provider: str | None,
@ -649,20 +658,20 @@ def _resolve_model_provider_for_responses(
if custom_llm_provider is not None and not litellm_params.custom_llm_provider:
litellm_params.custom_llm_provider = custom_llm_provider
(
model,
custom_llm_provider,
provider_model,
resolved_provider,
dynamic_api_key,
dynamic_api_base,
) = litellm.get_llm_provider(
model=model,
litellm_params=litellm_params,
)
local_vars["custom_llm_provider"] = custom_llm_provider
local_vars["custom_llm_provider"] = resolved_provider
if dynamic_api_key is not None:
litellm_params.api_key = dynamic_api_key
if dynamic_api_base is not None:
litellm_params.api_base = dynamic_api_base
return model, custom_llm_provider
return _strip_responses_routing_prefix(provider_model), resolved_provider
def _apply_managed_file_id_mapping(
@ -1997,7 +2006,7 @@ async def _aresponses_websocket(
litellm_params_dict: Final = get_litellm_params(**kwargs)
(
model,
provider_model,
_custom_llm_provider,
dynamic_api_key,
dynamic_api_base,
@ -2006,6 +2015,7 @@ async def _aresponses_websocket(
api_base=api_base,
api_key=api_key,
)
resolved_model: Final = _strip_responses_routing_prefix(provider_model)
litellm_params_dict["data_residency"] = infer_openai_data_residency(
_custom_llm_provider,
@ -2014,7 +2024,7 @@ async def _aresponses_websocket(
litellm_logging_obj.update_from_kwargs(
kwargs=kwargs,
model=model,
model=resolved_model,
user=user,
optional_params={},
litellm_params=litellm_params_dict,
@ -2024,7 +2034,7 @@ async def _aresponses_websocket(
responses_api_provider_config: BaseResponsesAPIConfig | None = None
if _custom_llm_provider is not None:
responses_api_provider_config = ProviderConfigManager.get_provider_responses_api_config(
model=model,
model=resolved_model,
provider=litellm.LlmProviders(_custom_llm_provider),
)
@ -2052,7 +2062,7 @@ async def _aresponses_websocket(
remaining_kwargs: Final = {k: v for k, v in kwargs.items() if k not in _explicit_keys}
await base_llm_http_handler.async_responses_websocket(
model=model,
model=resolved_model,
websocket=websocket,
logging_obj=litellm_logging_obj,
responses_api_provider_config=responses_api_provider_config,

View file

@ -367,3 +367,58 @@ async def test_aresponses_client_header_conflict_is_case_insensitive():
assert [name for name in request_headers if name.lower() == "x-shared"] == ["x-shared"]
assert request_headers["x-shared"] == "from-caller"
@pytest.mark.asyncio
@pytest.mark.parametrize(
("model", "custom_llm_provider"),
[
("openai/responses/gpt-5.6", None),
("responses/gpt-5.6", "openai"),
],
)
async def test_aresponses_strips_responses_routing_prefix_from_openai_model(model, custom_llm_provider):
"""
`responses/` is LiteLLM routing sugar, never part of the provider model id.
Deployments configured as openai/responses/<model> reach this path directly via
/v1/responses and via the /v1/messages adapter (which passes responses/<model>
with custom_llm_provider="openai"), so both shapes must hit OpenAI as <model>.
"""
with patch(
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
new_callable=AsyncMock,
) as mock_post:
mock_post.return_value = MockResponse(_minimal_responses_api_payload("resp_prefix_test", "gpt-5.6"), 200)
await litellm.aresponses(
model=model,
custom_llm_provider=custom_llm_provider,
input="ping",
api_key="sk-test",
)
mock_post.assert_called_once()
assert mock_post.call_args.kwargs["url"].endswith("/responses")
assert mock_post.call_args.kwargs["json"]["model"] == "gpt-5.6"
@pytest.mark.asyncio
async def test_aresponses_websocket_strips_responses_routing_prefix_from_openai_model():
from unittest.mock import MagicMock
from litellm.responses.main import _aresponses_websocket
with patch(
"litellm.responses.main.base_llm_http_handler.async_responses_websocket",
new_callable=AsyncMock,
) as mock_ws:
await _aresponses_websocket(
model="openai/responses/gpt-5.6",
websocket=MagicMock(),
api_key="sk-test",
litellm_logging_obj=MagicMock(),
)
mock_ws.assert_awaited_once()
assert mock_ws.call_args.kwargs["model"] == "gpt-5.6"
assert mock_ws.call_args.kwargs["custom_llm_provider"] == "openai"