llmrouter: Namensräume openrouter/, alt/, role/; Klasse über drei Indexe relativ
- openrouter/<id>[:effort] 1:1, alt/<id>[:effort] günstigste Alternative auf Abruf (ohne Last-Probe, im Takt erneuert), role/<name> aus config.rollen - Klasse: Intelligence, Coding, Agentic je >= 95 % der Referenz (relativ statt 3 Punkte), Kontext >= 90 %, billiger; Kandidaten auch ohne OpenRouter-Index, wenn AA Stufen kennt (OpenRouter führt Intelligence nur für 53 Modelle) - ACL-Muster mit *, /models mit allen Konto-Modellen je Namensraum und Stufe - Rollen aus der LiteLLM-Zeit übernommen (Führung high, Rest medium) Andreas 2026-09-05. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -9,37 +9,42 @@ Keine Format-Übersetzung, keine Datenbank, kein Admin-UI. Alle Upstreams
|
||||
sprechen OpenAI-Format (OpenRouter, llama-server, Ollama, Mistral), deshalb
|
||||
reicht Durchreichen. Entscheidung Andreas, 2026-09-05.
|
||||
|
||||
## Aliasse (config.json)
|
||||
## Namensräume
|
||||
|
||||
Zwei Arten:
|
||||
| Name | Bedeutung |
|
||||
|---|---|
|
||||
| `42i/marvin-2609:low\|medium\|high\|max` | konfigurierte Stufen (`aliases`, Art `schwelle`: Index ≥ 25/45/49/53, Kontext ≥ 256k/256k/500k/500k) |
|
||||
| `42i/marvin-2609-core:none\|medium` | lokales Qwen, fest |
|
||||
| `openrouter/<id>[:effort]` | 1:1 an OpenRouter, Effort als `reasoning.effort` |
|
||||
| `alt/<id>[:effort]` | **günstigste Alternative derselben Klasse** wie `<id>` auf dieser Stufe |
|
||||
| `role/<name>` | Rolle aus `rollen`, zeigt auf einen der obigen Namen |
|
||||
| alles andere | Durchreiche an LiteLLM (`fallback`), unverändert mit Client-Key |
|
||||
|
||||
- **`alternative`** — der Alias nennt ein Referenzmodell mit optionaler
|
||||
Effort-Stufe (`openai/gpt-5.6-luna:low`, `anthropic/claude-opus-5:medium`).
|
||||
Gerufen wird das **günstigste OpenRouter-Modell derselben Klasse**, nach der
|
||||
Regel in `../llmlite/openrouter-auswahl.ts` (Intelligence-Index je Stufe von
|
||||
Artificial Analysis, Kontext, Tool-Calls, Sperrliste, Live-Probe, Last-Probe
|
||||
bei Wechsel). Findet sich nichts Billigeres, wird die Referenz selbst gerufen.
|
||||
Die Effort-Stufe des Ziels wird als `reasoning.effort` gesetzt; ein vom
|
||||
Client mitgeschicktes `reasoning_effort` wird verworfen, die Klasse gehört
|
||||
zum Alias.
|
||||
- **`schwelle`** — Index-Schwelle (0–100) plus Mindestkontext, ohne Referenzmodell:
|
||||
das günstigste Modell, das beides erfüllt, mit der niedrigsten ausreichenden
|
||||
Effort-Stufe. Stand 2026-09-05: low 25/256k, medium 40/256k, high 47/500k,
|
||||
max 50/500k. Stabiler als eine Referenz, sagt aber weniger über die Bedeutung.
|
||||
- **`fest`** — Upstream und Modell stehen fest (lokales Qwen, TTS/STT,
|
||||
Embeddings, kira auf deizo). Optional ein `body`-Patch, etwa
|
||||
`chat_template_kwargs.enable_thinking` für die Qwen-Stufen.
|
||||
**Klasse** (`alt/`): Intelligence-, Coding- und Agentic-Index jeweils mindestens
|
||||
95 % der Referenz, Kontext mindestens 90 %, gewichtet billiger. Intelligence und
|
||||
Coding kommen je Effort-Stufe von Artificial Analysis, Agentic nur je Modell (AA
|
||||
misst ihn nicht je Stufe). Hat die Referenz einen Wert, den ein Kandidat nicht
|
||||
hat, fällt der Kandidat raus. Je Kandidat zählt die niedrigste Stufe, die die
|
||||
Klasse erreicht; die Stufe des Ziels wird als `reasoning.effort` gesetzt, ein
|
||||
vom Client mitgeschicktes `reasoning_effort` verworfen. Findet sich nichts
|
||||
Billigeres, wird die Referenz selbst gerufen.
|
||||
|
||||
Die Namen sind versioniert wie bei LiteLLM: `42i/marvin-2609:low` … `:max`,
|
||||
`42i/marvin-2609-core:none|medium`. **Unbekannte Namen** (alles aus der
|
||||
LiteLLM-Zeit, `42i/marvin-2608:*`) reicht der Router unverändert mit dem
|
||||
Client-Key an LiteLLM auf :4000 durch (`fallback`). Caddy zeigt `llm.lan`
|
||||
auf den Router; ein Agent wechselt also per Modellname 2608 → 2609, nicht
|
||||
per base_url, und rollt per Umbenennen zurück. Durchgereichte Anfragen stehen
|
||||
im Log mit Upstream `litellm`, ohne Kosten (die kennt nur LiteLLM).
|
||||
`alt/` wird **bei der ersten Anfrage** aufgelöst (Latenz- und Live-Probe, keine
|
||||
Last-Probe) und dann im 12-h-Takt mit erneuert. Die konfigurierten `42i/*`-Stufen
|
||||
fahren bei einem Wechsel zusätzlich die Last-Probe (78k Kontext, dreimal).
|
||||
|
||||
Wird LiteLLM abgeschaltet, entfällt `fallback`, und `altnamen` (Übersetzung
|
||||
2608 → 2609) übernimmt, damit auch Nachzügler nicht brechen.
|
||||
**Rollen** entkoppeln Agenten von Modellnamen: ein Agent ruft `role/worker`,
|
||||
welches Modell dahintersteht, stellt man in `config.json` um, ohne Container.
|
||||
Vorbelegung: xo/architect/teamleader/lead → `42i/marvin-2609:high`, alle
|
||||
anderen → `:medium`.
|
||||
|
||||
**Rechte** (`key-acls.json`): Name → erlaubte Namen, `*` am Ende ist ein
|
||||
Präfix-Muster (`openrouter/*`, `alt/*`, `role/*`); `[]` = alles. Geprüft wird
|
||||
der angefragte Name, vor der Rollenauflösung.
|
||||
|
||||
`/v1/models` zeigt je Key alles Erlaubte: Aliasse, Rollen, alle OpenRouter-
|
||||
Modelle des Kontos als `openrouter/…` und `alt/…`, jeweils auch mit den bei AA
|
||||
gemessenen Effort-Stufen, plus die LiteLLM-Liste im Parallelbetrieb.
|
||||
|
||||
## Auflösung und Takt
|
||||
|
||||
|
||||
+25
-1
@@ -128,5 +128,29 @@
|
||||
"fallback": {
|
||||
"upstream": "litellm",
|
||||
"_doku": "Unbekannte Modellnamen (z.B. 42i/marvin-2608:*) gehen unverändert mit dem Client-Key an LiteLLM. Umstellung je Agent = Modellname 2608 -> 2609. Fällt weg, wenn LiteLLM abgeschaltet wird; dann kommen die 2608-Namen als altnamen zurück."
|
||||
}
|
||||
},
|
||||
"_rollen_doku": "Rollen entkoppeln Agenten von Modellnamen: ein Agent ruft role/worker, welches Modell dahintersteht, stellt man HIER um -- aus der Ferne, ohne Container-Deploy. Ziele: 42i/*-Alias, openrouter/<id>[:effort] (1:1) oder alt/<id>[:effort] (guenstigste Alternative). Vorbelegung 2026-09-05 (Elton): Fuehrungsrollen high, Rest medium -- Andreas' Feinjustierung ausstehend.",
|
||||
"rollen": {
|
||||
"xo": "42i/marvin-2609:high",
|
||||
"architect": "42i/marvin-2609:high",
|
||||
"teamleader": "42i/marvin-2609:high",
|
||||
"lead": "42i/marvin-2609:high",
|
||||
"worker": "42i/marvin-2609:medium",
|
||||
"engineer": "42i/marvin-2609:medium",
|
||||
"po": "42i/marvin-2609:medium",
|
||||
"speaker": "42i/marvin-2609:medium",
|
||||
"support": "42i/marvin-2609:medium",
|
||||
"assistant": "42i/marvin-2609:medium",
|
||||
"privacy": "42i/marvin-2609:medium",
|
||||
"service": "42i/marvin-2609:medium",
|
||||
"ops": "42i/marvin-2609:medium",
|
||||
"marketing": "42i/marvin-2609:medium",
|
||||
"qa": "42i/marvin-2609:medium",
|
||||
"perf": "42i/marvin-2609:medium",
|
||||
"i18n": "42i/marvin-2609:medium",
|
||||
"docs": "42i/marvin-2609:medium",
|
||||
"ar": "42i/marvin-2609:medium",
|
||||
"archivist": "42i/marvin-2609:medium"
|
||||
},
|
||||
"_namen_doku": "Namensraeume: 42i/* = konfigurierte Aliasse (aliases); openrouter/<id>[:effort] = 1:1 an OpenRouter, Effort als reasoning.effort; alt/<id>[:effort] = guenstigste Alternative derselben Klasse (Intelligence, Coding, Agentic je >= 95 % der Referenz, Kontext >= 90 %, billiger), aufgeloest bei der ersten Anfrage und im 12h-Takt erneuert, OHNE Last-Probe; role/<name> = Rolle (rollen). Alles andere -> fallback (LiteLLM)."
|
||||
}
|
||||
|
||||
+25
-10
@@ -1,33 +1,40 @@
|
||||
{
|
||||
"_note": "Rechte je Client-Key: Name -> erlaubte Aliasse (neue Namen; Altnamen werden vor der Pruefung uebersetzt). [] = alles. Die Token selbst liegen NICHT hier, sondern in KEYS_FILE auf dem Host (token -> name). Abgeleitet aus llmlite/key-acls.json am 2026-09-05: role/* und -auto gestrichen (Andreas), -auto -> :medium. Stand 2026-09-05 abends: Stufen low/medium/high/max; none->low, extra->high, ultra->max, mass->low. Namen ab 2026-09-05: 42i/marvin-2609:*; die Rechte gelten nur fuer Router-Aliasse, unbekannte Namen (2608) reicht der Router an LiteLLM durch, dort gelten dessen ACLs.",
|
||||
"_note": "Rechte je Client-Key: Name -> erlaubte Aliasse (neue Namen; Altnamen werden vor der Pruefung uebersetzt). [] = alles. Die Token selbst liegen NICHT hier, sondern in KEYS_FILE auf dem Host (token -> name). Abgeleitet aus llmlite/key-acls.json am 2026-09-05: role/* und -auto gestrichen (Andreas), -auto -> :medium. Stand 2026-09-05 abends: Stufen low/medium/high/max; none->low, extra->high, ultra->max, mass->low. Namen ab 2026-09-05: 42i/marvin-2609:*; die Rechte gelten nur fuer Router-Aliasse, unbekannte Namen (2608) reicht der Router an LiteLLM durch, dort gelten dessen ACLs. Muster mit * erlaubt (openrouter/*, alt/*, role/*). Rollen aus der LiteLLM-Zeit je Key wieder eingetragen; Elton und Andreas duerfen openrouter/* und alt/*.",
|
||||
"keys": {
|
||||
"persona-spark": [
|
||||
"42i/marvin-2609:medium",
|
||||
"kira-reasoning",
|
||||
"kira-stt",
|
||||
"marvin-stt",
|
||||
"marvin-tts"
|
||||
"marvin-tts",
|
||||
"role/engineer",
|
||||
"role/worker"
|
||||
],
|
||||
"persona-buzz": [
|
||||
"42i/marvin-2609:medium",
|
||||
"kira-reasoning",
|
||||
"kira-stt",
|
||||
"marvin-stt",
|
||||
"marvin-tts"
|
||||
"marvin-tts",
|
||||
"role/ops",
|
||||
"role/worker"
|
||||
],
|
||||
"persona-jessie": [
|
||||
"42i/marvin-2609:medium",
|
||||
"kira-reasoning",
|
||||
"kira-stt",
|
||||
"marvin-stt",
|
||||
"marvin-tts"
|
||||
"marvin-tts",
|
||||
"role/assistant",
|
||||
"role/marketing"
|
||||
],
|
||||
"persona-klara": [
|
||||
"42i/marvin-2609:medium",
|
||||
"kira-reasoning",
|
||||
"kira-stt",
|
||||
"marvin-stt",
|
||||
"marvin-tts"
|
||||
"marvin-tts",
|
||||
"role/service"
|
||||
],
|
||||
"persona-elton": [
|
||||
"42i/marvin-2609:low",
|
||||
@@ -41,21 +48,26 @@
|
||||
"marvin-stt",
|
||||
"marvin-stt-turbo",
|
||||
"marvin-tts",
|
||||
"marvin-tts-turbo"
|
||||
"marvin-tts-turbo",
|
||||
"role/*",
|
||||
"openrouter/*",
|
||||
"alt/*"
|
||||
],
|
||||
"persona-festus": [
|
||||
"42i/marvin-2609-core:none",
|
||||
"42i/marvin-2609-core:medium",
|
||||
"42i/marvin-2609:medium",
|
||||
"kira-reasoning",
|
||||
"kira-stt"
|
||||
"kira-stt",
|
||||
"role/privacy"
|
||||
],
|
||||
"persona-aria": [
|
||||
"42i/marvin-2609-core:none",
|
||||
"42i/marvin-2609-core:medium",
|
||||
"42i/marvin-2609:medium",
|
||||
"kira-reasoning",
|
||||
"kira-stt"
|
||||
"kira-stt",
|
||||
"role/architect"
|
||||
],
|
||||
"persona-inka": [
|
||||
"42i/marvin-2609-core:none",
|
||||
@@ -92,14 +104,17 @@
|
||||
"marvin-stt",
|
||||
"marvin-stt-turbo",
|
||||
"marvin-tts",
|
||||
"marvin-tts-turbo"
|
||||
"marvin-tts-turbo",
|
||||
"role/worker"
|
||||
],
|
||||
"lab-xo": [
|
||||
"42i/marvin-2609:low",
|
||||
"42i/marvin-2609:high",
|
||||
"42i/marvin-2609:medium",
|
||||
"kira-reasoning",
|
||||
"kira-stt"
|
||||
"kira-stt",
|
||||
"role/xo",
|
||||
"role/*"
|
||||
],
|
||||
"team-copilot": [
|
||||
"42i/marvin-2609:low",
|
||||
|
||||
+9
-1
@@ -26,6 +26,8 @@ export type Config = {
|
||||
altnamen?: Record<string, string>;
|
||||
/** Unbekannte Modellnamen unveraendert an diesen Upstream (Parallelbetrieb mit LiteLLM). */
|
||||
fallback?: { upstream: string };
|
||||
/** 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), "..");
|
||||
@@ -45,7 +47,7 @@ export function configLaden(): Config {
|
||||
return c;
|
||||
}
|
||||
|
||||
/** Name -> erlaubte Aliasse; leere Liste = alles. */
|
||||
/** 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[]> };
|
||||
@@ -63,6 +65,12 @@ export function tokenLaden(): Map<string, string> {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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];
|
||||
}
|
||||
|
||||
+108
-37
@@ -1,19 +1,25 @@
|
||||
/**
|
||||
* resolver.ts — loest die "alternative"-Aliasse auf das billigste Modell
|
||||
* derselben Klasse auf (openrouter-auswahl.ts aus llmlite, gleiche Regel wie
|
||||
* das Anzeige-Werkzeug und der alte Tier-Updater).
|
||||
* resolver.ts — Klassen-Auflösung fuer den Router.
|
||||
*
|
||||
* Takt: OpenRouter-Preise aendern sich oft -> alle refreshHours neu holen.
|
||||
* AA-Indexe aendern sich nie, es kommen nur Modelle hinzu -> aa-cache.json,
|
||||
* alle aaRefreshDays erneuert. Ergebnis in state.json, damit ein Neustart
|
||||
* nicht mit leerer Zuordnung startet; Wechsel landen in changes.log.
|
||||
* Drei Verbraucher:
|
||||
* - konfigurierte Aliasse (42i/*): schwelle oder alternative, mit Last-Probe
|
||||
* bei Wechsel, alle refreshHours neu (Preise aendern sich oft)
|
||||
* - alt/<openrouter-id>[:effort] auf Abruf: erste Anfrage loest auf
|
||||
* (Latenz + Live-Probe, KEINE Last-Probe -- die kostet 210k Token und
|
||||
* darf nicht von jedem beliebigen Modellnamen ausgeloest werden), Ergebnis
|
||||
* landet im State und wird im Takt mit erneuert
|
||||
* - /models: braucht die Kontoliste und die AA-Varianten
|
||||
*
|
||||
* Aufruf als Skript: `bun run src/resolver.ts [--force] [--dry-run]`
|
||||
* Daten: OpenRouter-Listen im Speicher (refreshHours), AA-Varianten auf Platte
|
||||
* (aa-cache.json, aaRefreshDays -- Indexe aendern sich nie, nur Modelle kommen
|
||||
* dazu). State in state.json, Wechsel in changes.log.
|
||||
*
|
||||
* Skript: `bun run src/resolver.ts [--force] [--dry-run] [alt/<ref> ...]`
|
||||
*/
|
||||
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, statSync } from "fs";
|
||||
import {
|
||||
apiKeyHolen, aaKeyHolen, listenHolen, kandidatenBauen, sperrliste, guenstigereAlternative, billigsterUeberSchwelle,
|
||||
variantenHolen, referenzParsen, referenzFormat, type Variante, type Effort,
|
||||
variantenHolen, referenzFormat, type Variante, type Effort, type Candidate, type Model,
|
||||
} from "../../llmlite/openrouter-auswahl.ts";
|
||||
import { configLaden, type Config } from "./config.ts";
|
||||
|
||||
@@ -21,6 +27,8 @@ export type Zuordnung = {
|
||||
model: string;
|
||||
effort: Effort | null;
|
||||
index: number;
|
||||
coding: number | null;
|
||||
agentic: number | null;
|
||||
weighted: number;
|
||||
referenz: string;
|
||||
seit: string;
|
||||
@@ -29,6 +37,15 @@ export type Zuordnung = {
|
||||
};
|
||||
export type State = { aktualisiert: string; zuordnung: Record<string, Zuordnung> };
|
||||
|
||||
export type Daten = {
|
||||
geholt: number;
|
||||
konto: Model[];
|
||||
alle: Map<string, Candidate>;
|
||||
varianten: Variante[];
|
||||
gesperrt: Map<string, string>;
|
||||
key: string | undefined;
|
||||
};
|
||||
|
||||
const statePfad = (c: Config) => `${c.logDir}/state.json`;
|
||||
const aaPfad = (c: Config) => `${c.logDir}/aa-cache.json`;
|
||||
|
||||
@@ -39,12 +56,15 @@ export function stateLaden(c: Config): State {
|
||||
return { aktualisiert: "1970-01-01T00:00:00.000Z", zuordnung: {} };
|
||||
}
|
||||
}
|
||||
|
||||
export function stateVeraltet(c: Config, s: State): boolean {
|
||||
return Date.now() - Date.parse(s.aktualisiert) > c.refreshHours * 3_600_000;
|
||||
}
|
||||
function stateSchreiben(c: Config, s: State): void {
|
||||
mkdirSync(c.logDir, { recursive: true });
|
||||
writeFileSync(statePfad(c), JSON.stringify(s, null, 1));
|
||||
}
|
||||
|
||||
async function variantenMitCache(c: Config, modelle: Parameters<typeof variantenHolen>[1]): Promise<Variante[]> {
|
||||
async function variantenMitCache(c: Config, modelle: Model[]): Promise<Variante[]> {
|
||||
const pfad = aaPfad(c);
|
||||
const frisch = existsSync(pfad) && Date.now() - statSync(pfad).mtimeMs < c.aaRefreshDays * 86_400_000;
|
||||
if (frisch) return JSON.parse(readFileSync(pfad, "utf8")) as Variante[];
|
||||
@@ -59,19 +79,63 @@ async function variantenMitCache(c: Config, modelle: Parameters<typeof varianten
|
||||
return varianten;
|
||||
}
|
||||
|
||||
let datenCache: Daten | undefined;
|
||||
let datenLaeuft: Promise<Daten> | undefined;
|
||||
/** OpenRouter-Listen + AA-Varianten, im Speicher fuer refreshHours. */
|
||||
export async function datenHolen(c: Config, force = false): Promise<Daten> {
|
||||
if (!force && datenCache !== undefined && Date.now() - datenCache.geholt < c.refreshHours * 3_600_000) return datenCache;
|
||||
if (datenLaeuft !== undefined) return datenLaeuft;
|
||||
datenLaeuft = (async () => {
|
||||
const key = apiKeyHolen();
|
||||
const listen = await listenHolen(key);
|
||||
const gesperrt = sperrliste();
|
||||
const { alle } = kandidatenBauen(listen, gesperrt);
|
||||
const varianten = await variantenMitCache(c, listen.voll);
|
||||
datenCache = { geholt: Date.now(), konto: listen.konto ?? listen.voll, alle, varianten, gesperrt, key };
|
||||
return datenCache;
|
||||
})().finally(() => { datenLaeuft = undefined; });
|
||||
return datenLaeuft;
|
||||
}
|
||||
|
||||
function zuordnungAus(k: Candidate, referenz: string, seit: string, istReferenz: boolean): Zuordnung {
|
||||
const n = (v: number) => (Number.isFinite(v) ? v : null);
|
||||
return { model: k.id, effort: k.effort ?? null, index: k.index, coding: n(k.coding), agentic: n(k.agentic), weighted: k.weighted, referenz, seit, istReferenz };
|
||||
}
|
||||
|
||||
function wechselMelden(c: Config, alias: string, alt: Zuordnung | undefined, ziel: Zuordnung, log: (z: string) => void, dryRun: boolean): void {
|
||||
const zeile = `${alias}: ${alt ? referenzFormat(alt.model, alt.effort) : "(neu)"} -> ${referenzFormat(ziel.model, ziel.effort)} (Index ${ziel.index.toFixed(1)}${ziel.coding !== null ? `, Coding ${ziel.coding.toFixed(1)}` : ""}${ziel.agentic !== null ? `, Agentic ${ziel.agentic.toFixed(1)}` : ""}, ${ziel.weighted.toFixed(2)} $/1M gewichtet, ${ziel.referenz}${ziel.istReferenz ? ", = Referenz" : ""})`;
|
||||
log(zeile);
|
||||
if (!dryRun) { mkdirSync(c.logDir, { recursive: true }); appendFileSync(`${c.logDir}/changes.log`, `${new Date().toISOString()} ${zeile}\n`); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Loest alle alternative-Aliasse auf. Die Last-Probe (78k Kontext, dreimal,
|
||||
* kostet ~210k Token) laeuft nur fuer einen Kandidaten, der die bisherige
|
||||
* Zuordnung abloesen wuerde -- wie im alten Tier-Updater.
|
||||
* alt/<ref>: guenstigste Alternative derselben Klasse, ohne Last-Probe.
|
||||
* Schreibt ins uebergebene State-Objekt (Schluessel "alt/<ref>").
|
||||
*/
|
||||
export async function alternativeAufloesen(c: Config, state: State, ref: string, log: (z: string) => void = console.log, opts?: { dryRun?: boolean; daten?: Daten }): Promise<Zuordnung> {
|
||||
const d = opts?.daten ?? (await datenHolen(c));
|
||||
const schluessel = `alt/${ref}`;
|
||||
const alt = state.zuordnung[schluessel];
|
||||
const abgelehnt: string[] = [];
|
||||
const { referenz, auswahl } = await guenstigereAlternative(ref, d.alle, d.key, (id, grund) => abgelehnt.push(`${id}: ${grund}`), d.gesperrt, { varianten: d.varianten, lasttest: false });
|
||||
const jetzt = new Date().toISOString();
|
||||
const ziel = auswahl === null ? zuordnungAus(referenz, ref, alt?.seit ?? jetzt, true) : zuordnungAus(auswahl.candidate, ref, alt?.seit ?? jetzt, false);
|
||||
if (alt === undefined || alt.model !== ziel.model || alt.effort !== ziel.effort) { ziel.seit = jetzt; wechselMelden(c, schluessel, alt, ziel, log, opts?.dryRun === true); }
|
||||
for (const a of abgelehnt) log(` abgelehnt ${schluessel}: ${a}`);
|
||||
state.zuordnung[schluessel] = ziel;
|
||||
if (!opts?.dryRun) stateSchreiben(c, state);
|
||||
return ziel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Voller Lauf: konfigurierte Aliasse (mit Last-Probe bei Wechsel) und alle
|
||||
* bereits bekannten alt/-Eintraege (ohne Last-Probe) neu aufloesen.
|
||||
*/
|
||||
export async function aufloesen(c: Config, bisher: State, opts?: { dryRun?: boolean; log?: (z: string) => void }): Promise<State> {
|
||||
const log = opts?.log ?? ((z: string) => console.log(z));
|
||||
const key = apiKeyHolen();
|
||||
const listen = await listenHolen(key);
|
||||
const gesperrt = sperrliste();
|
||||
const { alle } = kandidatenBauen(listen, gesperrt);
|
||||
const varianten = await variantenMitCache(c, listen.voll);
|
||||
const d = await datenHolen(c, true);
|
||||
const neu: State = { aktualisiert: new Date().toISOString(), zuordnung: { ...bisher.zuordnung } };
|
||||
const dryRun = opts?.dryRun === true;
|
||||
|
||||
for (const [alias, def] of Object.entries(c.aliases)) {
|
||||
if (def.art === "fest") continue;
|
||||
@@ -79,33 +143,33 @@ export async function aufloesen(c: Config, bisher: State, opts?: { dryRun?: bool
|
||||
try {
|
||||
const abgelehnt: string[] = [];
|
||||
const melden = (id: string, grund: string) => abgelehnt.push(`${id}: ${grund}`);
|
||||
const o = { varianten, lasttest: c.lasttest, bestand: alt?.model };
|
||||
const o = { varianten: d.varianten, lasttest: c.lasttest, bestand: alt?.model };
|
||||
let ziel: Zuordnung;
|
||||
if (def.art === "alternative") {
|
||||
const { referenz, auswahl } = await guenstigereAlternative(def.referenz, alle, key, melden, gesperrt, o);
|
||||
ziel = auswahl === null
|
||||
? { model: referenz.id, effort: referenz.effort ?? null, index: referenz.index, weighted: referenz.weighted, referenz: def.referenz, seit: alt?.seit ?? neu.aktualisiert, istReferenz: true }
|
||||
: { model: auswahl.candidate.id, effort: auswahl.candidate.effort ?? null, index: auswahl.candidate.index, weighted: auswahl.candidate.weighted, referenz: def.referenz, seit: alt?.seit ?? neu.aktualisiert, istReferenz: false };
|
||||
const { referenz, auswahl } = await guenstigereAlternative(def.referenz, d.alle, d.key, melden, d.gesperrt, o);
|
||||
ziel = auswahl === null ? zuordnungAus(referenz, def.referenz, alt?.seit ?? neu.aktualisiert, true) : zuordnungAus(auswahl.candidate, def.referenz, alt?.seit ?? neu.aktualisiert, false);
|
||||
} else {
|
||||
const beschreibung = `Index>=${def.index}, Kontext>=${def.minContext}`;
|
||||
const { auswahl } = await billigsterUeberSchwelle(def.index, def.minContext, alle, key, melden, gesperrt, o);
|
||||
const { auswahl } = await billigsterUeberSchwelle(def.index, def.minContext, d.alle, d.key, melden, d.gesperrt, o);
|
||||
if (auswahl === null) throw new Error(`kein Modell erfuellt ${beschreibung}`);
|
||||
ziel = { model: auswahl.candidate.id, effort: auswahl.candidate.effort ?? null, index: auswahl.candidate.index, weighted: auswahl.candidate.weighted, referenz: beschreibung, seit: alt?.seit ?? neu.aktualisiert, istReferenz: false };
|
||||
}
|
||||
const gleich = alt !== undefined && alt.model === ziel.model && alt.effort === ziel.effort;
|
||||
if (!gleich) {
|
||||
ziel.seit = neu.aktualisiert;
|
||||
const zeile = `${neu.aktualisiert} ${alias}: ${alt ? referenzFormat(alt.model, alt.effort) : "(neu)"} -> ${referenzFormat(ziel.model, ziel.effort)} (Index ${ziel.index.toFixed(1)}, ${ziel.weighted.toFixed(2)} $/1M gewichtet, ${ziel.referenz}${ziel.istReferenz ? ", = Referenz" : ""})`;
|
||||
log(zeile);
|
||||
if (!opts?.dryRun) { mkdirSync(c.logDir, { recursive: true }); appendFileSync(`${c.logDir}/changes.log`, zeile + "\n"); }
|
||||
ziel = zuordnungAus(auswahl.candidate, beschreibung, alt?.seit ?? neu.aktualisiert, false);
|
||||
}
|
||||
if (alt === undefined || alt.model !== ziel.model || alt.effort !== ziel.effort) { ziel.seit = neu.aktualisiert; wechselMelden(c, alias, alt, ziel, log, dryRun); }
|
||||
for (const a of abgelehnt) log(` abgelehnt ${alias}: ${a}`);
|
||||
neu.zuordnung[alias] = ziel;
|
||||
} catch (e) {
|
||||
log(`FEHLER ${alias}: ${(e as Error).message}${alt ? " -- behalte " + referenzFormat(alt.model, alt.effort) : " -- KEINE Zuordnung"}`);
|
||||
}
|
||||
}
|
||||
if (!opts?.dryRun) { mkdirSync(c.logDir, { recursive: true }); writeFileSync(statePfad(c), JSON.stringify(neu, null, 1)); }
|
||||
for (const schluessel of Object.keys(bisher.zuordnung).filter((k) => k.startsWith("alt/"))) {
|
||||
try {
|
||||
await alternativeAufloesen(c, neu, schluessel.slice(4), log, { dryRun: true, daten: d });
|
||||
} catch (e) {
|
||||
log(`FEHLER ${schluessel}: ${(e as Error).message} -- Eintrag entfernt`);
|
||||
delete neu.zuordnung[schluessel];
|
||||
}
|
||||
}
|
||||
if (!dryRun) stateSchreiben(c, neu);
|
||||
return neu;
|
||||
}
|
||||
|
||||
@@ -114,10 +178,17 @@ if (import.meta.main) {
|
||||
const s = stateLaden(c);
|
||||
const force = process.argv.includes("--force");
|
||||
const dryRun = process.argv.includes("--dry-run");
|
||||
if (!force && !stateVeraltet(c, s)) {
|
||||
const refs = process.argv.slice(2).filter((a) => a.startsWith("alt/")).map((a) => a.slice(4));
|
||||
const tabelle = (st: State) => console.table(Object.entries(st.zuordnung).map(([alias, z]) => ({
|
||||
alias, ziel: referenzFormat(z.model, z.effort), index: z.index.toFixed(1), coding: z.coding?.toFixed(1) ?? "—", agentic: z.agentic?.toFixed(1) ?? "—", "$/1M": z.weighted.toFixed(2), referenz: z.referenz, seit: z.seit.slice(0, 16),
|
||||
})));
|
||||
if (refs.length > 0) {
|
||||
for (const r of refs) await alternativeAufloesen(c, s, r, console.log, { dryRun });
|
||||
tabelle({ ...s, zuordnung: Object.fromEntries(Object.entries(s.zuordnung).filter(([k]) => refs.some((r) => k === `alt/${r}`))) });
|
||||
} else if (!force && !stateVeraltet(c, s)) {
|
||||
console.log(`state.json ist frisch (${s.aktualisiert}); --force erzwingt.`);
|
||||
tabelle(s);
|
||||
} else {
|
||||
const neu = await aufloesen(c, s, { dryRun });
|
||||
console.table(Object.entries(neu.zuordnung).map(([alias, z]) => ({ alias, ziel: referenzFormat(z.model, z.effort), index: z.index.toFixed(1), "$/1M": z.weighted.toFixed(2), referenz: z.referenz, seit: z.seit.slice(0, 16) })));
|
||||
tabelle(await aufloesen(c, s, { dryRun }));
|
||||
}
|
||||
}
|
||||
|
||||
+100
-51
@@ -1,18 +1,23 @@
|
||||
/**
|
||||
* server.ts — der llmrouter: OpenAI-kompatibler Reverse-Proxy mit vier
|
||||
* Eingriffen je Anfrage: Client-Key pruefen, Alias aufloesen und Modell
|
||||
* Eingriffen je Anfrage: Client-Key pruefen, Namen aufloesen und Modell
|
||||
* umschreiben, Usage/Kosten aus der Antwort mitlesen, eine Zeile loggen.
|
||||
*
|
||||
* Keine Format-Uebersetzung: alle Upstreams sprechen OpenAI-Format
|
||||
* (OpenRouter, llama-server, Ollama, Mistral). Genau das macht ihn klein.
|
||||
* Namensraeume (Reihenfolge der Aufloesung):
|
||||
* role/<name> -> Ziel aus config.rollen, dann weiter
|
||||
* 42i/... (config.aliases) -> fest / schwelle / alternative (State)
|
||||
* openrouter/<id>[:effort] -> 1:1 an OpenRouter
|
||||
* alt/<id>[:effort] -> guenstigste Alternative (auf Abruf, State)
|
||||
* sonst -> fallback (LiteLLM), unveraendert
|
||||
*
|
||||
* Umgebung: PORT (optional), KEYS_FILE (Token -> Name), ADMIN_KEY (fuer
|
||||
* /stats und /zuordnung), OPENROUTER_API_KEY, MISTRAL_API_KEY.
|
||||
* Keine Format-Uebersetzung: alle Upstreams sprechen OpenAI-Format.
|
||||
* Umgebung: PORT, KEYS_FILE, ADMIN_KEY, OPENROUTER_API_KEY, MISTRAL_API_KEY,
|
||||
* KIRA_*_KEY, ARTIFICIALANALYSIS_API_KEY, LLMROUTER_CONFIG, MODEL_BLOCKLIST.
|
||||
*/
|
||||
import { configLaden, aclLaden, tokenLaden, upstreamKey, type Config, type Upstream } from "./config.ts";
|
||||
import { aufloesen, stateLaden, stateVeraltet, type State } from "./resolver.ts";
|
||||
import { configLaden, aclLaden, tokenLaden, upstreamKey, erlaubt, type Config, type Upstream } from "./config.ts";
|
||||
import { aufloesen, alternativeAufloesen, datenHolen, stateLaden, stateVeraltet, type State } from "./resolver.ts";
|
||||
import { Log, usageLesen, type Eintrag } from "./log.ts";
|
||||
import { referenzFormat } from "../../llmlite/openrouter-auswahl.ts";
|
||||
import { referenzFormat, referenzParsen, type Effort } from "../../llmlite/openrouter-auswahl.ts";
|
||||
|
||||
const config: Config = configLaden();
|
||||
const acl = aclLaden();
|
||||
@@ -37,8 +42,24 @@ async function auffrischen(grund: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
if (stateVeraltet(config, state)) void auffrischen("Start, state veraltet");
|
||||
else void datenHolen(config).catch((e) => sagen(`Daten laden fehlgeschlagen: ${(e as Error).message}`));
|
||||
setInterval(() => { if (stateVeraltet(config, state)) void auffrischen("Takt"); tokens = tokenLaden(); }, 15 * 60_000);
|
||||
|
||||
// alt/-Aufloesungen auf Abruf, je Referenz nur einmal gleichzeitig
|
||||
const altLaeuft = new Map<string, Promise<void>>();
|
||||
async function altSicherstellen(ref: string): Promise<{ model: string; effort: Effort | null } | { fehler: string }> {
|
||||
const z = state.zuordnung[`alt/${ref}`];
|
||||
if (z !== undefined) return { model: z.model, effort: z.effort };
|
||||
let p = altLaeuft.get(ref);
|
||||
if (p === undefined) {
|
||||
p = alternativeAufloesen(config, state, ref, sagen).then(() => undefined).finally(() => altLaeuft.delete(ref));
|
||||
altLaeuft.set(ref, p);
|
||||
}
|
||||
try { await p; } catch (e) { return { fehler: (e as Error).message }; }
|
||||
const neu = state.zuordnung[`alt/${ref}`];
|
||||
return neu === undefined ? { fehler: `alt/${ref}: keine Zuordnung` } : { model: neu.model, effort: neu.effort };
|
||||
}
|
||||
|
||||
// --- Nebenlaeufigkeit je Upstream (lokales Qwen: ein Slot) ------------------
|
||||
const semaphoren = new Map<string, { frei: number; warteschlange: (() => void)[] }>();
|
||||
async function belegen(name: string, u: Upstream): Promise<() => void> {
|
||||
@@ -50,29 +71,41 @@ async function belegen(name: string, u: Upstream): Promise<() => void> {
|
||||
return () => { const n = s!.warteschlange.shift(); if (n) n(); else s!.frei += 1; };
|
||||
}
|
||||
|
||||
// --- Aufloesung Alias -> Ziel ------------------------------------------------
|
||||
type Ziel = { alias: string; upstreamName: string; upstream: Upstream; model: string; effort: string | null; body?: Record<string, unknown>; alternative: boolean };
|
||||
function zielBestimmen(angefragt: string, keyName: string): Ziel | { fehler: string; status: number } {
|
||||
const alias = config.altnamen?.[angefragt] ?? angefragt;
|
||||
const def = config.aliases[alias];
|
||||
if (def === undefined) {
|
||||
// Durchreiche: unbekannter Name geht unveraendert an LiteLLM, mit dem
|
||||
// Client-Key -- dort gelten dessen ACLs. So wechselt ein Agent per
|
||||
// Modellname (2608 -> 2609), nicht per base_url.
|
||||
if (config.fallback !== undefined) {
|
||||
const name = config.fallback.upstream;
|
||||
return { alias: `(durchreiche)`, upstreamName: name, upstream: config.upstreams[name], model: angefragt, effort: null, alternative: false };
|
||||
}
|
||||
return { fehler: `Unbekanntes Modell ${angefragt}`, status: 404 };
|
||||
// --- Aufloesung Name -> Ziel ---------------------------------------------------
|
||||
type Ziel = { alias: string; upstreamName: string; upstream: Upstream; model: string; effort: Effort | null; body?: Record<string, unknown>; effortSetzen: boolean };
|
||||
type Fehler = { fehler: string; status: number };
|
||||
const OR = (): Upstream => config.upstreams.openrouter;
|
||||
|
||||
async function zielBestimmen(angefragt: string, keyName: string): Promise<Ziel | Fehler> {
|
||||
if (!erlaubt(acl.get(keyName) ?? [], angefragt)) return { fehler: `${keyName} darf ${angefragt} nicht rufen`, status: 403 };
|
||||
let name = angefragt;
|
||||
if (name.startsWith("role/")) {
|
||||
const ziel = config.rollen?.[name.slice(5)];
|
||||
if (ziel === undefined) return { fehler: `Unbekannte Rolle ${name}`, status: 404 };
|
||||
name = ziel;
|
||||
}
|
||||
const erlaubt = acl.get(keyName) ?? [];
|
||||
if (erlaubt.length > 0 && !erlaubt.includes(alias)) return { fehler: `${keyName} darf ${alias} nicht rufen`, status: 403 };
|
||||
if (def.art === "fest") {
|
||||
return { alias, upstreamName: def.upstream, upstream: config.upstreams[def.upstream], model: def.model, effort: null, body: def.body, alternative: false };
|
||||
const def = config.aliases[name];
|
||||
if (def !== undefined) {
|
||||
if (def.art === "fest") return { alias: name, upstreamName: def.upstream, upstream: config.upstreams[def.upstream], model: def.model, effort: null, body: def.body, effortSetzen: false };
|
||||
const z = state.zuordnung[name];
|
||||
if (z === undefined) return { fehler: `${name} ist noch nicht aufgeloest (Zuordnung fehlt) -- spaeter erneut`, status: 503 };
|
||||
return { alias: name, upstreamName: "openrouter", upstream: OR(), model: z.model, effort: z.effort, effortSetzen: true };
|
||||
}
|
||||
const z = state.zuordnung[alias];
|
||||
if (z === undefined) return { fehler: `${alias} ist noch nicht aufgeloest (Zuordnung fehlt) -- spaeter erneut`, status: 503 };
|
||||
return { alias, upstreamName: "openrouter", upstream: config.upstreams.openrouter, model: z.model, effort: z.effort, alternative: true };
|
||||
if (name.startsWith("openrouter/")) {
|
||||
const { id, effort } = referenzParsen(name.slice(11));
|
||||
return { alias: name, upstreamName: "openrouter", upstream: OR(), model: id, effort, effortSetzen: effort !== null };
|
||||
}
|
||||
if (name.startsWith("alt/")) {
|
||||
const z = await altSicherstellen(name.slice(4));
|
||||
if ("fehler" in z) return { fehler: z.fehler, status: 404 };
|
||||
return { alias: name, upstreamName: "openrouter", upstream: OR(), model: z.model, effort: z.effort, effortSetzen: true };
|
||||
}
|
||||
if (config.fallback !== undefined) {
|
||||
// Durchreiche: unbekannter Name geht unveraendert an LiteLLM, mit dem Client-Key -- dort gelten dessen ACLs.
|
||||
const u = config.fallback.upstream;
|
||||
return { alias: "(durchreiche)", upstreamName: u, upstream: config.upstreams[u], model: name, effort: null, effortSetzen: false };
|
||||
}
|
||||
return { fehler: `Unbekanntes Modell ${angefragt}`, status: 404 };
|
||||
}
|
||||
|
||||
// --- Anfrage bauen -------------------------------------------------------------
|
||||
@@ -85,15 +118,45 @@ function jsonBody(orig: Record<string, unknown>, ziel: Ziel): Record<string, unk
|
||||
b.usage = { include: true }; // Kosten kommen in der Antwort mit
|
||||
if (b.stream === true) b.stream_options = { ...(b.stream_options as object ?? {}), include_usage: true };
|
||||
}
|
||||
if (ziel.alternative) {
|
||||
// Die Stufe gehoert zum Alias, nicht zum Client: wer marvin:low ruft,
|
||||
// bekommt die Klasse von marvin:low -- auch wenn er reasoning_effort mitschickt.
|
||||
if (ziel.effortSetzen) {
|
||||
// Die Stufe gehoert zum Namen, nicht zum Client: wer 42i/marvin-2609:low
|
||||
// oder openrouter/x:medium ruft, bekommt genau diese Stufe.
|
||||
delete b.reasoning_effort; delete b.reasoning;
|
||||
if (ziel.effort !== null) b.reasoning = { effort: ziel.effort };
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
// --- Modellliste ---------------------------------------------------------------
|
||||
async function modellListe(keyName: string, auth: string): Promise<{ id: string; object: "model"; owned_by: string }[]> {
|
||||
const liste = acl.get(keyName) ?? [];
|
||||
const data: { id: string; object: "model"; owned_by: string }[] = [];
|
||||
const add = (id: string, owned_by: string) => { if (erlaubt(liste, id)) data.push({ id, object: "model", owned_by }); };
|
||||
for (const id of Object.keys(config.aliases)) add(id, "42i");
|
||||
for (const r of Object.keys(config.rollen ?? {})) add(`role/${r}`, "42i");
|
||||
try {
|
||||
const d = await datenHolen(config);
|
||||
const stufen = new Map<string, Effort[]>();
|
||||
for (const v of d.varianten) if (v.effort !== null) stufen.set(v.id, [...(stufen.get(v.id) ?? []), v.effort]);
|
||||
for (const m of d.konto) {
|
||||
if (m.id.includes(":")) { add(`openrouter/${m.id}`, "openrouter"); continue; } // :free, :exacto -- keine Stufen
|
||||
add(`openrouter/${m.id}`, "openrouter"); add(`alt/${m.id}`, "alt");
|
||||
for (const e of stufen.get(m.id) ?? []) { add(`openrouter/${m.id}:${e}`, "openrouter"); add(`alt/${m.id}:${e}`, "alt"); }
|
||||
}
|
||||
} catch (e) { sagen(`/models ohne OpenRouter-Liste: ${(e as Error).message}`); }
|
||||
if (config.fallback !== undefined) {
|
||||
// Im Parallelbetrieb auch die LiteLLM-Liste des Clients, sonst verlieren
|
||||
// Clients, die Modelle per /models entdecken, die 2608-Namen.
|
||||
try {
|
||||
const u = config.upstreams[config.fallback.upstream];
|
||||
const r = await fetch(`${u.base}/models`, { headers: { authorization: `Bearer ${auth}` }, signal: AbortSignal.timeout(10_000) });
|
||||
const bekannt = new Set(data.map((m) => m.id));
|
||||
if (r.ok) for (const m of ((await r.json()) as { data?: { id: string }[] }).data ?? []) if (!bekannt.has(m.id)) data.push({ id: m.id, object: "model", owned_by: config.fallback.upstream });
|
||||
} catch { /* LiteLLM nicht da: nur eigene Liste */ }
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// --- Server -------------------------------------------------------------------
|
||||
Bun.serve({
|
||||
port: Number(process.env.PORT ?? config.port),
|
||||
@@ -113,21 +176,7 @@ Bun.serve({
|
||||
// Client
|
||||
const keyName = tokens.get(auth);
|
||||
if (keyName === undefined) return Response.json({ error: { message: "Ungueltiger API-Key" } }, { status: 401 });
|
||||
if (pfad === "/models") {
|
||||
const erlaubt = acl.get(keyName) ?? [];
|
||||
const namen = Object.keys(config.aliases).filter((a) => erlaubt.length === 0 || erlaubt.includes(a));
|
||||
const data = namen.map((id) => ({ id, object: "model", owned_by: "42i" }));
|
||||
// Im Parallelbetrieb auch die LiteLLM-Liste des Clients zeigen, sonst
|
||||
// verlieren Clients, die Modelle per /models entdecken, die 2608-Namen.
|
||||
if (config.fallback !== undefined) {
|
||||
try {
|
||||
const u = config.upstreams[config.fallback.upstream];
|
||||
const r = await fetch(`${u.base}/models`, { headers: { authorization: `Bearer ${auth}` }, signal: AbortSignal.timeout(10_000) });
|
||||
if (r.ok) for (const m of ((await r.json()) as { data?: { id: string }[] }).data ?? []) if (!namen.includes(m.id)) data.push({ id: m.id, object: "model", owned_by: config.fallback.upstream });
|
||||
} catch { /* LiteLLM nicht da: nur eigene Liste */ }
|
||||
}
|
||||
return Response.json({ object: "list", data });
|
||||
}
|
||||
if (pfad === "/models") return Response.json({ object: "list", data: await modellListe(keyName, auth) });
|
||||
if (req.method !== "POST" || !(JSON_PFADE.has(pfad) || FORM_PFADE.has(pfad))) return Response.json({ error: { message: `Pfad ${pfad} wird nicht bedient` } }, { status: 404 });
|
||||
|
||||
const start = Date.now();
|
||||
@@ -141,7 +190,7 @@ Bun.serve({
|
||||
let orig: Record<string, unknown>;
|
||||
try { orig = (await req.json()) as Record<string, unknown>; } catch { return Response.json({ error: { message: "Body ist kein JSON" } }, { status: 400 }); }
|
||||
angefragt = String(orig.model ?? "");
|
||||
const z = zielBestimmen(angefragt, keyName);
|
||||
const z = await zielBestimmen(angefragt, keyName);
|
||||
if ("fehler" in z) return Response.json({ error: { message: z.fehler } }, { status: z.status });
|
||||
ziel = z;
|
||||
stream = orig.stream === true;
|
||||
@@ -150,7 +199,7 @@ Bun.serve({
|
||||
} else {
|
||||
const form = await req.formData();
|
||||
angefragt = String(form.get("model") ?? "");
|
||||
const z = zielBestimmen(angefragt, keyName);
|
||||
const z = await zielBestimmen(angefragt, keyName);
|
||||
if ("fehler" in z) return Response.json({ error: { message: z.fehler } }, { status: z.status });
|
||||
ziel = z;
|
||||
if (ziel.upstream.clientKey !== true) form.set("model", ziel.model);
|
||||
@@ -185,7 +234,7 @@ Bun.serve({
|
||||
const ct = antwort.headers.get("content-type") ?? "";
|
||||
const weiter = new Headers(antwort.headers);
|
||||
weiter.delete("content-length"); weiter.delete("content-encoding"); weiter.delete("transfer-encoding");
|
||||
weiter.set("x-llmrouter-model", referenzFormat(ziel.model, ziel.effort as never));
|
||||
weiter.set("x-llmrouter-model", referenzFormat(ziel.model, ziel.effort));
|
||||
|
||||
if (ct.startsWith("text/event-stream") && antwort.body !== null) {
|
||||
// Durchreichen und nebenbei den letzten usage-Chunk lesen.
|
||||
@@ -220,4 +269,4 @@ Bun.serve({
|
||||
return new Response(antwort.body, { status: antwort.status, headers: weiter });
|
||||
},
|
||||
});
|
||||
sagen(`llmrouter lauscht auf :${process.env.PORT ?? config.port}, ${Object.keys(config.aliases).length} Aliasse, ${tokens.size} Token, Zuordnung von ${state.aktualisiert}`);
|
||||
sagen(`llmrouter lauscht auf :${process.env.PORT ?? config.port}, ${Object.keys(config.aliases).length} Aliasse, ${Object.keys(config.rollen ?? {}).length} Rollen, ${tokens.size} Token, Zuordnung von ${state.aktualisiert}`);
|
||||
|
||||
Reference in New Issue
Block a user