Skip to content

Commit 9e9dc54

Browse files
authored
Merge pull request #89 from raylanlin/feat/default-models
feat: add per-modality default model configuration
2 parents c5c6d00 + 9ee6fe9 commit 9e9dc54

File tree

16 files changed

+627
-17
lines changed

16 files changed

+627
-17
lines changed

src/commands/config/set.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,22 @@ import { readConfigFile, writeConfigFile } from '../../config/loader';
66
import type { Config } from '../../config/schema';
77
import type { GlobalFlags } from '../../types/flags';
88

9-
const VALID_KEYS = ['region', 'base_url', 'output', 'timeout', 'api_key'];
9+
const VALID_KEYS = ['region', 'base_url', 'output', 'timeout', 'api_key', 'default_text_model', 'default_speech_model', 'default_video_model', 'default_music_model'];
10+
11+
// Allow hyphen-style keys (e.g. default-text-model → default_text_model)
12+
const KEY_ALIASES: Record<string, string> = {
13+
'default-text-model': 'default_text_model',
14+
'default-speech-model': 'default_speech_model',
15+
'default-video-model': 'default_video_model',
16+
'default-music-model': 'default_music_model',
17+
};
1018

1119
export default defineCommand({
1220
name: 'config set',
1321
description: 'Set a config value',
1422
usage: 'mmx config set --key <key> --value <value>',
1523
options: [
16-
{ flag: '--key <key>', description: 'Config key (region, base_url, output, timeout, api_key)' },
24+
{ flag: '--key <key>', description: 'Config key (region, base_url, output, timeout, api_key, default_text_model, default_speech_model, default_video_model, default_music_model)' },
1725
{ flag: '--value <value>', description: 'Value to set' },
1826
],
1927
examples: [
@@ -33,29 +41,32 @@ export default defineCommand({
3341
);
3442
}
3543

36-
if (!VALID_KEYS.includes(key)) {
44+
// Resolve hyphen aliases to underscore keys
45+
const resolvedKey: string = KEY_ALIASES[key] || key;
46+
47+
if (!VALID_KEYS.includes(resolvedKey)) {
3748
throw new CLIError(
3849
`Invalid config key "${key}". Valid keys: ${VALID_KEYS.join(', ')}`,
3950
ExitCode.USAGE,
4051
);
4152
}
4253

4354
// Validate specific values
44-
if (key === 'region' && !['global', 'cn'].includes(value)) {
55+
if (resolvedKey === 'region' && !['global', 'cn'].includes(value)) {
4556
throw new CLIError(
4657
`Invalid region "${value}". Valid values: global, cn`,
4758
ExitCode.USAGE,
4859
);
4960
}
5061

51-
if (key === 'output' && !['text', 'json'].includes(value)) {
62+
if (resolvedKey === 'output' && !['text', 'json'].includes(value)) {
5263
throw new CLIError(
5364
`Invalid output format "${value}". Valid values: text, json`,
5465
ExitCode.USAGE,
5566
);
5667
}
5768

58-
if (key === 'timeout') {
69+
if (resolvedKey === 'timeout') {
5970
const num = Number(value);
6071
if (isNaN(num) || num <= 0) {
6172
throw new CLIError(
@@ -68,16 +79,16 @@ export default defineCommand({
6879
const format = detectOutputFormat(config.output);
6980

7081
if (config.dryRun) {
71-
console.log(formatOutput({ would_set: { [key]: value } }, format));
82+
console.log(formatOutput({ would_set: { [resolvedKey]: value } }, format));
7283
return;
7384
}
7485

7586
const existing = readConfigFile() as Record<string, unknown>;
76-
existing[key] = key === 'timeout' ? Number(value) : value;
87+
existing[resolvedKey] = resolvedKey === 'timeout' ? Number(value) : value;
7788
await writeConfigFile(existing);
7889

7990
if (!config.quiet) {
80-
console.log(formatOutput({ [key]: existing[key] }, format));
91+
console.log(formatOutput({ [resolvedKey]: existing[resolvedKey] }, format));
8192
}
8293
},
8394
});

src/commands/config/show.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ export default defineCommand({
3131
result.api_key = maskToken(file.api_key);
3232
}
3333

34+
// Default models
35+
if (file.default_text_model) result.default_text_model = file.default_text_model;
36+
if (file.default_speech_model) result.default_speech_model = file.default_speech_model;
37+
if (file.default_video_model) result.default_video_model = file.default_video_model;
38+
if (file.default_music_model) result.default_music_model = file.default_music_model;
39+
3440
console.log(formatOutput(result, format));
3541
},
3642
});

src/commands/music/models.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,17 @@ export function isCodingPlan(config: Config): boolean {
1010
}
1111

1212
export function musicGenerateModel(config: Config): string {
13+
// Config default > key-type-based default
14+
if (config.defaultMusicModel) return config.defaultMusicModel;
1315
return isCodingPlan(config) ? 'music-2.6' : 'music-2.6-free';
1416
}
1517

18+
const VALID_COVER_MODELS = new Set(['music-cover', 'music-cover-free']);
19+
1620
export function musicCoverModel(config: Config): string {
21+
// Config default (only if it's a valid cover model) > key-type-based default
22+
if (config.defaultMusicModel && VALID_COVER_MODELS.has(config.defaultMusicModel)) {
23+
return config.defaultMusicModel;
24+
}
1725
return isCodingPlan(config) ? 'music-cover' : 'music-cover-free';
1826
}

src/commands/speech/synthesize.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ export default defineCommand({
5555
);
5656
}
5757

58-
const model = (flags.model as string) || 'speech-2.8-hd';
58+
const model = (flags.model as string)
59+
|| config.defaultSpeechModel
60+
|| 'speech-2.8-hd';
5961
const voice = (flags.voice as string) || 'English_expressive_narrator';
6062
const ts = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
6163
const ext = (flags.format as string) || 'mp3';

src/commands/text/chat.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ export default defineCommand({
117117
}
118118
}
119119

120-
const model = (flags.model as string) || 'MiniMax-M2.7';
120+
const model = (flags.model as string)
121+
|| config.defaultTextModel
122+
|| 'MiniMax-M2.7';
121123
const shouldStream = flags.stream === true || (flags.stream === undefined && process.stdout.isTTY);
122124
const format = detectOutputFormat(config.output);
123125

src/commands/video/generate.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,17 @@ export default defineCommand({
7171
);
7272
}
7373

74-
// Determine model: explicit --model overrides auto-switch
74+
// Determine model: explicit --model > auto-switch > config default > hardcoded
7575
const explicitModel = flags.model as string | undefined;
76-
let model = explicitModel || 'MiniMax-Hailuo-2.3';
77-
if (!explicitModel && flags.lastFrame) {
76+
let model: string;
77+
if (explicitModel) {
78+
model = explicitModel;
79+
} else if (flags.lastFrame) {
7880
model = 'MiniMax-Hailuo-02';
79-
} else if (!explicitModel && flags.subjectImage) {
81+
} else if (flags.subjectImage) {
8082
model = 'S2V-01';
83+
} else {
84+
model = config.defaultVideoModel || 'MiniMax-Hailuo-2.3';
8185
}
8286
const format = detectOutputFormat(config.output);
8387

src/config/loader.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ export function loadConfig(flags: GlobalFlags): Config {
6464
baseUrl,
6565
output,
6666
timeout,
67+
defaultTextModel: file.default_text_model,
68+
defaultSpeechModel: file.default_speech_model,
69+
defaultVideoModel: file.default_video_model,
70+
defaultMusicModel: file.default_music_model,
6771
verbose: flags.verbose || process.env.MINIMAX_VERBOSE === '1',
6872
quiet: flags.quiet || false,
6973
noColor: flags.noColor || process.env.NO_COLOR !== undefined || !process.stdout.isTTY,

src/config/schema.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export interface ConfigFile {
1616
base_url?: string;
1717
output?: 'text' | 'json';
1818
timeout?: number;
19+
default_text_model?: string;
20+
default_speech_model?: string;
21+
default_video_model?: string;
22+
default_music_model?: string;
1923
}
2024

2125
const VALID_REGIONS = new Set<string>(['global', 'cn']);
@@ -31,6 +35,10 @@ export function parseConfigFile(raw: unknown): ConfigFile {
3135
if (typeof obj.base_url === 'string' && obj.base_url.startsWith('http')) out.base_url = obj.base_url;
3236
if (typeof obj.output === 'string' && VALID_OUTPUTS.has(obj.output)) out.output = obj.output as ConfigFile['output'];
3337
if (typeof obj.timeout === 'number' && obj.timeout > 0) out.timeout = obj.timeout;
38+
if (typeof obj.default_text_model === 'string' && obj.default_text_model.length > 0) out.default_text_model = obj.default_text_model;
39+
if (typeof obj.default_speech_model === 'string' && obj.default_speech_model.length > 0) out.default_speech_model = obj.default_speech_model;
40+
if (typeof obj.default_video_model === 'string' && obj.default_video_model.length > 0) out.default_video_model = obj.default_video_model;
41+
if (typeof obj.default_music_model === 'string' && obj.default_music_model.length > 0) out.default_music_model = obj.default_music_model;
3442

3543
return out;
3644
}
@@ -44,6 +52,10 @@ export interface Config {
4452
baseUrl: string;
4553
output: 'text' | 'json';
4654
timeout: number;
55+
defaultTextModel?: string;
56+
defaultSpeechModel?: string;
57+
defaultVideoModel?: string;
58+
defaultMusicModel?: string;
4759
verbose: boolean;
4860
quiet: boolean;
4961
noColor: boolean;

test/commands/config/set.test.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import { describe, it, expect } from 'bun:test';
1+
import { describe, it, expect, mock } from 'bun:test';
22
import { default as setCommand } from '../../../src/commands/config/set';
33

4+
// Mock file I/O
5+
mock.module('../../../src/config/loader', () => ({
6+
readConfigFile: () => ({}),
7+
writeConfigFile: mock(() => Promise.resolve()),
8+
}));
9+
410
describe('config set command', () => {
511
it('has correct name', () => {
612
expect(setCommand.name).toBe('config set');
@@ -65,4 +71,68 @@ describe('config set command', () => {
6571
}),
6672
).rejects.toThrow('Invalid config key');
6773
});
74+
75+
it('accepts default_text_model key', async () => {
76+
const config = {
77+
region: 'global' as const,
78+
baseUrl: 'https://api.mmx.io',
79+
output: 'text' as const,
80+
timeout: 10,
81+
verbose: false,
82+
quiet: false,
83+
noColor: true,
84+
yes: false,
85+
dryRun: true,
86+
nonInteractive: true,
87+
async: false,
88+
};
89+
90+
// Should not throw — key is valid
91+
await expect(
92+
setCommand.execute(config, {
93+
key: 'default_text_model',
94+
value: 'MiniMax-M2.7-highspeed',
95+
quiet: false,
96+
verbose: false,
97+
noColor: true,
98+
yes: false,
99+
dryRun: true,
100+
help: false,
101+
nonInteractive: true,
102+
async: false,
103+
}),
104+
).resolves.toBeUndefined();
105+
});
106+
107+
it('accepts hyphen alias default-text-model', async () => {
108+
const config = {
109+
region: 'global' as const,
110+
baseUrl: 'https://api.mmx.io',
111+
output: 'text' as const,
112+
timeout: 10,
113+
verbose: false,
114+
quiet: false,
115+
noColor: true,
116+
yes: false,
117+
dryRun: true,
118+
nonInteractive: true,
119+
async: false,
120+
};
121+
122+
// Hyphen alias should resolve to default_text_model
123+
await expect(
124+
setCommand.execute(config, {
125+
key: 'default-text-model',
126+
value: 'MiniMax-M2.7-highspeed',
127+
quiet: false,
128+
verbose: false,
129+
noColor: true,
130+
yes: false,
131+
dryRun: true,
132+
help: false,
133+
nonInteractive: true,
134+
async: false,
135+
}),
136+
).resolves.toBeUndefined();
137+
});
68138
});

test/commands/config/show.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1-
import { describe, it, expect } from 'bun:test';
1+
import { describe, it, expect, mock } from 'bun:test';
22
import { default as showCommand } from '../../../src/commands/config/show';
33

4+
// Mock file I/O
5+
mock.module('../../../src/config/loader', () => ({
6+
readConfigFile: () => ({
7+
api_key: 'sk-cp-test-key',
8+
default_text_model: 'MiniMax-M2.7-highspeed',
9+
default_speech_model: 'speech-2.8-hd',
10+
default_video_model: 'MiniMax-Hailuo-2.3-6s-768p',
11+
default_music_model: 'music-2.6',
12+
}),
13+
}));
14+
415
describe('config show command', () => {
516
it('has correct name', () => {
617
expect(showCommand.name).toBe('config show');
@@ -45,4 +56,45 @@ describe('config show command', () => {
4556
console.log = originalLog;
4657
}
4758
});
59+
60+
it('includes default models in output', async () => {
61+
const config = {
62+
region: 'global' as const,
63+
baseUrl: 'https://api.mmx.io',
64+
output: 'json' as const,
65+
timeout: 300,
66+
verbose: false,
67+
quiet: false,
68+
noColor: true,
69+
yes: false,
70+
dryRun: false,
71+
nonInteractive: true,
72+
async: false,
73+
};
74+
75+
const originalLog = console.log;
76+
let output = '';
77+
console.log = (msg: string) => { output += msg; };
78+
79+
try {
80+
await showCommand.execute(config, {
81+
quiet: false,
82+
verbose: false,
83+
noColor: true,
84+
yes: false,
85+
dryRun: false,
86+
help: false,
87+
nonInteractive: true,
88+
async: false,
89+
});
90+
91+
const parsed = JSON.parse(output);
92+
expect(parsed.default_text_model).toBe('MiniMax-M2.7-highspeed');
93+
expect(parsed.default_speech_model).toBe('speech-2.8-hd');
94+
expect(parsed.default_video_model).toBe('MiniMax-Hailuo-2.3-6s-768p');
95+
expect(parsed.default_music_model).toBe('music-2.6');
96+
} finally {
97+
console.log = originalLog;
98+
}
99+
});
48100
});

0 commit comments

Comments
 (0)