From 61625723a7b692f40d462001b83f560daca6d00a Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:28:22 -0700 Subject: [PATCH] fix(responses): strip the responses/ routing prefix on the Responses API path openai/responses/ deployments reached OpenAI as the literal model id responses/ 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 --- litellm/responses/main.py | 26 ++++++--- .../test_responses_api_request_body.py | 55 +++++++++++++++++++ 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/litellm/responses/main.py b/litellm/responses/main.py index e0af363b1a5..d09a30a7e3a 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -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, diff --git a/tests/test_litellm/responses/test_responses_api_request_body.py b/tests/test_litellm/responses/test_responses_api_request_body.py index e7f87f1e326..38dc61c5aa0 100644 --- a/tests/test_litellm/responses/test_responses_api_request_body.py +++ b/tests/test_litellm/responses/test_responses_api_request_body.py @@ -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/ reach this path directly via + /v1/responses and via the /v1/messages adapter (which passes responses/ + with custom_llm_provider="openai"), so both shapes must hit OpenAI as . + """ + 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"