litellm/tests/unit/proxy/common_utils/test_model_deprecation.py
devin-ai-integration[bot] a76b59db9f
test(proxy): move middleware, spend_tracking, pass_through, common_utils and root proxy tests into tests/unit/proxy (#44015)
Co-authored-by: yuneng <yuneng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-01 18:23:31 +00:00

359 lines
12 KiB
Python

"""Tests for the model deprecation helper module.
These tests focus on the helper itself — not on the proxy endpoint or
Slack integration — so they can run without the full proxy stack.
"""
from datetime import date, datetime, timezone
from unittest.mock import MagicMock
import litellm
from litellm.proxy.common_utils.model_deprecation import (
_classify,
_parse_deprecation_date,
collect_model_deprecations,
format_deprecation_alert_message,
)
def _make_router(deployments):
router = MagicMock()
router.get_model_list.return_value = deployments
return router
class TestParseDeprecationDate:
def test_should_parse_iso_string(self):
assert _parse_deprecation_date("2026-12-31") == date(2026, 12, 31)
def test_should_pass_through_date_object(self):
d = date(2026, 1, 1)
assert _parse_deprecation_date(d) == d
def test_should_return_none_for_documentation_sentinel(self):
# The JSON map ships a sentinel string under the "sample_spec" key.
assert (
_parse_deprecation_date(
"date when the model becomes deprecated in the format YYYY-MM-DD"
)
is None
)
def test_should_return_none_for_none(self):
assert _parse_deprecation_date(None) is None
def test_should_return_none_for_unsupported_type(self):
assert _parse_deprecation_date(12345) is None
def test_should_narrow_datetime_to_date(self):
assert _parse_deprecation_date(
datetime(2026, 12, 31, 23, 59, tzinfo=timezone.utc)
) == date(2026, 12, 31)
class TestClassify:
def test_should_classify_past_dates_as_deprecated(self):
assert _classify(-1, warn_within_days=30) == "deprecated"
assert _classify(-365, warn_within_days=30) == "deprecated"
def test_should_classify_inside_window_as_imminent(self):
assert _classify(0, warn_within_days=30) == "imminent"
assert _classify(15, warn_within_days=30) == "imminent"
assert _classify(30, warn_within_days=30) == "imminent"
def test_should_classify_outside_window_as_upcoming(self):
assert _classify(31, warn_within_days=30) == "upcoming"
assert _classify(365, warn_within_days=30) == "upcoming"
class TestCollectModelDeprecations:
def test_should_return_empty_response_when_router_is_none(self):
snapshot = collect_model_deprecations(llm_router=None)
assert snapshot.deprecated == []
assert snapshot.imminent == []
assert snapshot.upcoming == []
def test_should_skip_models_without_deprecation_metadata(self, monkeypatch):
monkeypatch.setattr(litellm, "model_cost", {})
router = _make_router(
[
{
"model_name": "gpt-4o",
"litellm_params": {"model": "openai/gpt-4o"},
"model_info": {"id": "abc"},
}
]
)
snapshot = collect_model_deprecations(llm_router=router)
assert snapshot.deprecated == []
assert snapshot.imminent == []
assert snapshot.upcoming == []
def test_should_classify_into_three_buckets(self, monkeypatch):
today = date(2026, 6, 1)
monkeypatch.setattr(
litellm,
"model_cost",
{
"deprecated-model": {
"deprecation_date": "2026-01-01",
"litellm_provider": "openai",
},
"imminent-model": {
"deprecation_date": "2026-06-15",
"litellm_provider": "openai",
},
"upcoming-model": {
"deprecation_date": "2027-01-01",
"litellm_provider": "openai",
},
},
)
router = _make_router(
[
{
"model_name": "deprecated-alias",
"litellm_params": {"model": "openai/deprecated-model"},
"model_info": {"id": "1"},
},
{
"model_name": "imminent-alias",
"litellm_params": {"model": "imminent-model"},
"model_info": {"id": "2"},
},
{
"model_name": "upcoming-alias",
"litellm_params": {"model": "openai/upcoming-model"},
"model_info": {"id": "3"},
},
]
)
snapshot = collect_model_deprecations(
llm_router=router, warn_within_days=30, today=today
)
assert [m.model_name for m in snapshot.deprecated] == ["deprecated-alias"]
assert [m.model_name for m in snapshot.imminent] == ["imminent-alias"]
assert [m.model_name for m in snapshot.upcoming] == ["upcoming-alias"]
assert snapshot.deprecated[0].days_until_deprecation < 0
assert snapshot.imminent[0].days_until_deprecation == 14
assert snapshot.upcoming[0].days_until_deprecation > 30
def test_should_prefer_explicit_deployment_override(self, monkeypatch):
today = date(2026, 6, 1)
monkeypatch.setattr(
litellm,
"model_cost",
{"some-model": {"deprecation_date": "2030-01-01"}},
)
router = _make_router(
[
{
"model_name": "my-alias",
"litellm_params": {"model": "some-model"},
"model_info": {
"id": "x",
"deprecation_date": "2026-06-10",
},
}
]
)
snapshot = collect_model_deprecations(
llm_router=router, warn_within_days=30, today=today
)
assert len(snapshot.imminent) == 1
assert snapshot.imminent[0].deprecation_date == date(2026, 6, 10)
def test_should_dedupe_duplicate_deployments_in_same_group(self, monkeypatch):
today = date(2026, 6, 1)
monkeypatch.setattr(
litellm,
"model_cost",
{"shared-model": {"deprecation_date": "2026-06-10"}},
)
router = _make_router(
[
{
"model_name": "alias",
"litellm_params": {"model": "shared-model"},
"model_info": {"id": "1"},
},
{
"model_name": "alias",
"litellm_params": {"model": "shared-model"},
"model_info": {"id": "2"},
},
]
)
snapshot = collect_model_deprecations(
llm_router=router, warn_within_days=30, today=today
)
assert len(snapshot.imminent) == 1
def test_should_resolve_via_unprefixed_model_name(self, monkeypatch):
monkeypatch.setattr(
litellm,
"model_cost",
{"gpt-4o": {"deprecation_date": "2026-06-10"}},
)
router = _make_router(
[
{
"model_name": "alias",
"litellm_params": {"model": "openai/gpt-4o"},
"model_info": {"id": "1"},
}
]
)
snapshot = collect_model_deprecations(
llm_router=router, warn_within_days=30, today=date(2026, 6, 1)
)
assert [m.litellm_model for m in snapshot.imminent] == ["gpt-4o"]
def test_should_keep_both_dates_when_group_has_conflicting_dates(self, monkeypatch):
monkeypatch.setattr(
litellm,
"model_cost",
{"shared-model": {"deprecation_date": "2026-06-10"}},
)
router = _make_router(
[
{
"model_name": "alias",
"litellm_params": {"model": "shared-model"},
"model_info": {"id": "1"},
},
{
"model_name": "alias",
"litellm_params": {"model": "shared-model"},
"model_info": {"id": "2", "deprecation_date": "2027-01-01"},
},
]
)
snapshot = collect_model_deprecations(
llm_router=router, warn_within_days=30, today=date(2026, 6, 1)
)
assert len(snapshot.imminent) == 1
assert len(snapshot.upcoming) == 1
def test_should_resolve_via_base_model(self, monkeypatch):
today = date(2026, 6, 1)
monkeypatch.setattr(
litellm,
"model_cost",
{"base-thing": {"deprecation_date": "2026-06-10"}},
)
router = _make_router(
[
{
"model_name": "alias",
"litellm_params": {"model": "azure/some-deployment-name"},
"model_info": {"id": "1", "base_model": "base-thing"},
}
]
)
snapshot = collect_model_deprecations(
llm_router=router, warn_within_days=30, today=today
)
assert len(snapshot.imminent) == 1
assert snapshot.imminent[0].litellm_model == "base-thing"
class TestFormatDeprecationAlertMessage:
def test_should_return_none_when_nothing_to_alert(self):
snapshot = collect_model_deprecations(llm_router=None)
assert format_deprecation_alert_message(snapshot) is None
def test_should_render_imminent_and_deprecated_sections(self, monkeypatch):
today = date(2026, 6, 1)
monkeypatch.setattr(
litellm,
"model_cost",
{
"dead-model": {
"deprecation_date": "2026-01-01",
"litellm_provider": "openai",
},
"soon-model": {
"deprecation_date": "2026-06-15",
"litellm_provider": "anthropic",
},
"later-model": {
"deprecation_date": "2027-01-01",
"litellm_provider": "anthropic",
},
},
)
router = _make_router(
[
{
"model_name": "dead",
"litellm_params": {"model": "dead-model"},
"model_info": {"id": "1"},
},
{
"model_name": "soon",
"litellm_params": {"model": "soon-model"},
"model_info": {"id": "2"},
},
{
"model_name": "later",
"litellm_params": {"model": "later-model"},
"model_info": {"id": "3"},
},
]
)
snapshot = collect_model_deprecations(
llm_router=router, warn_within_days=30, today=today
)
message = format_deprecation_alert_message(snapshot)
assert message is not None
assert "Already deprecated" in message
assert "Deprecating within 30 days" in message
assert "`dead`" in message
assert "`soon`" in message
# Upcoming models must NOT be in the alert (avoid alert fatigue).
assert "`later`" not in message
def test_should_neutralize_slack_markup_from_model_metadata(self):
today = date(2026, 6, 1)
router = _make_router(
[
{
"model_name": "<!channel> pwned",
"litellm_params": {"model": "openai/whatever"},
"model_info": {
"id": "1",
"deprecation_date": "2026-06-10",
"litellm_provider": "<https://evil.example|openai> & co",
},
}
]
)
snapshot = collect_model_deprecations(
llm_router=router, warn_within_days=30, today=today
)
message = format_deprecation_alert_message(snapshot)
assert message is not None
assert "<!channel>" not in message
assert "<https://evil.example|openai>" not in message
assert "&lt;!channel&gt; pwned" in message
assert "&lt;https://evil.example|openai&gt; &amp; co" in message