litellm/tests/test_litellm/proxy/test_health_check_functions.py
yassin c6a36fb8b6 fix(health): skip background health check DB writes when the latest-row read fails
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
(cherry picked from commit 6d2c4899b0)
2026-09-14 23:03:30 +00:00

910 lines
32 KiB
Python

import asyncio
import time
from datetime import datetime, timedelta, timezone
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 (
_aggregate_health_check_results,
_build_model_param_to_info_mapping,
_perform_health_check_and_save,
_save_background_health_checks_to_db,
_save_health_check_results_if_changed,
_save_health_check_to_db,
latest_health_checks_endpoint,
)
from litellm.proxy.utils import PrismaClient
@pytest.fixture
def mock_prisma():
"""Simplified mock PrismaClient with bound methods"""
client = MagicMock()
client.db.litellm_healthchecktable.create = AsyncMock(return_value={"id": "test-id"})
client.db.litellm_healthchecktable.find_many = AsyncMock(return_value=[{"id": "1", "model_name": "test"}])
# Bind actual methods
import types
for method in [
"save_health_check_result",
"_validate_response_time",
"_clean_details",
"get_health_check_history",
"get_all_latest_health_checks",
]:
setattr(client, method, types.MethodType(getattr(PrismaClient, method), client))
return client
@pytest.mark.asyncio
@pytest.mark.parametrize(
"status,healthy,unhealthy,should_succeed",
[
("healthy", 1, 0, True),
("unhealthy", 0, 1, True),
("healthy", 1, 0, False), # Database error case
],
)
async def test_save_health_check_result(mock_prisma, status, healthy, unhealthy, should_succeed):
"""Test health check result saving with various scenarios"""
if not should_succeed:
mock_prisma.db.litellm_healthchecktable.create.side_effect = Exception("DB Error")
result = await mock_prisma.save_health_check_result(
model_name="test-model",
status=status,
healthy_count=healthy,
unhealthy_count=unhealthy,
)
if should_succeed:
mock_prisma.db.litellm_healthchecktable.create.assert_called_once()
else:
assert result is None
@pytest.mark.asyncio
async def test_get_health_check_history(mock_prisma):
"""Test health check history retrieval"""
result = await mock_prisma.get_health_check_history(model_name="test", limit=50)
mock_prisma.db.litellm_healthchecktable.find_many.assert_called_once()
assert len(result) == 1
@pytest.mark.asyncio
@pytest.mark.parametrize(
"healthy_count,unhealthy_count,expected_status",
[
(1, 0, "healthy"),
(0, 1, "unhealthy"),
(2, 1, "healthy"),
],
)
async def test_save_health_check_to_db(healthy_count, unhealthy_count, expected_status):
"""Test _save_health_check_to_db function with different endpoint counts"""
mock_client = MagicMock()
mock_client.save_health_check_result = AsyncMock()
healthy_endpoints = [{"model": "test"}] * healthy_count
unhealthy_endpoints = [{"error": "test error"}] * unhealthy_count
await _save_health_check_to_db(
mock_client,
"test-model",
healthy_endpoints,
unhealthy_endpoints,
1234567890.0,
"test-user",
)
call_args = mock_client.save_health_check_result.call_args[1]
assert call_args["status"] == expected_status
assert call_args["healthy_count"] == healthy_count
assert call_args["unhealthy_count"] == unhealthy_count
@pytest.mark.asyncio
async def test_save_health_check_to_db_no_client():
"""Test graceful handling when no database client"""
result = await _save_health_check_to_db(None, "test", [], [], 0.0, "user")
assert result is None
# Tests for background health check functions
def test_build_model_param_to_info_mapping():
"""Test building model parameter to info mapping"""
model_list = [
{
"model_name": "gpt-3.5-turbo",
"model_info": {"id": "model-123"},
"litellm_params": {"model": "gpt-3.5-turbo"},
},
{
"model_name": "gpt-4",
"model_info": {"id": "model-456"},
"litellm_params": {"model": "gpt-4"},
},
{
"model_name": "gpt-3.5-turbo-alias",
"model_info": {"id": "model-789"},
"litellm_params": {"model": "gpt-3.5-turbo"}, # Same model param
},
]
result = _build_model_param_to_info_mapping(model_list)
assert "gpt-3.5-turbo" in result
assert "gpt-4" in result
assert len(result["gpt-3.5-turbo"]) == 2 # Two models share same param
assert len(result["gpt-4"]) == 1
assert result["gpt-3.5-turbo"][0]["model_name"] == "gpt-3.5-turbo"
assert result["gpt-3.5-turbo"][0]["model_id"] == "model-123"
assert result["gpt-3.5-turbo"][1]["model_name"] == "gpt-3.5-turbo-alias"
assert result["gpt-3.5-turbo"][1]["model_id"] == "model-789"
def test_build_model_param_to_info_mapping_no_model_name():
"""Test mapping skips models without model_name"""
model_list = [
{
"model_info": {"id": "model-123"},
"litellm_params": {"model": "gpt-3.5-turbo"},
},
]
result = _build_model_param_to_info_mapping(model_list)
assert len(result) == 0
def test_aggregate_health_check_results():
"""Test aggregating health check results per model"""
model_param_to_info = {
"gpt-3.5-turbo": [
{"model_name": "gpt-3.5-turbo", "model_id": "model-123"},
],
"gpt-4": [
{"model_name": "gpt-4", "model_id": "model-456"},
],
}
healthy_endpoints = [
{"model": "gpt-3.5-turbo"},
]
unhealthy_endpoints = [
{"model": "gpt-4", "error": "Rate limit exceeded"},
]
result = _aggregate_health_check_results(model_param_to_info, healthy_endpoints, unhealthy_endpoints)
# Check gpt-3.5-turbo is healthy
gpt35_key = ("model-123", "gpt-3.5-turbo")
assert gpt35_key in result
assert result[gpt35_key]["healthy_count"] == 1
assert result[gpt35_key]["unhealthy_count"] == 0
assert result[gpt35_key]["error_message"] is None
# Check gpt-4 is unhealthy
gpt4_key = ("model-456", "gpt-4")
assert gpt4_key in result
assert result[gpt4_key]["healthy_count"] == 0
assert result[gpt4_key]["unhealthy_count"] == 1
assert "Rate limit" in result[gpt4_key]["error_message"]
def test_aggregate_health_check_results_multiple_endpoints():
"""Test aggregation with multiple endpoints for same model"""
model_param_to_info = {
"gpt-3.5-turbo": [
{"model_name": "gpt-3.5-turbo", "model_id": "model-123"},
],
}
healthy_endpoints = [
{"model": "gpt-3.5-turbo"},
{"model": "gpt-3.5-turbo"},
]
unhealthy_endpoints = []
result = _aggregate_health_check_results(model_param_to_info, healthy_endpoints, unhealthy_endpoints)
key = ("model-123", "gpt-3.5-turbo")
assert result[key]["healthy_count"] == 2
assert result[key]["unhealthy_count"] == 0
@pytest.mark.asyncio
async def test_save_health_check_results_if_changed_status_changed():
"""Test saving when status changes"""
mock_prisma = MagicMock()
mock_prisma.save_health_check_result = AsyncMock()
model_results = {
("model-123", "gpt-3.5-turbo"): {
"model_name": "gpt-3.5-turbo",
"model_id": "model-123",
"healthy_count": 1,
"unhealthy_count": 0,
"error_message": None,
},
}
# Latest check shows unhealthy, new result is healthy (status changed)
latest_checks_map = {
"model-123": MagicMock(
status="unhealthy",
checked_at=datetime.now(timezone.utc) - timedelta(minutes=5),
),
}
start_time = 1234567890.0
await _save_health_check_results_if_changed(
mock_prisma,
model_results,
latest_checks_map,
start_time,
"background_health_check",
)
# Should save because status changed
mock_prisma.save_health_check_result.assert_called_once()
call_kwargs = mock_prisma.save_health_check_result.call_args[1]
assert call_kwargs["status"] == "healthy"
assert call_kwargs["model_name"] == "gpt-3.5-turbo"
assert call_kwargs["checked_by"] == "background_health_check"
@pytest.mark.asyncio
async def test_save_health_check_results_if_changed_status_unchanged_recent():
"""Test skipping save when status unchanged and checked recently"""
mock_prisma = MagicMock()
mock_prisma.save_health_check_result = AsyncMock()
model_results = {
("model-123", "gpt-3.5-turbo"): {
"model_name": "gpt-3.5-turbo",
"model_id": "model-123",
"healthy_count": 1,
"unhealthy_count": 0,
"error_message": None,
},
}
# Latest check shows healthy, new result is healthy (status unchanged)
# And checked recently (within 1 hour)
latest_checks_map = {
"model-123": MagicMock(
status="healthy",
checked_at=datetime.now(timezone.utc) - timedelta(minutes=30),
),
}
start_time = 1234567890.0
await _save_health_check_results_if_changed(
mock_prisma,
model_results,
latest_checks_map,
start_time,
"background_health_check",
)
# Should NOT save because status unchanged and checked recently
mock_prisma.save_health_check_result.assert_not_called()
@pytest.mark.asyncio
async def test_save_health_check_results_if_changed_status_unchanged_old():
"""Test saving when status unchanged but last check is old (>1 hour)"""
mock_prisma = MagicMock()
mock_prisma.save_health_check_result = AsyncMock()
model_results = {
("model-123", "gpt-3.5-turbo"): {
"model_name": "gpt-3.5-turbo",
"model_id": "model-123",
"healthy_count": 1,
"unhealthy_count": 0,
"error_message": None,
},
}
# Latest check shows healthy, new result is healthy (status unchanged)
# But checked >1 hour ago
latest_checks_map = {
"model-123": MagicMock(
status="healthy",
checked_at=datetime.now(timezone.utc) - timedelta(hours=2),
),
}
start_time = 1234567890.0
await _save_health_check_results_if_changed(
mock_prisma,
model_results,
latest_checks_map,
start_time,
"background_health_check",
)
# Should save because last check is old (>1 hour)
mock_prisma.save_health_check_result.assert_called_once()
@pytest.mark.asyncio
async def test_save_health_check_results_if_changed_no_previous_check():
"""Test saving when there's no previous check"""
mock_prisma = MagicMock()
mock_prisma.save_health_check_result = AsyncMock()
model_results = {
("model-123", "gpt-3.5-turbo"): {
"model_name": "gpt-3.5-turbo",
"model_id": "model-123",
"healthy_count": 1,
"unhealthy_count": 0,
"error_message": None,
},
}
# No previous check
latest_checks_map = {}
start_time = 1234567890.0
await _save_health_check_results_if_changed(
mock_prisma,
model_results,
latest_checks_map,
start_time,
"background_health_check",
)
# Should save because no previous check
mock_prisma.save_health_check_result.assert_called_once()
@pytest.mark.asyncio
async def test_save_background_health_checks_to_db():
"""Test the main background health check save function"""
mock_prisma = MagicMock()
mock_prisma.save_health_check_result = AsyncMock()
mock_prisma.db.query_raw = AsyncMock(return_value=[])
model_list = [
{
"model_name": "gpt-3.5-turbo",
"model_info": {"id": "model-123"},
"litellm_params": {"model": "gpt-3.5-turbo"},
},
]
healthy_endpoints = [{"model": "gpt-3.5-turbo"}]
unhealthy_endpoints = []
start_time = 1234567890.0
persisted = await _save_background_health_checks_to_db(
mock_prisma,
model_list,
healthy_endpoints,
unhealthy_endpoints,
start_time,
"background_health_check",
)
# Should read the latest rows and save_health_check_result, and report completion
assert persisted is True
mock_prisma.db.query_raw.assert_awaited_once()
mock_prisma.save_health_check_result.assert_called_once()
call_kwargs = mock_prisma.save_health_check_result.call_args[1]
assert call_kwargs["model_name"] == "gpt-3.5-turbo"
assert call_kwargs["model_id"] == "model-123"
assert call_kwargs["status"] == "healthy"
assert call_kwargs["checked_by"] == "background_health_check"
def _two_model_results():
return {
("model-1", "gpt-4"): {
"model_name": "gpt-4",
"model_id": "model-1",
"healthy_count": 1,
"unhealthy_count": 0,
"error_message": None,
},
("model-2", "gpt-4o"): {
"model_name": "gpt-4o",
"model_id": "model-2",
"healthy_count": 0,
"unhealthy_count": 1,
"error_message": "boom",
},
}
@pytest.mark.asyncio
async def test_save_health_check_results_if_changed_awaits_every_write_and_reports_success():
"""Writes are awaited, not detached, so the caller can tell the cycle's persistence completed."""
mock_prisma = MagicMock()
mock_prisma.save_health_check_result = AsyncMock(return_value={"id": "row"})
persisted = await _save_health_check_results_if_changed(
mock_prisma, _two_model_results(), {}, 1234567890.0, "background_health_check"
)
assert (persisted, mock_prisma.save_health_check_result.await_count) == (True, 2)
@pytest.mark.asyncio
async def test_save_health_check_results_if_changed_reports_failure_when_a_write_returns_none():
"""save_health_check_result swallows DB errors and returns None; that must surface as False."""
mock_prisma = MagicMock()
mock_prisma.save_health_check_result = AsyncMock(side_effect=[{"id": "row"}, None])
persisted = await _save_health_check_results_if_changed(
mock_prisma, _two_model_results(), {}, 1234567890.0, "background_health_check"
)
assert (persisted, mock_prisma.save_health_check_result.await_count) == (False, 2)
@pytest.mark.asyncio
async def test_save_health_check_results_if_changed_reports_success_when_nothing_needed_writing():
mock_prisma = MagicMock()
mock_prisma.save_health_check_result = AsyncMock()
model_results = {
("model-1", "gpt-4"): {
"model_name": "gpt-4",
"model_id": "model-1",
"healthy_count": 1,
"unhealthy_count": 0,
"error_message": None,
},
}
latest_checks_map = {
"model-1": MagicMock(status="healthy", checked_at=datetime.now(timezone.utc) - timedelta(minutes=5)),
}
persisted = await _save_health_check_results_if_changed(
mock_prisma, model_results, latest_checks_map, 1234567890.0, "background_health_check"
)
assert (persisted, mock_prisma.save_health_check_result.await_count) == (True, 0)
def _one_model_setup():
model_list = [
{
"model_name": "gpt-3.5-turbo",
"model_info": {"id": "model-123"},
"litellm_params": {"model": "gpt-3.5-turbo"},
},
]
return model_list, [{"model": "gpt-3.5-turbo"}], []
@pytest.mark.asyncio
async def test_save_background_health_checks_to_db_returns_false_when_a_write_fails():
mock_prisma = MagicMock()
mock_prisma.db.query_raw = AsyncMock(return_value=[])
mock_prisma.save_health_check_result = AsyncMock(return_value=None)
model_list, healthy_endpoints, unhealthy_endpoints = _one_model_setup()
persisted = await _save_background_health_checks_to_db(
mock_prisma, model_list, healthy_endpoints, unhealthy_endpoints, 1234567890.0, "background_health_check"
)
assert (persisted, mock_prisma.save_health_check_result.await_count) == (False, 1)
@pytest.mark.asyncio
async def test_save_background_health_checks_to_db_writes_nothing_when_the_latest_row_read_fails(mock_prisma):
"""
A failed dedup read must not read as an empty table. Treated that way, every model was written on every
cycle by every pod while the read kept failing, which is what filled the table in production.
"""
mock_prisma.db.query_raw = AsyncMock(side_effect=RuntimeError("db down"))
mock_prisma.save_health_check_result = AsyncMock(return_value={"id": "row"})
model_list, healthy_endpoints, unhealthy_endpoints = _one_model_setup()
persisted = await _save_background_health_checks_to_db(
mock_prisma, model_list, healthy_endpoints, unhealthy_endpoints, 1234567890.0, "background_health_check"
)
assert (persisted, mock_prisma.save_health_check_result.await_count) == (False, 0)
@pytest.mark.asyncio
async def test_save_background_health_checks_to_db_no_prisma():
"""Test graceful handling when no prisma client"""
result = await _save_background_health_checks_to_db(None, [], [], [], 0.0, "background_health_check")
assert result is False
@pytest.mark.asyncio
async def test_save_background_health_checks_to_db_exception_handling():
"""Test exception handling in background health check save"""
mock_prisma = MagicMock()
mock_prisma.db.query_raw = AsyncMock(side_effect=Exception("DB Error"))
model_list = [
{
"model_name": "gpt-3.5-turbo",
"model_info": {"id": "model-123"},
"litellm_params": {"model": "gpt-3.5-turbo"},
},
]
# Must not raise (the health check loop has to survive a DB outage) but must report
# the failure, so the window lock can be released for another pod to retry
persisted = await _save_background_health_checks_to_db(
mock_prisma, model_list, [], [], 0.0, "background_health_check"
)
assert persisted is False
def _raw_latest_row(model_name: str, model_id, checked_at: datetime) -> dict:
return {
"health_check_id": f"hc-{model_id or 'no-id'}-{model_name}",
"model_name": model_name,
"model_id": model_id,
"status": "healthy",
"healthy_count": 1,
"unhealthy_count": 0,
"error_message": None,
"response_time_ms": 10.0,
"details": None,
"checked_by": "pod-1",
"checked_at": checked_at.isoformat(),
"created_at": checked_at.isoformat(),
"updated_at": checked_at.isoformat(),
}
@pytest.mark.asyncio
async def test_get_all_latest_health_checks_keeps_every_distinct_group_with_its_own_checked_at(mock_prisma):
"""
Postgres owns the dedup. (id, name), (other id, name) and (NULL, name) are distinct groups and each row
must arrive typed, with its own checked_at, for the 1h re-save compare and the id-or-name lookup key.
"""
now = datetime.now(timezone.utc)
mock_prisma.db.query_raw = AsyncMock(
return_value=[
_raw_latest_row("gpt-3.5-turbo", "model-123", now - timedelta(minutes=1)),
_raw_latest_row("gpt-3.5-turbo", "model-456", now - timedelta(minutes=5)),
_raw_latest_row("gpt-4", "deployment-abc", now - timedelta(minutes=2)),
_raw_latest_row("gpt-4", None, now - timedelta(minutes=3)),
]
)
result = await mock_prisma.get_all_latest_health_checks()
assert {(check.model_id, check.model_name): check.checked_at for check in result} == {
("model-123", "gpt-3.5-turbo"): now - timedelta(minutes=1),
("model-456", "gpt-3.5-turbo"): now - timedelta(minutes=5),
("deployment-abc", "gpt-4"): now - timedelta(minutes=2),
(None, "gpt-4"): now - timedelta(minutes=3),
}
@pytest.mark.asyncio
async def test_save_background_health_checks_compares_raw_checked_at_against_utc_now(mock_prisma):
"""
Raw rows carry ISO strings and the engine may omit the offset. A naive checked_at would TypeError
inside the 1h compare, be swallowed, and silently stop every save; a stale row must still re-save.
"""
stale = (datetime.now(timezone.utc) - timedelta(hours=2)).replace(tzinfo=None)
fresh = datetime.now(timezone.utc) - timedelta(minutes=5)
mock_prisma.db.query_raw = AsyncMock(
return_value=[
_raw_latest_row("stale-model", "stale-id", stale),
_raw_latest_row("fresh-model", "fresh-id", fresh),
]
)
mock_prisma.save_health_check_result = AsyncMock()
model_list = [
{"model_name": "stale-model", "model_info": {"id": "stale-id"}, "litellm_params": {"model": "openai/stale"}},
{"model_name": "fresh-model", "model_info": {"id": "fresh-id"}, "litellm_params": {"model": "openai/fresh"}},
]
await _save_background_health_checks_to_db(
mock_prisma,
model_list,
[{"model": "openai/stale"}, {"model": "openai/fresh"}],
[],
time.time(),
"pod-1",
)
await asyncio.sleep(0)
assert [call.kwargs["model_id"] for call in mock_prisma.save_health_check_result.await_args_list] == ["stale-id"]
@pytest.mark.asyncio
async def test_latest_health_checks_endpoint_serialises_raw_rows(monkeypatch):
row = LatestHealthCheckRow(
health_check_id="hc-1",
model_name="gpt-4",
model_id="deployment-abc",
status="healthy",
healthy_count=1,
unhealthy_count=0,
error_message=None,
response_time_ms=12.5,
details='{"region": "eu"}',
checked_by="pod-1",
checked_at=datetime(2026, 8, 25),
created_at=datetime(2026, 8, 25, tzinfo=timezone.utc),
updated_at=datetime(2026, 8, 25, tzinfo=timezone.utc),
)
prisma = MagicMock()
prisma.get_all_latest_health_checks = AsyncMock(return_value=(row,))
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", prisma)
response = await latest_health_checks_endpoint(user_api_key_dict=UserAPIKeyAuth())
assert response == {
"latest_health_checks": {
"deployment-abc": {
"health_check_id": "hc-1",
"model_name": "gpt-4",
"model_id": "deployment-abc",
"status": "healthy",
"healthy_count": 1,
"unhealthy_count": 0,
"error_message": None,
"response_time_ms": 12.5,
"details": {"region": "eu"},
"checked_by": "pod-1",
"checked_at": "2026-08-25T00:00:00+00:00",
"created_at": "2026-08-25T00:00:00+00:00",
}
},
"total_models": 1,
}
@pytest.mark.asyncio
async def test_perform_health_check_and_save_passes_model_id_to_perform_health_check():
"""Test that _perform_health_check_and_save passes model_id to perform_health_check so health checks run by model id."""
model_list = [
{
"model_name": "gpt-4",
"model_info": {"id": "deployment-abc"},
"litellm_params": {"model": "gpt-4"},
},
]
healthy = [{"model": "gpt-4"}]
unhealthy = []
async def mock_perform_health_check(
model_list,
model=None,
cli_model=None,
details=True,
model_id=None,
max_concurrency=None,
**kwargs,
):
return healthy, unhealthy, {}
with patch(
"litellm.proxy.health_endpoints._health_endpoints.perform_health_check",
side_effect=mock_perform_health_check,
) as mock_perform:
result = await _perform_health_check_and_save(
model_list=model_list,
target_model=None,
cli_model=None,
details=True,
prisma_client=None,
start_time=0.0,
user_id="user-1",
model_id="deployment-abc",
)
mock_perform.assert_called_once()
call_kwargs = mock_perform.call_args[1]
assert call_kwargs["model_id"] == "deployment-abc"
assert result["healthy_count"] == 1
assert result["unhealthy_count"] == 0
@pytest.mark.asyncio
async def test_perform_health_check_and_save_forwards_skip_disabled_background_flag():
"""health_check_skip_disabled_background_models should reach perform_health_check."""
model_list = [
{
"model_name": "gpt-4",
"model_info": {"id": "deployment-abc"},
"litellm_params": {"model": "gpt-4"},
},
]
async def mock_perform_health_check(**kwargs):
return [], [], {}
with patch(
"litellm.proxy.health_endpoints._health_endpoints.perform_health_check",
side_effect=mock_perform_health_check,
) as mock_perform:
await _perform_health_check_and_save(
model_list=model_list,
target_model=None,
cli_model=None,
details=True,
prisma_client=None,
start_time=0.0,
user_id="user-1",
model_id=None,
health_check_skip_disabled_background_models=True,
)
call_kwargs = mock_perform.call_args[1]
assert call_kwargs["health_check_skip_disabled_background_models"] is True
@pytest.mark.asyncio
async def test_perform_health_check_narrows_to_a_team_deployment_by_its_public_name():
"""``/health?model=<team_public_model_name>`` must probe the team deployment, not an empty list."""
from litellm.proxy.health_check import perform_health_check
team_deployment = {
"model_name": "bedrock-nova_team-b_9f2c",
"litellm_params": {"model": "bedrock/us.amazon.nova-2-lite-v1:0"},
"model_info": {"id": "id-team-b", "team_id": "team-b", "team_public_model_name": "bedrock-nova"},
}
other_deployment = {
"model_name": "gpt-5.4-mini",
"litellm_params": {"model": "openai/gpt-5.4-mini"},
"model_info": {"id": "id-openai"},
}
probe = AsyncMock(return_value=([{"model": "bedrock/us.amazon.nova-2-lite-v1:0", "model_id": "id-team-b"}], [], {}))
with patch( # test-quality-ok: the deployments handed to the probe are the assertion; no injection seam
"litellm.proxy.health_check._perform_health_check", probe
):
healthy, unhealthy, _ = await perform_health_check(
model_list=[team_deployment, other_deployment], model="bedrock-nova", team_id="team-b"
)
assert [m["model_info"]["id"] for m in probe.call_args.args[0]] == ["id-team-b"]
assert [ep["model_id"] for ep in healthy] == ["id-team-b"]
assert unhealthy == []
@pytest.mark.asyncio
async def test_perform_health_check_keeps_a_public_name_off_another_team():
"""A team's public model name is not a global alias: a caller from another team must not probe its deployment."""
from litellm.proxy.health_check import perform_health_check
team_deployment = {
"model_name": "bedrock-nova_team-b_9f2c",
"litellm_params": {"model": "bedrock/us.amazon.nova-2-lite-v1:0"},
"model_info": {"id": "id-team-b", "team_id": "team-b", "team_public_model_name": "bedrock-nova"},
}
probe = AsyncMock(return_value=([{"model": "bedrock/us.amazon.nova-2-lite-v1:0", "model_id": "id-team-b"}], [], {}))
with patch( # test-quality-ok: the deployments handed to the probe are the assertion; no injection seam
"litellm.proxy.health_check._perform_health_check", probe
):
healthy, unhealthy, _ = await perform_health_check(
model_list=[team_deployment], model="bedrock-nova", team_id="team-a"
)
probe.assert_not_awaited()
assert healthy == []
assert unhealthy == []
_GLOBAL_DEPLOYMENT = {
"model_name": "bedrock-nova",
"litellm_params": {"model": "bedrock/us.amazon.nova-2-lite-v1:0"},
"model_info": {"id": "id-bedrock"},
}
_TEAM_B_COPY = {
"model_name": "bedrock-nova_team-b_9f2c",
"litellm_params": {"model": "bedrock/us.amazon.nova-2-lite-v1:0"},
"model_info": {"id": "id-team-b", "team_id": "team-b", "team_public_model_name": "bedrock-nova"},
}
_GLOBAL_BARE_NAME = {
"model_name": "gpt-5.4-nano",
"litellm_params": {"model": "gpt-5.4-nano"},
"model_info": {"id": "id-nano"},
}
_TEAM_B_BARE_COPY = {
"model_name": "gpt-5.4-nano_team-b_7c3d",
"litellm_params": {"model": "gpt-5.4-nano"},
"model_info": {"id": "id-nano-team-b", "team_id": "team-b", "team_public_model_name": "gpt-5.4-nano"},
}
@pytest.mark.asyncio
@pytest.mark.parametrize(
("team_id", "model", "model_list", "expected_ids"),
[
(None, "bedrock-nova", [_TEAM_B_COPY], ["id-team-b"]),
(None, "bedrock-nova", [_GLOBAL_DEPLOYMENT, _TEAM_B_COPY], ["id-bedrock"]),
("team-b", "bedrock-nova", [_GLOBAL_DEPLOYMENT, _TEAM_B_COPY], ["id-team-b"]),
("team-b", "gpt-5.4-nano", [_GLOBAL_BARE_NAME, _TEAM_B_BARE_COPY], ["id-nano-team-b"]),
(None, "gpt-5.4-nano", [_GLOBAL_BARE_NAME, _TEAM_B_BARE_COPY], ["id-nano"]),
(None, "bedrock/us.amazon.nova-2-lite-v1:0", [_GLOBAL_DEPLOYMENT, _TEAM_B_COPY], ["id-bedrock", "id-team-b"]),
],
ids=[
"a team-less caller reaches a public name nothing else carries",
"model_name wins over a public name for a team-less caller",
"a team's own copy wins over the global model_name",
"a team's own copy wins over a litellm_params.model equal to the public name",
"model_name wins over a litellm_params.model equal to it for a team-less caller",
"a provider model string no name carries still matches litellm_params.model",
],
)
async def test_perform_health_check_targets_a_name_the_way_a_request_for_it_routes(
team_id, model, model_list, expected_ids
):
"""``/health?model=<name>`` probes the deployments a request for that name from the same caller would route to."""
from litellm.proxy.health_check import perform_health_check
probe = AsyncMock(
return_value=([{"model": "bedrock/us.amazon.nova-2-lite-v1:0", "model_id": i} for i in expected_ids], [], {})
)
with patch( # test-quality-ok: the deployments handed to the probe are the assertion; no injection seam
"litellm.proxy.health_check._perform_health_check", probe
):
healthy, unhealthy, _ = await perform_health_check(model_list=model_list, model=model, team_id=team_id)
assert [m["model_info"]["id"] for m in probe.call_args.args[0]] == expected_ids
assert [ep["model_id"] for ep in healthy] == expected_ids
assert unhealthy == []
def test_parse_background_health_check_model_groups_unset_returns_none():
from litellm.proxy.health_check import parse_background_health_check_model_groups
assert parse_background_health_check_model_groups(None) is None
assert parse_background_health_check_model_groups({}) is None
assert parse_background_health_check_model_groups({"background_health_check_model_groups": None}) is None
def test_parse_background_health_check_model_groups_list_returns_frozenset():
from litellm.proxy.health_check import parse_background_health_check_model_groups
parsed = parse_background_health_check_model_groups(
{"background_health_check_model_groups": ["prod-openai", "prod-claude"]}
)
assert parsed == frozenset({"prod-openai", "prod-claude"})
@pytest.mark.parametrize("bad_value", ["prod-openai", 42, {"a": 1}, [1, 2], [None]])
def test_parse_background_health_check_model_groups_malformed_raises(bad_value):
from litellm.proxy.health_check import parse_background_health_check_model_groups
with pytest.raises(ValueError, match="must be a list of model group names"):
parse_background_health_check_model_groups({"background_health_check_model_groups": bad_value})
def test_filter_deployments_to_model_groups():
from litellm.proxy.health_check import filter_deployments_to_model_groups
model_list = [
{"model_name": "prod-openai", "model_info": {"id": "a"}},
{"model_name": "internal-claude", "model_info": {"id": "b"}},
{"model_name": "prod-openai", "model_info": {"id": "c"}},
]
assert filter_deployments_to_model_groups(model_list, None) == tuple(model_list)
assert filter_deployments_to_model_groups(model_list, frozenset({"prod-openai"})) == (model_list[0], model_list[2])
assert filter_deployments_to_model_groups(model_list, frozenset()) == ()
if __name__ == "__main__":
pytest.main([__file__])