diff --git a/strix/report/pricing.py b/strix/report/pricing.py index 57c89959..dc4c8938 100644 --- a/strix/report/pricing.py +++ b/strix/report/pricing.py @@ -7,7 +7,9 @@ from typing import Any, cast @lru_cache(maxsize=512) -def resolve_litellm_model(model: str) -> str | None: +def resolve_litellm_model( # noqa: PLR0911 - each resolution path returns its own name + model: str, +) -> str | None: """Return a provider-qualified model name that LiteLLM can price.""" try: import litellm @@ -39,6 +41,22 @@ def resolve_litellm_model(model: str) -> str | None: matches = sorted(key for key in model_cost if key.endswith(f"/{name}")) if not matches: continue + # Prefer the first-party provider's own listing over an aggregator's + # re-listing of the same model. Aggregators (OpenRouter, DeepInfra, + # Perplexity, ...) re-key a model under a namespaced path such as + # "openrouter/x-ai/grok-4.5"; the native listing is the one whose key + # is exactly "/" ("xai/grok-4.5"). When that + # is unique it is the canonical name, so tie-break on it before + # falling back to price consensus (which would otherwise pick the + # alphabetically first key, an aggregator). + native = [ + key + for key in matches + if isinstance(model_cost.get(key), dict) + and key == f"{model_cost[key].get('litellm_provider')}/{name}" + ] + if len(native) == 1: + return native[0] prices = { ( model_cost[key].get("input_cost_per_token"), diff --git a/tests/test_pricing.py b/tests/test_pricing.py index 8916982d..c06b62b5 100644 --- a/tests/test_pricing.py +++ b/tests/test_pricing.py @@ -127,3 +127,52 @@ def test_resolver_does_not_guess_between_differently_priced_providers() -> None: finally: litellm.model_cost = original resolve_litellm_model.cache_clear() + + +def test_resolver_prefers_native_provider_over_aggregator() -> None: + # The same model re-listed by an aggregator (which sorts first) and by its + # first-party provider at the same price. The resolver must return the + # native listing, not the alphabetically first aggregator key. + original = litellm.model_cost + litellm.model_cost = { + "openrouter/x-ai/grok-4.5": { + "litellm_provider": "openrouter", + "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, + }, + } + try: + resolve_litellm_model.cache_clear() + assert resolve_litellm_model("grok-4.5") == "xai/grok-4.5" + finally: + litellm.model_cost = original + resolve_litellm_model.cache_clear() + + +def test_resolver_prefers_unique_native_provider_over_differently_priced_aggregators() -> None: + # A unique first-party listing wins even when aggregators price the model + # differently, where price consensus alone would give up and return None. + original = litellm.model_cost + litellm.model_cost = { + "deepinfra/x/example-model": { + "litellm_provider": "deepinfra", + "input_cost_per_token": 1e-06, + "output_cost_per_token": 2e-06, + }, + "example/example-model": { + "litellm_provider": "example", + "input_cost_per_token": 5e-06, + "output_cost_per_token": 9e-06, + }, + } + try: + resolve_litellm_model.cache_clear() + assert resolve_litellm_model("example-model") == "example/example-model" + finally: + litellm.model_cost = original + resolve_litellm_model.cache_clear()