/** * config.ts — laedt config.json, key-acls.json und die Token-Datei. * * Drei Quellen, bewusst getrennt: * - config.json versioniert: Aliasse, Upstreams, Altnamen * - key-acls.json versioniert: Name -> erlaubte Aliasse * - KEYS_FILE NUR auf dem Host: Token -> Name (Secret) * Upstream-Schluessel kommen aus der Umgebung (keyEnv je Upstream). */ import { readFileSync } from "fs"; import { dirname, resolve } from "path"; export type Upstream = { base: string; keyEnv?: string; maxParallel?: number; /** Client-Token unveraendert weitergeben (Durchreiche an LiteLLM) */ clientKey?: boolean }; export type Alias = | { art: "alternative"; referenz: string } | { art: "schwelle"; index: number; minContext: number } | { art: "fest"; upstream: string; model: string; body?: Record }; export type Config = { port: number; logDir: string; refreshHours: number; aaRefreshDays: number; lasttest: boolean; upstreams: Record; aliases: Record; altnamen?: Record; /** Unbekannte Modellnamen unveraendert an diesen Upstream (Parallelbetrieb mit LiteLLM). */ fallback?: { upstream: string }; /** Nackte Anthropic-IDs (Claude Code/Desktop): "alt" = guenstigste Alternative der Klasse, "openrouter" = 1:1. */ anthropicModus?: "alt" | "openrouter"; /** role/ -> Zielname (42i/*, openrouter/*, alt/*). Agenten kennen nur Rollen; Modelle stellt man hier um. */ rollen?: Record; }; export const BASE = resolve(dirname(import.meta.path), ".."); export const CONFIG_PATH = process.env.LLMROUTER_CONFIG ?? `${BASE}/config.json`; export function configLaden(): Config { const c = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Config; for (const [name, a] of Object.entries(c.aliases)) { if (a.art === "fest" && c.upstreams[a.upstream] === undefined) throw new Error(`Alias ${name}: Upstream ${a.upstream} fehlt`); } if (c.fallback !== undefined && c.upstreams[c.fallback.upstream] === undefined) throw new Error(`fallback: Upstream ${c.fallback.upstream} fehlt`); for (const [alt, ziel] of Object.entries(c.altnamen ?? {})) { if (c.aliases[ziel] === undefined) throw new Error(`Altname ${alt} zeigt auf unbekannten Alias ${ziel}`); } c.logDir = resolve(dirname(CONFIG_PATH), c.logDir); if (process.env.LLMROUTER_LASTTEST === "0") c.lasttest = false; // fuer lokale Tests: spart ~210k Token je Wechsel return c; } /** Name -> erlaubte Namen oder Muster (`openrouter/*`, `alt/*`, `role/*`); leere Liste = alles. */ export function aclLaden(): Map { const pfad = process.env.LLMROUTER_ACLS ?? `${dirname(CONFIG_PATH)}/key-acls.json`; const d = JSON.parse(readFileSync(pfad, "utf8")) as { keys: Record }; return new Map(Object.entries(d.keys)); } /** Token -> Name. Datei fehlt = kein Client kommt rein (bewusst, nicht "alle"). */ export function tokenLaden(): Map { const pfad = process.env.KEYS_FILE; if (pfad === undefined) return new Map(); try { return new Map(Object.entries(JSON.parse(readFileSync(pfad, "utf8")) as Record)); } catch { return new Map(); } } /** Prueft einen angefragten Namen gegen die ACL-Liste; `*` am Ende ist ein Praefix-Muster. */ export function erlaubt(liste: string[], name: string): boolean { if (liste.length === 0) return true; return liste.some((m) => (m.endsWith("*") ? name.startsWith(m.slice(0, -1)) : m === name)); } export function upstreamKey(u: Upstream): string | undefined { return u.keyEnv === undefined ? undefined : process.env[u.keyEnv]; }