mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-31 06:56:59 +00:00
fix(aiand)!: show ai&'s name and logo, and sync ai&'s model list (#49158)
* feat(ui): show ai&'s name and logo in the dashboard The Add Model form already lists ai& from its provider metadata, but the dashboard had no entry for it, so the provider showed no logo and model tables printed the raw aiand slug. Adds ai& to the dashboard provider list with its official mark, plated in dark mode so the red and black mark keep their colours, and the aiand/openai/gpt-oss-120b model placeholder * fix(aiand): retire deepseek-v4-flash, glm-5.2 and kimi-k2.7-code ai& retired these three models on 2026-10-27 and https://api.aiand.com/v1/models now lists ten. The rows keep their prices with a deprecation_date, so earlier spend still prices, and aiand/* stops listing them, the same way baseten handles retired rows. The other ten rows already match the live prices, context windows and output limits * feat(aiand): add deepseek-v4.1-flash-1017 ai& launches deepseek-ai/deepseek-v4.1-flash-1017 on 2026-11-02. It is served on https://staging-api.aiand.com/v1/models with the same price, context window, output limit and capabilities as deepseek-v4.1-flash, so its row copies that one * fix(aiand): retire deepseek-v4.1-flash ai& retires deepseek-ai/deepseek-v4.1-flash on 2026-10-30, now that deepseek-v4.1-flash-1017 is live in production on https://api.aiand.com/v1/models with the same price, limits and capabilities. Its row keeps its prices with a deprecation_date, like the other three retired ai& rows * fix(aiand): retire deepseek-v4.1-flash on 2026-11-06 https://api.aiand.com/v1/models now gives deepseek-v4.1-flash a deprecation_date of 2026-11-05T15:00Z, which is 2026-11-06 00:00 in Japan, so its row uses 2026-11-06 instead of 2026-10-30 and aiand/* keeps listing it until then. The wildcard test now leaves a model out only once its retirement date has passed * test(aiand): derive retired models from the rows instead of pinning dates The wildcard test now reads each row's deprecation_date, so it holds whichever dates ai& sets, and the test that only restated the JSON dates is gone * fix(aiand): retire models on their UTC date while the proxy runs, and add muse-spark-open The wildcard listing now checks each row's deprecation_date when it expands, against the UTC date the field documents, so a model drops on its day without a restart. deepseek-v4.1-flash now carries 2026-11-05, the UTC day ai& retires it, and meta/muse-spark-open gets its price row from ai&'s live /v1/models * fix(aiand): hide retired models only from bundled listings, including the wildcard health check * fix(aiand): share the UTC retirement check with Together AI and the health check sort --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
parent
17d1f58336
commit
707ac1ffee
13 changed files with 296 additions and 36 deletions
|
|
@ -7,7 +7,6 @@ _apply_process_defaults()
|
|||
### INIT VARIABLES #########################
|
||||
import threading
|
||||
import os
|
||||
import datetime
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import (
|
||||
|
|
@ -550,6 +549,7 @@ _key_management_system: Optional["KeyManagementSystem"] = None
|
|||
output_parse_pii: bool = False
|
||||
#############################################
|
||||
from litellm.litellm_core_utils.get_model_cost_map import get_model_cost_map, mark_litellm_import_complete
|
||||
from litellm.litellm_core_utils.model_retirement import is_hidden_as_retired
|
||||
|
||||
model_cost = get_model_cost_map(url=model_cost_map_url)
|
||||
cost_discount_config: Dict[str, float] = {} # Provider-specific cost discounts {"vertex_ai": 0.05} = 5% discount
|
||||
|
|
@ -756,16 +756,6 @@ def is_openai_finetune_model(key: str) -> bool:
|
|||
return key.startswith("ft:") and not key.count(":") > 1
|
||||
|
||||
|
||||
def _is_past_deprecation_date(value: Mapping[str, object]) -> bool:
|
||||
deprecation_date: Final = value.get("deprecation_date")
|
||||
if not isinstance(deprecation_date, str):
|
||||
return False
|
||||
try:
|
||||
return datetime.date.fromisoformat(deprecation_date) <= datetime.date.today()
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _populate_provider_model_sets(model_cost_map: Mapping[str, Mapping[str, object]]) -> None:
|
||||
for key, value in tuple(model_cost_map.items()):
|
||||
if value.get("litellm_provider") == "openai" and not is_openai_finetune_model(key):
|
||||
|
|
@ -873,12 +863,12 @@ def _populate_provider_model_sets(model_cost_map: Mapping[str, Mapping[str, obje
|
|||
if "-to-" not in key and "fireworks-ai-default" not in key:
|
||||
fireworks_ai_models.add(key)
|
||||
elif value.get("litellm_provider") == "together_ai":
|
||||
if _is_past_deprecation_date(value):
|
||||
if is_hidden_as_retired(value):
|
||||
together_ai_models.discard(key)
|
||||
elif not key.startswith("together-ai-"):
|
||||
together_ai_models.add(key)
|
||||
elif value.get("litellm_provider") == "baseten":
|
||||
if _is_past_deprecation_date(value):
|
||||
if is_hidden_as_retired(value):
|
||||
baseten_models.discard(key)
|
||||
else:
|
||||
baseten_models.add(key)
|
||||
|
|
@ -1051,7 +1041,10 @@ def _populate_provider_model_sets(model_cost_map: Mapping[str, Mapping[str, obje
|
|||
elif value.get("litellm_provider") == "coralbricks":
|
||||
coralbricks_models.add(key)
|
||||
elif value.get("litellm_provider") == "aiand":
|
||||
aiand_models.add(key)
|
||||
if is_hidden_as_retired(value):
|
||||
aiand_models.discard(key)
|
||||
else:
|
||||
aiand_models.add(key)
|
||||
elif value.get("litellm_provider") == "reducto":
|
||||
reducto_models.add(key)
|
||||
elif value.get("litellm_provider") == "bedrock_mantle":
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import datetime
|
||||
from collections.abc import Iterable, Iterator, Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import Final, cast
|
||||
|
|
@ -9,6 +8,7 @@ from typing_extensions import NotRequired, ReadOnly, TypedDict
|
|||
import litellm
|
||||
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
|
||||
from litellm.litellm_core_utils.get_llm_provider_logic import inferred_provider
|
||||
from litellm.litellm_core_utils.model_retirement import is_past_deprecation_date
|
||||
|
||||
|
||||
def _form_field_value(value: object) -> str:
|
||||
|
|
@ -168,7 +168,9 @@ class RankedChatModel:
|
|||
|
||||
|
||||
def rank_chat_models_from_llm_provider(custom_llm_provider: str) -> tuple[RankedChatModel, ...]:
|
||||
known_models: Final[Iterable[str]] = litellm.models_by_provider.get(custom_llm_provider, ())
|
||||
from litellm.utils import bundled_provider_models
|
||||
|
||||
known_models: Final = bundled_provider_models(custom_llm_provider)
|
||||
ranked_models: Final = (
|
||||
RankedChatModel(
|
||||
model=model, route_rank=route_rank, cost=cost, is_past_deprecation_date=is_past_deprecation_date
|
||||
|
|
@ -187,18 +189,11 @@ _COST_MAP_DEPRECATION: Final = TypeAdapter(_CostMapDeprecation)
|
|||
|
||||
|
||||
def _is_past_deprecation_date(cost_map_key: str) -> bool:
|
||||
deprecation_date: Final = _COST_MAP_DEPRECATION.validate_python(
|
||||
litellm.model_cost.get(cost_map_key) or {} # pyright: ignore[reportUnknownMemberType] # untyped cost map JSON
|
||||
).get("deprecation_date")
|
||||
if not isinstance(deprecation_date, str):
|
||||
return False
|
||||
try:
|
||||
return (
|
||||
datetime.date.fromisoformat(deprecation_date)
|
||||
<= datetime.datetime.now(datetime.timezone.utc).astimezone().date()
|
||||
return is_past_deprecation_date(
|
||||
_COST_MAP_DEPRECATION.validate_python(
|
||||
litellm.model_cost.get(cost_map_key) or {} # pyright: ignore[reportUnknownMemberType] # untyped cost map JSON
|
||||
)
|
||||
except ValueError:
|
||||
return False
|
||||
)
|
||||
|
||||
|
||||
def _chat_row_costs(models: Iterable[str], custom_llm_provider: str) -> Iterator[tuple[str, float, bool]]:
|
||||
|
|
|
|||
19
litellm/litellm_core_utils/model_retirement.py
Normal file
19
litellm/litellm_core_utils/model_retirement.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import datetime
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
PROVIDERS_HIDING_RETIRED_MODELS: Final = frozenset({"aiand", "baseten", "together_ai"})
|
||||
|
||||
|
||||
def is_past_deprecation_date(value: Mapping[str, object]) -> bool:
|
||||
deprecation_date: Final = value.get("deprecation_date")
|
||||
if not isinstance(deprecation_date, str):
|
||||
return False
|
||||
try:
|
||||
return datetime.date.fromisoformat(deprecation_date) <= datetime.datetime.now(datetime.timezone.utc).date()
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def is_hidden_as_retired(value: Mapping[str, object]) -> bool:
|
||||
return value.get("litellm_provider") in PROVIDERS_HIDING_RETIRED_MODELS and is_past_deprecation_date(value)
|
||||
|
|
@ -72249,6 +72249,7 @@
|
|||
},
|
||||
"aiand/deepseek-ai/deepseek-v4-flash": {
|
||||
"cache_read_input_token_cost": 8e-08,
|
||||
"deprecation_date": "2026-10-27",
|
||||
"input_cost_per_token": 1.5e-07,
|
||||
"litellm_provider": "aiand",
|
||||
"max_input_tokens": 1048576,
|
||||
|
|
@ -72278,6 +72279,22 @@
|
|||
"supports_vision": false
|
||||
},
|
||||
"aiand/deepseek-ai/deepseek-v4.1-flash": {
|
||||
"cache_read_input_token_cost": 2e-08,
|
||||
"deprecation_date": "2026-11-05",
|
||||
"input_cost_per_token": 3e-07,
|
||||
"litellm_provider": "aiand",
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 384000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 6e-07,
|
||||
"source": "https://api.aiand.com/v1/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"aiand/deepseek-ai/deepseek-v4.1-flash-1017": {
|
||||
"cache_read_input_token_cost": 2e-08,
|
||||
"input_cost_per_token": 3e-07,
|
||||
"litellm_provider": "aiand",
|
||||
|
|
@ -72307,8 +72324,24 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"aiand/meta/muse-spark-open": {
|
||||
"cache_read_input_token_cost": 1.5e-07,
|
||||
"input_cost_per_token": 1.25e-06,
|
||||
"litellm_provider": "aiand",
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 943718,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 4.25e-06,
|
||||
"source": "https://api.aiand.com/v1/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"aiand/moonshotai/kimi-k2.7-code": {
|
||||
"cache_read_input_token_cost": 2e-07,
|
||||
"deprecation_date": "2026-10-27",
|
||||
"input_cost_per_token": 7.5e-07,
|
||||
"litellm_provider": "aiand",
|
||||
"max_input_tokens": 262144,
|
||||
|
|
@ -72399,6 +72432,7 @@
|
|||
},
|
||||
"aiand/zai-org/glm-5.2": {
|
||||
"cache_read_input_token_cost": 3e-07,
|
||||
"deprecation_date": "2026-10-27",
|
||||
"input_cost_per_token": 1e-06,
|
||||
"litellm_provider": "aiand",
|
||||
"max_input_tokens": 1048576,
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ from litellm.litellm_core_utils.fallback_generalizations import (
|
|||
)
|
||||
from litellm.litellm_core_utils.get_model_cost_map import AliasedCostEntry, GetModelCostMap
|
||||
from litellm.litellm_core_utils.model_param_helper import peek_reasoning_summary_aliases
|
||||
from litellm.litellm_core_utils.model_retirement import is_hidden_as_retired
|
||||
from litellm.litellm_core_utils.offline_mode import is_offline_mode
|
||||
from litellm.litellm_core_utils.sensitive_data_masker import redact_credentials_in_payload
|
||||
from litellm.litellm_core_utils.tokenizer import strip_special_tokens
|
||||
|
|
@ -8806,6 +8807,12 @@ def _get_valid_models_from_provider_api(
|
|||
return []
|
||||
|
||||
|
||||
def bundled_provider_models(provider: str) -> tuple[str, ...]:
|
||||
bundled: Final[tuple[str, ...]] = tuple(litellm.models_by_provider.get(provider, ()))
|
||||
cost_map: Final = _builtin_cost_map()
|
||||
return tuple(model for model in bundled if not is_hidden_as_retired(cost_map.get(model, {})))
|
||||
|
||||
|
||||
def get_valid_models(
|
||||
check_provider_endpoint: bool | None = None,
|
||||
custom_llm_provider: str | None = None,
|
||||
|
|
@ -8871,8 +8878,7 @@ def get_valid_models(
|
|||
)
|
||||
)
|
||||
else:
|
||||
models_for_provider = copy.deepcopy(litellm.models_by_provider.get(provider, []))
|
||||
valid_models.extend(models_for_provider)
|
||||
valid_models.extend(bundled_provider_models(provider))
|
||||
|
||||
return valid_models
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -72249,6 +72249,7 @@
|
|||
},
|
||||
"aiand/deepseek-ai/deepseek-v4-flash": {
|
||||
"cache_read_input_token_cost": 8e-08,
|
||||
"deprecation_date": "2026-10-27",
|
||||
"input_cost_per_token": 1.5e-07,
|
||||
"litellm_provider": "aiand",
|
||||
"max_input_tokens": 1048576,
|
||||
|
|
@ -72278,6 +72279,22 @@
|
|||
"supports_vision": false
|
||||
},
|
||||
"aiand/deepseek-ai/deepseek-v4.1-flash": {
|
||||
"cache_read_input_token_cost": 2e-08,
|
||||
"deprecation_date": "2026-11-05",
|
||||
"input_cost_per_token": 3e-07,
|
||||
"litellm_provider": "aiand",
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 384000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 6e-07,
|
||||
"source": "https://api.aiand.com/v1/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"aiand/deepseek-ai/deepseek-v4.1-flash-1017": {
|
||||
"cache_read_input_token_cost": 2e-08,
|
||||
"input_cost_per_token": 3e-07,
|
||||
"litellm_provider": "aiand",
|
||||
|
|
@ -72307,8 +72324,24 @@
|
|||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"aiand/meta/muse-spark-open": {
|
||||
"cache_read_input_token_cost": 1.5e-07,
|
||||
"input_cost_per_token": 1.25e-06,
|
||||
"litellm_provider": "aiand",
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 943718,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 4.25e-06,
|
||||
"source": "https://api.aiand.com/v1/models",
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"aiand/moonshotai/kimi-k2.7-code": {
|
||||
"cache_read_input_token_cost": 2e-07,
|
||||
"deprecation_date": "2026-10-27",
|
||||
"input_cost_per_token": 7.5e-07,
|
||||
"litellm_provider": "aiand",
|
||||
"max_input_tokens": 262144,
|
||||
|
|
@ -72399,6 +72432,7 @@
|
|||
},
|
||||
"aiand/zai-org/glm-5.2": {
|
||||
"cache_read_input_token_cost": 3e-07,
|
||||
"deprecation_date": "2026-10-27",
|
||||
"input_cost_per_token": 1e-06,
|
||||
"litellm_provider": "aiand",
|
||||
"max_input_tokens": 1048576,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import datetime
|
||||
import time
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -245,3 +247,39 @@ def test_rank_chat_models_flags_a_retired_model_the_provider_lists_without_its_c
|
|||
(ranked_model.model, ranked_model.is_past_deprecation_date)
|
||||
for ranked_model in rank_chat_models_from_llm_provider("vertex_ai")
|
||||
] == [("fake-retired-vertex-chat", True), ("fake-live-vertex-chat", False)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host_zone", ["Etc/GMT+12", "Etc/GMT-14"])
|
||||
def test_rank_chat_models_flags_retirement_by_the_utc_date_whatever_the_host_zone(
|
||||
monkeypatch: pytest.MonkeyPatch, host_zone: str
|
||||
) -> None:
|
||||
utc_today: Final = datetime.datetime.now(datetime.timezone.utc).date()
|
||||
retirement_dates: Final = {
|
||||
"deepseek/fake-retires-today-chat": utc_today,
|
||||
"deepseek/fake-retires-tomorrow-chat": utc_today + datetime.timedelta(days=1),
|
||||
}
|
||||
for name, retirement_date in retirement_dates.items():
|
||||
monkeypatch.setitem(
|
||||
litellm.model_cost,
|
||||
name,
|
||||
{
|
||||
"input_cost_per_token": 1e-12,
|
||||
"output_cost_per_token": 1e-12,
|
||||
"mode": "chat",
|
||||
"litellm_provider": PICKER_PROVIDER,
|
||||
"deprecation_date": retirement_date.isoformat(),
|
||||
},
|
||||
)
|
||||
monkeypatch.setitem(litellm.models_by_provider, PICKER_PROVIDER, tuple(retirement_dates))
|
||||
monkeypatch.setenv("TZ", host_zone)
|
||||
time.tzset()
|
||||
try:
|
||||
flags: Final = {
|
||||
ranked_model.model: ranked_model.is_past_deprecation_date
|
||||
for ranked_model in rank_chat_models_from_llm_provider(PICKER_PROVIDER)
|
||||
}
|
||||
finally:
|
||||
monkeypatch.undo()
|
||||
time.tzset()
|
||||
|
||||
assert flags == {"deepseek/fake-retires-today-chat": True, "deepseek/fake-retires-tomorrow-chat": False}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ import pytest
|
|||
import respx
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.model_retirement import is_past_deprecation_date
|
||||
from litellm.llms.baseten.chat import BasetenConfig
|
||||
from litellm.utils import get_valid_models
|
||||
from litellm.utils import _model_cache, get_valid_models
|
||||
|
||||
|
||||
class TestBasetenRouting:
|
||||
|
|
@ -65,7 +66,7 @@ class TestBasetenModelDiscovery:
|
|||
servable_registry_models: Final = {
|
||||
key
|
||||
for key, value in litellm.model_cost.items()
|
||||
if value.get("litellm_provider") == "baseten" and not litellm._is_past_deprecation_date(value)
|
||||
if value.get("litellm_provider") == "baseten" and not is_past_deprecation_date(value)
|
||||
}
|
||||
|
||||
assert servable_registry_models
|
||||
|
|
@ -92,6 +93,20 @@ class TestBasetenModelDiscovery:
|
|||
litellm.baseten_models.discard(model)
|
||||
litellm.add_known_models({})
|
||||
|
||||
def test_wildcard_drops_a_model_whose_retirement_date_passes_while_running(self, monkeypatch: pytest.MonkeyPatch):
|
||||
model: Final = "baseten/acme/retires-while-running"
|
||||
monkeypatch.setitem(litellm.model_cost, model, {"litellm_provider": "baseten", "deprecation_date": "2999-01-01"})
|
||||
try:
|
||||
litellm.add_known_models({model: litellm.model_cost[model]})
|
||||
assert model in get_valid_models(custom_llm_provider="baseten")
|
||||
|
||||
monkeypatch.setitem(litellm.model_cost, model, {"litellm_provider": "baseten", "deprecation_date": "2000-01-01"})
|
||||
|
||||
assert model not in get_valid_models(custom_llm_provider="baseten")
|
||||
finally:
|
||||
litellm.baseten_models.discard(model)
|
||||
litellm.add_known_models({})
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, expected",
|
||||
[
|
||||
|
|
@ -102,7 +117,7 @@ class TestBasetenModelDiscovery:
|
|||
],
|
||||
)
|
||||
def test_is_past_deprecation_date(self, value, expected):
|
||||
assert litellm._is_past_deprecation_date(value) is expected
|
||||
assert is_past_deprecation_date(value) is expected
|
||||
|
||||
def test_get_models_calls_model_api(self, respx_mock: respx.MockRouter) -> None:
|
||||
route: Final = respx_mock.get("https://inference.baseten.co/v1/models").mock(
|
||||
|
|
@ -156,6 +171,18 @@ class TestBasetenModelDiscovery:
|
|||
|
||||
assert models == ["baseten/brand-new/model"]
|
||||
|
||||
@patch.dict(os.environ, {"BASETEN_API_KEY": "test-key"})
|
||||
def test_live_catalog_keeps_a_model_the_cost_map_dates_as_retired(self, respx_mock: respx.MockRouter) -> None:
|
||||
assert is_past_deprecation_date(litellm.model_cost["baseten/zai-org/GLM-5"])
|
||||
_model_cache.flush_cache()
|
||||
respx_mock.get("https://inference.baseten.co/v1/models").mock(
|
||||
return_value=httpx.Response(200, json={"data": [{"id": "zai-org/GLM-5"}]})
|
||||
)
|
||||
|
||||
models: Final = get_valid_models(check_provider_endpoint=True, custom_llm_provider="baseten")
|
||||
|
||||
assert models == ["baseten/zai-org/GLM-5"]
|
||||
|
||||
|
||||
class TestBasetenReasoningEffort:
|
||||
def test_reasoning_effort_passthrough_with_drop_params(self):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import datetime
|
||||
import json
|
||||
import time
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -8,6 +10,8 @@ import respx
|
|||
import litellm
|
||||
from litellm.caching.llm_caching_handler import LLMClientCache
|
||||
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
|
||||
from litellm.litellm_core_utils.health_check_helpers import wildcard_health_check_models
|
||||
from litellm.litellm_core_utils.model_retirement import is_past_deprecation_date
|
||||
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
|
||||
from litellm.proxy.auth.model_checks import get_known_models_from_wildcard
|
||||
from litellm.utils import ProviderConfigManager
|
||||
|
|
@ -89,11 +93,99 @@ def _aiand_catalog() -> frozenset[str]:
|
|||
return frozenset(name for name, info in litellm.model_cost.items() if info.get("litellm_provider") == "aiand")
|
||||
|
||||
|
||||
def test_aiand_wildcard_lists_its_priced_models():
|
||||
catalog: Final = _aiand_catalog()
|
||||
def _utc_today() -> datetime.date:
|
||||
return datetime.datetime.now(datetime.timezone.utc).date()
|
||||
|
||||
assert catalog
|
||||
assert frozenset(get_known_models_from_wildcard("aiand/*")) == catalog
|
||||
|
||||
def test_aiand_wildcard_lists_only_models_not_yet_retired():
|
||||
catalog: Final = _aiand_catalog()
|
||||
retired: Final = frozenset(
|
||||
name
|
||||
for name in catalog
|
||||
if (retired_on := litellm.model_cost[name].get("deprecation_date")) is not None
|
||||
and datetime.date.fromisoformat(retired_on) <= _utc_today()
|
||||
)
|
||||
|
||||
assert catalog - retired
|
||||
assert frozenset(get_known_models_from_wildcard("aiand/*")) == catalog - retired
|
||||
|
||||
|
||||
def test_aiand_wildcard_drops_a_model_whose_retirement_date_passes_while_running(monkeypatch: pytest.MonkeyPatch):
|
||||
model: Final = "aiand/acme/retires-while-running"
|
||||
monkeypatch.setitem(litellm.model_cost, model, {"litellm_provider": "aiand", "deprecation_date": "2999-01-01"})
|
||||
try:
|
||||
litellm.add_known_models({model: litellm.model_cost[model]})
|
||||
assert model in get_known_models_from_wildcard("aiand/*")
|
||||
|
||||
monkeypatch.setitem(litellm.model_cost, model, {"litellm_provider": "aiand", "deprecation_date": "2000-01-01"})
|
||||
|
||||
assert model not in get_known_models_from_wildcard("aiand/*")
|
||||
finally:
|
||||
litellm.aiand_models.discard(model)
|
||||
litellm.add_known_models({})
|
||||
|
||||
|
||||
def test_aiand_models_by_provider_leaves_out_models_retired_at_load():
|
||||
retired: Final = frozenset(
|
||||
name
|
||||
for name, row in litellm.model_cost.items()
|
||||
if row.get("litellm_provider") == "aiand" and is_past_deprecation_date(row)
|
||||
)
|
||||
|
||||
assert retired
|
||||
assert retired.isdisjoint(litellm.models_by_provider["aiand"])
|
||||
|
||||
|
||||
def test_aiand_wildcard_health_check_skips_a_model_whose_retirement_date_passes_while_running(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
model: Final = "aiand/acme/free-and-retiring"
|
||||
live_row: Final = {
|
||||
"litellm_provider": "aiand",
|
||||
"mode": "chat",
|
||||
"input_cost_per_token": 0.0,
|
||||
"output_cost_per_token": 0.0,
|
||||
"deprecation_date": "2999-01-01",
|
||||
}
|
||||
monkeypatch.setitem(litellm.model_cost, model, live_row)
|
||||
try:
|
||||
litellm.add_known_models({model: live_row})
|
||||
assert model in wildcard_health_check_models(wildcard="aiand/*", custom_llm_provider="aiand")
|
||||
|
||||
monkeypatch.setitem(litellm.model_cost, model, {**live_row, "deprecation_date": "2000-01-01"})
|
||||
|
||||
assert model not in wildcard_health_check_models(wildcard="aiand/*", custom_llm_provider="aiand")
|
||||
finally:
|
||||
litellm.aiand_models.discard(model)
|
||||
litellm.add_known_models({})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host_zone", ["Etc/GMT+12", "Etc/GMT-14"])
|
||||
def test_aiand_models_retire_on_the_utc_date_whatever_the_host_zone(monkeypatch: pytest.MonkeyPatch, host_zone: str):
|
||||
today: Final = _utc_today()
|
||||
rows: Final = {
|
||||
"aiand/acme/retires-today": {"litellm_provider": "aiand", "deprecation_date": today.isoformat()},
|
||||
"aiand/acme/retires-tomorrow": {
|
||||
"litellm_provider": "aiand",
|
||||
"deprecation_date": (today + datetime.timedelta(days=1)).isoformat(),
|
||||
},
|
||||
}
|
||||
for name, row in rows.items():
|
||||
monkeypatch.setitem(litellm.model_cost, name, row)
|
||||
monkeypatch.setenv("TZ", host_zone)
|
||||
time.tzset()
|
||||
try:
|
||||
litellm.add_known_models(rows)
|
||||
listed: Final = frozenset(get_known_models_from_wildcard("aiand/*"))
|
||||
|
||||
assert "aiand/acme/retires-today" not in listed
|
||||
assert "aiand/acme/retires-tomorrow" in listed
|
||||
finally:
|
||||
monkeypatch.undo()
|
||||
time.tzset()
|
||||
for name in rows:
|
||||
litellm.aiand_models.discard(name)
|
||||
litellm.add_known_models({})
|
||||
|
||||
|
||||
def test_aiand_calls_are_billed_from_their_cost_rows():
|
||||
|
|
|
|||
4
ui/litellm-dashboard/public/assets/logos/aiand.svg
Normal file
4
ui/litellm-dashboard/public/assets/logos/aiand.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="154" height="170" viewBox="0 0 154 170" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M142.201 58.0338C147.015 54.3129 153.925 57.8127 153.925 63.9711V162.493C153.925 166.608 150.654 169.944 146.617 169.944H142.131C141.665 169.944 141.22 169.751 140.896 169.411L112.255 139.292C109.89 136.807 106.099 136.452 103.331 138.459L60.3523 169.62C60.0612 169.831 59.7129 169.944 59.3559 169.944H19.1938C12.1908 169.944 9.19832 160.871 14.7784 156.556L142.201 58.0338Z" fill="#151517"/>
|
||||
<path d="M142.474 1.14602C146.28 -1.75046 151.759 1.18386 151.457 6.14031L149.665 35.5148C149.561 37.2177 148.728 38.7803 147.4 39.7908L8.99184 145.042C5.31127 147.841 0.000248842 145.196 0 140.407V112.48C2.31386e-05 110.648 0.848057 108.933 2.27475 107.846L142.474 1.14602Z" fill="#C70007"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 802 B |
|
|
@ -32,6 +32,18 @@ describe("provider_info_helpers", () => {
|
|||
expect(getPlaceholder(Providers.BIGMODEL)).toMatch(/^bigmodel\//);
|
||||
});
|
||||
|
||||
it("resolves ai& metadata, name, logo and model placeholder", () => {
|
||||
const metadata = PROVIDER_CREATE_FIELDS.find((provider) => provider.provider === "AIAND");
|
||||
expect(provider_map.AIAND).toBe(metadata?.litellm_provider);
|
||||
expect(getProviderLogoAndName("aiand")).toEqual({
|
||||
displayName: "ai&",
|
||||
logo: providerLogoMap[Providers.AIAND],
|
||||
});
|
||||
expect(getProviderLogoAndName("AIAND")).toEqual(getProviderLogoAndName("aiand"));
|
||||
expect(providerLogoMap[Providers.AIAND]).toContain("aiand");
|
||||
expect(getPlaceholder(Providers.AIAND)).toBe("aiand/openai/gpt-oss-120b");
|
||||
});
|
||||
|
||||
it("maps Microsoft 365 Copilot to its chat model provider and placeholder", () => {
|
||||
expect(provider_map.MICROSOFT_365_COPILOT).toBe("microsoft_365_copilot");
|
||||
expect(getPlaceholder(Providers.MICROSOFT_365_COPILOT)).toBe("microsoft_365_copilot/chat");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { resolveLogoSrc } from "@/lib/assetPaths";
|
||||
import a2aAgentLogo from "../../public/assets/logos/a2a_agent.png";
|
||||
import ai21Logo from "../../public/assets/logos/ai21.svg";
|
||||
import aiandLogo from "../../public/assets/logos/aiand.svg";
|
||||
import aimlApiLogo from "../../public/assets/logos/aiml_api.svg";
|
||||
import anthropicLogo from "../../public/assets/logos/anthropic.svg";
|
||||
import assemblyaiSmallLogo from "../../public/assets/logos/assemblyai_small.png";
|
||||
|
|
@ -76,6 +77,7 @@ export enum Providers {
|
|||
A2A_Agent = "A2A Agent",
|
||||
AI21 = "Ai21",
|
||||
AI21_CHAT = "Ai21 Chat",
|
||||
AIAND = "ai&",
|
||||
AIML = "AI/ML API",
|
||||
AIOHTTP_OPENAI = "Aiohttp Openai",
|
||||
Anthropic = "Anthropic",
|
||||
|
|
@ -207,6 +209,7 @@ export const provider_map: Record<string, string> = {
|
|||
A2A_Agent: "a2a_agent",
|
||||
AI21: "ai21",
|
||||
AI21_CHAT: "ai21_chat",
|
||||
AIAND: "aiand",
|
||||
AIML: "aiml",
|
||||
AIOHTTP_OPENAI: "aiohttp_openai",
|
||||
Anthropic: "anthropic",
|
||||
|
|
@ -344,6 +347,7 @@ export const providerLogoMap: Partial<Record<Providers, string>> = {
|
|||
[Providers.A2A_Agent]: a2aAgentLogo.src,
|
||||
[Providers.AI21]: ai21Logo.src,
|
||||
[Providers.AI21_CHAT]: ai21Logo.src,
|
||||
[Providers.AIAND]: aiandLogo.src,
|
||||
[Providers.AIML]: aimlApiLogo.src,
|
||||
[Providers.AIOHTTP_OPENAI]: openaiSmallLogo.src,
|
||||
[Providers.Anthropic]: anthropicLogo.src,
|
||||
|
|
@ -472,6 +476,7 @@ export const getProviderLogoAndName = (providerValue: string): { logo: string; d
|
|||
|
||||
const providerPlaceholderMap: Partial<Record<Providers, string>> = {
|
||||
[Providers.AIML]: "aiml/flux-pro/v1.1",
|
||||
[Providers.AIAND]: "aiand/openai/gpt-oss-120b",
|
||||
[Providers.Anthropic]: "claude-3-opus",
|
||||
[Providers.Azure]: "my-deployment",
|
||||
[Providers.Azure_AI_Studio]: "azure_ai/command-r-plus",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ const TREATMENT_BY_ASSET: Readonly<Record<string, LogoTreatment>> = {
|
|||
"v0.svg": "invert",
|
||||
"vercel.svg": "invert",
|
||||
"watsonx.svg": "invert",
|
||||
"aiand.svg": "plate",
|
||||
"aiml_api.svg": "plate",
|
||||
"akto.svg": "plate",
|
||||
"aws.svg": "plate",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue