# v2 git bundle
7897fa9651952b5b89700bf8b8046ac6d9e502bb refs/heads/bundle
7897fa9651952b5b89700bf8b8046ac6d9e502bb HEAD

PACK      3x3tree b7128c81eb5a94ee372e8910e3ca196e18134ab2
author Elton Turing <elton@42i.org> 1788584592 +0200
committer Elton Turing <elton@42i.org> 1788584592 +0200

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>
x340031QK,L/Je(H=2oͫUE8<{*U/7Aıf.-oV3۪C%ee1̔	9cC,S(.;Jb#Ve"Mu8n*,HLNLO#,v:ɿeM@(CUw'7B)1E Wx log/
dist/
node_modules/
pxW]o}篸DI66ɮBHY,9gE3CCѧb1O'/3$( !=s/QQ4Q4*B?C4VNg[ZQ{	X%sIIǽ%Xi:_I=:UN
7XOLu{GxJ^FBjQӍea|,iF,%O]JX$U,
,6YGWR3:C@Q[dfխ+.$?$EABMs#L^7)7q2֋.-3R꤇":#Ei#22v
RV:]:S!.osQ"GGG=:'*dmm/9y,f%Eg[(F(N'Qt:yصy/\P gr又s_zM

_\".j-ENrSTOQg2VKdfr(A:A[p5G,@-;.i漀xr!6A(18ys?!DgI24@5W
 ;P
ݺ.MEQJSx.Fjg,Mo@%!Nܲ<f?)E,lx]G¼4ؓ/MU"RP$iҥO߽nqAgH˖OJ7.z{si]KVA^MU+Zker:2ʙ2,,2+yސ̪.'¥9^O\k'U_/,l]\%B'c"+?Ѣ0Cc?:{^$WR7^?wzb_"1$B4lem+YTkx紬%&LԌf">FDL-5.|aCJ֤}p*PO/	~Ɓ50O"n] (!P<If'x/m>FWs7lv0rwUmX{rDv
zuD`$4'=iS8BsnԤ#.vGw{K~# {FYFg$[AFgJ#_3oinULL> x~'̬$	GZϪBq奒yb=Gtlhyq~5= QsܺL&Ѯmo-CCo͌s"p>c݀cSJ߮_y[W`o .F둾VNr/K.iaCi'hz'3þyђ~)מ}ޙ^^9#
3+JI*>`![726kHqL,:hd EOɫ6V:~io-&R,PdimZ4ͥf8ց4/6)SWp$'&'Hb48NKvs3y{<%o4=oו4v;KA~H?T/<sP(a;B/wfzZIk?153g7802]So>lc6ݦv%l2]?ſΓ;fL]+?M<E0z$5ϵQq|{zyhwc$Qʡ6;:ګƜ2_>T,h/V-nano<:JkSP.Ր0[|Esa|re4Jn~g6&x=5i-O|ß:HK./ae(w Qrb@2[snTΌm	TYPՐbJsPֆا6{	d̰_iwvq4UMx``On6k5a1OCw0=N,+:
7"Cn|NoWG>!dZ!>3ܛ~kۇn.%Pɓ07L@Sԋ$sx{
  "_doku": "Versionierte Quelle des llmrouter. Secrets kommen NUR aus der Umgebung: OPENROUTER_API_KEY, MISTRAL_API_KEY, ADMIN_KEY; Client-Keys aus KEYS_FILE (token -> name), Rechte aus key-acls.json (name -> aliasse).",
  "port": 4001,
  "logDir": "./log",
  "refreshHours": 12,
  "aaRefreshDays": 7,
  "lasttest": true,
  "upstreams": {
    "openrouter": { "base": "https://openrouter.ai/api/v1", "keyEnv": "OPENROUTER_API_KEY" },
    "qwen":       { "base": "http://10.18.5.30:11507/v1", "maxParallel": 1 },
    "voice":      { "base": "http://10.18.5.30:8803/v1" },
    "ollama":     { "base": "http://10.18.5.30:11434/v1" },
    "mistral":    { "base": "https://api.mistral.ai/v1", "keyEnv": "MISTRAL_API_KEY" },
    "kira-chat":  { "base": "http://10.6.42.111:8000/v1" },
    "kira-stt":   { "base": "http://10.6.42.111:8001/v1" }
  },
  "aliases": {
    "marvin:none":   { "art": "alternative", "referenz": "openai/gpt-5.6-luna:none" },
    "marvin:low":    { "art": "alternative", "referenz": "openai/gpt-5.6-luna:low" },
    "marvin:medium": { "art": "alternative", "referenz": "openai/gpt-5.6-luna:medium" },
    "marvin:high":   { "art": "alternative", "referenz": "openai/gpt-5.6-luna:high" },
    "marvin:extra":  { "art": "alternative", "referenz": "anthropic/claude-opus-5:medium" },
    "marvin:ultra":  { "art": "alternative", "referenz": "anthropic/claude-fable-5.1:medium" },
    "marvin-mass":   { "art": "alternative", "referenz": "inclusionai/ling-3.0-flash" },
    "marvin-free":   { "art": "fest", "upstream": "openrouter", "model": "openrouter/free" },
    "marvin-core:none":   { "art": "fest", "upstream": "qwen", "model": "qwen36:35b", "body": { "chat_template_kwargs": { "enable_thinking": false } } },
    "marvin-core:medium": { "art": "fest", "upstream": "qwen", "model": "qwen36:35b", "body": { "chat_template_kwargs": { "enable_thinking": true } } },
    "kira-reasoning": { "art": "fest", "upstream": "kira-chat", "model": "kira-reasoning" },
    "kira-stt":       { "art": "fest", "upstream": "kira-stt",  "model": "kira-stt" },
    "marvin-tts":       { "art": "fest", "upstream": "voice",   "model": "tts-1" },
    "marvin-stt":       { "art": "fest", "upstream": "voice",   "model": "whisper-1" },
    "marvin-tts-turbo": { "art": "fest", "upstream": "mistral", "model": "voxtral-mini-tts-2603" },
    "marvin-stt-turbo": { "art": "fest", "upstream": "mistral", "model": "voxtral-mini-latest" },
    "marvin-embedding":       { "art": "fest", "upstream": "ollama",     "model": "bge-m3" },
    "marvin-embedding-turbo": { "art": "fest", "upstream": "openrouter", "model": "mistralai/mistral-embed-2312" }
  },
  "_altnamen_doku": "Übergangs-Aliasse aus der LiteLLM-Zeit, damit kein Container angefasst werden muss. Loggen unter dem NEUEN Namen. Nach und nach abbauen.",
  "altnamen": {
    "42i/marvin-2608":             "marvin:low",
    "42i/marvin-2608:none":        "marvin:none",
    "42i/marvin-2608:low":         "marvin:low",
    "42i/marvin-2608:medium":      "marvin:medium",
    "42i/marvin-2608:high":        "marvin:high",
    "42i/marvin-2608:extra":       "marvin:extra",
    "42i/marvin-2608:ultra":       "marvin:ultra",
    "42i/marvin-2608-auto":        "marvin:medium",
    "42i/marvin-2608-mass":        "marvin-mass",
    "42i/marvin-2608-mass:none":   "marvin-mass",
    "42i/marvin-2608-mass:medium": "marvin-mass",
    "42i/marvin-2608-core:none":   "marvin-core:none",
    "42i/marvin-2608-core:medium": "marvin-core:medium",
    "marvin-reasoning":            "marvin-core:medium",
    "marvin-core":                 "marvin-core:none"
  }
}

(2x{
  "_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.",
  "keys": {
    "persona-spark":  ["marvin:medium", "kira-reasoning", "kira-stt", "marvin-stt", "marvin-tts"],
    "persona-buzz":   ["marvin:medium", "kira-reasoning", "kira-stt", "marvin-stt", "marvin-tts"],
    "persona-jessie": ["marvin:medium", "kira-reasoning", "kira-stt", "marvin-stt", "marvin-tts"],
    "persona-klara":  ["marvin:medium", "kira-reasoning", "kira-stt", "marvin-stt", "marvin-tts"],
    "persona-elton":  ["marvin:none", "marvin:low", "marvin:medium", "marvin:high", "marvin:extra", "marvin:ultra", "marvin-core:none", "marvin-core:medium", "marvin-mass", "kira-reasoning", "kira-stt", "marvin-stt", "marvin-stt-turbo", "marvin-tts", "marvin-tts-turbo"],
    "persona-festus": ["marvin-core:none", "marvin-core:medium", "marvin:medium", "kira-reasoning", "kira-stt"],
    "persona-aria":   ["marvin-core:none", "marvin-core:medium", "marvin:medium", "kira-reasoning", "kira-stt"],
    "persona-inka":   ["marvin-core:none", "marvin-core:medium", "marvin:low", "marvin:medium", "kira-reasoning", "kira-stt", "marvin-stt", "marvin-stt-turbo", "marvin-tts", "marvin-tts-turbo"],
    "persona-inka-mx":["marvin-core:none", "marvin-core:medium", "marvin:low", "marvin:medium", "kira-reasoning", "kira-stt", "marvin-stt", "marvin-stt-turbo", "marvin-tts", "marvin-tts-turbo"],
    "persona-andreas": [],
    "nightjob":       ["marvin-core:none", "marvin:medium", "kira-reasoning", "kira-stt", "marvin-embedding", "marvin-embedding-turbo", "marvin-stt", "marvin-stt-turbo", "marvin-tts", "marvin-tts-turbo"],
    "lab-xo":         ["marvin-mass", "marvin:extra", "marvin:medium", "kira-reasoning", "kira-stt"],
    "team-copilot":   ["marvin:low", "marvin:medium", "kira-reasoning", "kira-stt"]
  }
}
\xN{
  "name": "llmrouter",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "bun run src/server.ts",
    "resolve": "bun run src/resolver.ts",
    "stats": "bun run src/stats.ts",
    "keys": "bun run src/keys.ts",
    "build": "bun build --compile --target=bun-linux-x64 src/server.ts --outfile dist/llmrouter"
  }
}
0hsx340031QHKL+)f(%Բ鲲QsN/4(N,)	<uxc;gw?8$'lΰ?p꣥ʼ5w*RsR@\jR{~|B\f	"^]?X+[%Jxlf0E%%`-L 9ۤ~ƃro VɿxVN#7jɶQfaK
!pf$&NՇIz=O2w=>>:Cdg5`2vo,&{DFqZ<8ey@0T#l1fh5Ji{.`jÕmg`V
t(f%[,)`b	YV9=8B9>(CF1h}P:s 
X%#C3&rǪwrgXt2bЎM_D\w((Uڊ9%ki׫zP!'sTLS"xY`F̵̈́Ed*F:ߧFrRVRX]E(ǩChĶQn:$6IpɿOBN)By*f3)[7^fERtrͲkLZYdr~SNZ+LxH-$rK,pr;oV\}
Iʇ`4|yq5^9ptw{naԻPВ
X{噸WiL6m0"ʉj>rץXir T{-f h8ջxH~0
»w	^MESc.1εz0Zi.޳#j=(I{;{:<q@$3GJl69K܉Lka[f6ΉU&fn)Ǔp<QF̄>t$%8B-^i_?~QLa`8A9&Gi>#n(8 7Lfﶌ"4x}s!U̢Wt|2վ۸jQ܊M<Irt~(ڔ%(p_q]빤:,S+nHέM6Օ]	/izbk˙H^z!G	Bf/i9D>KWWzvF#hwoe; _OWxy/**
 * keys.ts — Client-Token verwalten. Die Datei (KEYS_FILE) liegt nur auf dem
 * Host, nie im Repo. `add <name>` erzeugt ein Token und gibt es EINMAL aus;
 * `add <name> --token <t>` uebernimmt ein bestehendes (Migration von LiteLLM,
 * damit kein Container angefasst werden muss). `list` zeigt Namen.
 */
