diff --git a/config.live.lua b/config.live.lua index c9c6dc0..c3a9865 100644 --- a/config.live.lua +++ b/config.live.lua @@ -74,6 +74,16 @@ return { auth_env = "OPENROUTER_API_KEY", tier = "marketplace", market_price_cap = { input = 1000, output = 1000 }, + -- OpenRouter marketplace rows default `vendor/model` to the + -- provider-neutral family `model`, while wire_model_id keeps the + -- exact OpenRouter slug. Keep this map only for the canonicalization + -- exceptions where stripping the vendor isn't the right family + -- (dated or suffixed slugs). + service_aliases = { + ["anthropic/claude-opus-4.8"] = "claude-opus-4-8", + ["google/gemma-3-27b-it"] = "gemma-3-27b", + ["qwen/qwen3-235b-a22b-2507"] = "qwen3-235b-a22b", + }, }, -- AntSeed buyer proxies: candidates and prices come from the live -- peer market (sources/antseed.py reads the /market dump and feeds diff --git a/core b/core index ee31e81..97d0333 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit ee31e812281b178e736c5ec86441acadf335003b +Subproject commit 97d03334b153c77f9fbf787bd341235a1a128f53 diff --git a/sources/openrouter.py b/sources/openrouter.py index e50e552..0ec9275 100644 --- a/sources/openrouter.py +++ b/sources/openrouter.py @@ -36,7 +36,21 @@ def __init__(self, catalog: dict, env_get=os.environ.get, # sync hook — offers_sync runs inside rank, so it must not rebuild the # whole catalog per call. Empty until the first refresh populates it. self._offers: list[dict] = [] - # provider_model_id -> curated family, from the catalog's served_by + providers = catalog.get("providers") or {} + market_cfg = providers.get("openrouter_market") or {} + self._market_aliases: dict[str, str] = { + str(raw): str(family) + for aliases in ( + market_cfg.get("service_aliases") or {}, + market_cfg.get("model_aliases") or {}, + ) + for raw, family in aliases.items() + if raw and family + } + # provider_model_id -> curated family, from the catalog's served_by. + # Static curated routes keep the provider id `openrouter`; dynamic + # marketplace routes use `openrouter_market` and may alias raw model ids + # to policy-facing families below. self._family_by_id: dict[str, str] = {} for family, model in (catalog.get("models") or {}).items(): for served in model.get("served_by") or []: @@ -125,12 +139,16 @@ def _offer_for(self, m: dict) -> dict | None: caps["supports_seed"] = True if traits.get("in_image"): caps["supports_vision"] = True + family = self._market_family_for(mid) return { - # the raw OpenRouter id IS the family. NOT a second-class "uncurated" - # row: it carries the full live model_meta (benchmarks/modalities/caps - # + ranks) inline as `traits`, so it ranks on real benchmark just like - # a curated family — config.live.lua's mfield reads c.offer.traits. - "model_family": mid, + # The policy-facing family is provider-agnostic by default: + # `openai/gpt-5-mini` -> `gpt-5-mini`. Exact provider-local aliases + # handle cases where the OpenRouter tail is not the canonical family + # we want, while wire_model_id preserves the actual provider slug. + # Either way this is not a second-class "uncurated" row: it carries + # full live model_meta inline as `traits`, so it ranks on real + # benchmark just like a curated family. + "model_family": family, "wire_model_id": mid, "seller_endpoint": self._base_url, "price_in_usd_per_mtok": price_in, @@ -141,6 +159,19 @@ def _offer_for(self, m: dict) -> dict | None: "quality_hint": None, } + def _market_family_for(self, model_id: str) -> str: + """Policy-facing family for an OpenRouter marketplace model. + + OpenRouter ids are provider-scoped (`vendor/model`). Router policies are + provider-agnostic, so the default family is the model part. Exact aliases + override this only for canonicalization exceptions such as dated or + provider-specific suffixes. + """ + alias = self._market_aliases.get(model_id) + if alias: + return alias + return model_id.rsplit("/", 1)[-1] + def offers_sync(self, provider_id: str) -> list[dict]: """Whole live OpenRouter catalog as marketplace offers (the long tail beyond the curated families). Read from the cached /models snapshot the diff --git a/tests/test_sources.py b/tests/test_sources.py index cb36039..a83eff6 100644 --- a/tests/test_sources.py +++ b/tests/test_sources.py @@ -28,7 +28,6 @@ }, } - class FakeHost: def __init__(self): self.pushed = [] @@ -224,8 +223,8 @@ def test_openrouter_discovery_offers_carry_full_live_traits(): asyncio.run(s.pricing()) # populates the snapshot + live traits offers = {o["model_family"]: o for o in s.offers_sync("openrouter_market")} # curated family skipped (no dup); non-numeric pricing skipped - assert set(offers) == {"z-ai/glm-5.2", "acme/cheap-7b"} - glm = offers["z-ai/glm-5.2"] + assert set(offers) == {"glm-5.2", "cheap-7b"} + glm = offers["glm-5.2"] assert glm["wire_model_id"] == "z-ai/glm-5.2" assert glm["seller_endpoint"].endswith("openrouter.ai/api/v1") assert round(glm["price_in_usd_per_mtok"], 4) == 0.4 @@ -237,7 +236,55 @@ def test_openrouter_discovery_offers_carry_full_live_traits(): assert t["cap_tools"] is True and t["cap_reasoning"] is True # ranks are dynamic, across the live catalog (glm beats the weaker cheap-7b) assert t["bench_intelligence_rank"] == 1 - assert offers["acme/cheap-7b"]["traits"]["bench_intelligence_rank"] == 2 + assert offers["cheap-7b"]["traits"]["bench_intelligence_rank"] == 2 + + +def test_openrouter_discovery_derives_policy_families_from_raw_model_ids(): + from sources.openrouter import OpenRouterSource + + catalog = { + "providers": { + "openrouter": {"auth_env": "OPENROUTER_API_KEY"}, + "openrouter_market": { + "discovery": "marketplace", + "discovery_id": "openrouter_market", + "service_aliases": { + "qwen/qwen3-235b-a22b-2507": "qwen3-235b-a22b", + }, + }, + }, + "models": { + "gpt-5.5": {"served_by": [ + {"provider": "openrouter", "provider_model_id": "openai/gpt-5.5"}, + ]}, + }, + } + body = {"data": [ + {"id": "openai/gpt-5.5", "pricing": { + "prompt": "0.000005", "completion": "0.00003"}}, + {"id": "openai/gpt-5-mini", "pricing": { + "prompt": "0.00000025", "completion": "0.000002"}}, + {"id": "meta-llama/llama-4-maverick", "pricing": { + "prompt": "0.0000002", "completion": "0.0000006"}}, + {"id": "unknown/raw-family", "pricing": { + "prompt": "0.0000001", "completion": "0.0000001"}}, + {"id": "qwen/qwen3-235b-a22b-2507", "pricing": { + "prompt": "0.0000005", "completion": "0.000001"}}, + ]} + s = OpenRouterSource(catalog, env_get={"OPENROUTER_API_KEY": "sk-test"}.get, + client=FakeClient({"/models": FakeResponse(200, body)})) + + asyncio.run(s.pricing()) + + offers = {o["model_family"]: o for o in s.offers_sync("openrouter_market")} + assert set(offers) == { + "gpt-5-mini", "llama-4-maverick", "raw-family", "qwen3-235b-a22b"} + assert offers["gpt-5-mini"]["wire_model_id"] == "openai/gpt-5-mini" + assert offers["llama-4-maverick"]["wire_model_id"] == "meta-llama/llama-4-maverick" + assert offers["raw-family"]["wire_model_id"] == "unknown/raw-family" + assert offers["qwen3-235b-a22b"]["wire_model_id"] == "qwen/qwen3-235b-a22b-2507" + assert "openai/gpt-5-mini" not in offers + assert "gpt-5.5" not in offers # curated static OpenRouter route stays deduped def test_openrouter_market_book_exposes_meta_for_dashboard(): @@ -245,9 +292,9 @@ def test_openrouter_market_book_exposes_meta_for_dashboard(): asyncio.run(s.pricing()) book = s.market_book() fams = {r["model_family"]: r for r in book["rows"]} - assert set(fams) == {"z-ai/glm-5.2", "acme/cheap-7b"} - assert fams["z-ai/glm-5.2"]["source"] == "openrouter" - assert book["families"]["z-ai/glm-5.2"]["meta"]["bench_intelligence"] == 0.6 + assert set(fams) == {"glm-5.2", "cheap-7b"} + assert fams["glm-5.2"]["source"] == "openrouter" + assert book["families"]["glm-5.2"]["meta"]["bench_intelligence"] == 0.6 def test_openrouter_model_meta_still_keyed_by_curated_family(): @@ -263,7 +310,7 @@ def test_openrouter_discovery_rejects_negative_priced_models(): # negative = "cheapest") and bill a NEGATIVE cost. Free ($0) models stay. body = {"data": [ {"id": "z-ai/glm-5.2", "pricing": {"prompt": "0.0000004", "completion": "0.0000016"}}, - {"id": "free/model", "pricing": {"prompt": "0", "completion": "0"}}, + {"id": "free/free-model", "pricing": {"prompt": "0", "completion": "0"}}, {"id": "junk/sentinel", "pricing": {"prompt": "-1", "completion": "-1"}}, {"id": "junk/neg-out", "pricing": {"prompt": "0.000001", "completion": "-0.5"}}, ]} @@ -271,11 +318,11 @@ def test_openrouter_discovery_rejects_negative_priced_models(): prices = asyncio.run(s.pricing()) # populates the snapshot + offers cache # discovery offers: negatives gone, the $0 free model kept assert {o["model_family"] for o in s.offers_sync("openrouter_market")} == \ - {"z-ai/glm-5.2", "free/model"} + {"glm-5.2", "free-model"} # the curated price-enrichment list (pricing()) also drops negatives served = {p["served_model_id"] for p in prices} assert "junk/sentinel" not in served and "junk/neg-out" not in served - assert {"z-ai/glm-5.2", "free/model"} <= served + assert {"z-ai/glm-5.2", "free/free-model"} <= served def test_openrouter_discovery_stamps_capability_flags_for_meets_req(): @@ -294,13 +341,13 @@ def test_openrouter_discovery_stamps_capability_flags_for_meets_req(): s = _or_source({"/models": FakeResponse(200, body)}) asyncio.run(s.pricing()) offers = {o["model_family"]: o for o in s.offers_sync("openrouter_market")} - caps = offers["z-ai/glm-5.2"]["capabilities"] + caps = offers["glm-5.2"]["capabilities"] assert caps.get("supports_tools") is True assert caps.get("supports_json_mode") is True assert caps.get("supports_seed") is True assert caps.get("supports_vision") is True # a model that does not declare tools must NOT claim the capability - assert "supports_tools" not in offers["plain/text-only"]["capabilities"] + assert "supports_tools" not in offers["text-only"]["capabilities"] def test_pushed_price_lands_in_dump_state():