From 736ff14f116d3d36d1e656e479c02f039d8f439b Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:00:50 -0400 Subject: [PATCH] fix(health): attribute background health check results to their own deployment (#44982) * fix(health): attribute background health check results to their own deployment Co-authored-by: Dennis Pfisterer <302635+pfisterer@users.noreply.github.com> Co-authored-by: Suhas Hanamannavar Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(health): annotate locals with Final and split nested comprehension Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Mubashir Osmani Co-authored-by: Dennis Pfisterer <302635+pfisterer@users.noreply.github.com> Co-authored-by: Suhas Hanamannavar Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../health_endpoints/_health_endpoints.py | 17 +++++++- .../unit/proxy/test_health_check_functions.py | 40 ++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index 1ad01d19e50..fceae2d929c 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -730,6 +730,19 @@ def _build_model_param_to_info_mapping(model_list: list) -> dict: return model_param_to_info +def _model_infos_for_endpoint( + model_param_to_info: Mapping[str, list[Mapping[str, str | None]]], endpoint: Mapping[str, object] +) -> tuple[Mapping[str, str | None], ...]: + model_param: Final = endpoint.get("model") + if not isinstance(model_param, str): + return () + model_infos: Final = model_param_to_info.get(model_param, []) + endpoint_model_id: Final = endpoint.get("model_id") + if not endpoint_model_id: + return tuple(model_infos) + return tuple(info for info in model_infos if info["model_id"] == endpoint_model_id) + + def _aggregate_health_check_results( model_param_to_info: dict, healthy_endpoints: list, @@ -754,7 +767,7 @@ def _aggregate_health_check_results( for endpoint in healthy_endpoints: model_param = endpoint.get("model") if model_param and model_param in model_param_to_info: - for model_info in model_param_to_info[model_param]: + for model_info in _model_infos_for_endpoint(model_param_to_info, endpoint): key = (model_info["model_id"], model_info["model_name"]) if key not in model_results: model_results[key] = { @@ -771,7 +784,7 @@ def _aggregate_health_check_results( model_param = endpoint.get("model") error_message = endpoint.get("error") if model_param and model_param in model_param_to_info: - for model_info in model_param_to_info[model_param]: + for model_info in _model_infos_for_endpoint(model_param_to_info, endpoint): key = (model_info["model_id"], model_info["model_name"]) if key not in model_results: model_results[key] = { diff --git a/tests/unit/proxy/test_health_check_functions.py b/tests/unit/proxy/test_health_check_functions.py index 7d6c5d3cebe..1b2fc73fca7 100644 --- a/tests/unit/proxy/test_health_check_functions.py +++ b/tests/unit/proxy/test_health_check_functions.py @@ -1,11 +1,11 @@ import asyncio import time from datetime import datetime, timedelta, timezone +from typing import Final from unittest.mock import AsyncMock, MagicMock, patch import pytest - from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy.db.health_check_latest import LatestHealthCheckRow from litellm.proxy.health_endpoints._health_endpoints import ( @@ -220,6 +220,44 @@ def test_aggregate_health_check_results_multiple_endpoints(): assert result[key]["unhealthy_count"] == 0 +def test_aggregate_health_check_results_attributes_each_result_to_its_own_deployment(): + deployments: Final = (("a", "qwen"), ("a", "qwen-alias"), ("b", "qwen"), ("b", "qwen-alias")) + model_list: Final = [ + { + "model_name": name, + "litellm_params": {"model": "openai/qwen", "api_base": f"http://{host}:11434/v1"}, + "model_info": {"id": f"{host}-{name}"}, + } + for host, name in deployments + ] + healthy_endpoints: Final = [ + {"model": "openai/qwen", "api_base": "http://a:11434/v1", "model_id": "a-qwen"}, + {"model": "openai/qwen", "api_base": "http://a:11434/v1", "model_id": "a-qwen-alias"}, + ] + unhealthy_endpoints: Final = [ + {"model": "openai/qwen", "api_base": "http://b:11434/v1", "model_id": "b-qwen", "error": "Connection error."}, + { + "model": "openai/qwen", + "api_base": "http://b:11434/v1", + "model_id": "b-qwen-alias", + "error": "Connection error.", + }, + ] + + result: Final = _aggregate_health_check_results( + _build_model_param_to_info_mapping(model_list), healthy_endpoints, unhealthy_endpoints + ) + + assert { + key: (value["healthy_count"], value["unhealthy_count"], value["error_message"]) for key, value in result.items() + } == { + ("a-qwen", "qwen"): (1, 0, None), + ("a-qwen-alias", "qwen-alias"): (1, 0, None), + ("b-qwen", "qwen"): (0, 1, "Connection error."), + ("b-qwen-alias", "qwen-alias"): (0, 1, "Connection error."), + } + + @pytest.mark.asyncio async def test_save_health_check_results_if_changed_status_changed(): """Test saving when status changes"""