Skip to content

STT API (microsoft/mai-transcribe-2, Azure): maxSpeakers rejected, m4a format rejected on JSON path, provider options dropped on multipart path #576

Description

@vinzenzu

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:

  1. 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.
  2. "input_audio": {"format": "m4a"} (AAC bytes) → 400 on the JSON path, while identical audio as mp3/wav → 200. Docs list m4a as supported.
  3. 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.
  4. 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}
Image

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions