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
+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;