llmrouter: role/inka -> openrouter/qwen/qwen3.8-27b (Test, Andreas 2026-09-06); Inka-Keys dürfen die Rolle

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 10:02:50 +02:00
committed by andreas
co-authored by Claude Fable 5.1
parent eb1dcdd28e
commit 106eb633c5
3 changed files with 34 additions and 4 deletions
+3 -2
View File
@@ -143,7 +143,7 @@
"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_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. role/inka (2026-09-06): Inkas Telefon-Hirn; Test Qwen 3.8 27B als Kandidat fuer die A6000 bei Uwe, vorher openrouter/openai/gpt-5.6-luna.",
"rollen": {
"xo": "42i/marvin-2609:high",
"architect": "42i/marvin-2609:high",
@@ -164,7 +164,8 @@
"i18n": "42i/marvin-2609:medium",
"docs": "42i/marvin-2609:medium",
"ar": "42i/marvin-2609:medium",
"archivist": "42i/marvin-2609:medium"
"archivist": "42i/marvin-2609:medium",
"inka": "openrouter/qwen/qwen3.8-27b"
},
"_namen_doku": "Namensraeume: 42i/* = konfigurierte Aliasse (aliases); deizo/* = Modelle im Prod-RZ deizo (kira, fest); 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).",
"anthropicModus": "alt",
+4 -2
View File
@@ -88,7 +88,8 @@
"42i/marvin-stt-turbo",
"42i/marvin-tts",
"42i/marvin-tts-turbo",
"openrouter/openai/gpt-5.6-luna"
"openrouter/openai/gpt-5.6-luna",
"role/inka"
],
"persona-inka-mx": [
"42i/marvin-2609-core:none",
@@ -102,7 +103,8 @@
"42i/marvin-stt-turbo",
"42i/marvin-tts",
"42i/marvin-tts-turbo",
"openrouter/openai/gpt-5.6-luna"
"openrouter/openai/gpt-5.6-luna",
"role/inka"
],
"persona-andreas": [],
"nightjob": [
+27
View File
@@ -145,6 +145,9 @@ function jsonBody(orig: Record<string, unknown>, ziel: Ziel, pfad: string): Reco
// Sprechtext vor der Synthese normalisieren (Postleitzahlen als Ziffernfolge mit Pause),
// fuer jedes TTS-Ziel gleich -- die Modelle lesen Betraege und Daten, PLZ nicht.
if (pfad === "/audio/speech" && typeof b.input === "string") b.input = sprechtextNormalisieren(b.input);
// OpenRouter-TTS kennt nur mp3 und pcm. Clients wie Inkas Telefon-Front-End
// verlangen WAV -- wir holen PCM und setzen den RIFF-Kopf selbst (s. unten).
if (pfad === "/audio/speech" && ziel.upstreamName === "openrouter" && b.response_format === "wav") b.response_format = "pcm";
if (pfad.startsWith("/messages")) {
// Anthropic-Format: kein usage.include, Effort als reasoning.effort (OpenRouter nimmt es dort an)
if (ziel.effortSetzen) { delete b.thinking; delete b.output_config; if (ziel.effort !== null) b.reasoning = { effort: ziel.effort }; }
@@ -197,6 +200,18 @@ async function modellListe(keyName: string, auth: string): Promise<{ id: string;
return data;
}
/** Setzt einen RIFF/WAVE-Kopf (PCM 16 bit) vor rohe s16le-Daten. */
function wavVerpacken(pcm: Uint8Array, rate: number, kanaele: number): Uint8Array {
const kopf = new ArrayBuffer(44); const v = new DataView(kopf);
const str = (o: number, t: string) => { for (let i = 0; i < t.length; i++) v.setUint8(o + i, t.charCodeAt(i)); };
str(0, "RIFF"); v.setUint32(4, 36 + pcm.byteLength, true); str(8, "WAVE");
str(12, "fmt "); v.setUint32(16, 16, true); v.setUint16(20, 1, true); v.setUint16(22, kanaele, true);
v.setUint32(24, rate, true); v.setUint32(28, rate * kanaele * 2, true); v.setUint16(32, kanaele * 2, true); v.setUint16(34, 16, true);
str(36, "data"); v.setUint32(40, pcm.byteLength, true);
const aus = new Uint8Array(44 + pcm.byteLength); aus.set(new Uint8Array(kopf), 0); aus.set(pcm, 44);
return aus;
}
// --- Server -------------------------------------------------------------------
Bun.serve({
port: Number(process.env.PORT ?? config.port),
@@ -224,6 +239,7 @@ Bun.serve({
let angefragt = "";
let upstreamBody: BodyInit;
let stream = false;
let wavGewuenscht = false;
let ziel: Ziel;
const headers = new Headers();
@@ -237,6 +253,7 @@ Bun.serve({
if ("fehler" in z) return Response.json({ error: { message: z.fehler } }, { status: z.status });
ziel = z;
stream = orig.stream === true;
wavGewuenscht = pfad === "/audio/speech" && orig.response_format === "wav";
upstreamBody = JSON.stringify(ziel.upstream.clientKey === true ? orig : jsonBody(orig, ziel, pfad));
headers.set("content-type", "application/json");
} else {
@@ -310,6 +327,16 @@ Bun.serve({
return new Response(antwort.body.pipeThrough(tee), { status: antwort.status, headers: weiter });
}
freigeben();
if (ct.startsWith("audio/pcm") && wavGewuenscht) {
// PCM s16le -> WAV; Rate und Kanaele stehen im Content-Type (audio/pcm;rate=24000;channels=1)
const rate = Number(/rate=(\d+)/u.exec(ct)?.[1] ?? 24000);
const kanaele = Number(/channels=(\d+)/u.exec(ct)?.[1] ?? 1);
const pcm = new Uint8Array(await antwort.arrayBuffer());
const wav = wavVerpacken(pcm, rate, kanaele);
weiter.set("content-type", "audio/wav"); weiter.set("content-length", String(wav.byteLength));
abschliessen();
return new Response(wav, { status: antwort.status, headers: weiter });
}
if (ct.includes("application/json")) {
const text = await antwort.text();
let usage: unknown; let fehler: string | undefined;