From 6f08a089e796dfe8f4a2d602a7899b71eb1022f1 Mon Sep 17 00:00:00 2001 From: Ravi Tharuma <25951435+RaviTharuma@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:47:53 +0200 Subject: [PATCH] feat(speech): accept response_format=ogg as an opus alias (#10822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged — validated together with a batch of related RaviTharuma PRs in one combined worktree (typecheck:core clean, complexity/file-size/changelog gates green, focused tests passing). Thanks for the contribution! --- .../features/10587-ogg-speech-alias.md | 1 + open-sse/handlers/audioSpeech.ts | 12 +++++- .../unit/audio-speech-ogg-alias-10587.test.ts | 42 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 changelog.d/features/10587-ogg-speech-alias.md create mode 100644 tests/unit/audio-speech-ogg-alias-10587.test.ts diff --git a/changelog.d/features/10587-ogg-speech-alias.md b/changelog.d/features/10587-ogg-speech-alias.md new file mode 100644 index 0000000000..118e2a7b48 --- /dev/null +++ b/changelog.d/features/10587-ogg-speech-alias.md @@ -0,0 +1 @@ +- **feat(providers):** accept `response_format=ogg` on `/v1/audio/speech` as an alias for the existing Opus/Ogg encoder ([#10587](https://github.com/diegosouzapw/OmniRoute/issues/10587)) diff --git a/open-sse/handlers/audioSpeech.ts b/open-sse/handlers/audioSpeech.ts index 32ef003110..9dc499a1e4 100644 --- a/open-sse/handlers/audioSpeech.ts +++ b/open-sse/handlers/audioSpeech.ts @@ -229,6 +229,16 @@ async function handleDeepgramSpeech(providerConfig, body, modelId, token) { return audioStreamResponse(res); } +/** + * Voice-note clients send response_format=ogg. OpenAI TTS documents opus, not ogg. + * OmniRoute already returns Ogg/Opus bytes for opus — alias ogg → opus (#10587). + */ +export function normalizeSpeechResponseFormat(fmt) { + if (typeof fmt !== "string" || !fmt) return "mp3"; + const lower = fmt.toLowerCase(); + return lower === "ogg" ? "opus" : lower; +} + /** * Handle Soniox TTS (OpenAI speech shape → Soniox /tts, returns raw audio bytes) */ @@ -963,7 +973,7 @@ export async function handleAudioSpeech({ model: modelId, input: body.input, voice: body.voice || "alloy", - response_format: body.response_format || "mp3", + response_format: normalizeSpeechResponseFormat(body.response_format), speed: body.speed || 1.0, }), }); diff --git a/tests/unit/audio-speech-ogg-alias-10587.test.ts b/tests/unit/audio-speech-ogg-alias-10587.test.ts new file mode 100644 index 0000000000..409406657e --- /dev/null +++ b/tests/unit/audio-speech-ogg-alias-10587.test.ts @@ -0,0 +1,42 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { normalizeSpeechResponseFormat, handleAudioSpeech } = await import( + "../../open-sse/handlers/audioSpeech.ts" +); + +test("normalizeSpeechResponseFormat aliases ogg to opus (#10587)", () => { + assert.equal(normalizeSpeechResponseFormat("ogg"), "opus"); + assert.equal(normalizeSpeechResponseFormat("OGG"), "opus"); + assert.equal(normalizeSpeechResponseFormat("opus"), "opus"); + assert.equal(normalizeSpeechResponseFormat("mp3"), "mp3"); + assert.equal(normalizeSpeechResponseFormat(undefined), "mp3"); +}); + +test("OpenAI-compat speech path remaps ogg to opus before upstream", async () => { + const originalFetch = globalThis.fetch; + let captured; + globalThis.fetch = async (_url, options = {}) => { + captured = JSON.parse(String(options.body || "{}")); + return new Response(new Uint8Array([1, 2, 3]), { + status: 200, + headers: { "content-type": "audio/opus" }, + }); + }; + try { + const response = await handleAudioSpeech({ + body: { + model: "openai/tts-1", + input: "format check", + voice: "alloy", + response_format: "ogg", + }, + credentials: { apiKey: "openai-key" }, + }); + assert.equal(response.status, 200); + assert.equal(captured.response_format, "opus"); + assert.equal(captured.model, "tts-1"); + } finally { + globalThis.fetch = originalFetch; + } +});