llmrouter: leichtgewichtiger Gateway-Ersatz mit Alternativ-Auflösung
OpenAI-kompatibler Reverse-Proxy in Bun: Client-Key -> ACL, Alias -> Modell (alternative: günstigstes Modell derselben Klasse über llmlite/openrouter-auswahl inkl. Effort-Achse; fest: lokale Upstreams, TTS/STT, Embeddings), Usage/Kosten aus der OpenRouter-Antwort (auch im Stream), JSONL + SQLite-Log, Tagesstatistik je Key. Altnamen der LiteLLM-Zeit werden übersetzt. Kein 3456/stdcmpt, kein role/*, kein -auto (Andreas 2026-09-05). Lokal verifiziert: Chat, Stream, ACL, Altname, Kosten im Log. Parallelbetrieb auf :4001 geplant. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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 };
|
||||
export type Alias =
|
||||
| { art: "alternative"; referenz: string }
|
||||
| { art: "fest"; upstream: string; model: string; body?: Record<string, unknown> };
|
||||
export type Config = {
|
||||
port: number;
|
||||
logDir: string;
|
||||
refreshHours: number;
|
||||
aaRefreshDays: number;
|
||||
lasttest: boolean;
|
||||
upstreams: Record<string, Upstream>;
|
||||
aliases: Record<string, Alias>;
|
||||
altnamen: Record<string, string>;
|
||||
};
|
||||
|
||||
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`);
|
||||
}
|
||||
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 Aliasse; leere Liste = alles. */
|
||||
export function aclLaden(): Map<string, string[]> {
|
||||
const pfad = process.env.LLMROUTER_ACLS ?? `${dirname(CONFIG_PATH)}/key-acls.json`;
|
||||
const d = JSON.parse(readFileSync(pfad, "utf8")) as { keys: Record<string, string[]> };
|
||||
return new Map(Object.entries(d.keys));
|
||||
}
|
||||
|
||||
/** Token -> Name. Datei fehlt = kein Client kommt rein (bewusst, nicht "alle"). */
|
||||
export function tokenLaden(): Map<string, string> {
|
||||
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<string, string>));
|
||||
} catch {
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
||||
export function upstreamKey(u: Upstream): string | undefined {
|
||||
return u.keyEnv === undefined ? undefined : process.env[u.keyEnv];
|
||||
}
|
||||
Reference in New Issue
Block a user