From e0623b509663290d581cbc1d5cc787ebae9fa1d6 Mon Sep 17 00:00:00 2001 From: jav1er8 <93607250+jav1er8@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:25:21 -0400 Subject: [PATCH] fix(pricing): prefer the provider's own route when several are equally priced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolve_litellm_model returned matches[0] after sorting the candidate routes alphabetically. LiteLLM 1.90.1 lists grok-4.5 under three routes with identical pricing — xai/grok-4.5, openrouter/x-ai/grok-4.5 and perplexity/xai/grok-4.5 — so the aggregator route now sorts ahead of the provider's own and wins. This broke test_resolves_common_bare_model_names on main: assert resolve_litellm_model("grok-4.5") == "xai/grok-4.5" AssertionError: assert 'openrouter/x-ai/grok-4.5' == 'xai/grok-4.5' Pick the canonical route instead of the first one alphabetically: among equally priced matches, the provider's own route is the one shaped exactly {litellm_provider}/{name}. An explicitly aggregator-qualified name such as x-ai/grok-4.5 still resolves to the aggregator, since that is what was asked for. The added test pins the behaviour against a fixed model_cost mapping rather than the live LiteLLM registry, so it will not drift when the registry changes again. --- strix/report/pricing.py | 25 ++++++++++++++++++++++++- tests/test_pricing.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/strix/report/pricing.py b/strix/report/pricing.py index 57c899592..028c04596 100644 --- a/strix/report/pricing.py +++ b/strix/report/pricing.py @@ -6,6 +6,29 @@ from functools import lru_cache from typing import Any, cast +def _preferred_route( + matches: list[str], + model_cost: dict[str, dict[str, Any]], + name: str, +) -> str: + """Pick the provider's own route when several equally priced ones match. + + LiteLLM lists the same model under its native provider (``xai/grok-4.5``) and + under the aggregators that resell it (``openrouter/x-ai/grok-4.5``, + ``perplexity/xai/grok-4.5``). Alphabetical order would hand back an + aggregator; the canonical route is the one shaped exactly + ``{litellm_provider}/{name}``. + """ + for key in matches: + entry = model_cost.get(key) + if not isinstance(entry, dict): + continue + provider = entry.get("litellm_provider") + if isinstance(provider, str) and key == f"{provider}/{name}": + return key + return matches[0] + + @lru_cache(maxsize=512) def resolve_litellm_model(model: str) -> str | None: """Return a provider-qualified model name that LiteLLM can price.""" @@ -48,7 +71,7 @@ def resolve_litellm_model(model: str) -> str | None: if isinstance(model_cost.get(key), dict) } if len(matches) == 1 or len(prices) == 1: - return matches[0] + return _preferred_route(matches, model_cost, name) return None # noqa: TRY300 except Exception: # noqa: BLE001 return None diff --git a/tests/test_pricing.py b/tests/test_pricing.py index c9e9e5dfb..c97736789 100644 --- a/tests/test_pricing.py +++ b/tests/test_pricing.py @@ -20,6 +20,35 @@ def test_resolves_common_bare_model_names() -> None: assert resolve_litellm_model("minimax/MiniMax-M3") == "minimax/MiniMax-M3" +def test_prefers_native_provider_route_over_aggregators() -> None: + """Equally priced routes must resolve to the provider's own, not the first alphabetically.""" + resolve_litellm_model.cache_clear() + model_cost = { + "openrouter/x-ai/grok-4.5": { + "litellm_provider": "openrouter", + "input_cost_per_token": 2e-06, + "output_cost_per_token": 6e-06, + }, + "perplexity/xai/grok-4.5": { + "litellm_provider": "perplexity", + "input_cost_per_token": 2e-06, + "output_cost_per_token": 6e-06, + }, + "xai/grok-4.5": { + "litellm_provider": "xai", + "input_cost_per_token": 2e-06, + "output_cost_per_token": 6e-06, + }, + } + with patch.object(litellm, "model_cost", model_cost): + assert resolve_litellm_model("grok-4.5") == "xai/grok-4.5" + + # An explicitly aggregator-qualified name still resolves to that aggregator. + resolve_litellm_model.cache_clear() + with patch.object(litellm, "model_cost", model_cost): + assert resolve_litellm_model("x-ai/grok-4.5") == "openrouter/x-ai/grok-4.5" + + def test_resolver_returns_none_for_unresolvable_model() -> None: resolve_litellm_model.cache_clear() assert resolve_litellm_model("provider/not-a-real-model") is None