[Feature]: Add MiniMax TTS and STT provider families (T2A v2 + speech_to_text)
Summary
Add first-class MiniMax families for TTS ([providers.tts.minimax.<alias>]) and STT ([providers.transcription.minimax.<alias>]), matching the endpoint = "cn" | "intl" handling that providers.models.minimax already implements.
Problem statement
MiniMax is already a supported chat provider (zeroclaw providers lists minimax; MinimaxModelProviderConfig carries endpoint: cn | intl and a uri override). It has no TTS or STT family:
- TTS families today:
openai,elevenlabs,google,edge,piper - STT families today:
groq,openai,deepgram,assemblyai,google,local_whisper
So anyone using MiniMax speech — a vendor with 300+ system voices across 40 languages, including explicit Cantonese support — must run an out-of-tree HTTP proxy that impersonates an OpenAI endpoint and translates the wire format. That is what I am doing today, and it is a lot of surface for what should be a provider entry.
Two concrete consequences of the gap, both hit in practice:
1. Cantonese TTS is unreachable through the OpenAI shim. MiniMax only produces Cantonese phonology when the request carries language_boost: "Chinese,Yue". There is no OpenAI field that maps to it, so the shim must invent one and infer it from the voice id. Worse, MiniMax's legacy endpoint (POST /v1/text_to_speech, flat body) silently returns Mandarin for the same voice + boost combination, so a shim that guesses the endpoint wrong produces plausible-sounding wrong language with no error. Getting this right is provider knowledge, not something a generic OpenAI-compatible client should have to encode.
2. Cantonese STT cannot be requested at all. MiniMax's language is a request header (zh / yue / en / ja / …), not a body field — and neither OpenAiTranscriptionProviderConfig nor OpenAITtsProviderConfig exposes an extra_headers field (model providers do: AnthropicModelProviderConfig has one). So yue is unsettable through the existing families, and the transcription silently falls back to auto-detect. For a Telegram bot receiving Cantonese voice notes this is the difference between a correct transcript and a Mandarin one.
MiniMax API surfaces (for reference)
TTS — synchronous T2A
POST https://api.minimaxi.com/v1/t2a_v2 # CN
POST https://api.minimax.io/v1/t2a_v2 # intl
Authorization: Bearer <token>{
"model": "speech-2.8-hd",
"text": "…",
"stream": false,
"voice_setting": { "voice_id": "…", "speed": 0.95, "vol": 1, "pitch": 0, "emotion": "happy" },
"audio_setting": { "sample_rate": 32000, "bitrate": 128000, "format": "mp3", "channel": 1 },
"output_format": "hex",
"language_boost": "Chinese,Yue"
}- Models:
speech-2.8-hd,speech-2.8-turbo,speech-2.6-hd,speech-2.6-turbo,speech-02-hd,speech-02-turbo language_boostenum:auto,English,Chinese,Chinese,Yue,Japanese,Korean,Spanish,French,Portuguese,German,Italian,Arabic,Russian,Indonesian,Thai,Vietnameseemotionenum:auto,happy,sad,angry,fearful,disgusted,surprised,calm,fluent,neutraloutput_format: "hex"→ audio is hex-encoded inside JSON, not raw bytes- Response:
{"data":{"audio":"<hex>","status":2},"extra_info":{…},"trace_id":"…","base_resp":{"status_code":0,"status_msg":"success"}} base_resp.status_codeis 200-with-error-code:1004auth,2013invalid params,2049wrong-region key,2054unknown voice id,2056quota exhausted- Also offers WebSocket streaming, async long-form (up to 1M chars), voice cloning and voice design
STT — automatic speech recognition
POST https://api.minimaxi.com/v1/speech_to_text # CN
POST https://api.minimax.io/v1/speech_to_text # intl
Authorization: Bearer <token>
Content-Type: multipart/form-data
language: yue # REQUEST HEADER — this is the gapmultipart fields: model=asr-1.0, file, response_format (json | verbose_json | srt | vtt), timestamp_level (sentence | word), stream
- Response:
{"text":"…","duration":26.325,"trace_id":"…"} verbose_jsonadds segments, speaker diarization and timestamps;srt/vttexport subtitles- Limits: 50 MB / 500 s; containerised audio only (raw PCM rejected)
- Language header values:
zh,yue,en,ja,ko,th,vi,id,ms,fil,ar,tr,fr,de,es,it,pt; omit for mixed-language auto-detect
Endpoints. api.minimaxi.com (CN) and api.minimax.io (intl) are separate services with separate credentials. A CN key against .io returns 2049 invalid api key, not a redirect. The existing model provider already models this as endpoint: cn | intl, which is exactly the right shape to reuse.
Proposed solution
Two new typed families, following the existing MinimaxModelProviderConfig conventions:
[providers.tts.minimax.default]
endpoint = "cn" # cn | intl
api_key = "…"
model = "speech-2.8-hd"
voice = "Cantonese_CuteGirl"
language_boost = "Chinese,Yue"
emotion = "happy"
[providers.transcription.minimax.default]
endpoint = "cn"
api_key = "…"
model = "asr-1.0"
language = "yue" # sent as the `language` REQUEST HEADERMinimum viable field set:
- TTS:
endpoint,api_key,uri,model,voice,language_boost,emotion,speed,vol,pitch,sample_rate,bitrate,format,channel - STT:
endpoint,api_key,uri,model,language(header),response_format,timestamp_level
Implementation notes:
- The TTS provider must send
output_format: "hex"and decode hex fromdata.audio; it must also checkbase_resp.status_code != 0before trustingdata.audio, because the API returns HTTP 200 with an error payload. - Refuse
POST /v1/text_to_speech(the legacy flat-body endpoint) — it silently yields Mandarin for Cantonese voices. - If full families are too much for v1, the smaller unblocking change is
extra_headersonOpenAiTranscriptionProviderConfigandOpenAITtsProviderConfig(model providers already have it). That alone would let a user setlanguage: yueagainst the OpenAI-compatible path and get correct Cantonese STT, leaving only TTS needing a shim.
Non-goals / out of scope
- Voice cloning and voice design endpoints.
- Async long-form T2A (
task_id+ File API download). - WebSocket streaming T2A.
- Changing the existing
providers.models.minimaxchat provider.
Alternatives considered
- Keep using an HTTP proxy. This is the current state. It works, but every user who wants MiniMax speech has to re-derive the same translation, and the Cantonese-language behaviour is easy to get wrong in ways that produce plausible output rather than an error.
extra_headersonly. Much smaller, unblocks STT and gets most of the value. Does not address TTS'slanguage_boost/ nested body / hex response.- Route MiniMax through OpenRouter or a similar aggregator. Possible for TTS on some aggregators, but loses the vendor-specific fields (
language_boost,emotion) and adds a third party.
Acceptance criteria
[providers.tts.minimax.<alias>]and[providers.transcription.minimax.<alias>]are documented and schema-validated.endpoint = "cn" | "intl"selects the host, matchingproviders.models.minimax.- A Cantonese voice with
language_boost = "Chinese,Yue"produces Cantonese audio; a regression test asserts the request body carries the field. - STT with
language = "yue"sends it as a request header and returns Cantonese text. - TTS surfaces
base_resp.status_code != 0as an error rather than decoding a bogusdata.audio. - No behaviour change for existing TTS/STT families.
Architecture impact
crates/zeroclaw-providers/src/ (new MinimaxTtsProvider / MinimaxTranscriptionProvider), crates/zeroclaw-config/src/schema.rs (two config structs + family slots), crates/zeroclaw-channels/src/tts.rs and transcription.rs (registration), docs under docs/book/src/.
Risk and rollback
Risk is low: new opt-in families with no effect on existing ones. Rollback is removing the provider entry. The main correctness risk is the base_resp.status_code trap (HTTP 200 with an error body), which the acceptance criteria cover explicitly.
Expected routing
Ordinary feature triage
Next decision surface
Ordinary triage is enough. One scoping question: ship both families at once, or land extra_headers first (small, unblocks Cantonese STT) and follow with the TTS family.
Breaking change?
No
Data hygiene checks
- I removed personal/sensitive data from examples, payloads, and logs.
- I used neutral, project-focused wording and placeholders.
Additional context
I maintain an out-of-tree proxy doing this translation for a Telegram voice bot (Cantonese in, Cantonese out). Two bugs found while building it are worth noting because a native provider would not have them: MiniMax returns HTTP 200 with base_resp.status_code != 0 on failure (2056 Token Plan quota exhausted was being decoded as audio), and the legacy /v1/text_to_speech endpoint silently produces Mandarin for Cantonese voices. Both are provider-specific traps that argue for first-class support rather than a generic shim.
Source: zeroclaw-labs/zeroclaw