Docs page URL
https://openrouter.ai/docs/guides/overview/multimodal/stt
API endpoint
/api/v1/audio/transcriptions
Description
Model: microsoft/mai-transcribe-2 (Azure endpoint)
Findings, all reproducible with short slices:
- Base64 JSON path with
"provider": {"options": {"azure": {"diarization": {"enabled": true, "maxSpeakers": 2}}}} → 400 "Provider returned 400". Same request without maxSpeakers → 200 with working diarization (speaker labels on segments). So maxSpeakers is rejected even though Azure's Fast Transcription API documents it.
"input_audio": {"format": "m4a"} (AAC bytes) → 400 on the JSON path, while identical audio as mp3/wav → 200. Docs list m4a as supported.
- OpenAI-style multipart requests return 200 but silently drop "provider" options (tried "provider" as a JSON form field and Azure's native "definition" field) - diarization is unreachable via multipart.
- Masked error: all failures surface as
{"error":{"message":"Provider returned 400","code":400}} with no detail, which makes provider-option debugging hard.
Requested fix: honor maxSpeakers under provider.options.azure.diarization, accept m4a on the JSON path (docs list it), and forward provider options on multipart.
Every provider 4xx surfaced as "Provider returned 400" with no upstream detail. Could you pass through the upstream body?
Steps to reproduce
Save the below Python script, set OPENROUTER_TRANSCRIBE_API_KEY, then
ffmpeg -y -i "<PATH TO FILE>" -t 10 -b:a 64k /tmp/repro.mp3
ffmpeg -y -i "<PATH TO FILE>" -t 10 /tmp/repro.m4a
uv run testing.py /tmp/repro.mp3 /tmp/repro.m4a
"""Repro: OpenRouter STT quirks on microsoft/mai-transcribe-2 (Azure).
Usage: uv run testing.py <file.mp3> <same-audio.m4a>
Case 1: same audio as m4a -> 400, as mp3 -> 200 (format rejection).
Case 2: diarization enabled -> 200 with speaker labels;
adding maxSpeakers -> 400.
Case 3: multipart path returns 200 but drops provider options.
"""
import base64, os, sys
from pathlib import Path
import json
import requests
# load key from project .env
for line in Path(__file__).parent.joinpath(".env").read_text().splitlines():
line = line.strip()
if line and not line.startswith("#") and "=" in line:
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_TRANSCRIBE_API_KEY":
os.environ.setdefault(name.strip(), value.strip().strip("\"'"))
if "OPENROUTER_TRANSCRIBE_API_KEY" not in os.environ:
sys.exit("error: OPENROUTER_TRANSCRIBE_API_KEY not found in .env")
URL = "https://openrouter.ai/api/v1/audio/transcriptions"
MODEL = "microsoft/mai-transcribe-2"
HEADERS = {"Authorization": f"Bearer {os.environ['OPENROUTER_TRANSCRIBE_API_KEY']}"}
if len(sys.argv) != 3:
sys.exit("usage: uv run testing.py <file.mp3> <same-audio.m4a>")
mp3 = Path(sys.argv[1]).read_bytes() # 10 s of speech is enough
m4a = Path(sys.argv[2]).read_bytes()
b64_mp3 = base64.b64encode(mp3).decode()
b64_m4a = base64.b64encode(m4a).decode()
def post_json(payload):
r = requests.post(URL, headers=HEADERS, json=payload, timeout=120)
print(r.status_code, r.text if not r.ok else json.dumps(r.json(), indent=2))
return r
# Case 1: m4a rejected on the JSON path, mp3 accepted (same audio)
print("---Case 1---")
post_json({"model": MODEL, "input_audio": {"data": b64_m4a, "format": "m4a"}}) # -> 400
post_json({"model": MODEL, "input_audio": {"data": b64_mp3, "format": "mp3"}}) # -> 200
# Case 2a: diarization enabled works, segments carry "speaker"
print("---Case 2a---")
r = post_json({"model": MODEL, "input_audio": {"data": b64_mp3, "format": "mp3"},
"response_format": "verbose_json",
"timestamp_granularities": ["segment"],
"provider": {"options": {"azure": {"diarization": {"enabled": True}}}}})
assert r.ok and any("speaker" in s for s in r.json()["segments"]) # -> 200
# Case 2b: same + maxSpeakers -> 400 (Azure Fast Transcription documents it)
print("---Case 2b---")
post_json({"model": MODEL, "input_audio": {"data": b64_mp3, "format": "mp3"},
"response_format": "verbose_json",
"timestamp_granularities": ["segment"],
"provider": {"options": {"azure": {"diarization": {
"enabled": True, "maxSpeakers": 2}}}}}) # -> 400
# Case 3: multipart returns 200 but no speaker labels (options dropped)
print("---Case 3---")
r = requests.post(URL, headers=HEADERS,
files={"file": ("a.mp3", mp3, "audio/mpeg")},
data={"model": MODEL, "response_format": "verbose_json",
"provider": '{"options": {"azure": {"diarization": {"enabled": true}}}}'},
timeout=120)
segs = r.json().get("segments", []) if r.ok else []
print(r.status_code, "speakers:", {s.get("speaker") for s in segs}) # -> 200, {None}

Docs page URL
https://openrouter.ai/docs/guides/overview/multimodal/stt
API endpoint
/api/v1/audio/transcriptions
Description
Model: microsoft/mai-transcribe-2 (Azure endpoint)
Findings, all reproducible with short slices:
"provider": {"options": {"azure": {"diarization": {"enabled": true, "maxSpeakers": 2}}}}→ 400 "Provider returned 400". Same request withoutmaxSpeakers→ 200 with working diarization (speaker labels on segments). SomaxSpeakersis rejected even though Azure's Fast Transcription API documents it."input_audio": {"format": "m4a"}(AAC bytes) → 400 on the JSON path, while identical audio as mp3/wav → 200. Docs list m4a as supported.{"error":{"message":"Provider returned 400","code":400}}with no detail, which makes provider-option debugging hard.Requested fix: honor
maxSpeakersunder provider.options.azure.diarization, accept m4a on the JSON path (docs list it), and forward provider options on multipart.Every provider 4xx surfaced as "Provider returned 400" with no upstream detail. Could you pass through the upstream body?
Steps to reproduce
Save the below Python script, set
OPENROUTER_TRANSCRIBE_API_KEY, then