From 8a7801395bb4af66fe17af5baad92efb69d23210 Mon Sep 17 00:00:00 2001 From: willcai1984 Date: Thu, 30 Jul 2026 10:36:52 +0800 Subject: [PATCH 1/2] fix(health): fix chatgpt provider Test Connection endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs caused the LiteLLM UI "Test Connection" button to always fail for chatgpt provider models (e.g. chatgpt/gpt-5.6-sol, gpt-5.6-terra): 1. litellm/main.py — ahealth_check() mode auto-detection uses the model_cost map, but unlisted chatgpt models (any model not explicitly registered) fell through to the "chat" fallback. The chatgpt provider routes exclusively through the Responses API; sending a Chat Completions messages payload caused the backend to reject with: ChatgptException - {"detail": "Input must be a list"}. Fix: after get_llm_provider() resolves custom_llm_provider=="chatgpt" and mode is still None, default to "responses" instead of "chat". 2. litellm/litellm_core_utils/health_check_helpers.py — the "responses" mode handler passed input=prompt (a plain string) to litellm.aresponses(). The Responses API requires input to be a list of message objects, not a bare string. The ChatGPT/Codex backend rejected this with: ChatgptException - {"error": {"message": "Invalid type for 'input[0]': expected an input item, but got a string instead."}} Fix: pass input=[{"role": "user", "content": prompt or "test"}]. Both fixes were validated end-to-end against a live chatgpt provider deployment (gpt-5.6-sol, gpt-5.6-terra, two seats each — all returned status: success after the patch). --- litellm/litellm_core_utils/health_check_helpers.py | 2 +- litellm/main.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/health_check_helpers.py b/litellm/litellm_core_utils/health_check_helpers.py index 60e4b7495ed..69b12fea21b 100644 --- a/litellm/litellm_core_utils/health_check_helpers.py +++ b/litellm/litellm_core_utils/health_check_helpers.py @@ -283,7 +283,7 @@ class HealthCheckHelpers: ), "responses": lambda: litellm.aresponses( **_filter_model_params(model_params=model_params), - input=prompt or "test", + input=[{"role": "user", "content": prompt or "test"}], ), "anthropic_messages": lambda: litellm.anthropic_messages( **{ diff --git a/litellm/main.py b/litellm/main.py index 37b290f51ba..8ec1eb9403a 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -8812,6 +8812,10 @@ async def ahealth_check( ) model_params["cache"] = {"no-cache": True} # don't used cached responses for making health check calls + # chatgpt provider uses the Responses API exclusively; default to "responses" so the + # health check doesn't send a Chat Completions payload that the backend rejects. + if mode is None and custom_llm_provider == "chatgpt": + mode = "responses" mode = mode or default_health_check_mode( requested_model=requested_model, model=model, custom_llm_provider=custom_llm_provider ) From df8e815d8c4e164a80f9e3a6191da712fb2b374c Mon Sep 17 00:00:00 2001 From: willcai1984 Date: Thu, 30 Jul 2026 15:01:55 +0800 Subject: [PATCH 2/2] fix(health): use provider default mode for checks --- .../health_check_helpers.py | 4 +- litellm/llms/base_llm/chat/transformation.py | 3 + litellm/llms/chatgpt/chat/transformation.py | 3 + litellm/main.py | 14 ++-- .../test_health_check_helpers.py | 64 ++++++++++++++++++- 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/litellm/litellm_core_utils/health_check_helpers.py b/litellm/litellm_core_utils/health_check_helpers.py index 69b12fea21b..2c2980aac09 100644 --- a/litellm/litellm_core_utils/health_check_helpers.py +++ b/litellm/litellm_core_utils/health_check_helpers.py @@ -283,7 +283,9 @@ class HealthCheckHelpers: ), "responses": lambda: litellm.aresponses( **_filter_model_params(model_params=model_params), - input=[{"role": "user", "content": prompt or "test"}], + input=[ + {"role": "user", "content": prompt or "test"}, + ], ), "anthropic_messages": lambda: litellm.anthropic_messages( **{ diff --git a/litellm/llms/base_llm/chat/transformation.py b/litellm/llms/base_llm/chat/transformation.py index 8275829765b..a9bf1402bdf 100644 --- a/litellm/llms/base_llm/chat/transformation.py +++ b/litellm/llms/base_llm/chat/transformation.py @@ -89,6 +89,9 @@ class BaseConfig(ABC): def __init__(self): pass + def get_health_check_mode(self) -> str | None: + return None + @classmethod def get_config(cls): return { diff --git a/litellm/llms/chatgpt/chat/transformation.py b/litellm/llms/chatgpt/chat/transformation.py index 7c985b7dd5e..17f9a1519f4 100644 --- a/litellm/llms/chatgpt/chat/transformation.py +++ b/litellm/llms/chatgpt/chat/transformation.py @@ -26,6 +26,9 @@ class ChatGPTConfig(OpenAIConfig): def api_base_without_login(self) -> str: return self.authenticator.get_api_base() + def get_health_check_mode(self) -> str: + return "responses" + def _get_openai_compatible_provider_info( self, model: str, diff --git a/litellm/main.py b/litellm/main.py index 8ec1eb9403a..a2952f69638 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -8810,12 +8810,16 @@ async def ahealth_check( api_base=api_base_from_params, api_key=api_key_from_params, ) - model_params["cache"] = {"no-cache": True} # don't used cached responses for making health check calls - # chatgpt provider uses the Responses API exclusively; default to "responses" so the - # health check doesn't send a Chat Completions payload that the backend rejects. - if mode is None and custom_llm_provider == "chatgpt": - mode = "responses" + if mode is None and custom_llm_provider in {provider.value for provider in LlmProviders}: + provider_config: Final = ProviderConfigManager.get_provider_chat_config( + model=model, + provider=LlmProviders(custom_llm_provider), + ) + if provider_config is not None: + mode = ( # rebind-ok: resolve provider-config default + provider_config.get_health_check_mode() + ) mode = mode or default_health_check_mode( requested_model=requested_model, model=model, custom_llm_provider=custom_llm_provider ) diff --git a/tests/unit/litellm_core_utils/test_health_check_helpers.py b/tests/unit/litellm_core_utils/test_health_check_helpers.py index ffe50c137d2..8fbbf0b2601 100644 --- a/tests/unit/litellm_core_utils/test_health_check_helpers.py +++ b/tests/unit/litellm_core_utils/test_health_check_helpers.py @@ -151,6 +151,68 @@ async def test_ahealth_check_supports_image_edit_mode(): assert "error" not in result assert "Mode image_edit not supported" not in str(result) +@pytest.mark.asyncio +async def test_ahealth_check_uses_provider_mode_and_responses_message_input(): + mock_response = MagicMock() + mock_response._hidden_params = {} + mock_aresponses = AsyncMock(return_value=mock_response) + mock_acompletion = AsyncMock() + + with ( + patch( # test-quality-ok: isolate provider and handler calls for focused health-check routing + "litellm.main.get_llm_provider", + return_value=("gpt-5.6-sol", "chatgpt", None, None), + ), + patch.object( # test-quality-ok: isolate health-check metadata plumbing for routing assertion + HealthCheckHelpers, + "_update_model_params_with_health_check_tracking_information", + side_effect=lambda model_params: model_params, + ), + patch("litellm.aresponses", new=mock_aresponses), # test-quality-ok: verify provider-mode dispatch + patch("litellm.acompletion", new=mock_acompletion), # test-quality-ok: verify chat fallback dispatch + ): + result = await ahealth_check( + model_params={"model": "chatgpt/gpt-5.6-sol"}, + mode=None, + prompt="health check", + ) + + assert "error" not in result + mock_aresponses.assert_awaited_once() + assert mock_aresponses.await_args.kwargs["input"] == [ + {"role": "user", "content": "health check"} + ] + mock_acompletion.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_ahealth_check_provider_without_default_mode_keeps_chat(): + mock_response = MagicMock() + mock_response._hidden_params = {} + mock_aresponses = AsyncMock() + mock_acompletion = AsyncMock(return_value=mock_response) + + with ( + patch( # test-quality-ok: isolate provider and handler calls for focused health-check routing + "litellm.main.get_llm_provider", + return_value=("custom-model", "custom", None, None), + ), + patch.object( # test-quality-ok: isolate health-check metadata plumbing for routing assertion + HealthCheckHelpers, + "_update_model_params_with_health_check_tracking_information", + side_effect=lambda model_params: model_params, + ), + patch("litellm.aresponses", new=mock_aresponses), # test-quality-ok: verify provider-mode dispatch + patch("litellm.acompletion", new=mock_acompletion), # test-quality-ok: verify chat fallback dispatch + ): + result = await ahealth_check( + model_params={"model": "custom/custom-model"}, + mode=None, + ) + + assert "error" not in result + mock_acompletion.assert_awaited_once() + mock_aresponses.assert_not_awaited() @pytest.mark.asyncio @@ -472,7 +534,7 @@ async def test_realtime_health_check_uses_model_level_vertex_params(): "websockets.connect", lambda url, **kwargs: _FakeWebsocketConnect(connect_calls, url, **kwargs), ), - patch.object( + patch.object( # test-quality-ok: isolate health-check metadata plumbing for routing assertion HealthCheckHelpers, "update_model_params_with_health_check_tracking_information", staticmethod(lambda model_params: model_params),