import { readFileSync, writeFileSync } from "fs";
import { randomBytes } from "crypto";

const pfad = process.env.KEYS_FILE;
if (pfad === undefined) { console.error("KEYS_FILE nicht gesetzt"); process.exit(1); }
let daten: Record<string, string> = {};
try { daten = JSON.parse(readFileSync(pfad, "utf8")); } catch { /* neu */ }

const [befehl, name] = process.argv.slice(2);
if (befehl === "list") {
  for (const n of [...new Set(Object.values(daten))].sort()) console.log(n);
} else if (befehl === "add" && name) {
  const i = process.argv.indexOf("--token");
  const token = i > 0 ? process.argv[i + 1] : `sk-42i-${randomBytes(24).toString("base64url")}`;
  daten[token] = name;
  writeFileSync(pfad, JSON.stringify(daten, null, 1), { mode: 0o600 });
  console.log(i > 0 ? `uebernommen fuer ${name}` : token);
} else if (befehl === "remove" && name) {
  for (const [t, n] of Object.entries(daten)) if (n === name) delete daten[t];
  writeFileSync(pfad, JSON.stringify(daten, null, 1), { mode: 0o600 });
} else {
  console.log("keys.ts list | add <name> [--token <t>] | remove <name>");
}
ܲx}VrFa2
0ĖSb
.m1BZq"]JC	$З20ZhA:R84XW>_
=	&>Mǣ!8EzRiM2,3L!b3'$kܹDHN6JcaE>I2IA& o˧$duBs=&d[!S612s(pq<chብH%nRd;~`2`KWX=ج#%`&h.EdǋwŷAlCXþ Eތh]u,|*=J$UBf#QrI7)𗎈:.ʨQm(Zm^$I*㭼y[1q%O>`tLc+8@&+me:пע&6Y(ԡG%"ԡGH1R$FHp4|`So~8I[Bڃy3Zǧ[w?a0apt6\-3n)ٕ\αffv#nF,(fGB*kנT_ 86j		g/`tO鯂5U)UzPO$|@swTXdS//N	OWڎ[t;hΛ\>d??w3v+!gGcA/r FZPkRW_s- ^ЅwI%PM|jMp	mfQ\$ՂOto܃fZPGVݦI\yšE}Ujz*FgQ'-}U&*:1K2Kyyos6S_@dSX<N߹Пj`/%ɸtZ%"DGd 3؋4qqs	*M=U0%CEl0dc0~?@U6[z8_(xrO,.酭.j9wμfPs ;1wj)G7C ig`iF 5騁$O>)ǠzDms[f<TA=QPɺoMpA&ujY>!5uK4I9zws_/ݽנڙƲG(+}4mTl14bz_$17\}byNA~=7%-CӸ٤Y9xXn8!R-n$vlj#j$͊"ЊC	$/C9i,
(193oxxpGeYdR'_44Qsaԭe)ITS*Ԭ4e,k"u)U.uQA@TZ3>eL,*K3ZI+frԋfT>X7 󯫉(A,̈Kw{-"sl̩ITLN!%N(*]R.+Y^bݹβE\ؼҵ$Uh1HlJ~+<fa0!.q]I(+MBz&ǹ*I>8aI&aQ903*$5/2#	ht.,5:1YIWV͸I_C?;>B&nE7rB/]ƴֈn*V7\Lv*M|~-Mu`Z{9ꕼ{QP=g"eFx\I$FJrnV" ;(߼mydV~7կS8{Ir__BJ|fńϳ#CCۓFK%>*c@ZFwdoDu9֨ޗXJeÃ2iI?ZƒfrZ0Z1l vJO)E^A\gD{W\0l0+:h8eS<?u3qS=Z{?QNNf>yӛz߰7] Ԩ Ǫ6j1`/W&+N/5%LLQD@V+õTZOޮ'?00/-2_gMg
6-j#lUyN΋u8;W1SJ?=?AcG*:mQ'Pb)QSNb{%>^Z\V\N@$ۙV%|`ȫ#*diR))zӟQAhegIgOE\kNaTϝpSK2wXmBu`\5U<]U:*YrOdaJ1\6wF@Y&ضY.3r踵k{-6:!ޅQtܕE;jsБז9ѵ5%ZK
xW$}F7+-Efʢ@4P, Ȅ@LA\0k[mlCXs,Jb{>40#
=JXnQN'ȯZwf&1+S`eu,wװco5AXjrB6䁕ˑ3
wB&J}ኦ.7Kc	[4`ig|I.;(tRVKicC"˼ణ`L0JLݕuU%I⢗d	NQA?G}ʜekxz7M&9fe	H1=,.5`T^I%5e]m4Eu|4*yeRj!1'S\K&
@c6>تrzvDv&2ax0@KȨ;,WXgF1Ihc&g8fu	vp!&~=4C[tďYE[+FOp+v}A̮?,V(nT癱`N7'CZ&9k; jˋJUuIRC.ݢlZ`v.sa~i!AiQήط*Rh?`a>W$<q3DޠP|M{Wڿ#mwa'펀-M-۝WQ7fzkr}ػx_n:HoLbjzp/[<7ԍC
hkB7sK[O(Tҟ_x}qJ&^xA6)0˹ o)ۋ/քmDgPܡpY\#QT暘,q_)#Pyѣ.>^d]k;TJ)V=EY5AKNym?O;8h
hlقOn=bsU7Ovs#IYÝ&qaܒd_laDն6fp^#/*zL d2gjQI˶	mnaH}Ƴyv62SH[ 9YxZr8t^.N;d$ۭT]iWI\k/.VHs';-8U&sVZ6%KӬ,j)>;{Uds.aJt-ɮQI>-D`Sg?ee؎Az|ٛb,ҔY5"y,WE%Jx˛t}J"&\D
өc`"dˢ̸잋!/u>3|^Rbռn1\3#%jEH}؛hx"5f%]RtMPp|trƂb."وzӋ_ସ)}g[  &xz#uQs`'Gg/N.;<=;9xmVIK6*I2}$J>J'I4ϵ۹`?C#[IYd4RXVgt1^$(4(OiWH_&+y0FcY,aZh_xAH+z]Xث\HeK멐Z69-%'@wsqCr01' 
B`]Ʋ8<=:%${iO";Y*Ab;z=vO!~fӜCE=!'<`WM|D.CƤ/\%Q߂.Jp]$}b,@#zY 
DULK"l8cR]
{no83tc"1q	Q"l_}]8t*9>(,23Qѡ͆B	agnyTrui.$<x]ހZmZf)	.`\"k \pv'b=zgorg2	yG(o7$bmviEd|>+J2Zom	Xu3`7`NO޳>9ᚉE*rdkw}Gg8yI1ho҅z˕r
>q_(UY#֕:G_P62vCf>w#"A4[j;njvm֙V&ιjL$LDX+;OAC:H?%WAQ0'd.)Ymd21 L4MlXoQU^FY(qݰ@>91uBJ&Y%Q(˽>^'8*:-uelS%SlcEsgO2mdc"wֺF?iH<χd)\f}Qx1[52Pj@HHsT8T7߰m䣴*PHY+#*Kx4F1@ɡ3>;61{h]<;4;}dV(eRoӧm*WiWN[5Ů2^Zv"K(8ZvHDSA-Y;8R:	.@ ZzB~by1v91b]XZ;k1KwllPmJ==z{qOa:ьKQ;~<F$_y=N^5Bwˣ7.ԇըL%F2akڇȟ悢2cYx/Qk
Vwr
UxIgP֓j}&8`0	rGdz㭐ЕTOt!]нme
l b$'BQ)sbѳeh~PыƦbVe:82Kz$g7(uB,*wpR^!\<pP*pu4@HH"ѕVaYl8*~mBb*:wL~&ύ?41WtUY;I,X;f.$y'T%}ٷOК)"mXkkڎb011@ϹQu yS>A_?j'lĬf5블pbBML XJ%F>d8KrB^#1"*UthPѨ>~&x)ʟDO¼cϓ.rt|pji&K~BzCwF٩T#:}5, W
)5ago+C9}v"jBi%t.ɒ	( Q0VjGկvC[F5J=;EB|m+xYK3SG[+Lcli4!3@sOALei^AͿVɱrkZ5=R)h{0$yqL'Zv٧On@K$A2VW4G(b:Xa<+vVHoMxNST5WԴ+vcQߑ]U&t1T\ƨ+\ۅ}hVAT@lmzSXwJ&5H?[CZPGJCX= )!*{s;H\~`TsE.bbI%nDPu	۞E=7pGH݄u\|-b}pE?9`"~wxYi-oT %qZ6hu)hfixu Ib4ޡNi2RIGX WzԎj(27h&۠eTD@:jJqR5D^ޣ):CڨHl^퓟ڵQVffإV_՜QϤwCߣ8d&m]IoD{5Kaj$rcv:fA"~뻗=gZ6Gʤ֮";4-j<7Ñ;6Ds3n]B<vՌ(78ݵ
!8~lSӾz,mh5YWEuKfAL`L&Rtok}z]L/잗V-K^e\HTAu}xV,E7;x:oeHE,*Vةg`?1qթKǨ,i4=dہ[5Q6A胖_A7omlMNz90JmVw;PVaШ,~3v+`2F$Rɯ04h2f]SH@䣂&@ԈQ܇2l)~G=|N	t$XQ J!&j<0YZ]8ޡ&9P$lB9mt!1+}K\ںLkW c12Z:QB5Z"@JΩsӟʒ凂9XDjYzKZBC{D*4LZTKf\sq3.Sq,>MH!ߣwP@W<SJH %a/=dZtPFHkgQ!\'f[#yY	{"Ihjz2;6~;{ cHR탞D[yCDmk;6j"ͱzlMe|\Y p׎e.}m@j;Na}A~\OmA~e2]l.]QV-!sju2 GhۀW?aûPp]Fo⦤%#`T`.uוyd}T2#7,?7ioQ*AfX% ꗊ/z'GGYix4/** stats.ts — Tagesstatistik je Key aus requests.sqlite. `bun run src/stats.ts [tage]` */
import { configLaden } from "./config.ts";
import { Log } from "./log.ts";

const tage = Number(process.argv[2] ?? 7);
const log = new Log(configLaden().logDir);
const zeilen = log.statistik(tage);
if (zeilen.length === 0) console.log(`Keine Anfragen in den letzten ${tage} Tagen.`);
else console.table(zeilen.map((z) => ({ ...z, cost: `$${z.cost.toFixed(4)}` })));
<ݦ,i/X,0|gR