fix(health): pass responses-mode `input` as a list

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.
This commit is contained in:
Jason Cook 2026-04-23 15:02:35 -04:00
parent f2653df86b
commit 4ff0de18ef
2 changed files with 49 additions and 2 deletions

View file

@ -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),

View file

@ -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')}")
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"]