#10933·zeroclaw

[Feature]: Add MiniMax TTS and STT provider families (T2A v2 + speech_to_text)

Author: johnlam1968Created Sep 17, 2026Updated Sep 17, 2026
Labelsenhancementconfigproviderprovider:minimaxpriority:p2needs-maintainer-reviewrisk:medium

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>
json
{
  "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_boost enum: auto, English, Chinese, Chinese,Yue, Japanese, Korean, Spanish, French, Portuguese, German, Italian, Arabic, Russian, Indonesian, Thai, Vietnamese
  • emotion enum: auto, happy, sad, angry, fearful, disgusted, surprised, calm, fluent, neutral
  • output_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_code is 200-with-error-code: 1004 auth, 2013 invalid params, 2049 wrong-region key, 2054 unknown voice id, 2056 quota 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 gap

multipart 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_json adds segments, speaker diarization and timestamps; srt/vtt export 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:

toml
[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 HEADER

Minimum 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 from data.audio; it must also check base_resp.status_code != 0 before trusting data.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_headers on OpenAiTranscriptionProviderConfig and OpenAITtsProviderConfig (model providers already have it). That alone would let a user set language: yue against 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.minimax chat 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_headers only. Much smaller, unblocks STT and gets most of the value. Does not address TTS's language_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, matching providers.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 != 0 as an error rather than decoding a bogus data.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.