Skip to main content
Everything under https://api.fish.audio/compat follows the contract on this page.

How parameters are handled

Unsupported options return an explicit error — never a silent no-op. Every parameter is either mapped to the native equivalent (stability → temperature, speed → speech rate, bitrates snap to a tier), ignored when it doesn’t affect synthesis (similarity_boost, style, seed), or refused with a 4xx in the protocol’s own error format when honoring it would change the response content or structure — full list. Unknown JSON fields are ignored, matching OpenAI behavior.

What each protocol supports

Two protocols: OpenAI at /v1 (equivalently /api/v1 and /openai/v1 — the OpenAI, OpenRouter, and Groq SDKs all speak it), and ElevenLabs at /elevenlabs. Voices are always Fish voice IDs — empty or default selects the model’s default voice, and anything that isn’t a real voice ID, preset names (nova, echo, Rachel, …) included, is a 400. One exception: lowercase alloy matches a real Fish voice and synthesizes — an unrelated voice, not OpenAI’s Alloy (Alloy is refused). Where to find voice IDs: Voices.

Audio formats

Requesting a sample rate resamples the audio; a rate the codec doesn’t take is a 400, including all of 22.05 kHz (mp3_22050_32, pcm_22050, …). On REST, OpenAI pcm16 defaults to 24 kHz and pcm to 44.1 kHz; an explicit sample_rate overrides that. μ-law and A-law (ulaw_*, alaw_*, audio/pcmu, audio/pcma) are refused everywhere. How formats are written on the Realtime socket, and when rates reset, is in the Realtime protocol reference.
WAV responses carry a placeholder header — trust the byte count, not the header. The RIFF size field reads 4294967076 and the data chunk size 4294967040 however short the clip is, on buffered responses and streams alike. Anything that reads the header gets the duration wrong — Python’s wave module reports a one-second clip as 13.5 hours; players that decode to end-of-stream are unaffected. For a correct duration use the response length, re-mux (ffmpeg -i in.wav -c copy out.wav), or request MP3 or Opus.

Bitrate snapping

MP3 is encoded at 64, 128, or 192 kbps. An out-of-tier request snaps up to the nearest tier (mp3_32000_32 → 64 kbps at 32 kHz); anything above 192 kbps is a 400. To pick the tier exactly, set it as a Fish-native parameter — a non-tier value there is refused too. Opus bitrate is automatic: requested values are accepted and ignored.

What is explicitly refused

Each returns a 4xx in the protocol’s own error format — or, on WebSockets, an in-band error event: On OpenAI transcription, languages, keywords, prompt, and temperature are in the ignored tier. Booleans count as requested only when true: false, "False", 0, none, and off are treated as unset, so SDK defaults never trigger a refusal.

Placeholder values

Protocol fields with no Fish counterpart are filled with neutral constants — don’t build logic on them:
  • OpenAI verbose_json: the Whisper-engine internals (avg_logprob, no_speech_prob, compression_ratio, temperature, seek, tokens) are neutral constants.
  • ElevenLabs STT language_probability is always 1.0, and words[].logprob is always 0. Neither is a confidence score.
  • speaker_id is always null; requesting diarization returns an explicit error instead.
  • language in responses echoes your request — it is never a detection result. Send none and you get language: "" alongside language_probability: 1.0.

Reaching Fish-native parameters

The full native TTS parameter set — temperature, top_p, chunk_length, latency, normalize, prosody, reference audio — is reachable on synthesis endpoints, in two ways. One exception: reference audio is not available on the timestamped ElevenLabs endpoints (…/with-timestamps, …/stream/with-timestamps, the stream-input WebSocket) — a 400 on the REST variants, an in-band error frame at flush time on the WebSocket.

provider.options in the request body

On OpenAI-protocol speech and chat-synthesis requests, a provider.options.fish-audio block sets the corresponding native TTS fields:
Inline references — base64 audio plus its transcript — enable zero-shot voice cloning through any OpenAI SDK.

X-Fish-* request headers

On every synthesis endpoint — OpenAI POST /v1/audio/speech and chat synthesis, the ElevenLabs endpoints, and OpenAI Realtime synthesis sessions, including their WebSocket handshakes — an X-Fish-<Param> header sets the corresponding native TTS field:
Values parse as JSON when possible, and headers win over mapped values. Neither channel applies to transcription: every STT endpoint returns a 400 when sent provider.options.fish-audio or X-Fish-*. That includes a Realtime socket whose handshake carried X-Fish-* headers — the input_audio_buffer.commit there is refused with unsupported_native_options, so open a second socket without the headers to transcribe. On the native /v1/tts/live WebSocket, put native fields in each start.request payload rather than in handshake headers.
Synthesis on the Compatible APIs defaults to latency: "low" (streaming-optimized). For maximum quality on non-interactive workloads, set X-Fish-Latency: normal — your explicit value always wins.

Limits and operational contract

The 32 MiB frame limit bounds one WebSocket message, not a session. On a Realtime socket, item-borne audio and input_audio_buffer bytes share the same 100 MiB budget.
  • Billing — like native usage, against your key. Billable requests are never retried automatically; retrying is your call.
  • Rate limits — the Compatible APIs add none of their own today; a 429 means you reached your account’s concurrency limit, the same one native calls count against. Every 429 carries a Retry-After header.
  • Truncation is an error — a stream that fails mid-response returns an error frame or 502, never a truncated 200.
  • Every synthesis or transcription response carries an X-Generation-Id header — include it in support requests.
  • During service incidents, requests may return 503; back off exponentially.

Known client-side limitations

  • OpenAI Realtime: the endpoint speaks the GA event names (response.output_audio.*). Current openai packages emit these; very old beta-era clients expecting response.audio.* will not match.
  • ElevenLabs Python convert_realtime: pass model_id and voice_settings explicitly — left to the SDK’s OMIT sentinel, model_id reaches the handshake URL as the literal model_id=Ellipsis (a 400) and voice_settings raises AttributeError on the first frame. The SDK also force-upgrades http:// base URLs to wss://.