"""OpenRouter model registry for managing model configurations and aliases.""" from __future__ import annotations import importlib.resources import json import logging from pathlib import Path from utils.env import get_env from utils.file_utils import read_json_file from ..shared import ModelCapabilities, ProviderType from .base import CAPABILITY_FIELD_NAMES, CapabilityModelRegistry logger = logging.getLogger(__name__) class OpenRouterModelRegistry(CapabilityModelRegistry): LIVE_ENV_VAR_NAME = "OPENROUTER_LIVE_MODELS_CONFIG_PATH" LIVE_DEFAULT_FILENAME = "openrouter_models_live.json" def __init__(self, config_path: str | None = None, live_config_path: str | None = None) -> None: self._live_resource = None self._live_config_path: Path | None = None self._live_default_path = Path(__file__).resolve().parents[3] / "conf" / self.LIVE_DEFAULT_FILENAME if live_config_path: self._live_config_path = Path(live_config_path) else: env_path = get_env(self.LIVE_ENV_VAR_NAME) if env_path: self._live_config_path = Path(env_path) else: try: resource = importlib.resources.files("conf").joinpath(self.LIVE_DEFAULT_FILENAME) if hasattr(resource, "read_text"): self._live_resource = resource else: raise AttributeError("resource accessor not available") except Exception: self._live_config_path = self._live_default_path super().__init__( env_var_name="OPENROUTER_MODELS_CONFIG_PATH", default_filename="openrouter_models.json", provider=ProviderType.OPENROUTER, friendly_prefix="OpenRouter ({model})", config_path=config_path, ) def reload(self) -> None: live_data = self._load_live_config_data() curated_data = self._load_config_data() merged_data = self._merge_manifest_data(live_data, curated_data) self._extras = {} configs = [config for config in self._parse_models(merged_data) if config is not None] self._build_maps(configs) def _load_live_config_data(self) -> dict: if self._live_resource is not None: try: if hasattr(self._live_resource, "read_text"): config_text = self._live_resource.read_text(encoding="utf-8") else: with self._live_resource.open("r", encoding="utf-8") as handle: config_text = handle.read() data = json.loads(config_text) except FileNotFoundError: logger.debug("Packaged %s not found", self.LIVE_DEFAULT_FILENAME) return {"models": []} except Exception as exc: logger.warning("Failed to read packaged %s: %s", self.LIVE_DEFAULT_FILENAME, exc) return {"models": []} return data or {"models": []} if not self._live_config_path: return {"models": []} if not self._live_config_path.exists(): logger.debug("OpenRouter live registry config not found at %s", self._live_config_path) return {"models": []} data = read_json_file(str(self._live_config_path)) return data or {"models": []} @staticmethod def _merge_manifest_data(live_data: dict, curated_data: dict) -> dict: merged_models: dict[str, dict] = {} for source in (live_data, curated_data): for raw in source.get("models", []): if not isinstance(raw, dict): continue model_name = raw.get("model_name") if not model_name: continue existing = merged_models.get(model_name, {}) merged_models[model_name] = {**existing, **dict(raw)} return {"models": list(merged_models.values())} def _finalise_entry(self, entry: dict) -> tuple[ModelCapabilities, dict]: provider_override = entry.get("provider") if isinstance(provider_override, str): entry_provider = ProviderType(provider_override.lower()) elif isinstance(provider_override, ProviderType): entry_provider = provider_override else: entry_provider = ProviderType.OPENROUTER if entry_provider == ProviderType.CUSTOM: entry.setdefault("friendly_name", f"Custom ({entry['model_name']})") else: entry.setdefault("friendly_name", f"OpenRouter ({entry['model_name']})") filtered = {k: v for k, v in entry.items() if k in CAPABILITY_FIELD_NAMES} filtered.setdefault("provider", entry_provider) capability = ModelCapabilities(**filtered) return capability, {}