From b5a7d2ab34740619b1f5de52663e2617015bcf36 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 21 Jan 2026 10:44:18 -0800 Subject: [PATCH 1/3] Paginating model/info endpoint --- litellm/proxy/proxy_server.py | 31 ++- tests/test_litellm/proxy/test_proxy_server.py | 225 ++++++++++++++++++ 2 files changed, 254 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index eef2af89799..c4f2b45f388 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -7522,6 +7522,8 @@ async def model_info_v2( False, description="Return all models across all teams user is in." ), debug: Optional[bool] = False, + page: int = Query(1, description="Page number", ge=1), + size: int = Query(50, description="Page size", ge=1), ): """ BETA ENDPOINT. Might change unexpectedly. Use `/v1/model/info` for now. @@ -7530,7 +7532,13 @@ async def model_info_v2( # 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": []} + return { + "data": [], + "total_count": 0, + "current_page": page, + "total_pages": 0, + "size": size, + } if prisma_client is None: raise HTTPException( @@ -7620,7 +7628,26 @@ async def model_info_v2( ) verbose_proxy_logger.debug("all_models: %s", all_models) - return {"data": all_models} + + total_count = len(all_models) + + skip = (page - 1) * size + + total_pages = -(-total_count // size) if total_count > 0 else 0 + + paginated_models = all_models[skip : skip + size] + + verbose_proxy_logger.debug( + f"Pagination: skip={skip}, take={size}, total_count={total_count}, total_pages={total_pages}" + ) + + return { + "data": paginated_models, + "total_count": total_count, + "current_page": page, + "total_pages": total_pages, + "size": size, + } @router.get( diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index 751a9033871..d8970a76a9a 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -3216,3 +3216,228 @@ async def test_get_hierarchical_router_settings(): prisma_client=mock_prisma_client, ) assert result is None + + +@pytest.mark.asyncio +async def test_model_info_v2_pagination_basic(monkeypatch): + """ + Test basic pagination functionality for /v2/model/info endpoint. + Tests multiple pages with different page sizes. + """ + from unittest.mock import AsyncMock, MagicMock + + from litellm.proxy._types import UserAPIKeyAuth + from litellm.proxy.proxy_server import app, proxy_config, user_api_key_auth + + # Create 75 mock models for testing pagination + mock_models = [ + { + "model_name": f"model-{i}", + "litellm_params": {"model": f"gpt-{i}"}, + "model_info": {"id": f"model-{i}"}, + } + for i in range(1, 76) # 75 models total + ] + + # Mock llm_router + mock_router = MagicMock() + mock_router.model_list = mock_models + + # Mock prisma_client + mock_prisma_client = MagicMock() + + # Mock proxy_config.get_config + mock_get_config = AsyncMock(return_value={}) + + # Mock user authentication + mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth) + mock_user_api_key_dict.user_id = "test-user" + mock_user_api_key_dict.api_key = "test-key" + mock_user_api_key_dict.team_models = [] + mock_user_api_key_dict.models = [] + + # Apply monkeypatches + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + monkeypatch.setattr("litellm.proxy.proxy_server.user_model", None) + monkeypatch.setattr(proxy_config, "get_config", mock_get_config) + + # Override auth dependency + original_overrides = app.dependency_overrides.copy() + app.dependency_overrides[user_api_key_auth] = lambda: mock_user_api_key_dict + + client = TestClient(app) + try: + # Test page 1 with size 25 (should return models 1-25) + response = client.get("/v2/model/info", params={"page": 1, "size": 25}) + assert response.status_code == 200 + data = response.json() + assert data["total_count"] == 75 + assert data["current_page"] == 1 + assert data["size"] == 25 + assert data["total_pages"] == 3 # ceil(75/25) = 3 + assert len(data["data"]) == 25 + assert data["data"][0]["model_name"] == "model-1" + assert data["data"][24]["model_name"] == "model-25" + + # Test page 2 with size 25 (should return models 26-50) + response = client.get("/v2/model/info", params={"page": 2, "size": 25}) + assert response.status_code == 200 + data = response.json() + assert data["total_count"] == 75 + assert data["current_page"] == 2 + assert data["size"] == 25 + assert data["total_pages"] == 3 + assert len(data["data"]) == 25 + assert data["data"][0]["model_name"] == "model-26" + assert data["data"][24]["model_name"] == "model-50" + + # Test page 3 with size 25 (should return models 51-75) + response = client.get("/v2/model/info", params={"page": 3, "size": 25}) + assert response.status_code == 200 + data = response.json() + assert data["total_count"] == 75 + assert data["current_page"] == 3 + assert data["size"] == 25 + assert data["total_pages"] == 3 + assert len(data["data"]) == 25 + assert data["data"][0]["model_name"] == "model-51" + assert data["data"][24]["model_name"] == "model-75" + + # Test different page size (size 10) + response = client.get("/v2/model/info", params={"page": 1, "size": 10}) + assert response.status_code == 200 + data = response.json() + assert data["total_count"] == 75 + assert data["current_page"] == 1 + assert data["size"] == 10 + assert data["total_pages"] == 8 # ceil(75/10) = 8 + assert len(data["data"]) == 10 + + finally: + app.dependency_overrides = original_overrides + + +@pytest.mark.asyncio +async def test_model_info_v2_pagination_edge_cases(monkeypatch): + """ + Test edge cases for pagination in /v2/model/info endpoint. + Tests empty results, last page with partial results, and boundary conditions. + """ + from unittest.mock import AsyncMock, MagicMock + + from litellm.proxy._types import UserAPIKeyAuth + from litellm.proxy.proxy_server import app, proxy_config, user_api_key_auth + + # Mock prisma_client + mock_prisma_client = MagicMock() + + # Mock user authentication + mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth) + mock_user_api_key_dict.user_id = "test-user" + mock_user_api_key_dict.api_key = "test-key" + mock_user_api_key_dict.team_models = [] + mock_user_api_key_dict.models = [] + + # Mock proxy_config.get_config + mock_get_config = AsyncMock(return_value={}) + + # Apply monkeypatches + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + monkeypatch.setattr("litellm.proxy.proxy_server.user_model", None) + monkeypatch.setattr(proxy_config, "get_config", mock_get_config) + + # Override auth dependency + original_overrides = app.dependency_overrides.copy() + app.dependency_overrides[user_api_key_auth] = lambda: mock_user_api_key_dict + + client = TestClient(app) + try: + # Test Case 1: Empty model list (no models configured) + mock_router_empty = MagicMock() + mock_router_empty.model_list = [] + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router_empty) + + response = client.get("/v2/model/info", params={"page": 1, "size": 25}) + assert response.status_code == 200 + data = response.json() + assert data["total_count"] == 0 + assert data["current_page"] == 1 + assert data["size"] == 25 + assert data["total_pages"] == 0 + assert len(data["data"]) == 0 + + # Test Case 2: Last page with partial results (23 models, page size 10) + mock_models_partial = [ + { + "model_name": f"model-{i}", + "litellm_params": {"model": f"gpt-{i}"}, + "model_info": {"id": f"model-{i}"}, + } + for i in range(1, 24) # 23 models total + ] + mock_router_partial = MagicMock() + mock_router_partial.model_list = mock_models_partial + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router_partial) + + # Page 1 should have 10 models + response = client.get("/v2/model/info", params={"page": 1, "size": 10}) + assert response.status_code == 200 + data = response.json() + assert data["total_count"] == 23 + assert data["current_page"] == 1 + assert data["total_pages"] == 3 # ceil(23/10) = 3 + assert len(data["data"]) == 10 + + # Page 2 should have 10 models + response = client.get("/v2/model/info", params={"page": 2, "size": 10}) + assert response.status_code == 200 + data = response.json() + assert data["total_count"] == 23 + assert data["current_page"] == 2 + assert data["total_pages"] == 3 + assert len(data["data"]) == 10 + + # Page 3 (last page) should have only 3 models + response = client.get("/v2/model/info", params={"page": 3, "size": 10}) + assert response.status_code == 200 + data = response.json() + assert data["total_count"] == 23 + assert data["current_page"] == 3 + assert data["total_pages"] == 3 + assert len(data["data"]) == 3 + assert data["data"][0]["model_name"] == "model-21" + assert data["data"][2]["model_name"] == "model-23" + + # Test Case 3: Page beyond available pages (should return empty data) + response = client.get("/v2/model/info", params={"page": 4, "size": 10}) + assert response.status_code == 200 + data = response.json() + assert data["total_count"] == 23 + assert data["current_page"] == 4 + assert data["total_pages"] == 3 + assert len(data["data"]) == 0 # No data for page beyond total_pages + + # Test Case 4: Single model with page size 1 + mock_models_single = [ + { + "model_name": "single-model", + "litellm_params": {"model": "gpt-4"}, + "model_info": {"id": "single-model"}, + } + ] + mock_router_single = MagicMock() + mock_router_single.model_list = mock_models_single + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router_single) + + response = client.get("/v2/model/info", params={"page": 1, "size": 1}) + assert response.status_code == 200 + data = response.json() + assert data["total_count"] == 1 + assert data["current_page"] == 1 + assert data["total_pages"] == 1 + assert len(data["data"]) == 1 + assert data["data"][0]["model_name"] == "single-model" + + finally: + app.dependency_overrides = original_overrides From d0e35751a11db388b49a1c8f85ad9b6bb2f123f6 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 21 Jan 2026 11:02:39 -0800 Subject: [PATCH 2/3] Fixing tests and linting --- litellm/proxy/proxy_server.py | 126 +++++++----- .../proxy/test_empty_model_list.py | 58 +++++- tests/test_litellm/proxy/test_proxy_server.py | 184 ++++++++++++++++++ 3 files changed, 312 insertions(+), 56 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index c4f2b45f388..d168290e8a1 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -7503,6 +7503,77 @@ async def get_all_team_and_direct_access_models( return all_models +def _enrich_model_info_with_litellm_data( + model: Dict[str, Any], debug: bool = False, llm_router: Optional[Router] = None +) -> Dict[str, Any]: + """ + Enrich a model dictionary with litellm model info (pricing, context window, etc.) + and remove sensitive information. + + Args: + model: Model dictionary to enrich + debug: Whether to include debug information like openai_client + llm_router: Optional router instance for debug info + + Returns: + Enriched model dictionary with sensitive info removed + """ + # provided model_info in config.yaml + model_info = model.get("model_info", {}) + if debug is True: + _openai_client = "None" + if llm_router is not None: + _openai_client = ( + llm_router._get_client( + deployment=model, kwargs={}, client_type="async" + ) + or "None" + ) + else: + _openai_client = "llm_router_is_None" + openai_client = str(_openai_client) + model["openai_client"] = openai_client + + # read litellm model_prices_and_context_window.json to get the following: + # input_cost_per_token, output_cost_per_token, max_tokens + litellm_model_info = get_litellm_model_info(model=model) + + # 2nd pass on the model, try seeing if we can find model in litellm model_cost map + if litellm_model_info == {}: + # use litellm_param model_name to get model_info + litellm_params = model.get("litellm_params", {}) + litellm_model = litellm_params.get("model", None) + try: + litellm_model_info = litellm.get_model_info(model=litellm_model) + except Exception: + litellm_model_info = {} + # 3rd pass on the model, try seeing if we can find model but without the "/" in model cost map + if litellm_model_info == {}: + # use litellm_param model_name to get model_info + litellm_params = model.get("litellm_params", {}) + litellm_model = litellm_params.get("model", None) + if litellm_model: + split_model = litellm_model.split("/") + if len(split_model) > 0: + litellm_model = split_model[-1] + try: + litellm_model_info = litellm.get_model_info( + model=litellm_model, custom_llm_provider=split_model[0] + ) + except Exception: + litellm_model_info = {} + for k, v in litellm_model_info.items(): + if k not in model_info: + model_info[k] = v + model["model_info"] = model_info + # don't return the api key / vertex credentials + # don't return the llm credentials + model = remove_sensitive_info_from_deployment( + model, excluded_keys={"litellm_credential_name"} + ) + return model + + @router.get( "/v2/model/info", description="v2 - returns models available to the user based on their API key permissions. Shows model info from config.yaml (except api key and api base). Filter to just user-added models with ?user_models_only=true", @@ -7573,58 +7644,9 @@ async def model_info_v2( all_models=all_models, ) # fill in model info based on config.yaml and litellm model_prices_and_context_window.json - for _model in all_models: - # provided model_info in config.yaml - model_info = _model.get("model_info", {}) - if debug is True: - _openai_client = "None" - if llm_router is not None: - _openai_client = ( - llm_router._get_client( - deployment=_model, kwargs={}, client_type="async" - ) - or "None" - ) - else: - _openai_client = "llm_router_is_None" - openai_client = str(_openai_client) - _model["openai_client"] = openai_client - - # read litellm model_prices_and_context_window.json to get the following: - # input_cost_per_token, output_cost_per_token, max_tokens - litellm_model_info = get_litellm_model_info(model=_model) - - # 2nd pass on the model, try seeing if we can find model in litellm model_cost map - if litellm_model_info == {}: - # use litellm_param model_name to get model_info - litellm_params = _model.get("litellm_params", {}) - litellm_model = litellm_params.get("model", None) - try: - litellm_model_info = litellm.get_model_info(model=litellm_model) - except Exception: - litellm_model_info = {} - # 3rd pass on the model, try seeing if we can find model but without the "/" in model cost map - if litellm_model_info == {}: - # use litellm_param model_name to get model_info - litellm_params = _model.get("litellm_params", {}) - litellm_model = litellm_params.get("model", None) - split_model = litellm_model.split("/") - if len(split_model) > 0: - litellm_model = split_model[-1] - try: - litellm_model_info = litellm.get_model_info( - model=litellm_model, custom_llm_provider=split_model[0] - ) - except Exception: - litellm_model_info = {} - for k, v in litellm_model_info.items(): - if k not in model_info: - model_info[k] = v - _model["model_info"] = model_info - # don't return the api key / vertex credentials - # don't return the llm credentials - _model = remove_sensitive_info_from_deployment( - _model, excluded_keys={"litellm_credential_name"} + for i, _model in enumerate(all_models): + all_models[i] = _enrich_model_info_with_litellm_data( + model=_model, debug=debug, llm_router=llm_router ) verbose_proxy_logger.debug("all_models: %s", all_models) diff --git a/tests/test_litellm/proxy/test_empty_model_list.py b/tests/test_litellm/proxy/test_empty_model_list.py index 6b3e59d3194..dd900d3eb53 100644 --- a/tests/test_litellm/proxy/test_empty_model_list.py +++ b/tests/test_litellm/proxy/test_empty_model_list.py @@ -32,7 +32,7 @@ class TestEmptyModelListHandling: self, client, monkeypatch ): """ - Test that /v2/model/info returns {"data": []} instead of 500 + Test that /v2/model/info returns paginated empty response instead of 500 when llm_router is None. """ monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None) @@ -56,13 +56,18 @@ class TestEmptyModelListHandling: ) assert response.status_code == 200 - assert response.json() == {"data": []} + data = response.json() + assert data["data"] == [] + assert data["total_count"] == 0 + assert data["current_page"] == 1 + assert data["total_pages"] == 0 + assert data["size"] == 50 # default page size 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 + Test that /v2/model/info returns paginated empty response instead of 500 when llm_router exists but model_list is empty. """ mock_router = MagicMock() @@ -89,7 +94,52 @@ class TestEmptyModelListHandling: ) assert response.status_code == 200 - assert response.json() == {"data": []} + data = response.json() + assert data["data"] == [] + assert data["total_count"] == 0 + assert data["current_page"] == 1 + assert data["total_pages"] == 0 + assert data["size"] == 50 # default page size + + def test_v2_model_info_pagination_with_empty_results( + self, client, monkeypatch + ): + """ + Test that /v2/model/info pagination parameters work correctly + when there are no models (empty results). + """ + 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", + ), + ): + # Test with custom pagination parameters + response = client.get( + "/v2/model/info", + params={"page": 2, "size": 25}, + headers={"Authorization": "Bearer sk-test"}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["data"] == [] + assert data["total_count"] == 0 + assert data["current_page"] == 2 # Should respect the page parameter + assert data["total_pages"] == 0 + assert data["size"] == 25 # Should respect the size parameter def test_model_group_info_returns_empty_data_when_model_list_none( self, client, monkeypatch diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index d8970a76a9a..f1854380efe 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -3441,3 +3441,187 @@ async def test_model_info_v2_pagination_edge_cases(monkeypatch): finally: app.dependency_overrides = original_overrides + + +def test_enrich_model_info_with_litellm_data(): + """ + Test the _enrich_model_info_with_litellm_data helper function. + Tests model info enrichment, debug mode, and sensitive info removal. + """ + from unittest.mock import MagicMock, patch + + from litellm.proxy.proxy_server import _enrich_model_info_with_litellm_data + + # Test Case 1: Basic model enrichment without debug + model = { + "model_name": "test-model", + "litellm_params": {"model": "gpt-3.5-turbo"}, + "model_info": {"id": "test-model"}, + "api_key": "sk-secret-key", # Should be removed + } + + with patch("litellm.proxy.proxy_server.get_litellm_model_info") as mock_get_info, patch( + "litellm.proxy.proxy_server.remove_sensitive_info_from_deployment" + ) as mock_remove_sensitive: + mock_get_info.return_value = { + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + "max_tokens": 4096, + } + mock_remove_sensitive.return_value = { + "model_name": "test-model", + "litellm_params": {"model": "gpt-3.5-turbo"}, + "model_info": { + "id": "test-model", + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + "max_tokens": 4096, + }, + } + + result = _enrich_model_info_with_litellm_data(model=model, debug=False) + + # Verify get_litellm_model_info was called + mock_get_info.assert_called_once_with(model=model) + # Verify remove_sensitive_info_from_deployment was called + mock_remove_sensitive.assert_called_once() + # Verify result doesn't have api_key + assert "api_key" not in result + # Verify model_info was enriched + assert "input_cost_per_token" in result["model_info"] + + # Test Case 2: Model enrichment with debug mode + model_with_debug = { + "model_name": "test-model-debug", + "litellm_params": {"model": "gpt-4"}, + "model_info": {}, + } + + mock_router = MagicMock() + mock_client = MagicMock() + mock_router._get_client.return_value = mock_client + + with patch("litellm.proxy.proxy_server.get_litellm_model_info") as mock_get_info, patch( + "litellm.proxy.proxy_server.remove_sensitive_info_from_deployment" + ) as mock_remove_sensitive: + mock_get_info.return_value = {} + mock_remove_sensitive.return_value = { + "model_name": "test-model-debug", + "litellm_params": {"model": "gpt-4"}, + "model_info": {}, + "openai_client": str(mock_client), + } + + result = _enrich_model_info_with_litellm_data( + model=model_with_debug, debug=True, llm_router=mock_router + ) + + # Verify debug info was added + mock_remove_sensitive.assert_called_once() + call_args = mock_remove_sensitive.call_args[0][0] + assert "openai_client" in call_args + # Verify router._get_client was called for debug + mock_router._get_client.assert_called_once() + + # Test Case 3: Model with fallback to litellm.get_model_info + model_fallback = { + "model_name": "test-model-fallback", + "litellm_params": {"model": "claude-3-opus"}, + "model_info": {}, + } + + with patch("litellm.proxy.proxy_server.get_litellm_model_info") as mock_get_info, patch( + "litellm.get_model_info" + ) as mock_litellm_info, patch( + "litellm.proxy.proxy_server.remove_sensitive_info_from_deployment" + ) as mock_remove_sensitive: + # First call returns empty, triggering fallback + mock_get_info.return_value = {} + mock_litellm_info.return_value = { + "input_cost_per_token": 0.015, + "output_cost_per_token": 0.075, + "max_tokens": 200000, + } + mock_remove_sensitive.return_value = { + "model_name": "test-model-fallback", + "litellm_params": {"model": "claude-3-opus"}, + "model_info": { + "input_cost_per_token": 0.015, + "output_cost_per_token": 0.075, + "max_tokens": 200000, + }, + } + + result = _enrich_model_info_with_litellm_data(model=model_fallback, debug=False) + + # Verify fallback was attempted + mock_litellm_info.assert_called_once_with(model="claude-3-opus") + # Verify model_info was enriched with fallback data + call_args = mock_remove_sensitive.call_args[0][0] + assert call_args["model_info"]["input_cost_per_token"] == 0.015 + + # Test Case 4: Model with split model name fallback + model_split = { + "model_name": "test-model-split", + "litellm_params": {"model": "azure/gpt-4"}, + "model_info": {}, + } + + with patch("litellm.proxy.proxy_server.get_litellm_model_info") as mock_get_info, patch( + "litellm.get_model_info" + ) as mock_litellm_info, patch( + "litellm.proxy.proxy_server.remove_sensitive_info_from_deployment" + ) as mock_remove_sensitive: + # Both first and second pass return empty, triggering third pass + mock_get_info.return_value = {} + # Second pass (no split) + mock_litellm_info.side_effect = [ + {}, # First call returns empty + {"max_tokens": 8192}, # Third pass with split succeeds + ] + mock_remove_sensitive.return_value = { + "model_name": "test-model-split", + "litellm_params": {"model": "azure/gpt-4"}, + "model_info": {"max_tokens": 8192}, + } + + result = _enrich_model_info_with_litellm_data(model=model_split, debug=False) + + # Verify third pass was attempted with split model name + assert mock_litellm_info.call_count == 2 + # Check that second call used split model name + second_call = mock_litellm_info.call_args_list[1] + assert second_call[1]["model"] == "gpt-4" + assert second_call[1]["custom_llm_provider"] == "azure" + + # Test Case 5: Model with existing model_info (should preserve existing keys) + model_existing = { + "model_name": "test-model-existing", + "litellm_params": {"model": "gpt-3.5-turbo"}, + "model_info": {"id": "existing-id", "custom_key": "custom_value"}, + } + + with patch("litellm.proxy.proxy_server.get_litellm_model_info") as mock_get_info, patch( + "litellm.proxy.proxy_server.remove_sensitive_info_from_deployment" + ) as mock_remove_sensitive: + mock_get_info.return_value = { + "input_cost_per_token": 0.001, + "id": "new-id", # Should not override existing "id" + } + mock_remove_sensitive.return_value = { + "model_name": "test-model-existing", + "litellm_params": {"model": "gpt-3.5-turbo"}, + "model_info": { + "id": "existing-id", # Existing key preserved + "custom_key": "custom_value", # Existing key preserved + "input_cost_per_token": 0.001, # New key added + }, + } + + result = _enrich_model_info_with_litellm_data(model=model_existing, debug=False) + + # Verify existing keys are preserved + call_args = mock_remove_sensitive.call_args[0][0] + assert call_args["model_info"]["id"] == "existing-id" + assert call_args["model_info"]["custom_key"] == "custom_value" + assert call_args["model_info"]["input_cost_per_token"] == 0.001 From 3075b0e5a25462b76824f5236b0416c39c10de38 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 21 Jan 2026 11:22:20 -0800 Subject: [PATCH 3/3] fixing mypy linting --- litellm/proxy/proxy_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index d168290e8a1..8ae07121177 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -7646,7 +7646,7 @@ async def model_info_v2( # fill in model info based on config.yaml and litellm model_prices_and_context_window.json for i, _model in enumerate(all_models): all_models[i] = _enrich_model_info_with_litellm_data( - model=_model, debug=debug, llm_router=llm_router + model=_model, debug=debug if debug is not None else False, llm_router=llm_router ) verbose_proxy_logger.debug("all_models: %s", all_models)