mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-03 07:23:33 +00:00
fix(cost-map): give the bare gpt-4o, gpt-4o-mini, gpt-4.1 and gpt-4.1-mini aliases their 2027-01-14 shutdown date (#49707)
* fix(cost-map): give the bare gpt-4o, gpt-4o-mini, gpt-4.1 and gpt-4.1-mini aliases their 2027-01-14 shutdown date * fix(proxy): report a deployment's own provider retirement date before the bare model's An Azure deployment declared as model gpt-4o with custom_llm_provider azure, or as azure/<deployment> with base_model gpt-4o, now resolves azure/gpt-4o's date instead of the bare OpenAI row's. The GPT-4 series alias test now reads both cost maps with typed rows * fix(proxy): read Azure's retirement schedule for azure_ai deployments An azure_ai deployment with no azure_ai row of its own now reports the azure/<model> date, and a cost-map row wins over the provider-less copy the router registers under the deployment's key, which carries the bare model's date. * fix(proxy): let custom and registered base_model rows keep their deprecation date Only the copy the router writes at startup, which has no provider and inherits the bare row's date, gives way to a later candidate * fix(proxy): report a pinned snapshot's own earlier deprecation date over its base_model's * fix(proxy): recognize the router's copied cost-map rows by the loaded catalog * fix(proxy): read Azure's schedule for a provider-prefixed azure_ai base_model --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
parent
28c80048ed
commit
704e73d6fe
5 changed files with 263 additions and 66 deletions
|
|
@ -34809,6 +34809,7 @@
|
|||
"gpt-4.1": {
|
||||
"cache_read_input_token_cost": 5e-07,
|
||||
"cache_read_input_token_cost_priority": 8.75e-07,
|
||||
"deprecation_date": "2027-01-14",
|
||||
"input_cost_per_token": 2e-06,
|
||||
"input_cost_per_token_batches": 1e-06,
|
||||
"input_cost_per_token_priority": 3.5e-06,
|
||||
|
|
@ -34894,6 +34895,7 @@
|
|||
"gpt-4.1-mini": {
|
||||
"cache_read_input_token_cost": 1e-07,
|
||||
"cache_read_input_token_cost_priority": 1.75e-07,
|
||||
"deprecation_date": "2027-01-14",
|
||||
"input_cost_per_token": 4e-07,
|
||||
"input_cost_per_token_batches": 2e-07,
|
||||
"input_cost_per_token_priority": 7e-07,
|
||||
|
|
@ -35053,6 +35055,7 @@
|
|||
"gpt-4o": {
|
||||
"cache_read_input_token_cost": 1.25e-06,
|
||||
"cache_read_input_token_cost_priority": 2.125e-06,
|
||||
"deprecation_date": "2027-01-14",
|
||||
"input_cost_per_token": 2.5e-06,
|
||||
"input_cost_per_token_batches": 1.25e-06,
|
||||
"input_cost_per_token_priority": 4.25e-06,
|
||||
|
|
@ -35397,6 +35400,7 @@
|
|||
"gpt-4o-mini": {
|
||||
"cache_read_input_token_cost": 7.5e-08,
|
||||
"cache_read_input_token_cost_priority": 1.25e-07,
|
||||
"deprecation_date": "2027-01-14",
|
||||
"input_cost_per_token": 1.5e-07,
|
||||
"input_cost_per_token_batches": 7.5e-08,
|
||||
"input_cost_per_token_priority": 2.5e-07,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, timezone
|
||||
from itertools import groupby
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Final
|
||||
from typing import TYPE_CHECKING, Final, TypeAlias
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.get_model_cost_map import AliasedCostEntry, GetModelCostMap
|
||||
from litellm.types.proxy.model_deprecation import (
|
||||
DEFAULT_DEPRECATION_WARN_DAYS,
|
||||
DeprecationStatus,
|
||||
|
|
@ -20,6 +21,9 @@ if TYPE_CHECKING:
|
|||
from litellm.router import Router
|
||||
|
||||
_NO_MODEL_METADATA: Final[Mapping[str, object]] = MappingProxyType({})
|
||||
_SHARED_SCHEDULE_PROVIDER: Final[Mapping[str, str]] = MappingProxyType({"azure_ai": "azure"})
|
||||
|
||||
LoadedCatalog: TypeAlias = Callable[[], Mapping[str, Mapping[str, object]]]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -27,6 +31,7 @@ class _ResolvedDeprecation:
|
|||
deprecation_date: date
|
||||
litellm_model: str | None
|
||||
litellm_provider: str | None
|
||||
copied_from_another_row: bool = False
|
||||
|
||||
|
||||
def _parse_deprecation_date(raw_value: object) -> date | None:
|
||||
|
|
@ -47,8 +52,8 @@ def _successor_model(raw_value: object) -> str | None:
|
|||
return raw_value.strip() or None if isinstance(raw_value, str) else None
|
||||
|
||||
|
||||
def _cost_map_lookup(model_key: object) -> _ResolvedDeprecation | None:
|
||||
if not isinstance(model_key, str) or not model_key:
|
||||
def _cost_map_lookup(model_key: str | None, catalog: Mapping[str, object]) -> _ResolvedDeprecation | None:
|
||||
if model_key is None:
|
||||
return None
|
||||
entry: Final = litellm.model_cost.get(model_key)
|
||||
if not isinstance(entry, Mapping):
|
||||
|
|
@ -61,6 +66,51 @@ def _cost_map_lookup(model_key: object) -> _ResolvedDeprecation | None:
|
|||
deprecation_date=parsed,
|
||||
litellm_model=model_key,
|
||||
litellm_provider=provider if isinstance(provider, str) else None,
|
||||
copied_from_another_row=isinstance(entry, AliasedCostEntry)
|
||||
and "deprecation_date" not in entry.registered_fields
|
||||
and model_key not in catalog,
|
||||
)
|
||||
|
||||
|
||||
def _first_own_row(model_keys: Sequence[str | None], catalog: Mapping[str, object]) -> _ResolvedDeprecation | None:
|
||||
found: Final = tuple(
|
||||
resolved for resolved in (_cost_map_lookup(key, catalog) for key in model_keys) if resolved is not None
|
||||
)
|
||||
return next((resolved for resolved in found if not resolved.copied_from_another_row), next(iter(found), None))
|
||||
|
||||
|
||||
def _deployment_provider(litellm_params: Mapping[str, object]) -> str | None:
|
||||
declared: Final = litellm_params.get("custom_llm_provider")
|
||||
if isinstance(declared, str) and declared:
|
||||
return declared
|
||||
model: Final = litellm_params.get("model")
|
||||
prefix: Final = model.split("/", 1)[0] if isinstance(model, str) and "/" in model else None
|
||||
return prefix if prefix in litellm.provider_list else None
|
||||
|
||||
|
||||
def _provider_qualified(provider: str | None, model_key: str | None) -> str | None:
|
||||
if provider is None or model_key is None or model_key.startswith(f"{provider}/"):
|
||||
return None
|
||||
return f"{provider}/{model_key}"
|
||||
|
||||
|
||||
def _as_model_key(raw_value: object) -> str | None:
|
||||
return raw_value if isinstance(raw_value, str) and raw_value else None
|
||||
|
||||
|
||||
def _without_provider(provider: str | None, model_name: str | None) -> str | None:
|
||||
if provider is None or model_name is None:
|
||||
return model_name
|
||||
return model_name.removeprefix(f"{provider}/")
|
||||
|
||||
|
||||
def _schedule_keys(provider: str | None, model_name: str | None) -> tuple[str | None, ...]:
|
||||
shared_provider: Final = _SHARED_SCHEDULE_PROVIDER.get(provider) if provider is not None else None
|
||||
unprefixed: Final = _without_provider(provider, model_name)
|
||||
return (
|
||||
_provider_qualified(provider, unprefixed),
|
||||
_provider_qualified(shared_provider, unprefixed),
|
||||
model_name,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -70,34 +120,37 @@ def _mapping_field(deployment: Mapping[str, object], key: str) -> Mapping[str, o
|
|||
|
||||
|
||||
def _resolve_deployment_deprecation(
|
||||
deployment: Mapping[str, object],
|
||||
deployment: Mapping[str, object], catalog: Mapping[str, object]
|
||||
) -> _ResolvedDeprecation | None:
|
||||
"""Resolve a deployment's deprecation date, preferring its explicit override"""
|
||||
model_info: Final = _mapping_field(deployment, "model_info")
|
||||
raw_model: Final = _mapping_field(deployment, "litellm_params").get("model")
|
||||
litellm_params: Final = _mapping_field(deployment, "litellm_params")
|
||||
model_key: Final = _as_model_key(litellm_params.get("model"))
|
||||
|
||||
override: Final = _parse_deprecation_date(model_info.get("deprecation_date"))
|
||||
if override is not None:
|
||||
provider: Final = model_info.get("litellm_provider")
|
||||
override_provider: Final = model_info.get("litellm_provider")
|
||||
return _ResolvedDeprecation(
|
||||
deprecation_date=override,
|
||||
litellm_model=raw_model if isinstance(raw_model, str) else None,
|
||||
litellm_provider=provider if isinstance(provider, str) else None,
|
||||
litellm_model=model_key,
|
||||
litellm_provider=override_provider if isinstance(override_provider, str) else None,
|
||||
)
|
||||
|
||||
unprefixed: Final = raw_model.split("/", 1)[1] if isinstance(raw_model, str) and "/" in raw_model else None
|
||||
return next(
|
||||
(
|
||||
resolved
|
||||
for resolved in (
|
||||
_cost_map_lookup(model_info.get("base_model")),
|
||||
_cost_map_lookup(raw_model),
|
||||
_cost_map_lookup(unprefixed),
|
||||
)
|
||||
if resolved is not None
|
||||
),
|
||||
None,
|
||||
provider: Final = _deployment_provider(litellm_params)
|
||||
base_model: Final = _as_model_key(model_info.get("base_model"))
|
||||
model_name: Final = model_key.split("/", 1)[1] if model_key is not None and "/" in model_key else model_key
|
||||
picks: Final = tuple(
|
||||
pick
|
||||
for pick in (
|
||||
_first_own_row(_schedule_keys(provider, base_model), catalog),
|
||||
_first_own_row(
|
||||
(_provider_qualified(provider, model_key), model_key, *_schedule_keys(provider, model_name)), catalog
|
||||
),
|
||||
)
|
||||
if pick is not None
|
||||
)
|
||||
own_rows: Final = tuple(pick for pick in picks if not pick.copied_from_another_row)
|
||||
return min(own_rows or picks, key=lambda pick: pick.deprecation_date, default=None)
|
||||
|
||||
|
||||
def _classify(days_until: int, warn_within_days: int) -> DeprecationStatus:
|
||||
|
|
@ -124,13 +177,17 @@ def _checked_successor(model_name: str, model_info: Mapping[str, object], llm_ro
|
|||
|
||||
|
||||
def _build_info(
|
||||
deployment: Mapping[str, object], llm_router: Router, today: date, warn_within_days: int
|
||||
deployment: Mapping[str, object],
|
||||
llm_router: Router,
|
||||
today: date,
|
||||
warn_within_days: int,
|
||||
catalog: Mapping[str, object],
|
||||
) -> ModelDeprecationInfo | None:
|
||||
model_name: Final = deployment.get("model_name")
|
||||
if not isinstance(model_name, str) or not model_name:
|
||||
return None
|
||||
|
||||
resolved: Final = _resolve_deployment_deprecation(deployment)
|
||||
resolved: Final = _resolve_deployment_deprecation(deployment, catalog)
|
||||
if resolved is None:
|
||||
return None
|
||||
|
||||
|
|
@ -182,15 +239,12 @@ def _bucket(models: Sequence[ModelDeprecationInfo], status: DeprecationStatus) -
|
|||
)
|
||||
|
||||
|
||||
def _collect_from_router(llm_router: Router, today: date, warn_within_days: int) -> tuple[ModelDeprecationInfo, ...]:
|
||||
def _collect_from_router(
|
||||
llm_router: Router, today: date, warn_within_days: int, catalog: Mapping[str, object]
|
||||
) -> tuple[ModelDeprecationInfo, ...]:
|
||||
deployments: Final = llm_router.registry().deployments() or ()
|
||||
deduped: Final = _dedupe(
|
||||
tuple(
|
||||
info
|
||||
for info in (_build_info(deployment, llm_router, today, warn_within_days) for deployment in deployments)
|
||||
if info is not None
|
||||
)
|
||||
)
|
||||
infos: Final = (_build_info(deployment, llm_router, today, warn_within_days, catalog) for deployment in deployments)
|
||||
deduped: Final = _dedupe(tuple(info for info in infos if info is not None))
|
||||
|
||||
verbose_logger.debug(
|
||||
"model_deprecation: %d/%d deployments carry a deprecation date",
|
||||
|
|
@ -204,12 +258,15 @@ def collect_model_deprecations(
|
|||
llm_router: Router | None,
|
||||
warn_within_days: int = DEFAULT_DEPRECATION_WARN_DAYS,
|
||||
today: date | None = None,
|
||||
loaded_catalog: LoadedCatalog = GetModelCostMap.loaded_model_cost_map,
|
||||
) -> ModelDeprecationResponse:
|
||||
"""Bucket every deployment carrying a deprecation date by how urgent it is"""
|
||||
snapshot_time: Final = datetime.now(timezone.utc)
|
||||
effective_today: Final = today or snapshot_time.date()
|
||||
deduped: Final = (
|
||||
_collect_from_router(llm_router, effective_today, warn_within_days) if llm_router is not None else ()
|
||||
_collect_from_router(llm_router, effective_today, warn_within_days, loaded_catalog())
|
||||
if llm_router is not None
|
||||
else ()
|
||||
)
|
||||
|
||||
return ModelDeprecationResponse(
|
||||
|
|
|
|||
|
|
@ -34809,6 +34809,7 @@
|
|||
"gpt-4.1": {
|
||||
"cache_read_input_token_cost": 5e-07,
|
||||
"cache_read_input_token_cost_priority": 8.75e-07,
|
||||
"deprecation_date": "2027-01-14",
|
||||
"input_cost_per_token": 2e-06,
|
||||
"input_cost_per_token_batches": 1e-06,
|
||||
"input_cost_per_token_priority": 3.5e-06,
|
||||
|
|
@ -34894,6 +34895,7 @@
|
|||
"gpt-4.1-mini": {
|
||||
"cache_read_input_token_cost": 1e-07,
|
||||
"cache_read_input_token_cost_priority": 1.75e-07,
|
||||
"deprecation_date": "2027-01-14",
|
||||
"input_cost_per_token": 4e-07,
|
||||
"input_cost_per_token_batches": 2e-07,
|
||||
"input_cost_per_token_priority": 7e-07,
|
||||
|
|
@ -35053,6 +35055,7 @@
|
|||
"gpt-4o": {
|
||||
"cache_read_input_token_cost": 1.25e-06,
|
||||
"cache_read_input_token_cost_priority": 2.125e-06,
|
||||
"deprecation_date": "2027-01-14",
|
||||
"input_cost_per_token": 2.5e-06,
|
||||
"input_cost_per_token_batches": 1.25e-06,
|
||||
"input_cost_per_token_priority": 4.25e-06,
|
||||
|
|
@ -35397,6 +35400,7 @@
|
|||
"gpt-4o-mini": {
|
||||
"cache_read_input_token_cost": 7.5e-08,
|
||||
"cache_read_input_token_cost_priority": 1.25e-07,
|
||||
"deprecation_date": "2027-01-14",
|
||||
"input_cost_per_token": 1.5e-07,
|
||||
"input_cost_per_token_batches": 7.5e-08,
|
||||
"input_cost_per_token_priority": 2.5e-07,
|
||||
|
|
|
|||
|
|
@ -5,14 +5,17 @@ Slack integration — so they can run without the full proxy stack.
|
|||
"""
|
||||
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Final
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm import Router
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.get_model_cost_map import AliasedCostEntry
|
||||
from litellm.proxy.common_utils.model_deprecation import (
|
||||
_classify,
|
||||
_parse_deprecation_date,
|
||||
|
|
@ -37,12 +40,7 @@ class TestParseDeprecationDate:
|
|||
|
||||
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
|
||||
)
|
||||
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
|
||||
|
|
@ -51,9 +49,7 @@ class TestParseDeprecationDate:
|
|||
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)
|
||||
assert _parse_deprecation_date(datetime(2026, 12, 31, 23, 59, tzinfo=timezone.utc)) == date(2026, 12, 31)
|
||||
|
||||
|
||||
class TestClassify:
|
||||
|
|
@ -134,9 +130,7 @@ class TestCollectModelDeprecations:
|
|||
]
|
||||
)
|
||||
|
||||
snapshot = collect_model_deprecations(
|
||||
llm_router=router, warn_within_days=30, today=today
|
||||
)
|
||||
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"]
|
||||
|
|
@ -166,9 +160,7 @@ class TestCollectModelDeprecations:
|
|||
]
|
||||
)
|
||||
|
||||
snapshot = collect_model_deprecations(
|
||||
llm_router=router, warn_within_days=30, today=today
|
||||
)
|
||||
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)
|
||||
|
|
@ -195,9 +187,7 @@ class TestCollectModelDeprecations:
|
|||
]
|
||||
)
|
||||
|
||||
snapshot = collect_model_deprecations(
|
||||
llm_router=router, warn_within_days=30, today=today
|
||||
)
|
||||
snapshot = collect_model_deprecations(llm_router=router, warn_within_days=30, today=today)
|
||||
|
||||
assert len(snapshot.imminent) == 1
|
||||
|
||||
|
|
@ -217,9 +207,7 @@ class TestCollectModelDeprecations:
|
|||
]
|
||||
)
|
||||
|
||||
snapshot = collect_model_deprecations(
|
||||
llm_router=router, warn_within_days=30, today=date(2026, 6, 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"]
|
||||
|
||||
|
|
@ -244,9 +232,7 @@ class TestCollectModelDeprecations:
|
|||
]
|
||||
)
|
||||
|
||||
snapshot = collect_model_deprecations(
|
||||
llm_router=router, warn_within_days=30, today=date(2026, 6, 1)
|
||||
)
|
||||
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
|
||||
|
|
@ -268,14 +254,136 @@ class TestCollectModelDeprecations:
|
|||
]
|
||||
)
|
||||
|
||||
snapshot = collect_model_deprecations(
|
||||
llm_router=router, warn_within_days=30, today=today
|
||||
)
|
||||
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 TestProviderScopedResolution:
|
||||
CATALOG = {
|
||||
"gpt-4o": {"deprecation_date": "2027-01-14", "litellm_provider": "openai"},
|
||||
"azure/gpt-4o": {"deprecation_date": "2027-04-14", "litellm_provider": "azure"},
|
||||
"github_copilot/gpt-4o": {"litellm_provider": "github_copilot"},
|
||||
"azure_ai/gpt-4.1": AliasedCostEntry(
|
||||
{"deprecation_date": "2027-05-01", "litellm_provider": "azure_ai", "mode": "chat"}, ()
|
||||
),
|
||||
"custom-base": {"deprecation_date": "2026-12-01"},
|
||||
"gpt-4.1": {"deprecation_date": "2027-01-14", "litellm_provider": "openai"},
|
||||
"azure/gpt-4.1": {"deprecation_date": "2027-04-14", "litellm_provider": "azure"},
|
||||
"gpt-4o-2024-05-13": {"deprecation_date": "2026-10-23", "litellm_provider": "openai"},
|
||||
"azure/gpt-4o-2024-05-13": {"deprecation_date": "2026-12-09", "litellm_provider": "azure"},
|
||||
"azure/aliased-base": AliasedCostEntry(
|
||||
{"deprecation_date": "2026-11-20"}, ("azure/aliased-base", "azure/aliased-base-v1")
|
||||
),
|
||||
"aliased-base": {"deprecation_date": "2027-02-01"},
|
||||
}
|
||||
COST_MAP = {
|
||||
**CATALOG,
|
||||
"azure_ai/gpt-4o": AliasedCostEntry(
|
||||
{"deprecation_date": "2027-01-14", "litellm_provider": "azure", "mode": "chat"}, ()
|
||||
),
|
||||
"registered-base": AliasedCostEntry({"deprecation_date": "2026-12-15"}, (), frozenset({"deprecation_date"})),
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("litellm_params", "model_info", "expected"),
|
||||
[
|
||||
({"model": "gpt-4o"}, {}, ("gpt-4o", date(2027, 1, 14), "openai")),
|
||||
({"model": "openai/gpt-4o"}, {}, ("gpt-4o", date(2027, 1, 14), "openai")),
|
||||
(
|
||||
{"model": "gpt-4o", "custom_llm_provider": "azure"},
|
||||
{},
|
||||
("azure/gpt-4o", date(2027, 4, 14), "azure"),
|
||||
),
|
||||
({"model": "azure/gpt-4o"}, {}, ("azure/gpt-4o", date(2027, 4, 14), "azure")),
|
||||
(
|
||||
{"model": "azure/prod-chat"},
|
||||
{"base_model": "gpt-4o"},
|
||||
("azure/gpt-4o", date(2027, 4, 14), "azure"),
|
||||
),
|
||||
(
|
||||
{"model": "gpt-4o", "custom_llm_provider": "github_copilot"},
|
||||
{},
|
||||
("gpt-4o", date(2027, 1, 14), "openai"),
|
||||
),
|
||||
({"model": "azure_ai/gpt-4o"}, {}, ("azure/gpt-4o", date(2027, 4, 14), "azure")),
|
||||
(
|
||||
{"model": "azure_ai/prod-chat"},
|
||||
{"base_model": "gpt-4o"},
|
||||
("azure/gpt-4o", date(2027, 4, 14), "azure"),
|
||||
),
|
||||
(
|
||||
{"model": "azure_ai/gpt-4o"},
|
||||
{"base_model": "azure_ai/gpt-4o"},
|
||||
("azure/gpt-4o", date(2027, 4, 14), "azure"),
|
||||
),
|
||||
(
|
||||
{"model": "azure_ai/prod-chat"},
|
||||
{"base_model": "azure_ai/gpt-4o"},
|
||||
("azure/gpt-4o", date(2027, 4, 14), "azure"),
|
||||
),
|
||||
({"model": "azure_ai/gpt-4.1"}, {}, ("azure_ai/gpt-4.1", date(2027, 5, 1), "azure_ai")),
|
||||
({"model": "azure/gpt-4o"}, {"base_model": "custom-base"}, ("custom-base", date(2026, 12, 1), None)),
|
||||
(
|
||||
{"model": "azure/gpt-4o"},
|
||||
{"base_model": "registered-base"},
|
||||
("registered-base", date(2026, 12, 15), None),
|
||||
),
|
||||
(
|
||||
{"model": "azure/prod-chat"},
|
||||
{"base_model": "aliased-base"},
|
||||
("azure/aliased-base", date(2026, 11, 20), None),
|
||||
),
|
||||
(
|
||||
{"model": "openai/gpt-4o-2024-05-13"},
|
||||
{"base_model": "gpt-4o"},
|
||||
("gpt-4o-2024-05-13", date(2026, 10, 23), "openai"),
|
||||
),
|
||||
(
|
||||
{"model": "azure/gpt-4o-2024-05-13"},
|
||||
{"base_model": "gpt-4o"},
|
||||
("azure/gpt-4o-2024-05-13", date(2026, 12, 9), "azure"),
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"bare",
|
||||
"openai-prefix",
|
||||
"declared-azure",
|
||||
"azure-prefix",
|
||||
"azure-base-model",
|
||||
"undated-own-row",
|
||||
"azure-ai-reads-azure",
|
||||
"azure-ai-base-model-reads-azure",
|
||||
"azure-ai-copied-base-model-yields-to-azure",
|
||||
"azure-ai-prefixed-base-model-on-custom-deployment-reads-azure",
|
||||
"azure-ai-own-row-wins",
|
||||
"custom-base-model-row-wins",
|
||||
"registered-base-model-row-wins",
|
||||
"aliased-base-model-row-wins",
|
||||
"openai-snapshot-shuts-down-before-its-base-model",
|
||||
"azure-snapshot-shuts-down-before-its-base-model",
|
||||
],
|
||||
)
|
||||
def test_should_prefer_the_deployments_own_provider_date(
|
||||
self,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
litellm_params: Mapping[str, str],
|
||||
model_info: Mapping[str, str],
|
||||
expected: tuple[str, date, str | None],
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "model_cost", self.COST_MAP)
|
||||
router: Final = _make_router(
|
||||
[{"model_name": "alias", "litellm_params": litellm_params, "model_info": {"id": "1", **model_info}}]
|
||||
)
|
||||
|
||||
snapshot: Final = collect_model_deprecations(
|
||||
llm_router=router, warn_within_days=30, today=date(2026, 6, 1), loaded_catalog=lambda: self.CATALOG
|
||||
)
|
||||
|
||||
assert [(m.litellm_model, m.deprecation_date, m.litellm_provider) for m in snapshot.upcoming] == [expected]
|
||||
|
||||
|
||||
class TestFormatDeprecationAlertMessage:
|
||||
def test_should_return_none_when_nothing_to_alert(self):
|
||||
snapshot = collect_model_deprecations(llm_router=None)
|
||||
|
|
@ -321,9 +429,7 @@ class TestFormatDeprecationAlertMessage:
|
|||
]
|
||||
)
|
||||
|
||||
snapshot = collect_model_deprecations(
|
||||
llm_router=router, warn_within_days=30, today=today
|
||||
)
|
||||
snapshot = collect_model_deprecations(llm_router=router, warn_within_days=30, today=today)
|
||||
message = format_deprecation_alert_message(snapshot)
|
||||
|
||||
assert message is not None
|
||||
|
|
@ -350,9 +456,7 @@ class TestFormatDeprecationAlertMessage:
|
|||
]
|
||||
)
|
||||
|
||||
snapshot = collect_model_deprecations(
|
||||
llm_router=router, warn_within_days=30, today=today
|
||||
)
|
||||
snapshot = collect_model_deprecations(llm_router=router, warn_within_days=30, today=today)
|
||||
message = format_deprecation_alert_message(snapshot)
|
||||
|
||||
assert message is not None
|
||||
|
|
|
|||
|
|
@ -388,6 +388,34 @@ def test_vertex_gemini_flash_rows_carry_the_documented_retirement_date(prices: d
|
|||
assert prices[f"{prefix}{model}"]["deprecation_date"] == VERTEX_GEMINI_FLASH_RETIREMENT_DATES[model]
|
||||
|
||||
|
||||
# https://developers.openai.com/api/docs/deprecations, the 2026-10-14 GPT-4 series notice
|
||||
OPENAI_GPT_4_SERIES_SNAPSHOTS: Final = MappingProxyType(
|
||||
{
|
||||
"gpt-4o": ("gpt-4o-2024-08-06", "gpt-4o-2024-11-20"),
|
||||
"gpt-4o-mini": ("gpt-4o-mini-2024-07-18",),
|
||||
"gpt-4.1": ("gpt-4.1-2025-04-14",),
|
||||
"gpt-4.1-mini": ("gpt-4.1-mini-2025-04-14",),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class _ShutdownRow(BaseModel):
|
||||
model_config = ConfigDict(frozen=True, extra="ignore")
|
||||
|
||||
deprecation_date: str | None = None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", (PRICES_PATH, BACKUP_PRICES_PATH), ids=("main", "backup"))
|
||||
@pytest.mark.parametrize("alias", sorted(OPENAI_GPT_4_SERIES_SNAPSHOTS))
|
||||
def test_openai_gpt_4_series_aliases_retire_with_their_snapshots(path: Path, alias: str) -> None:
|
||||
prices: Final[dict[str, JsonValue]] = TypeAdapter(dict[str, JsonValue]).validate_json(path.read_bytes())
|
||||
snapshot_dates: Final = frozenset(
|
||||
_ShutdownRow.model_validate(prices[snapshot]).deprecation_date
|
||||
for snapshot in OPENAI_GPT_4_SERIES_SNAPSHOTS[alias]
|
||||
)
|
||||
assert snapshot_dates == {_ShutdownRow.model_validate(prices[alias]).deprecation_date}
|
||||
|
||||
|
||||
def _sampling_flags_across_provider_twins(prices: Mapping[str, Mapping[str, object]], key: str) -> frozenset[object]:
|
||||
bare: Final = key.split("/")[-1]
|
||||
twins: Final = tuple(twin for twin in (bare, f"vertex_ai/{bare}", f"gemini/{bare}") if twin in prices)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue