Skip to content

Commit 9020958

Browse files
committed
Switch text chat to Anthropic Messages API, improve agent ergonomics
- Use /anthropic/v1/messages endpoint with x-api-key auth for text chat - Native thinking block support (dim + header styling in streaming) - Bare --message strings default to user role (no "user:" prefix needed) - Add speech voices command (list system voices, filter by --language) - Default speech/music to URL output (--out opts into file save) - Video generate waits by default, returns download URL (--no-wait for async) - Warn on stderr when region detection fails both probes
1 parent a303ca1 commit 9020958

13 files changed

Lines changed: 358 additions & 198 deletions

File tree

src/args.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
4747

4848
// Boolean flags
4949
if (['quiet', 'verbose', 'noColor', 'yes', 'dryRun', 'help', 'stream',
50-
'subtitles', 'wait', 'noBrowser'].includes(camelKey)) {
50+
'subtitles', 'wait', 'noWait', 'noBrowser'].includes(camelKey)) {
5151
(flags as Record<string, unknown>)[camelKey] = true;
5252
i++;
5353
continue;

src/client/endpoints.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
export function chatEndpoint(baseUrl: string): string {
2-
return `${baseUrl}/v1/text/chatcompletion_v2`;
2+
return `${baseUrl}/anthropic/v1/messages`;
33
}
44

55
export function speechEndpoint(baseUrl: string): string {
66
return `${baseUrl}/v1/t2a_v2`;
77
}
88

9+
export function voicesEndpoint(baseUrl: string): string {
10+
return `${baseUrl}/v1/get_voice`;
11+
}
12+
913
export function imageEndpoint(baseUrl: string): string {
1014
return `${baseUrl}/v1/image_generation`;
1115
}

src/client/http.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface RequestOpts {
1313
timeout?: number;
1414
stream?: boolean;
1515
noAuth?: boolean;
16+
authStyle?: 'bearer' | 'x-api-key';
1617
}
1718

1819
export async function request(
@@ -27,11 +28,15 @@ export async function request(
2728

2829
if (!opts.noAuth) {
2930
const credential = await resolveCredential(config);
30-
headers['Authorization'] = `Bearer ${credential.token}`;
31+
if (opts.authStyle === 'x-api-key') {
32+
headers['x-api-key'] = credential.token;
33+
} else {
34+
headers['Authorization'] = `Bearer ${credential.token}`;
35+
}
3136

3237
if (config.verbose) {
3338
process.stderr.write(`> ${opts.method || 'GET'} ${opts.url}\n`);
34-
process.stderr.write(`> Authorization: Bearer ${credential.token.slice(0, 8)}...\n`);
39+
process.stderr.write(`> Auth: ${credential.token.slice(0, 8)}...\n`);
3540
}
3641
}
3742

src/commands/music/generate.ts

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,12 @@ export default defineCommand({
2121
{ flag: '--sample-rate <hz>', description: 'Sample rate (default: 44100)' },
2222
{ flag: '--bitrate <bps>', description: 'Bitrate (default: 256000)' },
2323
{ flag: '--stream', description: 'Stream raw audio to stdout' },
24-
{ flag: '--out <path>', description: 'Output file path' },
25-
{ flag: '--out-format <fmt>', description: 'Output format: hex (default), url' },
24+
{ flag: '--out <path>', description: 'Save audio to file (uses hex decoding)' },
2625
],
2726
examples: [
27+
'minimax music generate --prompt "Upbeat pop" --lyrics "La la la..."',
2828
'minimax music generate --prompt "Indie folk, melancholic" --lyrics-file song.txt --out my_song.mp3',
2929
'minimax music generate --prompt "Upbeat pop" --lyrics "La la la..." --out summer.mp3',
30-
'minimax music generate --prompt "Jazz lounge" --out-format url --output json',
3130
],
3231
async run(config: Config, flags: GlobalFlags) {
3332
const prompt = flags.prompt as string | undefined;
@@ -52,7 +51,8 @@ export default defineCommand({
5251
process.stderr.write('Warning: No lyrics provided. Use --lyrics or --lyrics-file to include lyrics.\n');
5352
}
5453

55-
const outFormat = (flags.outFormat as string) || 'hex';
54+
const outPath = flags.out as string | undefined;
55+
const outFormat = outPath ? 'hex' : 'url';
5656
const format = detectOutputFormat(config.output);
5757

5858
const body: MusicRequest = {
@@ -64,7 +64,7 @@ export default defineCommand({
6464
sample_rate: (flags.sampleRate as number) || 44100,
6565
bitrate: (flags.bitrate as number) || 256000,
6666
},
67-
output_format: outFormat as 'url' | 'hex',
67+
output_format: outFormat,
6868
stream: flags.stream === true,
6969
};
7070

@@ -94,41 +94,28 @@ export default defineCommand({
9494
body,
9595
});
9696

97-
if (outFormat === 'url' && response.data.audio_url) {
97+
if (outPath && response.data.audio) {
98+
const audioBuffer = Buffer.from(response.data.audio, 'hex');
99+
writeFileSync(outPath, audioBuffer);
100+
98101
if (config.quiet) {
99-
console.log(response.data.audio_url);
102+
console.log(outPath);
100103
} else {
101104
console.log(formatOutput({
102-
url: response.data.audio_url,
105+
saved: outPath,
103106
duration_ms: response.extra_info?.audio_length,
104107
size_bytes: response.extra_info?.audio_size,
108+
sample_rate: response.extra_info?.audio_sample_rate,
105109
}, format));
106110
}
107-
return;
108-
}
109-
110-
// Hex format — decode and write to file
111-
const outPath = flags.out as string;
112-
if (!outPath) {
113-
throw new CLIError(
114-
'--out is required when using hex output format.',
115-
ExitCode.USAGE,
116-
'minimax music generate --prompt <text> --out song.mp3',
117-
);
118-
}
119-
120-
if (response.data.audio) {
121-
const audioBuffer = Buffer.from(response.data.audio, 'hex');
122-
writeFileSync(outPath, audioBuffer);
123-
111+
} else if (response.data.audio_url) {
124112
if (config.quiet) {
125-
console.log(outPath);
113+
console.log(response.data.audio_url);
126114
} else {
127115
console.log(formatOutput({
128-
saved: outPath,
116+
url: response.data.audio_url,
129117
duration_ms: response.extra_info?.audio_length,
130118
size_bytes: response.extra_info?.audio_size,
131-
sample_rate: response.extra_info?.audio_sample_rate,
132119
}, format));
133120
}
134121
}

src/commands/speech/synthesize.ts

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { readFileSync, writeFileSync } from 'fs';
1212
export default defineCommand({
1313
name: 'speech synthesize',
1414
description: 'Synchronous TTS, up to 10k chars (speech-2.8-hd / 2.6 / 02)',
15-
usage: 'minimax speech synthesize --text <text> --out <path> [flags]',
15+
usage: 'minimax speech synthesize --text <text> [--out <path>] [flags]',
1616
options: [
1717
{ flag: '--model <model>', description: 'Model ID (default: speech-2.8-hd)' },
1818
{ flag: '--text <text>', description: 'Text to synthesize' },
@@ -29,14 +29,13 @@ export default defineCommand({
2929
{ flag: '--subtitles', description: 'Include subtitle timing data' },
3030
{ flag: '--pronunciation <from/to>', description: 'Custom pronunciation (repeatable)' },
3131
{ flag: '--sound-effect <effect>', description: 'Add sound effect' },
32-
{ flag: '--out <path>', description: 'Output file path' },
33-
{ flag: '--out-format <fmt>', description: 'Output format: hex (default), url' },
32+
{ flag: '--out <path>', description: 'Save audio to file (uses hex decoding)' },
3433
{ flag: '--stream', description: 'Stream raw audio to stdout' },
3534
],
3635
examples: [
36+
'minimax speech synthesize --text "Hello, world!"',
3737
'minimax speech synthesize --text "Hello, world!" --out hello.mp3',
3838
'echo "Breaking news." | minimax speech synthesize --text-file - --out news.mp3',
39-
'minimax speech synthesize --text "Link" --out-format url --output json',
4039
'minimax speech synthesize --text "Stream" --stream | mpv --no-terminal -',
4140
],
4241
async run(config: Config, flags: GlobalFlags) {
@@ -59,7 +58,8 @@ export default defineCommand({
5958

6059
const model = (flags.model as string) || 'speech-2.8-hd';
6160
const voice = (flags.voice as string) || 'English_expressive_narrator';
62-
const outFormat = (flags.outFormat as string) || 'hex';
61+
const outPath = flags.out as string | undefined;
62+
const outFormat = outPath ? 'hex' : 'url';
6363
const format = detectOutputFormat(config.output);
6464

6565
const body: SpeechRequest = {
@@ -77,7 +77,7 @@ export default defineCommand({
7777
bitrate: (flags.bitrate as number) || 128000,
7878
channel: (flags.channels as number) || 1,
7979
},
80-
output_format: outFormat as 'url' | 'hex',
80+
output_format: outFormat,
8181
stream: flags.stream === true,
8282
};
8383

@@ -117,41 +117,30 @@ export default defineCommand({
117117
body,
118118
});
119119

120-
if (outFormat === 'url' && response.data.audio_url) {
120+
if (outPath && response.data.audio) {
121+
// --out given: decode hex and save to file
122+
const audioBuffer = Buffer.from(response.data.audio, 'hex');
123+
writeFileSync(outPath, audioBuffer);
124+
121125
if (config.quiet) {
122-
console.log(response.data.audio_url);
126+
console.log(outPath);
123127
} else {
124128
console.log(formatOutput({
125-
url: response.data.audio_url,
129+
saved: outPath,
126130
duration_ms: response.extra_info?.audio_length,
127131
size_bytes: response.extra_info?.audio_size,
132+
sample_rate: response.extra_info?.audio_sample_rate,
128133
}, format));
129134
}
130-
return;
131-
}
132-
133-
// Hex format — decode and write to file
134-
const outPath = flags.out as string;
135-
if (!outPath && !config.quiet) {
136-
throw new CLIError(
137-
'--out is required when using hex output format.',
138-
ExitCode.USAGE,
139-
'minimax speech synthesize --text "Hello" --out hello.mp3',
140-
);
141-
}
142-
143-
if (response.data.audio && outPath) {
144-
const audioBuffer = Buffer.from(response.data.audio, 'hex');
145-
writeFileSync(outPath, audioBuffer);
146-
135+
} else if (response.data.audio_url) {
136+
// No --out: return URL
147137
if (config.quiet) {
148-
console.log(outPath);
138+
console.log(response.data.audio_url);
149139
} else {
150140
console.log(formatOutput({
151-
saved: outPath,
141+
url: response.data.audio_url,
152142
duration_ms: response.extra_info?.audio_length,
153143
size_bytes: response.extra_info?.audio_size,
154-
sample_rate: response.extra_info?.audio_sample_rate,
155144
}, format));
156145
}
157146
}

src/commands/speech/voices.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { defineCommand } from '../../command';
2+
import { requestJson } from '../../client/http';
3+
import { voicesEndpoint } from '../../client/endpoints';
4+
import { formatOutput, detectOutputFormat } from '../../output/formatter';
5+
import type { Config } from '../../config/schema';
6+
import type { GlobalFlags } from '../../types/flags';
7+
import type { VoiceListResponse, SystemVoiceInfo } from '../../types/api';
8+
9+
function extractLanguage(voiceId: string): string {
10+
// voice_id format: "Language_VoiceName" or "Language (Dialect)_VoiceName"
11+
// Match up to the last underscore-separated word that starts with uppercase
12+
const match = voiceId.match(/^(.+?)_[A-Z]/);
13+
return match ? match[1] : voiceId;
14+
}
15+
16+
function filterByLanguage(voices: SystemVoiceInfo[], language: string): SystemVoiceInfo[] {
17+
const lang = language.toLowerCase();
18+
return voices.filter(v => extractLanguage(v.voice_id).toLowerCase().includes(lang));
19+
}
20+
21+
export default defineCommand({
22+
name: 'speech voices',
23+
description: 'List available system voices',
24+
usage: 'minimax speech voices [--language <lang>]',
25+
options: [
26+
{ flag: '--language <lang>', description: 'Filter voices by language (e.g. english, korean, japanese)' },
27+
],
28+
examples: [
29+
'minimax speech voices',
30+
'minimax speech voices --language english',
31+
'minimax speech voices --language korean',
32+
'minimax speech voices --output json',
33+
],
34+
async run(config: Config, flags: GlobalFlags) {
35+
const format = detectOutputFormat(config.output);
36+
37+
if (config.dryRun) {
38+
console.log(formatOutput({ request: { voice_type: 'system' } }, format));
39+
return;
40+
}
41+
42+
const response = await requestJson<VoiceListResponse>(config, {
43+
url: voicesEndpoint(config.baseUrl),
44+
method: 'POST',
45+
body: { voice_type: 'system' },
46+
});
47+
48+
const voices = response.system_voice ?? [];
49+
const language = flags.language as string | undefined;
50+
51+
if (language) {
52+
const filtered = filterByLanguage(voices, language);
53+
54+
if (format !== 'text') {
55+
console.log(formatOutput(filtered, format));
56+
return;
57+
}
58+
59+
for (const v of filtered) {
60+
const desc = v.description?.join('; ') || '';
61+
const name = v.voice_name ? ` (${v.voice_name})` : '';
62+
console.log(` ${v.voice_id}${name}`);
63+
if (desc) console.log(` ${desc}`);
64+
}
65+
} else {
66+
if (format !== 'text') {
67+
console.log(formatOutput(voices.map(v => v.voice_id), format));
68+
return;
69+
}
70+
71+
for (const v of voices) {
72+
console.log(v.voice_id);
73+
}
74+
}
75+
},
76+
});

0 commit comments

Comments
 (0)