From 106eb633c526fa59ed56e208e212f92dcd6ac8a2 Mon Sep 17 00:00:00 2001 From: Elton Turing Date: Sun, 6 Sep 2026 09:39:00 +0200 Subject: [PATCH] =?UTF-8?q?llmrouter:=20role/inka=20->=20openrouter/qwen/q?= =?UTF-8?q?wen3.8-27b=20(Test,=20Andreas=202026-09-06);=20Inka-Keys=20d?= =?UTF-8?q?=C3=BCrfen=20die=20Rolle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- config.json | 5 +++-- key-acls.json | 6 ++++-- src/server.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/config.json b/config.json index accbe5f..ad4b7a4 100644 --- a/config.json +++ b/config.json @@ -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/[:effort] (1:1) oder alt/[: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/[:effort] (1:1) oder alt/[: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/[:effort] = 1:1 an OpenRouter, Effort als reasoning.effort; alt/[: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/ = Rolle (rollen). Alles andere -> fallback (LiteLLM).", "anthropicModus": "alt", diff --git a/key-acls.json b/key-acls.json index 00b4d43..051b7b3 100644 --- a/key-acls.json +++ b/key-acls.json @@ -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": [ diff --git a/src/server.ts b/src/server.ts index 2122d21..7074603 100644 --- a/src/server.ts +++ b/src/server.ts @@ -145,6 +145,9 @@ function jsonBody(orig: Record, 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;