feat(openrouter): add live model sync

This commit is contained in:
Torbjørn Lindahl
2026-04-01 11:41:53 +02:00
parent 4585833df1
commit cc8f6380d6
5 changed files with 6013 additions and 4 deletions

View File

@@ -2,14 +2,30 @@
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
class OpenRouterModelRegistry(CapabilityModelRegistry):
"""Capability registry backed by ``conf/openrouter_models.json``."""
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_use_resources = False
self._live_config_path: Path | None = None
self._live_default_path = Path(__file__).resolve().parents[3] / "conf" / self.LIVE_DEFAULT_FILENAME
def __init__(self, config_path: str | None = None) -> None:
super().__init__(
env_var_name="OPENROUTER_MODELS_CONFIG_PATH",
default_filename="openrouter_models.json",
@@ -18,6 +34,79 @@ class OpenRouterModelRegistry(CapabilityModelRegistry):
config_path=config_path,
)
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_use_resources = True
else:
raise AttributeError("resource accessor not available")
except Exception:
self._live_config_path = self._live_default_path
self.reload()
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_use_resources:
try:
resource = importlib.resources.files("conf").joinpath(self.LIVE_DEFAULT_FILENAME)
if hasattr(resource, "read_text"):
config_text = resource.read_text(encoding="utf-8")
else:
with 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):