139 lines
5.4 KiB
Python
139 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Resolve stable Switchyard selectors through the stewarded model catalog."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
CATALOG_PATH = Path(
|
|
os.environ.get("HERMES_ROUTING_CATALOG_PATH", "/routing-catalog/catalog.json")
|
|
)
|
|
DEFAULTS = {
|
|
"codex": {
|
|
"low": "gpt-5.6-luna",
|
|
"medium": "gpt-5.6-terra",
|
|
"high": "gpt-5.6-sol",
|
|
"xhigh": "gpt-5.6-sol",
|
|
},
|
|
"claude": {
|
|
"low": "claude-haiku-4-5-20251001",
|
|
"medium": "claude-sonnet-5",
|
|
"high": "claude-opus-5",
|
|
"xhigh": "claude-opus-5",
|
|
},
|
|
}
|
|
PREFIXES = {"codex": "gpt-", "claude": "claude-"}
|
|
CAPABILITY_ROLES = frozenset({"economy", "balanced", "advanced", "frontier"})
|
|
TIER_DEFAULTS = {
|
|
"codex": {
|
|
"luna": "gpt-5.6-luna",
|
|
"terra": "gpt-5.6-terra",
|
|
"sol": "gpt-5.6-sol",
|
|
},
|
|
"claude": {
|
|
"haiku": "claude-haiku-4-5-20251001",
|
|
"fable": "claude-fable-5",
|
|
"sonnet": "claude-sonnet-5",
|
|
"opus": "claude-opus-5",
|
|
},
|
|
}
|
|
|
|
|
|
def load_catalog(path: Path = CATALOG_PATH) -> dict[str, Any]:
|
|
"""Load a valid routing catalog, falling back to an empty mapping."""
|
|
try:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, ValueError, json.JSONDecodeError):
|
|
return {}
|
|
return value if isinstance(value, dict) else {}
|
|
|
|
|
|
def catalog_contains(provider: str, model: str, catalog: dict[str, Any] | None = None) -> bool:
|
|
"""Return whether a provider's stewarded current catalog advertises a model."""
|
|
document = catalog if catalog is not None else load_catalog()
|
|
providers = document.get("providers", {}) if isinstance(document, dict) else {}
|
|
record = providers.get(provider, {}) if isinstance(providers, dict) else {}
|
|
models = record.get("models", []) if isinstance(record, dict) else []
|
|
return isinstance(model, str) and model in models
|
|
|
|
|
|
def resolve_model(
|
|
provider: str,
|
|
selector: str,
|
|
effort: str,
|
|
catalog: dict[str, Any] | None = None,
|
|
) -> str:
|
|
"""Resolve an automatic or capability-tier selector to an exact model ID."""
|
|
if provider not in DEFAULTS or effort not in DEFAULTS[provider]:
|
|
raise ValueError("unsupported provider or effort")
|
|
prefix = PREFIXES[provider]
|
|
document = catalog if catalog is not None else load_catalog()
|
|
if selector.startswith(prefix) or catalog_contains(provider, selector, document):
|
|
if catalog_contains(provider, selector, document):
|
|
return selector
|
|
providers = document.get("providers", {}) if isinstance(document, dict) else {}
|
|
record = providers.get(provider, {}) if isinstance(providers, dict) else {}
|
|
if isinstance(record, dict) and record.get("live") is True:
|
|
raise ValueError(f"model is not in the current {provider} catalog")
|
|
return selector
|
|
|
|
providers = document.get("providers", {}) if isinstance(document, dict) else {}
|
|
record = providers.get(provider, {}) if isinstance(providers, dict) else {}
|
|
if not isinstance(record, dict):
|
|
record = {}
|
|
capability = selector.removeprefix("auto-") if selector.startswith("auto-") else ""
|
|
if capability and capability not in CAPABILITY_ROLES:
|
|
raise ValueError(f"unsupported capability selector {selector!r}")
|
|
if capability:
|
|
mappings = record.get("capability_resolved", {})
|
|
mapping = mappings.get(capability, {}) if isinstance(mappings, dict) else {}
|
|
candidate = mapping.get(effort, "") if isinstance(mapping, dict) else ""
|
|
else:
|
|
mapping_name = "resolved" if selector == "auto" else "tiers"
|
|
mapping = record.get(mapping_name, {})
|
|
candidate = mapping.get(effort if selector == "auto" else selector, "") if isinstance(mapping, dict) else ""
|
|
if (
|
|
selector != "auto"
|
|
and not capability
|
|
and isinstance(candidate, str)
|
|
and candidate
|
|
and selector not in candidate.lower()
|
|
):
|
|
candidate = ""
|
|
advertised = record.get("models", []) if isinstance(record, dict) else []
|
|
candidate_allowed = isinstance(candidate, str) and candidate and (
|
|
candidate in advertised or (not advertised and candidate.startswith(prefix))
|
|
)
|
|
if not candidate_allowed:
|
|
# A fresh account list is authoritative. Do not turn a deliberately
|
|
# unresolved/retired selector into a stale baseline model.
|
|
if record.get("live") is True or capability:
|
|
raise ValueError(f"no current {provider} model for selector {selector!r}")
|
|
candidate = (
|
|
DEFAULTS[provider][effort]
|
|
if selector == "auto"
|
|
else TIER_DEFAULTS[provider].get(selector, DEFAULTS[provider][effort])
|
|
)
|
|
return candidate
|
|
|
|
|
|
def resolve_route(route: str, catalog: dict[str, Any] | None = None) -> str:
|
|
"""Resolve a route/<provider>/<selector>/<effort> target to a model ID."""
|
|
parts = route.split("/")
|
|
if len(parts) != 4 or parts[0] != "route":
|
|
return route
|
|
return resolve_model(parts[1], parts[2], parts[3], catalog)
|
|
|
|
|
|
def resolve_worker_route(route: str, catalog: dict[str, Any] | None = None) -> str:
|
|
"""Resolve a worker selector while preserving the worker route envelope."""
|
|
parts = route.split("/")
|
|
if len(parts) != 4 or parts[0] != "worker":
|
|
raise ValueError("unsupported worker route")
|
|
model = resolve_model(parts[1], parts[2], parts[3], catalog)
|
|
return f"worker/{parts[1]}/{model}/{parts[3]}"
|