From 3bd309f808ab62b5e045f106caf111ee080a9f0b Mon Sep 17 00:00:00 2001 From: Alex Gertz <69761220+agertz7@users.noreply.github.com> Date: Fri, 2 Jan 2026 13:06:05 -0500 Subject: [PATCH] fix: return empty data array instead of 500 when no models configured (#18556) - /v2/model/info now returns {"data": []} when llm_router is None or model_list is empty - /model_group/info now returns {"data": []} when llm_model_list is None or empty - Fixes UI crash on fresh installs with STORE_MODEL_IN_DB=True - Added 4 unit tests for empty model list scenarios --- litellm/proxy/proxy_server.py | 21 +-- .../proxy/test_empty_model_list.py | 155 ++++++++++++++++++ 2 files changed, 161 insertions(+), 15 deletions(-) create mode 100644 tests/test_litellm/proxy/test_empty_model_list.py diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index f56c0c2b07..d9d2083601 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -7322,13 +7322,9 @@ async def model_info_v2( """ global llm_model_list, general_settings, user_config_file_path, proxy_config, llm_router - if llm_router is None: - raise HTTPException( - status_code=500, - detail={ - "error": f"No model list passed, models router={llm_router}. You can add a model through the config.yaml or on the LiteLLM Admin UI." - }, - ) + # Return empty data array when no models are configured (graceful handling for fresh installs) + if llm_router is None or not llm_router.model_list: + return {"data": []} if prisma_client is None: raise HTTPException( @@ -8228,14 +8224,9 @@ async def model_group_info( """ global llm_model_list, general_settings, user_config_file_path, proxy_config, llm_router - if llm_model_list is None: - raise HTTPException( - status_code=500, detail={"error": "LLM Model List not loaded in"} - ) - if llm_router is None: - raise HTTPException( - status_code=500, detail={"error": "LLM Router is not loaded in"} - ) + # Return empty data array when no models are configured (graceful handling for fresh installs) + if llm_model_list is None or llm_router is None or not llm_model_list: + return {"data": []} from litellm.proxy.utils import get_available_models_for_user diff --git a/tests/test_litellm/proxy/test_empty_model_list.py b/tests/test_litellm/proxy/test_empty_model_list.py new file mode 100644 index 0000000000..6b3e59d319 --- /dev/null +++ b/tests/test_litellm/proxy/test_empty_model_list.py @@ -0,0 +1,155 @@ +""" +Tests for graceful handling of empty model list scenarios. + +These tests verify that /v2/model/info and /model_group/info endpoints +return empty data arrays instead of 500 errors when no models are configured. +""" + +import os +import sys +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from fastapi.testclient import TestClient + +sys.path.insert( + 0, os.path.abspath("../../..") +) # Adds the parent directory to the system-path + +from litellm.proxy.proxy_server import app + + +@pytest.fixture +def client(): + """Create a test client for the FastAPI app.""" + return TestClient(app) + + +class TestEmptyModelListHandling: + """Test suite for empty model list scenarios.""" + + def test_v2_model_info_returns_empty_data_when_router_is_none( + self, client, monkeypatch + ): + """ + Test that /v2/model/info returns {"data": []} instead of 500 + when llm_router is None. + """ + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_model_list", None) + monkeypatch.setattr("litellm.proxy.proxy_server.user_model", None) + monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {}) + + with patch( + "litellm.proxy.auth.user_api_key_auth.user_api_key_auth", + return_value=MagicMock( + user_id="test-user", + team_id=None, + team_models=[], + models=[], + user_role="proxy_admin", + ), + ): + response = client.get( + "/v2/model/info", + headers={"Authorization": "Bearer sk-test"}, + ) + + assert response.status_code == 200 + assert response.json() == {"data": []} + + def test_v2_model_info_returns_empty_data_when_model_list_empty( + self, client, monkeypatch + ): + """ + Test that /v2/model/info returns {"data": []} instead of 500 + when llm_router exists but model_list is empty. + """ + mock_router = MagicMock() + mock_router.model_list = [] + + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_model_list", []) + monkeypatch.setattr("litellm.proxy.proxy_server.user_model", None) + monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {}) + + with patch( + "litellm.proxy.auth.user_api_key_auth.user_api_key_auth", + return_value=MagicMock( + user_id="test-user", + team_id=None, + team_models=[], + models=[], + user_role="proxy_admin", + ), + ): + response = client.get( + "/v2/model/info", + headers={"Authorization": "Bearer sk-test"}, + ) + + assert response.status_code == 200 + assert response.json() == {"data": []} + + def test_model_group_info_returns_empty_data_when_model_list_none( + self, client, monkeypatch + ): + """ + Test that /model_group/info returns {"data": []} instead of 500 + when llm_model_list is None. + """ + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_model_list", None) + monkeypatch.setattr("litellm.proxy.proxy_server.user_model", None) + monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {}) + + with patch( + "litellm.proxy.auth.user_api_key_auth.user_api_key_auth", + return_value=MagicMock( + user_id="test-user", + team_id=None, + team_models=[], + models=[], + user_role="proxy_admin", + ), + ): + response = client.get( + "/model_group/info", + headers={"Authorization": "Bearer sk-test"}, + ) + + assert response.status_code == 200 + assert response.json() == {"data": []} + + def test_model_group_info_returns_empty_data_when_model_list_empty( + self, client, monkeypatch + ): + """ + Test that /model_group/info returns {"data": []} instead of 500 + when llm_model_list is empty. + """ + mock_router = MagicMock() + mock_router.model_list = [] + + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_model_list", []) + monkeypatch.setattr("litellm.proxy.proxy_server.user_model", None) + monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {}) + + with patch( + "litellm.proxy.auth.user_api_key_auth.user_api_key_auth", + return_value=MagicMock( + user_id="test-user", + team_id=None, + team_models=[], + models=[], + user_role="proxy_admin", + ), + ): + response = client.get( + "/model_group/info", + headers={"Authorization": "Bearer sk-test"}, + ) + + assert response.status_code == 200 + assert response.json() == {"data": []}