Skip to content

fix(api): accept .opus uploads on /v1/audio/transcriptions - #10607

Merged
diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.50from
pucedoteth:fix/audio-opus-filename
Aug 18, 2026
Merged

diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.50from
pucedoteth:fix/audio-opus-filename

Conversation

@pucedoteth

Copy link
Copy Markdown
Contributor

Fixes #10588.

Problem

Whisper-compatible upstreams choose the decoder from the multipart filename, against an allow-list (flac, m4a, mp3, mp4, mpeg, mpga, oga, ogg, wav, webm) that has no opus. getUploadedFileName forwarded the client's filename verbatim, so byte-identical audio transcribed as note.ogg and 400'd as note.opus.

That closes a loop on this gateway: /v1/audio/speech emits audio/opus for response_format=opus, so any client that saves its own output and re-uploads it — OpenClaw Talk, Telegram voice notes — fails every round trip.

Fix

A .opus file is Opus audio in an Ogg container (RFC 7845), so .ogg is a truthful relabel and is already on the allow-list. The rewrite goes in getUploadedFileName, the single choke point that feeds buildMultipartBody.

While confirming the fix I found the same root cause in the OpenRouter STT path, with a quieter symptom. resolveOpenRouterAudioFormat matched .opus against neither its extension list nor its MIME map, so it fell through to the "wav" default — announcing Opus bytes to the upstream as WAV rather than erroring. Mapped both the extension and audio/opus onto its already-supported ogg.

Deliberately narrow

  • The rewrite is anchored (/\.opus$/i), so opus-demo.wav and take.opus.mp3 are untouched — opus elsewhere in a name is part of the name, not the container.
  • The audio.wav fallback for nameless blobs is unchanged.
  • The Deepgram path already handled .opus in resolveAudioContentType; it needed nothing.
  • I left the multipart part's Content-Type alone. It still reflects the client's file.type, so an audio/opus upload now sends filename="note.ogg" with Content-Type: audio/opus. Whisper validates on the filename, so this is enough to fix the 400 — say the word if you would rather normalize the MIME too.

Tests

New file tests/unit/audio-transcription-opus-filename.test.ts, 8 cases: the rename, case-insensitivity, the two anchoring guards, untouched extensions, the nameless-blob fallback, and three for the OpenRouter resolver.

  • Against this branch: 8/8 pass
  • With only the two handlers reverted and the tests kept: 4 fail, 4 pass — the four that fail are exactly the .opus cases, and the four guard-rail tests pass either way, which is what they are for.
  • Neighbouring suites green: audio-transcription-handler, audio-transcription-openrouter, multipart-body-arraybuffer-backing, kie-audio-transcription-result, speechmatics-transcription-provider — 41 pass, 0 fail including the new file.
  • eslint clean on all three changed files.

Per CONTRIBUTING I ran the focused loop rather than the full suite. One note for reproducing locally: a plain npm install fails here on the opencode-ai postinstall (It seems your package manager failed to install the right opencode CLI package), unrelated to this change — npm install --ignore-scripts gets you a working dev tree.

Whisper-compatible upstreams pick the decoder from the multipart filename
against an allow-list (flac, m4a, mp3, mp4, mpeg, mpga, oga, ogg, wav,
webm) that has no `opus`, and OmniRoute forwarded the client's filename
verbatim. The same bytes transcribed as `note.ogg` and 400'd as
`note.opus`. Since /v1/audio/speech emits audio/opus for
`response_format=opus`, clients re-uploading their own voice notes hit
this on every round trip.

A `.opus` file is Opus in an Ogg container (RFC 7845), so `.ogg` is a
truthful relabel and is already on the allow-list. Rewrite the extension
in getUploadedFileName, the single choke point feeding
buildMultipartBody.

The OpenRouter STT path had the same root cause with a quieter symptom:
`.opus` matched neither its extension list nor its MIME map, so it fell
through to the "wav" default and announced Opus bytes as WAV. Map both
the extension and audio/opus to its already-supported ogg container.

Fixes cheaper-inference#10588
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks for the clear write-up and the RFC 7845 rationale — this is a well-scoped fix. I re-ran your new test file locally (8/8 pass) and linted the three changed files (0 problems), so the code side is solid. The pending checks you're seeing are GitHub's first-time-contributor workflow-approval gate, not an actual failure (only semgrep-cloud-platform/scan has run so far, and it passed) — we'll approve those runs and get this merged once they're green.

@diegosouzapw
diegosouzapw merged commit 735d2c9 into cheaper-inference:release/v3.8.50 Aug 18, 2026
3 checks passed
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…nference#10607)

Whisper-compatible upstreams pick the decoder from the multipart filename
against an allow-list (flac, m4a, mp3, mp4, mpeg, mpga, oga, ogg, wav,
webm) that has no `opus`, and OmniRoute forwarded the client's filename
verbatim. The same bytes transcribed as `note.ogg` and 400'd as
`note.opus`. Since /v1/audio/speech emits audio/opus for
`response_format=opus`, clients re-uploading their own voice notes hit
this on every round trip.

A `.opus` file is Opus in an Ogg container (RFC 7845), so `.ogg` is a
truthful relabel and is already on the allow-list. Rewrite the extension
in getUploadedFileName, the single choke point feeding
buildMultipartBody.

The OpenRouter STT path had the same root cause with a quieter symptom:
`.opus` matched neither its extension list nor its MIME map, so it fell
through to the "wav" default and announced Opus bytes as WAV. Map both
the extension and audio/opus to its already-supported ogg container.

Fixes cheaper-inference#10588
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(audio): /v1/audio/transcriptions rejects .opus filenames that are valid Ogg/Opus

2 participants