- anthropicModus (alt|openrouter), Normalisierung der IDs (Datum, [1m], Punkte), Client-Effort aus output_config.effort als Stufe, HEAD /api/hello -> 200 - Auswahl: nicht gemessene Stufe faellt auf die naechste gemessene darunter (sonst darueber, sonst Modellzahl) -- Sonnet 5 hat nur max/none - /models: alt/ nur bei bestimmbarer Klasse; opencode-normalize nimmt openrouter/+alt/ genau dann (GLM 5.3 Flash war weggefallen) - README: Abschnitt Claude Code und Claude Desktop Verifiziert per claude -p mit claude-sonnet-5, claude-opus-5, claude-haiku-4-5-20251001. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
79 lines
3.6 KiB
TypeScript
79 lines
3.6 KiB
TypeScript
/**
|
|
* 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<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>;
|
|
/** 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/<name> -> Zielname (42i/*, openrouter/*, alt/*). Agenten kennen nur Rollen; Modelle stellt man hier um. */
|
|
rollen?: 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`);
|
|
}
|
|
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<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();
|
|
}
|
|
}
|
|
|
|
/** 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];
|
|
}
|