diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index d213ea628bc..6513ea1960b 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -8885,6 +8885,7 @@ async def _get_db_only_models( sort_by: Optional[str] = None, page: int = 1, size: int = 50, + filtered_router_count: Optional[int] = None, ) -> Tuple[List[Dict[str, Any]], int]: """ Fetch models from the database that are NOT already loaded in the router. @@ -8894,13 +8895,16 @@ async def _get_db_only_models( those models still appear in the model list. Args: - all_models: Models already in the router + all_models: Models already in the router (used to collect DB model IDs to exclude) prisma_client: Prisma client for database queries proxy_config: Proxy config for decrypting models search: Optional search term (case-insensitive) sort_by: Optional sort field page: Current page number size: Page size + filtered_router_count: Number of router models after search filtering. + When provided, used for pagination instead of len(all_models). + This ensures correct pagination when search reduces the visible router models. Returns: Tuple of (db_only_models, db_only_count) @@ -8936,8 +8940,8 @@ async def _get_db_only_models( if db_only_count == 0: return [], 0 - # Determine how many to fetch - router_models_count = len(all_models) + # Use filtered count for pagination when searching, full count otherwise + router_models_count = filtered_router_count if filtered_router_count is not None else len(all_models) models_needed_for_page = size * page if sort_by: @@ -9018,6 +9022,7 @@ async def _apply_search_filter_to_models( sort_by=sort_by, page=page, size=size, + filtered_router_count=len(filtered_router_models), ) total_count = len(filtered_router_models) + db_only_count diff --git a/tests/test_litellm/proxy/test_model_list_db_only_models.py b/tests/test_litellm/proxy/test_model_list_db_only_models.py index 8842a633cbb..e6808ba7f79 100644 --- a/tests/test_litellm/proxy/test_model_list_db_only_models.py +++ b/tests/test_litellm/proxy/test_model_list_db_only_models.py @@ -258,3 +258,46 @@ class TestApplySearchFilterIncludesDbOnlyModels: assert result == router_models assert total_count is None + + @pytest.mark.asyncio + async def test_search_pagination_uses_filtered_count(self): + """When searching, pagination should use the filtered router count, not the full count. + + Regression test: if the router has 50 models but only 1 matches search, + DB-only models should still be fetched (take should be based on 1, not 50). + """ + # 50 router models, but only "gpt-5" matches the search "gpt-5" + router_models = [_make_router_model(f"model-{i}", f"id-{i}", db_model=True) for i in range(49)] + router_models.append(_make_router_model("gpt-5", "id-49", db_model=True)) + + db_record = _make_db_record("gpt-5.1", "id-missing") + + prisma_client = MagicMock() + prisma_client.db.litellm_proxymodeltable.count = AsyncMock(return_value=1) + prisma_client.db.litellm_proxymodeltable.find_many = AsyncMock( + return_value=[db_record] + ) + + decrypted_model = _make_router_model("gpt-5.1", "id-missing", db_model=True) + proxy_config = MagicMock() + proxy_config.decrypt_model_list_from_db = MagicMock( + return_value=[decrypted_model] + ) + + result, total_count = await _apply_search_filter_to_models( + all_models=router_models, + search="gpt-5", + page=1, + size=50, + prisma_client=prisma_client, + proxy_config=proxy_config, + ) + + model_names = [m["model_name"] for m in result] + # Only gpt-5 matches search from router, gpt-5.1 from DB + assert "gpt-5" in model_names + assert "gpt-5.1" in model_names + assert len(result) == 2 + assert total_count == 2 + # Verify find_many was actually called (not skipped due to bad take calculation) + prisma_client.db.litellm_proxymodeltable.find_many.assert_called_once()