mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
feat: add model cost aliases expansion support
This commit is contained in:
parent
a788b21092
commit
7ccb14cab4
3 changed files with 299 additions and 5 deletions
|
|
@ -13,6 +13,7 @@ Here's the full specification with all available fields:
|
|||
```json
|
||||
{
|
||||
"sample_spec": {
|
||||
"aliases": ["optional list of alternate names for this model, e.g. dated versions like sample_spec-20250101"],
|
||||
"code_interpreter_cost_per_session": 0.0,
|
||||
"computer_use_input_cost_per_1k_tokens": 0.0,
|
||||
"computer_use_output_cost_per_1k_tokens": 0.0,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export LITELLM_LOCAL_MODEL_COST_MAP=True
|
|||
import json
|
||||
import os
|
||||
from importlib.resources import files
|
||||
from typing import Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
|
|
@ -183,6 +183,52 @@ def get_model_cost_map_source_info() -> dict:
|
|||
}
|
||||
|
||||
|
||||
def _expand_model_aliases(model_cost: dict) -> dict:
|
||||
"""
|
||||
Expand ``aliases`` lists in model cost entries into top-level entries.
|
||||
|
||||
Each alias gets a reference to the **same** dict object as the canonical
|
||||
entry (zero memory overhead). The ``aliases`` key is removed from the
|
||||
entry so downstream code never sees it.
|
||||
|
||||
If an alias collides with an existing canonical entry the alias is
|
||||
silently skipped and a warning is logged.
|
||||
"""
|
||||
aliases_to_add: Dict[str, dict] = {}
|
||||
keys_with_aliases: List[str] = []
|
||||
|
||||
for model_name, model_info in model_cost.items():
|
||||
aliases: Optional[list] = model_info.get("aliases")
|
||||
if not aliases:
|
||||
continue
|
||||
keys_with_aliases.append(model_name)
|
||||
for alias in aliases:
|
||||
if alias in model_cost:
|
||||
verbose_logger.warning(
|
||||
"LiteLLM model alias conflict: alias '%s' (from '%s') "
|
||||
"already exists as a canonical entry — skipping.",
|
||||
alias,
|
||||
model_name,
|
||||
)
|
||||
continue
|
||||
if alias in aliases_to_add:
|
||||
verbose_logger.warning(
|
||||
"LiteLLM model alias conflict: alias '%s' (from '%s') "
|
||||
"was already claimed by another entry — skipping.",
|
||||
alias,
|
||||
model_name,
|
||||
)
|
||||
continue
|
||||
aliases_to_add[alias] = model_info # same dict reference
|
||||
|
||||
# Remove the ``aliases`` key from entries so it doesn't pollute model info
|
||||
for key in keys_with_aliases:
|
||||
model_cost[key].pop("aliases", None)
|
||||
|
||||
model_cost.update(aliases_to_add)
|
||||
return model_cost
|
||||
|
||||
|
||||
def get_model_cost_map(url: str) -> dict:
|
||||
"""
|
||||
Public entry point — returns the model cost map dict.
|
||||
|
|
@ -202,7 +248,7 @@ def get_model_cost_map(url: str) -> dict:
|
|||
_cost_map_source_info.url = None
|
||||
_cost_map_source_info.is_env_forced = True
|
||||
_cost_map_source_info.fallback_reason = None
|
||||
return GetModelCostMap.load_local_model_cost_map()
|
||||
return _expand_model_aliases(GetModelCostMap.load_local_model_cost_map())
|
||||
|
||||
_cost_map_source_info.url = url
|
||||
_cost_map_source_info.is_env_forced = False
|
||||
|
|
@ -218,7 +264,7 @@ def get_model_cost_map(url: str) -> dict:
|
|||
)
|
||||
_cost_map_source_info.source = "local"
|
||||
_cost_map_source_info.fallback_reason = f"Remote fetch failed: {str(e)}"
|
||||
return GetModelCostMap.load_local_model_cost_map()
|
||||
return _expand_model_aliases(GetModelCostMap.load_local_model_cost_map())
|
||||
|
||||
# Validate using cached count (cheap int comparison, no file I/O)
|
||||
if not GetModelCostMap.validate_model_cost_map(
|
||||
|
|
@ -232,8 +278,8 @@ def get_model_cost_map(url: str) -> dict:
|
|||
)
|
||||
_cost_map_source_info.source = "local"
|
||||
_cost_map_source_info.fallback_reason = "Remote data failed integrity validation"
|
||||
return GetModelCostMap.load_local_model_cost_map()
|
||||
return _expand_model_aliases(GetModelCostMap.load_local_model_cost_map())
|
||||
|
||||
_cost_map_source_info.source = "remote"
|
||||
_cost_map_source_info.fallback_reason = None
|
||||
return content
|
||||
return _expand_model_aliases(content)
|
||||
|
|
|
|||
247
tests/test_litellm/test_model_cost_aliases.py
Normal file
247
tests/test_litellm/test_model_cost_aliases.py
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
"""
|
||||
Tests for the ``aliases`` feature in the model cost map.
|
||||
|
||||
The ``_expand_model_aliases`` function processes ``aliases`` lists from model
|
||||
entries, creating shared dict references for alias entries at load time.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.litellm_core_utils.get_model_cost_map import _expand_model_aliases
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_model_cost(**entries) -> dict:
|
||||
"""Build a small model_cost dict from keyword args (model_name → info)."""
|
||||
return dict(entries)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Core expansion behaviour
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestExpandModelAliases:
|
||||
"""Unit tests for _expand_model_aliases."""
|
||||
|
||||
def test_basic_expansion(self):
|
||||
"""Aliases are added as top-level entries in model_cost."""
|
||||
model_cost = {
|
||||
"my-model-latest": {
|
||||
"aliases": ["my-model-20250101"],
|
||||
"input_cost_per_token": 1e-06,
|
||||
"litellm_provider": "test",
|
||||
"mode": "chat",
|
||||
},
|
||||
}
|
||||
result = _expand_model_aliases(model_cost)
|
||||
|
||||
assert "my-model-20250101" in result
|
||||
assert result["my-model-20250101"]["input_cost_per_token"] == 1e-06
|
||||
assert result["my-model-20250101"]["litellm_provider"] == "test"
|
||||
|
||||
def test_multiple_aliases(self):
|
||||
"""A single entry can declare multiple aliases."""
|
||||
model_cost = {
|
||||
"provider/model-latest": {
|
||||
"aliases": ["provider/model-v1", "provider/model-v2"],
|
||||
"input_cost_per_token": 5e-06,
|
||||
"litellm_provider": "provider",
|
||||
"mode": "chat",
|
||||
},
|
||||
}
|
||||
result = _expand_model_aliases(model_cost)
|
||||
|
||||
assert "provider/model-v1" in result
|
||||
assert "provider/model-v2" in result
|
||||
|
||||
def test_shared_dict_reference(self):
|
||||
"""Alias entries share the same dict object as the canonical entry (no copy)."""
|
||||
model_cost = {
|
||||
"canonical-model": {
|
||||
"aliases": ["alias-model"],
|
||||
"input_cost_per_token": 2e-06,
|
||||
"litellm_provider": "test",
|
||||
"mode": "chat",
|
||||
},
|
||||
}
|
||||
result = _expand_model_aliases(model_cost)
|
||||
|
||||
assert result["alias-model"] is result["canonical-model"]
|
||||
|
||||
def test_aliases_key_removed(self):
|
||||
"""The ``aliases`` key is removed from the entry after expansion."""
|
||||
model_cost = {
|
||||
"my-model": {
|
||||
"aliases": ["my-model-alias"],
|
||||
"input_cost_per_token": 1e-06,
|
||||
"litellm_provider": "test",
|
||||
"mode": "chat",
|
||||
},
|
||||
}
|
||||
result = _expand_model_aliases(model_cost)
|
||||
|
||||
assert "aliases" not in result["my-model"]
|
||||
assert "aliases" not in result["my-model-alias"]
|
||||
|
||||
def test_entries_without_aliases_unchanged(self):
|
||||
"""Entries with no ``aliases`` key are left untouched."""
|
||||
model_cost = {
|
||||
"plain-model": {
|
||||
"input_cost_per_token": 3e-06,
|
||||
"litellm_provider": "test",
|
||||
"mode": "chat",
|
||||
},
|
||||
}
|
||||
result = _expand_model_aliases(model_cost)
|
||||
|
||||
assert "plain-model" in result
|
||||
assert result["plain-model"]["input_cost_per_token"] == 3e-06
|
||||
assert len(result) == 1
|
||||
|
||||
def test_empty_aliases_list(self):
|
||||
"""An empty ``aliases`` list is treated the same as no aliases."""
|
||||
model_cost = {
|
||||
"model-a": {
|
||||
"aliases": [],
|
||||
"input_cost_per_token": 1e-06,
|
||||
"litellm_provider": "test",
|
||||
"mode": "chat",
|
||||
},
|
||||
}
|
||||
result = _expand_model_aliases(model_cost)
|
||||
|
||||
assert len(result) == 1
|
||||
assert "model-a" in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Conflict handling
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAliasConflicts:
|
||||
"""Tests for alias conflict detection and handling."""
|
||||
|
||||
def test_alias_conflicts_with_canonical_entry(self, caplog):
|
||||
"""Alias that matches an existing canonical entry is skipped with a warning."""
|
||||
model_cost = {
|
||||
"model-latest": {
|
||||
"aliases": ["model-dated"],
|
||||
"input_cost_per_token": 1e-06,
|
||||
"litellm_provider": "test",
|
||||
"mode": "chat",
|
||||
},
|
||||
"model-dated": {
|
||||
"input_cost_per_token": 1e-06,
|
||||
"litellm_provider": "test",
|
||||
"mode": "chat",
|
||||
},
|
||||
}
|
||||
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
|
||||
result = _expand_model_aliases(model_cost)
|
||||
|
||||
# The canonical "model-dated" entry is preserved, not overwritten
|
||||
assert "model-dated" in result
|
||||
assert "alias conflict" in caplog.text.lower() or len(result) == 2
|
||||
|
||||
def test_duplicate_alias_across_entries(self, caplog):
|
||||
"""Same alias claimed by two different entries: second one is skipped."""
|
||||
model_cost = {
|
||||
"model-a": {
|
||||
"aliases": ["shared-alias"],
|
||||
"input_cost_per_token": 1e-06,
|
||||
"litellm_provider": "test",
|
||||
"mode": "chat",
|
||||
},
|
||||
"model-b": {
|
||||
"aliases": ["shared-alias"],
|
||||
"input_cost_per_token": 2e-06,
|
||||
"litellm_provider": "test",
|
||||
"mode": "chat",
|
||||
},
|
||||
}
|
||||
with caplog.at_level(logging.WARNING, logger="LiteLLM"):
|
||||
result = _expand_model_aliases(model_cost)
|
||||
|
||||
# "shared-alias" should point to model-a (first one wins)
|
||||
assert "shared-alias" in result
|
||||
assert result["shared-alias"]["input_cost_per_token"] == 1e-06
|
||||
|
||||
def test_canonical_entry_not_overwritten_by_alias(self):
|
||||
"""An alias must never overwrite an existing canonical entry's data."""
|
||||
original_cost = 9.99e-06
|
||||
model_cost = {
|
||||
"existing-model": {
|
||||
"input_cost_per_token": original_cost,
|
||||
"litellm_provider": "test",
|
||||
"mode": "chat",
|
||||
},
|
||||
"other-model": {
|
||||
"aliases": ["existing-model"],
|
||||
"input_cost_per_token": 1e-06,
|
||||
"litellm_provider": "test",
|
||||
"mode": "chat",
|
||||
},
|
||||
}
|
||||
result = _expand_model_aliases(model_cost)
|
||||
|
||||
# Original entry must be preserved
|
||||
assert result["existing-model"]["input_cost_per_token"] == original_cost
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Integration with model_cost dict mutation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAliasIntegration:
|
||||
"""Higher-level tests verifying aliases work with the model_cost dict."""
|
||||
|
||||
def test_mutation_through_alias_visible_on_canonical(self):
|
||||
"""Since alias is a shared reference, mutations are visible on both."""
|
||||
model_cost = {
|
||||
"canonical": {
|
||||
"aliases": ["alias"],
|
||||
"input_cost_per_token": 1e-06,
|
||||
"litellm_provider": "test",
|
||||
"mode": "chat",
|
||||
},
|
||||
}
|
||||
result = _expand_model_aliases(model_cost)
|
||||
|
||||
# Mutate via alias
|
||||
result["alias"]["input_cost_per_token"] = 999
|
||||
assert result["canonical"]["input_cost_per_token"] == 999
|
||||
|
||||
def test_mixed_entries_with_and_without_aliases(self):
|
||||
"""A model_cost dict with a mix of aliased and plain entries."""
|
||||
model_cost = {
|
||||
"model-with-alias": {
|
||||
"aliases": ["alias-1", "alias-2"],
|
||||
"input_cost_per_token": 1e-06,
|
||||
"litellm_provider": "test",
|
||||
"mode": "chat",
|
||||
},
|
||||
"plain-model": {
|
||||
"input_cost_per_token": 2e-06,
|
||||
"litellm_provider": "test",
|
||||
"mode": "chat",
|
||||
},
|
||||
}
|
||||
result = _expand_model_aliases(model_cost)
|
||||
|
||||
assert len(result) == 4 # 2 canonical + 2 aliases
|
||||
assert "alias-1" in result
|
||||
assert "alias-2" in result
|
||||
assert "plain-model" in result
|
||||
assert "model-with-alias" in result
|
||||
|
||||
def test_expand_on_empty_dict(self):
|
||||
"""Expanding an empty dict returns an empty dict."""
|
||||
assert _expand_model_aliases({}) == {}
|
||||
Loading…
Add table
Reference in a new issue