From 17c858b02d3d9ea80dbf5ba9dcbcecbf1f06de2d 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 a0f027cd58f..d1381e06bcf 100644 --- a/litellm/litellm_core_utils/health_check_helpers.py +++ b/litellm/litellm_core_utils/health_check_helpers.py @@ -251,7 +251,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"}], ), "ocr": lambda: litellm.aocr( **_filter_model_params(model_params=model_params), diff --git a/litellm/main.py b/litellm/main.py index 6c85adf3ae8..3923dc91701 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -8755,6 +8755,10 @@ async def ahealth_check( mode = litellm.model_cost[model].get("mode") 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 "chat" if "*" in model: return await HealthCheckHelpers.ahealth_check_wildcard_models( From 87ab9a74ea9cdf7fef0f0d7409c0b06167abff04 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 | 13 ++-- .../test_health_check_helpers.py | 64 ++++++++++++++++++- 5 files changed, 81 insertions(+), 6 deletions(-) diff --git a/litellm/litellm_core_utils/health_check_helpers.py b/litellm/litellm_core_utils/health_check_helpers.py index d1381e06bcf..96713f33ce5 100644 --- a/litellm/litellm_core_utils/health_check_helpers.py +++ b/litellm/litellm_core_utils/health_check_helpers.py @@ -251,7 +251,9 @@ class HealthCheckHelpers: ), "responses": lambda: litellm.aresponses( **_filter_model_params(model_params=model_params), - input=[{"role": "user", "content": prompt or "test"}], + input=[ # mutable-ok: Responses API requires a JSON message list + {"role": "user", "content": prompt or "test"}, # mutable-ok: JSON message object + ], ), "ocr": lambda: litellm.aocr( **_filter_model_params(model_params=model_params), diff --git a/litellm/llms/base_llm/chat/transformation.py b/litellm/llms/base_llm/chat/transformation.py index f1b41a2302d..9dddfefa46e 100644 --- a/litellm/llms/base_llm/chat/transformation.py +++ b/litellm/llms/base_llm/chat/transformation.py @@ -69,6 +69,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 1b110704c8b..8567b91afc4 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 3923dc91701..649f3ec3268 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -8751,14 +8751,19 @@ async def ahealth_check( api_base=api_base_from_params, api_key=api_key_from_params, ) + if mode is None and custom_llm_provider in { # mutable-ok: short-lived provider-value membership set + provider.value for provider in LlmProviders + }: + provider_config = ProviderConfigManager.get_provider_chat_config( + model=model, + provider=LlmProviders(custom_llm_provider), + ) + if provider_config is not None: + mode = provider_config.get_health_check_mode() if model in litellm.model_cost and mode is None: mode = litellm.model_cost[model].get("mode") 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 "chat" if "*" in model: return await HealthCheckHelpers.ahealth_check_wildcard_models( 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 47c4576f91f..bd90399f632 100644 --- a/tests/unit/litellm_core_utils/test_health_check_helpers.py +++ b/tests/unit/litellm_core_utils/test_health_check_helpers.py @@ -140,6 +140,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() def test_update_model_params_with_health_check_tracking_information(): @@ -439,7 +501,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),