feat: add MiniMax TTS provider - #39
Conversation
Reviewer's GuideAdds a MiniMax text-to-speech provider with regional (global/China) routing, HTTP sync, async task, and WebSocket streaming synthesis, wires it into the speech runtime, resolver, and provider catalog, and covers behavior with targeted tests. Sequence diagram for MiniMax WebSocket TTS synthesissequenceDiagram
actor Client
participant SpeechRuntime
participant MiniMaxTextToSpeechProvider
participant MiniMaxWebSocket
Client->>SpeechRuntime: synthesizeWebSocket(text, options)
SpeechRuntime->>MiniMaxTextToSpeechProvider: synthesizeWebSocket(text, options)
MiniMaxTextToSpeechProvider->>MiniMaxWebSocket: new WebSocket(socketUrl, headers)
MiniMaxWebSocket-->>MiniMaxTextToSpeechProvider: event connected_success
MiniMaxTextToSpeechProvider->>MiniMaxWebSocket: send task_start(buildRequest(...))
MiniMaxWebSocket-->>MiniMaxTextToSpeechProvider: event task_started
MiniMaxTextToSpeechProvider->>MiniMaxWebSocket: send task_continue(text)
MiniMaxTextToSpeechProvider->>MiniMaxWebSocket: send task_finish()
MiniMaxWebSocket-->>MiniMaxTextToSpeechProvider: event task_continued(audio hex)
MiniMaxTextToSpeechProvider->>MiniMaxTextToSpeechProvider: decodeHexAudio(audio)
MiniMaxWebSocket-->>MiniMaxTextToSpeechProvider: event task_finished
MiniMaxTextToSpeechProvider->>MiniMaxTextToSpeechProvider: result(Buffer.concat(chunks), text, options, audioFormat)
MiniMaxTextToSpeechProvider-->>SpeechRuntime: SpeechSynthesisResult
SpeechRuntime-->>Client: SpeechSynthesisResult
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
📝 WalkthroughWalkthroughAdded the MiniMax text-to-speech provider with synchronous HTTP, asynchronous task, and WebSocket synthesis. Added regional configuration, audio decoding, runtime registration, catalog metadata, public exports, and Vitest coverage. ChangesMiniMax TTS integration
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant SpeechRuntime
participant MiniMaxTextToSpeechProvider
participant MiniMaxHTTPAPI
participant MiniMaxWebSocket
SpeechRuntime->>MiniMaxTextToSpeechProvider: register configured provider
MiniMaxTextToSpeechProvider->>MiniMaxHTTPAPI: submit HTTP synthesis request
MiniMaxHTTPAPI-->>MiniMaxTextToSpeechProvider: return audio or task response
MiniMaxTextToSpeechProvider->>MiniMaxWebSocket: open streaming synthesis session
MiniMaxWebSocket-->>MiniMaxTextToSpeechProvider: send task events and audio chunks
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Summary by QodoAdd MiniMax TTS provider with global/china routing, async, and WebSocket support
AI Description
Diagram
High-Level Assessment
Files changed (8)
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The
MINIMAX_SPEECH_MODELSconstant is currently unused; consider either validatingoptions.model/config.modelagainst this list or removing it to avoid dead code. - The
audioFormatresolution inaudioFormat()usesoptions.outputFormat, which elsewhere represents HTTP/WebSocketoutput_format(hex vs url); consider separating transport output format from container/audio format to avoid semantic confusion and misconfiguration.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `MINIMAX_SPEECH_MODELS` constant is currently unused; consider either validating `options.model`/`config.model` against this list or removing it to avoid dead code.
- The `audioFormat` resolution in `audioFormat()` uses `options.outputFormat`, which elsewhere represents HTTP/WebSocket `output_format` (hex vs url); consider separating transport output format from container/audio format to avoid semantic confusion and misconfiguration.
## Individual Comments
### Comment 1
<location path="src/io/speech/providers/MiniMaxTextToSpeechProvider.ts" line_range="183-192" />
<code_context>
+ if (
</code_context>
<issue_to_address>
**suggestion:** Expose more detailed error information for WebSocket task failures.
For the `task_failed` event, the error is always reported as "MiniMax WebSocket synthesis failed." even though `base_resp` likely contains a `status_msg` (and possibly an error code) similar to the HTTP responses. Consider exposing these details so WebSocket errors are easier to debug and consistent with the HTTP error handling.
Suggested implementation:
```typescript
if (
message.base_resp?.status_code !== undefined &&
message.base_resp.status_code !== 0
) {
const { status_code, status_msg } = message.base_resp;
const errorMessage = status_msg
? `MiniMax WebSocket synthesis failed: [${status_code}] ${status_msg}`
: `MiniMax WebSocket synthesis failed with status code ${status_code}.`;
const error = new Error(errorMessage);
// Attach raw response details to the error object for easier debugging
(error as any).status_code = status_code;
(error as any).status_msg = status_msg;
(error as any).base_resp = message.base_resp;
fail(error);
return;
```
To fully align WebSocket error handling with HTTP error handling:
1. If you have a dedicated MiniMax error type (e.g. `MiniMaxError` used for HTTP responses), consider constructing that instead of a plain `Error` and moving the status_code/status_msg/base_resp attachment into that class.
2. Ensure any upstream error handling that currently assumes a generic `Error` can also interpret the attached `status_code`, `status_msg`, or `base_resp` fields (or the dedicated error type) for richer logging or user-facing messages.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| if ( | ||
| message.base_resp?.status_code && | ||
| message.base_resp.status_code !== 0 | ||
| ) { | ||
| fail( | ||
| new Error( | ||
| message.base_resp.status_msg ?? | ||
| "MiniMax WebSocket synthesis failed.", | ||
| ), | ||
| ); |
There was a problem hiding this comment.
suggestion: Expose more detailed error information for WebSocket task failures.
For the task_failed event, the error is always reported as "MiniMax WebSocket synthesis failed." even though base_resp likely contains a status_msg (and possibly an error code) similar to the HTTP responses. Consider exposing these details so WebSocket errors are easier to debug and consistent with the HTTP error handling.
Suggested implementation:
if (
message.base_resp?.status_code !== undefined &&
message.base_resp.status_code !== 0
) {
const { status_code, status_msg } = message.base_resp;
const errorMessage = status_msg
? `MiniMax WebSocket synthesis failed: [${status_code}] ${status_msg}`
: `MiniMax WebSocket synthesis failed with status code ${status_code}.`;
const error = new Error(errorMessage);
// Attach raw response details to the error object for easier debugging
(error as any).status_code = status_code;
(error as any).status_msg = status_msg;
(error as any).base_resp = message.base_resp;
fail(error);
return;To fully align WebSocket error handling with HTTP error handling:
- If you have a dedicated MiniMax error type (e.g.
MiniMaxErrorused for HTTP responses), consider constructing that instead of a plainErrorand moving the status_code/status_msg/base_resp attachment into that class. - Ensure any upstream error handling that currently assumes a generic
Errorcan also interpret the attachedstatus_code,status_msg, orbase_respfields (or the dedicated error type) for richer logging or user-facing messages.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 060833edad
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
|
|
||
| async queryAsync(taskId: string): Promise<MiniMaxAsyncSpeechResponse> { | ||
| return this.request("/v1/query/t2a_async_query_v2", { task_id: taskId }); |
There was a problem hiding this comment.
Query async task status with GET
For real MiniMax async status checks, this sends a JSON POST, but the official API documents GET /v1/query/t2a_async_query_v2 with task_id as a required query parameter (see https://platform.minimax.io/docs/api-reference/speech-t2a-async-query). As written, queryAsync() will not poll submitted tasks against the documented endpoint even though the unit test accepts the mocked POST.
Useful? React with 👍 / 👎.
| message.event === "task_continued" && | ||
| message.data?.audio |
There was a problem hiding this comment.
Preserve documented audio chunks in WebSocket synthesis
When using the real WebSocket API, MiniMax's documented Task Continued receive payload contains data.audio/is_final but does not include an event field (https://platform.minimax.io/docs/api-reference/speech-t2a-websocket), so this branch ignores the audio chunks unless the server sends the extra field used by the test mock. In that scenario synthesizeWebSocket() resolves with an empty or incomplete buffer after task_finished.
Useful? React with 👍 / 👎.
| requested !== "mp3" && | ||
| requested !== "wav" && | ||
| requested !== "flac" && | ||
| requested !== "pcm" | ||
| ) { |
There was a problem hiding this comment.
Reject PCM before sending MiniMax requests
This validation advertises and forwards pcm, but MiniMax's T2A docs list non-streaming output as mp3, wav, or flac and WebSocket streaming as mp3 only (https://platform.minimax.io/docs/api-reference/speech-t2a-http). A provider-agnostic caller that requests outputFormat: 'pcm' will get a remote parameter error from this provider instead of a correct local rejection or fallback format.
Useful? React with 👍 / 👎.
Code Review by Qodo
1. Unvalidated audio URL fetch
|
| const audioBuffer = | ||
| outputFormat === "url" | ||
| ? Buffer.from(await (await this.fetchImpl(audio)).arrayBuffer()) | ||
| : decodeHexAudio(audio); |
There was a problem hiding this comment.
1. Unvalidated audio url fetch 🐞 Bug ⛨ Security
MiniMaxTextToSpeechProvider.synthesize() will fetch and download whatever URL comes back in data.audio when outputFormat === "url" without validating the URL or checking the download response status, which can lead to unintended server-side requests and hard-to-debug failures. This is triggered by provider-specific options (outputFormat: "url") and is new behavior in this PR.
Agent Prompt
## Issue description
When `providerSpecificOptions.outputFormat` is set to `"url"`, `MiniMaxTextToSpeechProvider.synthesize()` performs a second `fetch()` to download audio bytes from `response.data.audio` without validating the URL (scheme/host/private IPs/redirects) and without checking the download response status (`ok`). This can cause unintended outbound requests (SSRF-style behavior if the upstream response is untrusted/compromised) and yields confusing runtime errors when the download fails.
## Issue Context
- The secondary fetch target is controlled by the provider response (`data.audio`).
- `fetch()` follows redirects by default; even if you validate the initial URL, redirects can change the final destination unless constrained.
## Fix Focus Areas
- src/io/speech/providers/MiniMaxTextToSpeechProvider.ts[106-131]
- src/io/speech/providers/MiniMaxTextToSpeechProvider.ts[35-57]
## Suggested fix
- Parse `audio` via `new URL(audio)` and reject non-HTTPS URLs.
- Consider restricting to an allowlist (e.g., known MiniMax/CDN domains) and/or explicitly blocking localhost/private network destinations.
- Set `redirect: 'error'` (or `manual`) for the download fetch, or validate the final resolved URL before reading bytes.
- Check the download response (`if (!res.ok) throw ...`) before calling `arrayBuffer()` and include status/body snippet in the error for debuggability.
- (Optional) enforce a maximum download size to avoid unbounded memory use.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| return new Promise<SpeechSynthesisResult>((resolve, reject) => { | ||
| const chunks: Buffer[] = []; | ||
| let settled = false; | ||
| const fail = (error: Error) => { |
There was a problem hiding this comment.
2. Websocket synthesize can hang 🐞 Bug ☼ Reliability
MiniMaxTextToSpeechProvider.synthesizeWebSocket() has no handshake/overall timeout, so if the socket stays open but the server never sends task_finished/task_failed, the returned Promise never settles and callers can hang indefinitely. This can also leak the socket and event handlers until process shutdown.
Agent Prompt
## Issue description
`synthesizeWebSocket()` returns a Promise that only resolves/rejects on specific MiniMax events (`task_finished`, `task_failed`) or socket `error/close`. Without any timeout (connect, inactivity, or overall), the Promise can remain pending forever if the upstream stalls while keeping the connection open.
## Issue Context
Other WebSocket-based implementations in this repo add explicit timers to prevent “zombie” sessions (e.g., connect timeouts and handling of rejected upgrades).
## Fix Focus Areas
- src/io/speech/providers/MiniMaxTextToSpeechProvider.ts[153-231]
- src/io/voice-pipeline/providers/DeepgramAuraStreamingTTS.ts[160-205]
## Suggested fix
- Add a connect/handshake timeout (e.g., fail if `connected_success` isn’t received within N ms).
- Add an overall or inactivity timeout that resets on each valid message; if it fires, close the socket and reject with a clear error.
- Ensure timers are cleared on settle.
- (Optional, but recommended) handle `unexpected-response` from `ws` to surface HTTP rejection details during upgrade (pattern used in DeepgramAuraStreamingTTS).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/io/speech/providers/MiniMaxTextToSpeechProvider.ts`:
- Around line 126-129: Update the URL branch in MiniMaxTextToSpeechProvider to
store the fetch response, validate its ok status before reading arrayBuffer(),
and handle unsuccessful downloads through the existing error path. Extend the
URL-output tests to cover both successful and non-successful response cases.
- Around line 149-150: Update MiniMaxTextToSpeechProvider.ts lines 149-150 in
queryAsync to call the request helper with GET, passing task_id as a query
parameter and no request body. Update the request implementation at lines
282-294 to support bodyless GET requests while preserving existing request
behavior, and update MiniMaxTextToSpeechProvider.test.ts lines 127-139 to assert
the GET method, query string, and absent body.
In `@src/io/speech/SpeechProviderResolver.ts`:
- Line 450: Update the resolver refresh logic around the MiniMax core entry in
SpeechProviderResolver so registering { id: 'minimax-tts', kind: 'tts' } cannot
overwrite an existing non-null provider with null; preserve the live
registration or supply the MiniMax provider factory before this entry is added.
Add a regression test that constructs SpeechRuntime, calls refresh(), and
verifies resolveTTS() still returns the registered MiniMax provider.
In `@src/io/speech/SpeechRuntime.ts`:
- Line 99: Update the region mapping in SpeechRuntime to normalize
MINIMAX_REGION and map both “cn” and “china” to the China endpoint, while
mapping only the supported global value to “global.” Reject unsupported or
missing values instead of silently defaulting to “global.”
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e7fd3bb4-8ea6-4d47-b0e6-cb71344f4f4b
📒 Files selected for processing (8)
src/io/speech/SpeechProviderResolver.tssrc/io/speech/SpeechRuntime.tssrc/io/speech/__tests__/MiniMaxTextToSpeechProvider.test.tssrc/io/speech/__tests__/SpeechRuntime.test.tssrc/io/speech/__tests__/providerCatalog.test.tssrc/io/speech/index.tssrc/io/speech/providerCatalog.tssrc/io/speech/providers/MiniMaxTextToSpeechProvider.ts
| const audioBuffer = | ||
| outputFormat === "url" | ||
| ? Buffer.from(await (await this.fetchImpl(audio)).arrayBuffer()) | ||
| : decodeHexAudio(audio); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- provider structure ---'
ast-grep outline src/io/speech/providers/MiniMaxTextToSpeechProvider.ts
printf '%s\n' '--- relevant provider code ---'
cat -n src/io/speech/providers/MiniMaxTextToSpeechProvider.ts | sed -n '1,190p'
printf '%s\n' '--- related tests ---'
rg -n -i -C 5 'MiniMax|outputFormat|arrayBuffer|download' . -g '*test*' -g '*spec*'Repository: framerslab/agentos
Length of output: 50375
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- MiniMax-related files ---'
fd -i 'minimax' .
printf '%s\n' '--- direct provider references ---'
rg -n -i -C 4 'MiniMaxTextToSpeechProvider|MiniMax TTS|t2a_v2' src tests --glob '!**/node_modules/**' --glob '!**/dist/**' | head -n 240Repository: framerslab/agentos
Length of output: 15721
Validate the URL audio download response.
If the download response is not successful, check downloadResponse.ok before reading arrayBuffer(). Add successful and non-success response cases to the URL-output tests.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/io/speech/providers/MiniMaxTextToSpeechProvider.ts` around lines 126 -
129, Update the URL branch in MiniMaxTextToSpeechProvider to store the fetch
response, validate its ok status before reading arrayBuffer(), and handle
unsuccessful downloads through the existing error path. Extend the URL-output
tests to cover both successful and non-successful response cases.
| async queryAsync(taskId: string): Promise<MiniMaxAsyncSpeechResponse> { | ||
| return this.request("/v1/query/t2a_async_query_v2", { task_id: taskId }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- provider symbols ---'
rg -n -C 12 'queryAsync|private async request|t2a_async_query_v2' src/io/speech/providers/MiniMaxTextToSpeechProvider.ts
printf '%s\n' '--- related tests ---'
rg -n -C 18 'queryAsync|t2a_async_query_v2|fetchImpl.mock.calls' src/io/speech/__tests__/MiniMaxTextToSpeechProvider.test.ts
printf '%s\n' '--- MiniMax endpoint references ---'
rg -n -i -C 3 't2a_async_query_v2|async.?query|task_id' src/io/speech README.md package.json 2>/dev/null || trueRepository: framerslab/agentos
Length of output: 10502
🌐 Web query:
MiniMax platform API speech t2a async query t2a_async_query_v2 GET task_id
💡 Result:
The MiniMax t2a_async_query_v2 API endpoint is used to check the status of an asynchronous long-form text-to-speech (TTS) generation task [1][2]. Endpoint Details: Method: GET [1][2] Endpoint: /v1/query/t2a_async_query_v2 [1][2] Required Parameter: task_id (integer/int64) [1][3] Authentication: Bearer token (API Key) passed in the request header [2] Usage: After submitting a long-form TTS task via the Create Speech Generation Task API, you receive a task_id [2][4]. You must then poll the t2a_async_query_v2 endpoint using this task_id to monitor the task's progress [5][2]. Response States: The API returns the task status, which typically includes: - Processing: The task is currently being generated [1][3]. - Success: The task is finished. Upon success, the response includes a file_id, which you can use with the File Retrieve API to download the generated audio [1][3][4]. - Failed/Expired: The task encountered an error or the information is no longer available [3][6]. Important Notes: - The file_id (and the associated download URL) provided upon a successful status is valid for 9 hours (32,400 seconds) from the time it is generated [1][3][4]. - The MiniMax async TTS API supports processing up to 1 million characters per request [2][4].
Citations:
- 1: https://platform.minimax.io/docs/api-reference/speech-t2a-async-query
- 2: https://platform.minimax.io/docs/guides/speech-t2a-async
- 3: https://platform.minimaxi.com/docs/api-reference/speech-t2a-async-query
- 4: https://platform.minimax.io/docs/api-reference/api-overview
- 5: https://github.com/laplaceliu/minimax-api/blob/main/README.md
- 6: https://williamchong.github.io/minimax-speech-ts/
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re
provider = Path("src/io/speech/providers/MiniMaxTextToSpeechProvider.ts").read_text()
calls = re.findall(r'\bthis\.request\((.*?)\);', provider, re.S)
print(f"request call count: {len(calls)}")
for i, call in enumerate(calls, 1):
compact = " ".join(call.split())
print(f"call {i}: {compact[:240]}")
helper = re.search(
r'private async request<T>\(\s*path: string,\s*body: Record<string, unknown>,\s*\)\s*: Promise<T> \{\s*(.*?)\n\s*\}',
provider,
re.S,
)
if not helper:
raise SystemExit("request helper not found")
body = helper.group(1)
print("helper fixes POST:", 'method: "POST"' in body)
print("helper always serializes body:", 'body: JSON.stringify(body)' in body)
PY
printf '%s\n' '--- direct documentation excerpt ---'
curl -L --max-time 15 -sS https://platform.minimax.io/docs/api-reference/speech-t2a-async-query \
| tr '\n' ' ' \
| grep -o -E '.{0,180}(GET|task_id|t2a_async_query_v2).{0,240}' \
| head -n 8 || trueRepository: framerslab/agentos
Length of output: 3929
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
import html
import re
import urllib.request
url = "https://platform.minimax.io/docs/api-reference/speech-t2a-async-query"
raw = urllib.request.urlopen(url, timeout=15).read().decode("utf-8", "replace")
text = html.unescape(re.sub(r"<[^>]+>", " ", raw))
text = re.sub(r"\s+", " ", text)
for match in re.finditer(r"task[_ -]?id", text, re.I):
start = max(0, match.start() - 300)
end = min(len(text), match.end() + 500)
print(text[start:end])
print("---")
PY
printf '%s\n' '--- exact helper and all request calls ---'
sed -n '132,154p;280,300p' src/io/speech/providers/MiniMaxTextToSpeechProvider.ts
rg -n -C 4 'this\.request\(' src/io/speech/providers/MiniMaxTextToSpeechProvider.tsRepository: framerslab/agentos
Length of output: 2442
🏁 Script executed:
#!/bin/bash
set +e
curl -k -L --max-time 15 -sS https://platform.minimax.io/docs/api-reference/speech-t2a-async-query > /tmp/minimax-doc.html
python3 - <<'PY'
import html
import re
from pathlib import Path
raw = Path("/tmp/minimax-doc.html").read_text(errors="replace")
text = html.unescape(re.sub(r"<[^>]+>", " ", raw))
text = re.sub(r"\s+", " ", text)
matches = list(re.finditer(r"task[_ -]?id", text, re.I))
print("documentation task_id occurrences:", len(matches))
for match in matches[:20]:
print(text[max(0, match.start()-250):match.end()+450])
print("---")
PY
printf '%s\n' '--- exact local implementation ---'
sed -n '132,154p;280,300p' src/io/speech/providers/MiniMaxTextToSpeechProvider.ts
printf '%s\n' '--- all request calls ---'
rg -n -C 4 'this\.request\(' src/io/speech/providers/MiniMaxTextToSpeechProvider.tsRepository: framerslab/agentos
Length of output: 14831
Use GET for asynchronous task queries.
queryAsync currently sends POST with a JSON body. MiniMax requires GET with task_id as a query parameter. Update request to support bodyless GET requests and update the test to assert the method, query string, and absent body.
📍 Affects 2 files
src/io/speech/providers/MiniMaxTextToSpeechProvider.ts#L149-L150(this comment)src/io/speech/providers/MiniMaxTextToSpeechProvider.ts#L282-L294src/io/speech/__tests__/MiniMaxTextToSpeechProvider.test.ts#L127-L139
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/io/speech/providers/MiniMaxTextToSpeechProvider.ts` around lines 149 -
150, Update MiniMaxTextToSpeechProvider.ts lines 149-150 in queryAsync to call
the request helper with GET, passing task_id as a query parameter and no request
body. Update the request implementation at lines 282-294 to support bodyless GET
requests while preserving existing request behavior, and update
MiniMaxTextToSpeechProvider.test.ts lines 127-139 to assert the GET method,
query string, and absent body.
| { id: 'openai-tts', kind: 'tts' as const, envVars: ['OPENAI_API_KEY'] }, | ||
| { id: 'elevenlabs', kind: 'tts' as const, envVars: ['ELEVENLABS_API_KEY'] }, | ||
| { id: 'deepgram-aura', kind: 'tts' as const, envVars: ['DEEPGRAM_API_KEY'] }, | ||
| { id: 'minimax-tts', kind: 'tts' as const, envVars: ['MINIMAX_API_KEY'] }, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Preserve the live provider during resolver refresh.
After SpeechRuntime registers the real MiniMax provider, refresh() registers the same ID with provider: null. The existing registration is overwritten. resolveTTS() then returns null, and synthesis fails.
Preserve non-null registrations or add a MiniMax provider factory before adding this core entry. Add a refresh-after-construction regression test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/io/speech/SpeechProviderResolver.ts` at line 450, Update the resolver
refresh logic around the MiniMax core entry in SpeechProviderResolver so
registering { id: 'minimax-tts', kind: 'tts' } cannot overwrite an existing
non-null provider with null; preserve the live registration or supply the
MiniMax provider factory before this entry is added. Add a regression test that
constructs SpeechRuntime, calls refresh(), and verifies resolveTTS() still
returns the registered MiniMax provider.
| if (miniMaxApiKey) { | ||
| const tts = new MiniMaxTextToSpeechProvider({ | ||
| apiKey: miniMaxApiKey, | ||
| region: env['MINIMAX_REGION'] === 'china' ? 'china' : 'global', |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
sed -n '1,140p' src/io/speech/SpeechRuntime.ts
printf '\n-- region references --\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
"MINIMAX_REGION|MiniMaxTextToSpeechProvider|region:.*china|region.*global" .
printf '\n-- documentation references --\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
"MINIMAX_REGION|MiniMax|MiniMax.*region|minimax.*region" README.md docs .github 2>/dev/null || trueRepository: framerslab/agentos
Length of output: 7653
🏁 Script executed:
printf '%s\n' '-- provider implementation --'
sed -n '1,180p' src/io/speech/providers/MiniMaxTextToSpeechProvider.ts
printf '%s\n' '-- provider tests around region behavior --'
sed -n '1,120p' src/io/speech/__tests__/MiniMaxTextToSpeechProvider.test.ts
printf '%s\n' '-- all environment/config documentation files --'
git ls-files | rg -i '(^|/)(readme|docs|env|config)|minimax|speechruntime'Repository: framerslab/agentos
Length of output: 14341
🌐 Web query:
MiniMax API documentation domestic region value cn global endpoint MiniMax TTS
💡 Result:
MiniMax API service regions are distinguished by the platform where your account and subscription were purchased, rather than a simple configuration parameter [1][2]. You must match your API Key to the correct regional base URL [1][2]. For MiniMax Text-to-Speech (TTS) and other API services, use the following endpoints: Global/International Region: Base URL: https://api.minimax.io [1][2] Platform: https://platform.minimax.io [1] Mainland China Region (cn): Base URL: https://api.minimaxi.com [1][2] Platform: https://platform.minimaxi.com [1] Key implementation details: 1. Authentication: A Global API Key will fail if used with the Mainland China host (and vice versa) [1][2]. Ensure your credentials correspond to the platform where they were generated [1]. 2. TTS Endpoints: Once you have selected the correct base URL for your region, TTS endpoints are accessed via standard paths such as /v1/t2a_v2 (for synchronous HTTP) or /ws/v1/t2a_v2 (for WebSocket) [3][4][5]. 3. Tooling: If using tools like the MiniMax CLI (mmx-cli), the region is often auto-detected from the API Key, but it can be manually configured if necessary using: mmx config set --key region --value global # For international services mmx config set --key region --value cn # For mainland China services [6][7]
Citations:
- 1: https://minimax-ai.chat/docs/minimax-api-key-base-url/
- 2: https://deepwiki.com/MiniMax-AI/MiniMax-MCP/1.2-configuration
- 3: https://platform.minimax.io/docs/api-reference/speech-t2a-http
- 4: https://platform.minimax.io/docs/guides/speech-t2a-websocket
- 5: https://platform.minimax.io/docs/solutions/short-video
- 6: https://platform.minimax.io/docs/token-plan/openclaw
- 7: https://platform.minimax.io/docs/token-plan/minimax-cli
🏁 Script executed:
printf '%s\n' '-- SpeechRuntime tests --'
rg -n -A18 -B8 "MINIMAX|MiniMax|autoRegisterFromEnv|register.*Tts" \
src/io/speech/__tests__/SpeechRuntime.test.ts
printf '%s\n' '-- deterministic mapping probe --'
python3 - <<'PY'
from pathlib import Path
import re
source = Path("src/io/speech/SpeechRuntime.ts").read_text()
match = re.search(
r"region:\s*env\['MINIMAX_REGION'\]\s*===\s*'china'\s*\?\s*'china'\s*:\s*'global'",
source,
)
if not match:
raise SystemExit("mapping expression not found")
def current(value):
return "china" if value == "china" else "global"
for value in ["china", "cn", "CN", " cn ", "global", "unsupported"]:
print(f"{value!r} -> {current(value)!r}")
PYRepository: framerslab/agentos
Length of output: 3882
Map MINIMAX_REGION=cn to the China endpoint.
MINIMAX_REGION='cn' currently selects global, so China credentials use https://api.minimax.io instead of https://api.minimaxi.com. Normalize the value and map both cn and china to china. Reject unsupported values instead of defaulting to global.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/io/speech/SpeechRuntime.ts` at line 99, Update the region mapping in
SpeechRuntime to normalize MINIMAX_REGION and map both “cn” and “china” to the
China endpoint, while mapping only the supported global value to “global.”
Reject unsupported or missing values instead of silently defaulting to “global.”
Source: MCP tools
Reason: add a MiniMax TTS provider to the existing speech registry with global and China endpoints, current speech models, HTTP, async, and WebSocket operations, request options, and audio response parsing
This adds an environment-backed speech provider with regional routing and the current model catalog. It supports synchronous HTTP synthesis, asynchronous task creation and querying, and the WebSocket start/continue/finish protocol while forwarding voice, pronunciation, audio, language, subtitle, and voice-modification options.
Checks:
pnpm exec vitest run src/io/speech/__tests__/MiniMaxTextToSpeechProvider.test.ts src/io/speech/__tests__/providerCatalog.test.ts src/io/speech/__tests__/SpeechRuntime.test.tspnpm exec tsc --noEmit --pretty falsepnpm exec eslint src/io/speech/providers/MiniMaxTextToSpeechProvider.ts src/io/speech/providerCatalog.ts src/io/speech/SpeechProviderResolver.ts src/io/speech/SpeechRuntime.ts src/io/speech/index.ts --no-warn-ignoredgit diff --checkSummary by Sourcery
Add a MiniMax text-to-speech provider with regional routing and full integration into the speech runtime and provider catalog.
New Features:
Enhancements:
Tests:
Summary by CodeRabbit
New Features
Tests