From c0dbff21a65af671c03406344af7764f7d2a27ef Mon Sep 17 00:00:00 2001 From: Chesars Date: Thu, 19 Feb 2026 17:13:30 -0300 Subject: [PATCH 1/9] feat: add model cost aliases expansion support --- .../add_model_pricing.md | 1 + .../litellm_core_utils/get_model_cost_map.py | 56 +++- tests/test_litellm/test_model_cost_aliases.py | 247 ++++++++++++++++++ 3 files changed, 299 insertions(+), 5 deletions(-) create mode 100644 tests/test_litellm/test_model_cost_aliases.py diff --git a/docs/my-website/docs/provider_registration/add_model_pricing.md b/docs/my-website/docs/provider_registration/add_model_pricing.md index ebf35c42e32..76251516fac 100644 --- a/docs/my-website/docs/provider_registration/add_model_pricing.md +++ b/docs/my-website/docs/provider_registration/add_model_pricing.md @@ -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, diff --git a/litellm/litellm_core_utils/get_model_cost_map.py b/litellm/litellm_core_utils/get_model_cost_map.py index f9398979f97..0aabedae603 100644 --- a/litellm/litellm_core_utils/get_model_cost_map.py +++ b/litellm/litellm_core_utils/get_model_cost_map.py @@ -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) diff --git a/tests/test_litellm/test_model_cost_aliases.py b/tests/test_litellm/test_model_cost_aliases.py new file mode 100644 index 00000000000..29a37ed54d7 --- /dev/null +++ b/tests/test_litellm/test_model_cost_aliases.py @@ -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({}) == {} From b00e507574ef73ae9e069d3e32ec8717d1409284 Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 10 Mar 2026 16:09:49 -0300 Subject: [PATCH 2/9] fix: address Greptile review feedback - Clean up aliases key from entries with empty aliases list - Strengthen test assertion for alias conflict warning --- litellm/litellm_core_utils/get_model_cost_map.py | 4 +++- tests/test_litellm/test_model_cost_aliases.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/litellm/litellm_core_utils/get_model_cost_map.py b/litellm/litellm_core_utils/get_model_cost_map.py index 0aabedae603..406702503ef 100644 --- a/litellm/litellm_core_utils/get_model_cost_map.py +++ b/litellm/litellm_core_utils/get_model_cost_map.py @@ -199,9 +199,11 @@ def _expand_model_aliases(model_cost: dict) -> dict: for model_name, model_info in model_cost.items(): aliases: Optional[list] = model_info.get("aliases") - if not aliases: + if aliases is None: continue keys_with_aliases.append(model_name) + if not aliases: + continue for alias in aliases: if alias in model_cost: verbose_logger.warning( diff --git a/tests/test_litellm/test_model_cost_aliases.py b/tests/test_litellm/test_model_cost_aliases.py index 29a37ed54d7..c6702ab13e4 100644 --- a/tests/test_litellm/test_model_cost_aliases.py +++ b/tests/test_litellm/test_model_cost_aliases.py @@ -148,7 +148,7 @@ class TestAliasConflicts: # 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 + 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.""" From a5f0e1a74197eaa6ef3435face95fa6ec46e2c1d Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 10 Mar 2026 16:54:55 -0300 Subject: [PATCH 3/9] docs: expand aliases section in add_model_pricing guide Add usage example with concrete model entry, explanation of load-time expansion, and cross-reference to model_alias_map to clarify the difference between the two features. --- .../add_model_pricing.md | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/my-website/docs/provider_registration/add_model_pricing.md b/docs/my-website/docs/provider_registration/add_model_pricing.md index 76251516fac..b3df1865cdd 100644 --- a/docs/my-website/docs/provider_registration/add_model_pricing.md +++ b/docs/my-website/docs/provider_registration/add_model_pricing.md @@ -122,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. +::: From 18a48d0a0b44fead26f12dba5d64a52d5f8513ae Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 10 Mar 2026 22:46:21 -0300 Subject: [PATCH 4/9] fix(tests): add missing assertions for alias conflict warning and aliases key removal --- tests/test_litellm/test_model_cost_aliases.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_litellm/test_model_cost_aliases.py b/tests/test_litellm/test_model_cost_aliases.py index c6702ab13e4..5e10dd43607 100644 --- a/tests/test_litellm/test_model_cost_aliases.py +++ b/tests/test_litellm/test_model_cost_aliases.py @@ -118,6 +118,7 @@ class TestExpandModelAliases: assert len(result) == 1 assert "model-a" in result + assert "aliases" not in result["model-a"] # --------------------------------------------------------------------------- @@ -172,6 +173,7 @@ class TestAliasConflicts: # "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.""" From f51a5b9c3ea64ef97fc2f910c64ba7d637190cf6 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:53:02 -0300 Subject: [PATCH 5/9] Update tests/test_litellm/test_model_cost_aliases.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- tests/test_litellm/test_model_cost_aliases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_litellm/test_model_cost_aliases.py b/tests/test_litellm/test_model_cost_aliases.py index 5e10dd43607..69a0622f563 100644 --- a/tests/test_litellm/test_model_cost_aliases.py +++ b/tests/test_litellm/test_model_cost_aliases.py @@ -7,7 +7,7 @@ entries, creating shared dict references for alias entries at load time. import logging -import pytest +import logging from litellm.litellm_core_utils.get_model_cost_map import _expand_model_aliases From 2ed4119542d9be9151313c03cb7a29ec30d8f176 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:56:08 -0300 Subject: [PATCH 6/9] Update tests/test_litellm/test_model_cost_aliases.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- tests/test_litellm/test_model_cost_aliases.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_litellm/test_model_cost_aliases.py b/tests/test_litellm/test_model_cost_aliases.py index 69a0622f563..ee01484922a 100644 --- a/tests/test_litellm/test_model_cost_aliases.py +++ b/tests/test_litellm/test_model_cost_aliases.py @@ -7,7 +7,6 @@ entries, creating shared dict references for alias entries at load time. import logging -import logging from litellm.litellm_core_utils.get_model_cost_map import _expand_model_aliases From 7b3621518b53d7515d98158ee9b87ee0bc32cc61 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:56:27 -0300 Subject: [PATCH 7/9] Update litellm/litellm_core_utils/get_model_cost_map.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- litellm/litellm_core_utils/get_model_cost_map.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/litellm/litellm_core_utils/get_model_cost_map.py b/litellm/litellm_core_utils/get_model_cost_map.py index 406702503ef..81182f06dbf 100644 --- a/litellm/litellm_core_utils/get_model_cost_map.py +++ b/litellm/litellm_core_utils/get_model_cost_map.py @@ -192,8 +192,7 @@ def _expand_model_aliases(model_cost: dict) -> dict: 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. - """ + skipped and a warning is logged. aliases_to_add: Dict[str, dict] = {} keys_with_aliases: List[str] = [] From 8b385120b986f0b8e29f14cd1ba7caa5710f9427 Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 10 Mar 2026 22:57:58 -0300 Subject: [PATCH 8/9] fix: add isinstance guard for aliases field and remove unused helper --- litellm/litellm_core_utils/get_model_cost_map.py | 7 +++++++ tests/test_litellm/test_model_cost_aliases.py | 10 ---------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/litellm/litellm_core_utils/get_model_cost_map.py b/litellm/litellm_core_utils/get_model_cost_map.py index 81182f06dbf..b97cf32a67f 100644 --- a/litellm/litellm_core_utils/get_model_cost_map.py +++ b/litellm/litellm_core_utils/get_model_cost_map.py @@ -201,6 +201,13 @@ def _expand_model_aliases(model_cost: dict) -> dict: 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: diff --git a/tests/test_litellm/test_model_cost_aliases.py b/tests/test_litellm/test_model_cost_aliases.py index ee01484922a..6e30cbfe157 100644 --- a/tests/test_litellm/test_model_cost_aliases.py +++ b/tests/test_litellm/test_model_cost_aliases.py @@ -7,19 +7,9 @@ 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 -# --------------------------------------------------------------------------- -# 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 # --------------------------------------------------------------------------- From 4eead432a64d3e5565d88eb864fab8966f4dd08b Mon Sep 17 00:00:00 2001 From: Chesars Date: Tue, 10 Mar 2026 23:08:06 -0300 Subject: [PATCH 9/9] fix: close docstring in _expand_model_aliases --- litellm/litellm_core_utils/get_model_cost_map.py | 1 + 1 file changed, 1 insertion(+) diff --git a/litellm/litellm_core_utils/get_model_cost_map.py b/litellm/litellm_core_utils/get_model_cost_map.py index b97cf32a67f..5673064a238 100644 --- a/litellm/litellm_core_utils/get_model_cost_map.py +++ b/litellm/litellm_core_utils/get_model_cost_map.py @@ -193,6 +193,7 @@ def _expand_model_aliases(model_cost: dict) -> dict: 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] = []