Files
llmrouter/src/config.ts
T
eltonandClaude Fable 5.1 91e68d9ea9 llmrouter: Claude Code/Desktop — nackte claude-IDs -> alt/anthropic/<id>[:effort]
- 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>
2026-09-05 16:23:26 +02:00

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];
}