Merge pull request #23314 from Chesars/feat/model-cost-aliases-clean

feat: add model_cost aliases expansion support
This commit is contained in:
Cesar Garcia 2026-03-10 23:19:29 -03:00 committed by GitHub
commit 0d9afb2200
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 324 additions and 6 deletions

View file

@ -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,
@ -121,4 +122,28 @@ Here's the full specification with all available fields:
}
```
That's it! Your PR will be reviewed and merged.
### Using Aliases
Many providers release the same model under multiple names — for example, a `latest` tag and a dated version like `claude-sonnet-4-5-20250929`. Instead of duplicating the entire entry, you can use the `aliases` field:
```json
{
"claude-sonnet-4-5": {
"aliases": ["claude-sonnet-4-5-20250929"],
"input_cost_per_token": 3e-06,
"output_cost_per_token": 1.5e-05,
"litellm_provider": "anthropic",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"mode": "chat",
"supports_function_calling": true,
"supports_tool_choice": true
}
}
```
At load time, each alias is expanded into a top-level entry sharing the same data as the canonical entry. The example above makes both `claude-sonnet-4-5` and `claude-sonnet-4-5-20250929` resolve with the same pricing and capabilities.
:::info
This is different from [`model_alias_map`](../completion/model_alias.md), which is a runtime SDK/proxy feature for mapping user-facing model names to LiteLLM model identifiers. The `aliases` field here is for the model cost JSON only — it avoids duplicate entries for models that share identical pricing and capabilities.
:::

View file

@ -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,61 @@ 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
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 aliases is None:
continue
keys_with_aliases.append(model_name)
if not isinstance(aliases, list):
verbose_logger.warning(
"LiteLLM model alias field for '%s' is not a list (got %s) — skipping.",
model_name,
type(aliases).__name__,
)
continue
if not aliases:
continue
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 +257,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 +273,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 +287,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)

View file

@ -0,0 +1,238 @@
"""
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
from litellm.litellm_core_utils.get_model_cost_map import _expand_model_aliases
# ---------------------------------------------------------------------------
# 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
assert "aliases" not in result["model-a"]
# ---------------------------------------------------------------------------
# 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()
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
assert "alias conflict" in caplog.text.lower()
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({}) == {}