From a54c42218a0e306b2ad4cd1aea9ad70f54392e8f Mon Sep 17 00:00:00 2001 From: kerry Date: Sat, 7 Nov 2026 07:26:24 +0000 Subject: [PATCH] fix(cost-map): let alias lists win over alias_of rows in both loaders and never serve a malformed pointer Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../crates/model-catalog/src/catalog.rs | 5 +++- .../crates/model-catalog/tests/catalog.rs | 13 ++++++++++ .../litellm_core_utils/get_model_cost_map.py | 25 ++++++++++++------- .../test_get_model_cost_map.py | 7 ++++-- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/litellm-rust/crates/model-catalog/src/catalog.rs b/litellm-rust/crates/model-catalog/src/catalog.rs index d502531d86c..55a95416da3 100644 --- a/litellm-rust/crates/model-catalog/src/catalog.rs +++ b/litellm-rust/crates/model-catalog/src/catalog.rs @@ -159,13 +159,16 @@ impl Catalog { } }; let row = read_row(&fields); + let readable = row.problems.is_empty(); problems.extend( row.problems .into_iter() .map(|problem| problem.prepend(&name)), ); if let Some(target) = row.alias_of { - pointer_rows.push((name.clone(), target)); + if readable { + pointer_rows.push((name.clone(), target)); + } sections.push(Section { index, key: name, diff --git a/litellm-rust/crates/model-catalog/tests/catalog.rs b/litellm-rust/crates/model-catalog/tests/catalog.rs index 23d9beb363e..679c67412c8 100644 --- a/litellm-rust/crates/model-catalog/tests/catalog.rs +++ b/litellm-rust/crates/model-catalog/tests/catalog.rs @@ -451,6 +451,19 @@ fn a_dangling_alias_of_row_is_rejected_and_dropped_from_the_written_document() { assert_eq!(written(&catalog), r#"{"Real":{"litellm_provider":"t"}}"#); } +#[rstest] +fn an_alias_of_row_with_other_fields_is_never_served() { + let parsed = loaded(json!({ + "Real": {"litellm_provider": "t"}, + "real-latest": {"alias_of": "Real", "mode": "chat"} + })); + assert!(parsed.value.lookup("real-latest").is_none()); + let (catalog, removed) = parsed.value.without_rejected(&parsed.errors); + assert_eq!(removed.len(), 1); + assert!(catalog.lookup("real-latest").is_none()); + assert_eq!(written(&catalog), r#"{"Real":{"litellm_provider":"t"}}"#); +} + #[rstest] fn a_pointer_row_that_repeats_a_listed_alias_is_dropped_and_the_model_is_kept() { let parsed = loaded(json!({ diff --git a/litellm/litellm_core_utils/get_model_cost_map.py b/litellm/litellm_core_utils/get_model_cost_map.py index e8feeb75351..d2045c947fe 100644 --- a/litellm/litellm_core_utils/get_model_cost_map.py +++ b/litellm/litellm_core_utils/get_model_cost_map.py @@ -596,14 +596,6 @@ def _expand_model_aliases(model_cost: dict[str, dict[str, object]]) -> dict[str, alias_owners: Final[dict[str, str]] = {} keys_with_aliases: Final[list[str]] = [] - for alias, target in pointer_rows.items(): - if target not in model_cost or target in pointer_rows: - verbose_logger.warning( - "LiteLLM model alias '%s' points at '%s', which is not a model entry, dropping it.", alias, target - ) - continue - alias_owners[alias] = target - for model_name, model_info in model_cost.items(): aliases: object = model_info.get("aliases") if aliases is None or model_name in pointer_rows: @@ -619,7 +611,7 @@ def _expand_model_aliases(model_cost: dict[str, dict[str, object]]) -> dict[str, if not aliases: continue for alias in aliases: - if alias in model_cost: + if alias in model_cost and alias not in pointer_rows: verbose_logger.warning( "LiteLLM model alias conflict: alias '%s' (from '%s') " "already exists as a canonical entry — skipping.", @@ -637,6 +629,21 @@ def _expand_model_aliases(model_cost: dict[str, dict[str, object]]) -> dict[str, continue alias_owners[alias] = model_name + for alias, target in pointer_rows.items(): + if target not in model_cost or target in pointer_rows: + verbose_logger.warning( + "LiteLLM model alias '%s' points at '%s', which is not a model entry, dropping it.", alias, target + ) + elif alias in alias_owners: + verbose_logger.warning( + "LiteLLM model alias conflict: alias '%s' (pointing at '%s') " + "was already claimed by another entry — skipping.", + alias, + target, + ) + else: + alias_owners[alias] = target + # 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) diff --git a/tests/unit/litellm_core_utils/test_get_model_cost_map.py b/tests/unit/litellm_core_utils/test_get_model_cost_map.py index 1391fff257f..6267b841a95 100644 --- a/tests/unit/litellm_core_utils/test_get_model_cost_map.py +++ b/tests/unit/litellm_core_utils/test_get_model_cost_map.py @@ -135,7 +135,9 @@ def test_alias_of_rows_are_served_as_their_target_and_dangling_ones_are_dropped( ) -> None: raw: Final = { "real": {"litellm_provider": "openai", "mode": "chat", "input_cost_per_token": 1e-6}, + "other": {"litellm_provider": "openai", "mode": "chat", "aliases": ["twin-latest"]}, "real-latest": {"alias_of": "real"}, + "twin-latest": {"alias_of": "real"}, "ghost-latest": {"alias_of": "ghost"}, "chain-latest": {"alias_of": "real-latest"}, "listed-latest": {"alias_of": "missing", "aliases": ["extra"]}, @@ -144,9 +146,10 @@ def test_alias_of_rows_are_served_as_their_target_and_dangling_ones_are_dropped( finalized: Final = _finalize_model_cost_map(raw) assert finalized["real-latest"] == finalized["real"] assert "alias_of" not in finalized["real"] - assert set(finalized) == {"real", "real-latest"} + assert set(finalized) == {"real", "other", "real-latest", "twin-latest"} assert finalized["real-latest"]["litellm_provider"] == "openai" - assert {"ghost-latest", "chain-latest", "listed-latest"} == { + assert finalized["twin-latest"] == finalized["other"] + assert {"ghost-latest", "chain-latest", "listed-latest", "twin-latest"} == { record.getMessage().split("'")[1] for record in caplog.records }