labs / src /models /config.py
3v324v23's picture
deploy: unified router + dreamy website (2026-06-16T09:46:52Z)
c1a683f
Raw
History Blame Contribute Delete
4.31 kB
"""Loads models.json and resolves client model names to target lists.""" # module docstring
from __future__ import annotations # defer annotation evaluation (PEP 563)
import json # stdlib json for parsing the models.json config file
import logging # logging for fetch/load events
import os # read MODELS_URL + AES_KEY env vars (runtime fetch + decrypt)
import urllib.request # fetch models.json from a secret URL if configured
from pathlib import Path # Path for locating the data directory
from typing import Dict, List, Optional, Tuple # type aliases for signatures
from cryptography.fernet import Fernet # AES-128-CBC decrypt of the fetched token blob
logger = logging.getLogger("router.config") # named logger for this module
def _fetch_models(url: str, aes_key: str) -> dict: # download + decrypt models.json at startup
"""Download + Fernet-decrypt models.json from a URL (e.g. a raw gist).
The gist stores the encrypted token (Fernet.encrypt output), NOT raw JSON.
We fetch the bytes, decrypt with AES_KEY, then parse to a dict.
""" # docstring
logger.info("fetching models.json from %s", url[:60]) # log the source (truncated)
req = urllib.request.Request(url, headers={"User-Agent": "router"}) # build the request
with urllib.request.urlopen(req, timeout=30) as r: # open the stream
token = r.read() # read the raw encrypted token bytes
plaintext = Fernet(aes_key.encode()).decrypt(token).decode("utf-8") # AES-128-CBC decrypt -> JSON text
return json.loads(plaintext) # parse to a Python dict
class ModelConfig: # holds the loaded models.json and resolves client names
def __init__(self, data_dir: Path): # data_dir is the folder holding models.json
self.models: Dict[str, dict] = {} # exact name -> {"targets": [...]} (raw JSON)
self._lookup: Dict[str, str] = {} # lowercased name -> exact name (case-insensitive search)
self._load(data_dir / "models.json") # parse the config immediately
def _load(self, path: Path) -> None: # read + parse the JSON config
# If MODELS_URL + AES_KEY are set, fetch the encrypted config from the
# gist and decrypt at runtime (keeps keys out of the Docker image).
# Otherwise fall back to the local data/models.json (dev/test only).
url = os.environ.get("MODELS_URL", "").strip() # secret gist raw URL, optional
aes_key = os.environ.get("AES_KEY", "").strip() # Fernet key for decryption, optional
if url and aes_key: # both secrets present -> runtime fetch + decrypt
raw = _fetch_models(url, aes_key) # download + decrypt + parse
else:
raw = json.loads(path.read_text(encoding="utf-8")) # local file fallback (utf-8 for locale safety)
self.models = raw # store the full dict keyed by model name
for name in raw: # build the case-insensitive lookup index
self._lookup[name.lower()] = name # map lowercase -> exact case
def resolve(self, requested: Optional[str]) -> Tuple[Optional[str], Optional[List[dict]]]: # client name -> (exact_name, targets)
"""Map a client model name to (exact_name, targets). Returns (None, None) if unknown.""" # docstring
exact = self._lookup.get((requested or "").strip().lower()) # case-insensitive lookup; empty-safe
if exact is None: # unknown model name
return None, None # caller returns 404
return exact, self.models[exact].get("targets", []) # return the exact name + its target list
def list_models(self) -> List[dict]: # build the data array for GET /v1/models
out = [] # the list to return
for name, cfg in self.models.items(): # iterate in insertion order
out.append({ # build one OpenAI-shaped model descriptor
"id": name, # the client-facing model name
"object": "model", # OpenAI type tag
"targets": len(cfg.get("targets", [])), # how many targets serve this model
})
return out # the full list
def total_targets(self) -> int: # sum of all targets across all models (for status display)
return sum(len(c.get("targets", [])) for c in self.models.values()) # flatten + count