From 4ff0de18efd9abe450b0bccd7a4fc19d526ffbc8 Mon Sep 17 00:00:00 2001 From: Jason Cook Date: Thu, 23 Apr 2026 15:02:35 -0400 Subject: [PATCH] fix(health): pass responses-mode ``input`` as a list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When ``/health/test_connection`` hits a ``mode: responses`` model, the handler called ``litellm.aresponses(input=prompt or 'test')`` — a bare string. OpenAI's public Responses API is tolerant of either shape, but the ChatGPT/Codex backend enforces a list and returns ``{"detail": "Input must be a list"}``, so the "Test Connection" button on the models list page failed against ChatGPT OAuth models even though normal inference worked fine. Wrap the fallback in a list (``input or [prompt or 'test']``) so both backends are happy. The calling site in ``main.ahealth_check`` already forwards ``input=['test from litellm']`` for this mode, so the common path flows through unchanged. Regression test mocks ``litellm.aresponses`` and asserts the handler passes a list for both the ``input=[...]`` and prompt-only cases. --- .../health_check_helpers.py | 5 +- .../test_health_check_helpers.py | 46 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/litellm/litellm_core_utils/health_check_helpers.py b/litellm/litellm_core_utils/health_check_helpers.py index 9e972f1910b..601c3292e73 100644 --- a/litellm/litellm_core_utils/health_check_helpers.py +++ b/litellm/litellm_core_utils/health_check_helpers.py @@ -206,8 +206,11 @@ class HealthCheckHelpers: filtered_model_params=_filter_model_params(model_params=model_params), ), "responses": lambda: litellm.aresponses( + # The ChatGPT/Codex backend rejects string input with + # ``{"detail": "Input must be a list"}``; OpenAI's own + # Responses API accepts either, so a list works for both. **_filter_model_params(model_params=model_params), - input=prompt or "test", + input=input or [prompt or "test"], ), "ocr": lambda: litellm.aocr( **_filter_model_params(model_params=model_params), diff --git a/tests/test_litellm/litellm_core_utils/test_health_check_helpers.py b/tests/test_litellm/litellm_core_utils/test_health_check_helpers.py index 867ab675943..6a960a943e6 100644 --- a/tests/test_litellm/litellm_core_utils/test_health_check_helpers.py +++ b/tests/test_litellm/litellm_core_utils/test_health_check_helpers.py @@ -134,4 +134,48 @@ async def test_ahealth_check_failure_masks_raw_request_headers(): if "Content-Type" in headers: assert headers["Content-Type"] == "application/json" - print(f"Masked Authorization header: {headers.get('Authorization', 'NOT FOUND')}") \ No newline at end of file + print(f"Masked Authorization header: {headers.get('Authorization', 'NOT FOUND')}") + + +def test_get_mode_handlers_responses_wraps_input_in_list(): + """ + Regression: the ChatGPT/Codex responses backend rejects string input + with ``{"detail": "Input must be a list"}``. OpenAI's public Responses + API accepts either shape, so we always pass a list from the health + check to keep both backends happy. + + See ``litellm/proxy/health_endpoints/_health_endpoints.py`` — + ``test_connection`` calls ``ahealth_check`` with + ``input=["test from litellm"]``; that must flow through unchanged, + and the ``prompt``-only fallback must still produce a list. + """ + import asyncio + + handlers = HealthCheckHelpers.get_mode_handlers( + model="chatgpt/gpt-5.4-codex", + custom_llm_provider="chatgpt", + model_params={"model": "chatgpt/gpt-5.4-codex"}, + prompt="test from litellm", + input=["test from litellm"], + ) + assert "responses" in handlers + + with patch( + "litellm.aresponses", new=AsyncMock(return_value=MagicMock()) + ) as mock_ar: + asyncio.get_event_loop().run_until_complete(handlers["responses"]()) + assert mock_ar.call_args.kwargs["input"] == ["test from litellm"] + + # Fallback: no ``input`` provided, prompt-only → still a list. + prompt_only_handlers = HealthCheckHelpers.get_mode_handlers( + model="chatgpt/gpt-5.4-codex", + custom_llm_provider="chatgpt", + model_params={"model": "chatgpt/gpt-5.4-codex"}, + prompt="hello", + input=None, + ) + with patch( + "litellm.aresponses", new=AsyncMock(return_value=MagicMock()) + ) as mock_ar: + asyncio.get_event_loop().run_until_complete(prompt_only_handlers["responses"]()) + assert mock_ar.call_args.kwargs["input"] == ["hello"]