#!/usr/bin/env python3 """LLM router: model-prefix → OpenRouter (cloud) or local model-aware fanout. Local backends: model-name routed when possible, first-up failover otherwise. - On POST with a known model id, the request goes to the backend whose /v1/models advertises it (cached 30s). - On unknown model or empty body, falls back to first-up wins, preserving legacy semantics. Cloud routing: HTTPS to openrouter.ai, server-side Bearer auth from OPENROUTER_API_KEY. /v1/models: merged cloud catalog (live-filtered against OpenRouter's /api/v1/models) + union of every reachable local backend's model list. Entries are visually tagged in their id: - "[$] vendor/model" paid cloud (manually curated or auto-discovered) - "[free] vendor/model" free cloud (auto-detected by :free suffix) - "[local] alias" local backend A periodic background thread (every 3 h) re-fetches the full OpenRouter model list and: a) adds newly free models >7 B parameters to the curated free catalog b) removes models that are no longer free c) probes each free model with a tiny 1-token request; if it returns 429 the model id gets a "-na" suffix (not available). d) auto-discovers cheap paid models (prompt < $0.50/MTok) from known prefixes and adds them to the paid catalog. Paid models can also be added manually by editing cloud-catalog.json's "paid" section — the background checker never removes them. Inbound requests have the tag + optional "-na" suffix stripped before routing, so OWUI sending back the displayed id Just Works. Config: /etc/llm-proxy/local-backends.json — list of {name, host, port} dicts /etc/llm-proxy/cloud-catalog.json — persisted cloud model catalog + status Signals: SIGHUP reload local-backends.json SIGUSR1 drop discover caches + trigger an immediate background re-check """ import http.server import http.client import ssl import select import socket import sys import os import os.path import time import json import threading import signal import re import copy import traceback CONFIG_DIR = os.environ.get("LLM_PROXY_CONFIG_DIR", "/etc/llm-proxy") LOCAL_BACKENDS_FILE = os.path.join(CONFIG_DIR, "local-backends.json") CLOUD_CATALOG_FILE = os.path.join(CONFIG_DIR, "cloud-catalog.json") # Fallback only, when local-backends.json is absent. Real backends belong in # $LLM_PROXY_CONFIG_DIR/local-backends.json (see local-backends.example.json). DEFAULT_LOCAL_BACKENDS = [ ("local", "127.0.0.1", 8080), ] # Optional display aliases {"": "