feat(health): opt-in model-group allowlist for background health checks and health-check routing (#38539)

* feat(health): opt-in model-group allowlist for background health checks and health-check routing

* fix(health): merge shared health states per writer scope instead of replacing

* refactor(health): drop restating comment and parameterize test scope annotations

* chore: remove stray generated prisma migration file

* fix(health): merge health states against the Redis snapshot, not the pod-local copy

* fix(health): fall back to the pod-local snapshot when the Redis read returns nothing
This commit is contained in:
Mateo Wang 2026-08-27 12:25:56 -07:00 • committed by GitHub
parent 493bca667b
commit 452254963e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 452 additions and 12 deletions

View file

@ -2510,6 +2510,18 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase):
"are skipped for on-demand GET /health as well as the background health loop."
),
)
background_health_check_model_groups: tuple[str, ...] | None = Field(
None,
description=(
"Opt-in allowlist of model group names for background health checks and "
"health-check routing. When set, the background loop probes only deployments "
"whose model_name is listed, and enable_health_check_routing filters unhealthy "
"deployments only within the listed groups; every other group, including newly "
"added deployments, is skipped and keeps its configured routing strategy. "
"When unset, all deployments participate (opt out per deployment via "
"model_info.disable_background_health_check)."
),
)
model_list_healthy_only: bool | None = Field(
None,
description=(

View file

@ -7,8 +7,11 @@ import sys
import threading
import time
from collections.abc import Mapping, Sequence
from collections.abc import Set as AbstractSet
from types import MappingProxyType
from typing import TYPE_CHECKING, Final
from typing import TYPE_CHECKING, Final, TypeVar
from pydantic import TypeAdapter, ValidationError
import litellm
@ -16,6 +19,7 @@ if TYPE_CHECKING:
from litellm.router import Router
logger: Final = logging.getLogger(__name__)
_DeploymentT: Final = TypeVar("_DeploymentT", bound=Mapping[str, object])
from litellm.constants import (
BACKGROUND_HEALTH_CHECK_MAX_TOKENS,
BACKGROUND_HEALTH_CHECK_MAX_TOKENS_REASONING,
@ -167,6 +171,38 @@ def health_check_filter_kwargs_from_general_settings(
}
def parse_background_health_check_model_groups(
general_settings: Mapping[str, object] | None,
) -> frozenset[str] | None:
"""
Read ``general_settings.background_health_check_model_groups``.
``None`` means the allowlist is unset and every deployment participates
(legacy behavior). A list scopes background health checks and health-check
routing to deployments whose ``model_name`` is listed. A malformed value
raises so the proxy fails at startup instead of silently probing everything.
"""
raw: Final = (general_settings or {}).get("background_health_check_model_groups")
if raw is None:
return None
try:
return frozenset(TypeAdapter(list[str]).validate_python(raw))
except ValidationError as e:
raise ValueError(
"general_settings.background_health_check_model_groups must be a list of model group names"
) from e
def filter_deployments_to_model_groups(
model_list: Sequence[_DeploymentT],
model_groups: AbstractSet[str] | None,
) -> tuple[_DeploymentT, ...]:
"""Deployments whose ``model_name`` is in ``model_groups``; all of them when unset."""
if model_groups is None:
return tuple(model_list)
return tuple(x for x in model_list if x.get("model_name") in model_groups)
def filter_deployments_by_id(
model_list: Sequence[Mapping[str, object]],
) -> list:

View file

@ -411,7 +411,9 @@ from litellm.proxy.guardrails.init_guardrails import (
initialize_guardrails,
)
from litellm.proxy.health_check import (
filter_deployments_to_model_groups,
health_check_filter_kwargs_from_general_settings,
parse_background_health_check_model_groups,
perform_health_check,
)
from litellm.proxy.health_endpoints._health_endpoints import router as health_router
@ -3660,6 +3662,13 @@ async def _run_background_health_check():
_llm_model_list = [
m for m in _llm_model_list if not m.get("model_info", {}).get("disable_background_health_check", False)
]
scoped_model_groups = llm_router.background_health_check_model_groups if llm_router is not None else None
_llm_model_list = list(filter_deployments_to_model_groups(_llm_model_list, scoped_model_groups))
if scoped_model_groups is not None and not _llm_model_list:
verbose_proxy_logger.warning(
"background_health_check_model_groups matched no deployments; groups=%s",
sorted(scoped_model_groups),
)
model_count_enabled = len(_llm_model_list)
expected_peak_in_flight = model_count_enabled
if isinstance(health_check_concurrency, int) and health_check_concurrency > 0 and model_count_enabled > 0:
@ -5239,6 +5248,7 @@ class ProxyConfig:
general_settings = config.get("general_settings", {})
if general_settings is None:
general_settings = {}
_bg_hc_model_groups: Final = parse_background_health_check_model_groups(general_settings)
_enable_hc_routing = False
_hc_staleness = None
_hc_ignore_transient = False
@ -5434,13 +5444,14 @@ class ProxyConfig:
_hc_staleness = general_settings.get("health_check_staleness_threshold", None)
_hc_ignore_transient = general_settings.get("health_check_ignore_transient_errors", False)
verbose_proxy_logger.info(
"background_health_check_config enabled=%s shared=%s interval_seconds=%s max_concurrency=%s details=%s health_check_routing=%s",
"background_health_check_config enabled=%s shared=%s interval_seconds=%s max_concurrency=%s details=%s health_check_routing=%s model_groups=%s",
use_background_health_checks,
use_shared_health_check,
health_check_interval,
health_check_concurrency,
health_check_details,
_enable_hc_routing,
sorted(_bg_hc_model_groups) if _bg_hc_model_groups is not None else None,
)
### RBAC ###
@ -5472,6 +5483,8 @@ class ProxyConfig:
router_params["health_check_staleness_threshold"] = _hc_staleness
if _hc_ignore_transient:
router_params["health_check_ignore_transient_errors"] = True
if _bg_hc_model_groups is not None:
router_params["background_health_check_model_groups"] = sorted(_bg_hc_model_groups)
## MODEL LIST
model_list: Final = config.get("model_list", None)
if model_list:

View file

@ -602,6 +602,7 @@ class Router:
enable_health_check_routing: bool = False,
health_check_staleness_threshold: int | None = None,
health_check_ignore_transient_errors: bool = False,
background_health_check_model_groups: Sequence[str] | None = None,
enable_weighted_failover: bool = False,
) -> None:
"""
@ -811,6 +812,11 @@ class Router:
self.enable_health_check_routing = enable_health_check_routing
self.enable_weighted_failover = enable_weighted_failover
self.health_check_ignore_transient_errors = health_check_ignore_transient_errors
self.background_health_check_model_groups: frozenset[str] | None = (
frozenset(background_health_check_model_groups)
if background_health_check_model_groups is not None
else None
)
_staleness: Final = health_check_staleness_threshold or (
DEFAULT_HEALTH_CHECK_INTERVAL * DEFAULT_HEALTH_CHECK_STALENESS_MULTIPLIER
)
@ -12719,6 +12725,10 @@ class Router:
"""
Filter out deployments marked unhealthy by background health checks.
No-op when enable_health_check_routing is False.
When background_health_check_model_groups is set, only deployments in the
listed model groups are filtered; every other group keeps its configured
routing strategy untouched, and a router-level allowed_fails_policy no
longer disables the filter for the listed groups.
Returns all deployments if health state is unavailable, stale, or would
exclude every candidate (safety net).
"""
@ -12727,8 +12737,10 @@ class Router:
# When allowed_fails_policy is set, cooldown is the sole routing exclusion
# mechanism -- skip the binary health check filter so the policy threshold
# is respected before any deployment is excluded.
if self.allowed_fails_policy is not None:
# is respected before any deployment is excluded. With a model-group
# allowlist the filter is already scoped, so listed groups keep it.
scoped_groups: Final = self.background_health_check_model_groups
if self.allowed_fails_policy is not None and scoped_groups is None:
return healthy_deployments
unhealthy_ids: Final = await self.health_state_cache.async_get_unhealthy_deployment_ids(
@ -12737,7 +12749,12 @@ class Router:
if not unhealthy_ids:
return healthy_deployments
filtered: Final = [d for d in healthy_deployments if d["model_info"]["id"] not in unhealthy_ids]
filtered: Final = [
d
for d in healthy_deployments
if d["model_info"]["id"] not in unhealthy_ids
or (scoped_groups is not None and d["model_name"] not in scoped_groups)
]
if not filtered:
verbose_router_logger.warning("All deployments marked unhealthy by health checks, bypassing health filter")
@ -12754,14 +12771,20 @@ class Router:
if not self.enable_health_check_routing:
return healthy_deployments
if self.allowed_fails_policy is not None:
scoped_groups: Final = self.background_health_check_model_groups
if self.allowed_fails_policy is not None and scoped_groups is None:
return healthy_deployments
unhealthy_ids: Final = self.health_state_cache.get_unhealthy_deployment_ids(parent_otel_span=parent_otel_span)
if not unhealthy_ids:
return healthy_deployments
filtered: Final = [d for d in healthy_deployments if d["model_info"]["id"] not in unhealthy_ids]
filtered: Final = [
d
for d in healthy_deployments
if d["model_info"]["id"] not in unhealthy_ids
or (scoped_groups is not None and d["model_name"] not in scoped_groups)
]
if not filtered:
verbose_router_logger.warning("All deployments marked unhealthy by health checks, bypassing health filter")

View file

@ -43,12 +43,33 @@ class DeploymentHealthCache:
self.staleness_threshold = staleness_threshold
def set_deployment_health_states(self, states: dict[str, DeploymentHealthStateValue]) -> None:
"""Bulk-write all deployment health states as a single cache entry."""
"""Merge the given states into the shared cache entry, pruning expired ones.
Merging instead of replacing lets writers probing different deployment
scopes (e.g. pods with different background health check allowlists)
coexist on the one shared entry without erasing each other's results.
The snapshot is read from Redis when available, since a pod-local read
would only ever see this writer's own previous merge. When the Redis
read comes back empty (a miss, or a swallowed connection error), the
pod-local copy of the last merge is used so peers are not erased.
"""
try:
redis_raw: Final = (
self.cache.redis_cache.get_cache(self.CACHE_KEY) if self.cache.redis_cache is not None else None
)
raw: Final = redis_raw if isinstance(redis_raw, dict) else self.cache.get_cache(key=self.CACHE_KEY)
existing: Final = raw if isinstance(raw, dict) else {}
expiry_seconds: Final = self.staleness_threshold * 1.5
now: Final = time.time()
merged: Final = {
model_id: state
for model_id, state in {**existing, **states}.items()
if isinstance(state, dict) and (now - state.get("timestamp", 0)) < expiry_seconds
}
self.cache.set_cache(
key=self.CACHE_KEY,
value=states,
ttl=int(self.staleness_threshold * 1.5),
value=merged,
ttl=int(expiry_seconds),
)
except Exception as e:
verbose_logger.error(

View file

@ -581,3 +581,73 @@ async def test_run_background_health_check_runs_one_cycle_then_cancels(monkeypat
"unhealthy_count": 1,
"sleep_invoked": True,
}
@pytest.mark.asyncio
async def test_run_background_health_check_probes_only_listed_model_groups(monkeypatch):
monkeypatch.setattr(proxy_server, "health_check_interval", 60)
monkeypatch.setattr(proxy_server, "health_check_concurrency", 1)
monkeypatch.setattr(proxy_server, "health_check_details", True)
monkeypatch.setattr(proxy_server, "use_shared_health_check", False)
monkeypatch.setattr(proxy_server, "redis_usage_cache", None)
monkeypatch.setattr(proxy_server, "prisma_client", None)
monkeypatch.setattr(proxy_server, "background_health_check_loop_active", False)
monkeypatch.setattr(
proxy_server,
"llm_router",
SimpleNamespace(background_health_check_model_groups=frozenset({"prod-openai"})),
)
monkeypatch.setattr(
proxy_server,
"llm_model_list",
[
{"model_name": "prod-openai", "model_info": {"id": "listed-1"}},
{"model_name": "prod-openai", "model_info": {"id": "listed-2"}},
{"model_name": "internal-claude", "model_info": {"id": "unlisted-1"}},
{
"model_name": "prod-openai",
"model_info": {
"id": "listed-disabled",
"disable_background_health_check": True,
},
},
],
)
monkeypatch.setattr(
proxy_server,
"health_check_results",
{"healthy_endpoints": [], "unhealthy_endpoints": []},
)
probed = {}
async def _fake_direct(model_list, *_a, **_kw):
probed["ids"] = [m["model_info"]["id"] for m in model_list]
return ([], [], {})
monkeypatch.setattr(
proxy_server,
"_run_direct_health_check_with_instrumentation",
_fake_direct,
)
monkeypatch.setattr(
proxy_server, "_schedule_background_health_check_db_save", lambda *a, **kw: None
)
monkeypatch.setattr(
proxy_server, "_write_health_state_to_router_cache", lambda *a, **kw: None
)
monkeypatch.setattr(
proxy_server,
"health_check_filter_kwargs_from_general_settings",
lambda _gs: {},
)
async def _stop_sleep(_seconds):
raise asyncio.CancelledError()
monkeypatch.setattr(proxy_server.asyncio, "sleep", _stop_sleep)
with pytest.raises(asyncio.CancelledError):
await _run_background_health_check()
assert probed["ids"] == ["listed-1", "listed-2"]

View file

@ -623,5 +623,53 @@ async def test_perform_health_check_and_save_forwards_skip_disabled_background_f
assert call_kwargs["health_check_skip_disabled_background_models"] is True
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__])

View file

@ -502,6 +502,68 @@ class TestHealthCheckFilterBypassWithPolicy:
)
assert len(result) == 2
def _make_scoped_router_with_unhealthy(self, policy) -> Router:
import time
from litellm.caching.caching import DualCache
from litellm.router_utils.health_state_cache import DeploymentHealthCache
router = Router(
model_list=[
_make_model("bad-listed"),
_make_model("ok-listed"),
_make_model("bad-unlisted", "gpt-5"),
],
allowed_fails_policy=policy,
enable_health_check_routing=True,
background_health_check_model_groups=["gpt-4"],
)
cache = DualCache()
health_cache = DeploymentHealthCache(cache=cache, staleness_threshold=60.0)
health_cache.set_deployment_health_states(
{
model_id: {
"is_healthy": False,
"timestamp": time.time(),
"reason": "test",
}
for model_id in ("bad-listed", "bad-unlisted")
}
)
router.health_state_cache = health_cache
return router
def test_filter_with_policy_still_applies_to_listed_groups(self):
"""A model-group allowlist keeps the filter active for listed groups even with a policy set."""
router = self._make_scoped_router_with_unhealthy(
AllowedFailsPolicy(AuthenticationErrorAllowedFails=3)
)
deployments = [
_make_model("bad-listed"),
_make_model("ok-listed"),
_make_model("bad-unlisted", "gpt-5"),
]
result = router._filter_health_check_unhealthy_deployments(deployments)
assert [d["model_info"]["id"] for d in result] == ["ok-listed", "bad-unlisted"]
@pytest.mark.asyncio
async def test_async_filter_with_policy_still_applies_to_listed_groups(self):
"""Async version: listed groups stay filtered with a policy set, unlisted stay untouched."""
router = self._make_scoped_router_with_unhealthy(
AllowedFailsPolicy(TimeoutErrorAllowedFails=2)
)
deployments = [
_make_model("bad-listed"),
_make_model("ok-listed"),
_make_model("bad-unlisted", "gpt-5"),
]
result = await router._async_filter_health_check_unhealthy_deployments(
deployments
)
assert [d["model_info"]["id"] for d in result] == ["ok-listed", "bad-unlisted"]
class TestAllDeploymentsInCooldownSafetyNet:
"""

View file

@ -111,3 +111,84 @@ def test_malformed_state_entries_are_skipped(health_cache):
health_cache.set_deployment_health_states(states)
result = health_cache.get_unhealthy_deployment_ids()
assert result == {"deploy-1"}
def test_set_merges_states_from_scoped_writers(health_cache):
"""A writer covering one scope must not erase another scope's fresh states."""
now = time.time()
health_cache.set_deployment_health_states(
{"listed-bad": {"is_healthy": False, "timestamp": now, "reason": "check_failed"}}
)
health_cache.set_deployment_health_states(
{"other-ok": {"is_healthy": True, "timestamp": now, "reason": ""}}
)
assert health_cache.get_unhealthy_deployment_ids() == {"listed-bad"}
def test_set_prunes_expired_entries(health_cache, cache):
"""Entries older than 1.5x the staleness threshold are dropped on write."""
expired_time = time.time() - 100 # threshold 60s, prune horizon 90s
health_cache.set_deployment_health_states(
{"gone": {"is_healthy": False, "timestamp": expired_time, "reason": "check_failed"}}
)
now = time.time()
health_cache.set_deployment_health_states(
{"fresh": {"is_healthy": False, "timestamp": now, "reason": "check_failed"}}
)
stored = cache.get_cache(key=DeploymentHealthCache.CACHE_KEY)
assert set(stored.keys()) == {"fresh"}
class _SharedRedisFake:
"""Shared get/set key-value store standing in for the Redis layer of a DualCache."""
def __init__(self):
self.store = {}
self.fail_get = False
def get_cache(self, key, parent_otel_span=None, **kwargs):
if self.fail_get:
return None # RedisCache.get_cache swallows connection errors and returns None
return self.store.get(key)
def set_cache(self, key, value, **kwargs):
self.store[key] = value
def test_scoped_writers_on_shared_redis_preserve_each_other():
"""Pods with different allowlists share one Redis entry; each merge must keep the peer's scope."""
redis_fake = _SharedRedisFake()
pod_a = DeploymentHealthCache(cache=DualCache(redis_cache=redis_fake), staleness_threshold=60.0)
pod_b = DeploymentHealthCache(cache=DualCache(redis_cache=redis_fake), staleness_threshold=60.0)
pod_a.set_deployment_health_states(
{"prod-bad": {"is_healthy": False, "timestamp": time.time(), "reason": "check_failed"}}
)
pod_b.set_deployment_health_states(
{"internal-bad": {"is_healthy": False, "timestamp": time.time(), "reason": "timeout"}}
)
pod_a.set_deployment_health_states(
{"prod-bad": {"is_healthy": False, "timestamp": time.time(), "reason": "check_failed"}}
)
assert set(redis_fake.store[DeploymentHealthCache.CACHE_KEY]) == {"prod-bad", "internal-bad"}
assert pod_a.get_unhealthy_deployment_ids() == {"prod-bad", "internal-bad"}
def test_failed_redis_read_falls_back_to_local_copy():
"""A swallowed Redis GET error must not make a writer erase peer scopes it already saw."""
redis_fake = _SharedRedisFake()
pod_a = DeploymentHealthCache(cache=DualCache(redis_cache=redis_fake), staleness_threshold=60.0)
pod_b = DeploymentHealthCache(cache=DualCache(redis_cache=redis_fake), staleness_threshold=60.0)
pod_a.set_deployment_health_states(
{"prod-bad": {"is_healthy": False, "timestamp": time.time(), "reason": "check_failed"}}
)
pod_b.set_deployment_health_states(
{"internal-bad": {"is_healthy": False, "timestamp": time.time(), "reason": "timeout"}}
)
pod_a.set_deployment_health_states(
{"prod-bad": {"is_healthy": False, "timestamp": time.time(), "reason": "check_failed"}}
)
redis_fake.fail_get = True
pod_a.set_deployment_health_states(
{"prod-bad": {"is_healthy": False, "timestamp": time.time(), "reason": "check_failed"}}
)
assert set(redis_fake.store[DeploymentHealthCache.CACHE_KEY]) == {"prod-bad", "internal-bad"}

View file

@ -43,7 +43,12 @@ def _make_health_cache(
class TestFilterHealthCheckUnhealthyDeployments:
"""Test the sync filter method."""
def _make_router_like(self, enable: bool, health_cache: DeploymentHealthCache):
def _make_router_like(
self,
enable: bool,
health_cache: DeploymentHealthCache,
model_groups: frozenset[str] | None = None,
):
"""Create a minimal object that behaves like Router for filter testing."""
class FakeRouter:
@ -51,6 +56,7 @@ class TestFilterHealthCheckUnhealthyDeployments:
self.enable_health_check_routing = enable
self.health_state_cache = health_cache
self.allowed_fails_policy = None
self.background_health_check_model_groups = model_groups
# Import the actual method and bind it
from litellm.router import Router
@ -115,11 +121,50 @@ class TestFilterHealthCheckUnhealthyDeployments:
result = router._filter_health_check_unhealthy_deployments(deployments)
assert len(result) == 2
def test_filter_scoped_to_listed_model_groups(self):
"""With an allowlist, only deployments in listed groups are filtered on health."""
health_cache = _make_health_cache(unhealthy_ids={"bad-listed", "bad-unlisted"})
router = self._make_router_like(
enable=True, health_cache=health_cache, model_groups=frozenset({"prod"})
)
deployments = [
_make_deployment("bad-listed", model_name="prod"),
_make_deployment("ok-listed", model_name="prod"),
_make_deployment("bad-unlisted", model_name="other"),
_make_deployment("ok-unlisted", model_name="other"),
]
result = router._filter_health_check_unhealthy_deployments(deployments)
assert [d["model_info"]["id"] for d in result] == [
"ok-listed",
"bad-unlisted",
"ok-unlisted",
]
def test_filter_unscoped_when_model_groups_unset(self):
"""Without an allowlist, unhealthy deployments in every group are filtered."""
health_cache = _make_health_cache(unhealthy_ids={"bad-listed", "bad-unlisted"})
router = self._make_router_like(enable=True, health_cache=health_cache)
deployments = [
_make_deployment("bad-listed", model_name="prod"),
_make_deployment("ok-listed", model_name="prod"),
_make_deployment("bad-unlisted", model_name="other"),
_make_deployment("ok-unlisted", model_name="other"),
]
result = router._filter_health_check_unhealthy_deployments(deployments)
assert [d["model_info"]["id"] for d in result] == ["ok-listed", "ok-unlisted"]
class TestAsyncFilterHealthCheckUnhealthyDeployments:
"""Test the async filter method."""
def _make_router_like(self, enable: bool, health_cache: DeploymentHealthCache):
def _make_router_like(
self,
enable: bool,
health_cache: DeploymentHealthCache,
model_groups: frozenset[str] | None = None,
):
from litellm.router import Router
class FakeRouter:
@ -127,6 +172,7 @@ class TestAsyncFilterHealthCheckUnhealthyDeployments:
self.enable_health_check_routing = enable
self.health_state_cache = health_cache
self.allowed_fails_policy = None
self.background_health_check_model_groups = model_groups
fake = FakeRouter()
fake._async_filter_health_check_unhealthy_deployments = (
@ -168,6 +214,29 @@ class TestAsyncFilterHealthCheckUnhealthyDeployments:
)
assert len(result) == 2 # safety net
@pytest.mark.asyncio
async def test_async_filter_scoped_to_listed_model_groups(self):
"""Async version: only deployments in listed groups are filtered on health."""
health_cache = _make_health_cache(unhealthy_ids={"bad-listed", "bad-unlisted"})
router = self._make_router_like(
enable=True, health_cache=health_cache, model_groups=frozenset({"prod"})
)
deployments = [
_make_deployment("bad-listed", model_name="prod"),
_make_deployment("ok-listed", model_name="prod"),
_make_deployment("bad-unlisted", model_name="other"),
_make_deployment("ok-unlisted", model_name="other"),
]
result = await router._async_filter_health_check_unhealthy_deployments(
healthy_deployments=deployments
)
assert [d["model_info"]["id"] for d in result] == [
"ok-listed",
"bad-unlisted",
"ok-unlisted",
]
class TestBuildDeploymentHealthStates:
"""Test the build_deployment_health_states function."""

View file

@ -24878,6 +24878,11 @@ export interface components {
* @description If True, a user's personal max_budget is enforced on every request they make, including requests made with a team-scoped key. Defaults to False, where a team-scoped key is governed only by the team and team-member budgets and the key owner's personal max_budget does not apply (see GitHub issue #12905).
*/
apply_user_budget_to_team_keys?: boolean | null;
/**
* Background Health Check Model Groups
* @description Opt-in allowlist of model group names for background health checks and health-check routing. When set, the background loop probes only deployments whose model_name is listed, and enable_health_check_routing filters unhealthy deployments only within the listed groups; every other group, including newly added deployments, is skipped and keeps its configured routing strategy. When unset, all deployments participate (opt out per deployment via model_info.disable_background_health_check).
*/
background_health_check_model_groups?: string[] | null;
/**
* Background Health Checks
* @description run health checks in background