Skip to content

Commit 607c99e

Browse files
committed
feat: add MiniMax Flash text profiles for global and China endpoints
1 parent ec2059b commit 607c99e

9 files changed

Lines changed: 247 additions & 31 deletions

File tree

‎packages/commands/src/commands/auth/login-api-key.ts‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
BailianError,
33
ExitCode,
44
getApiKeyLoginKind,
5+
getMiniMaxProfilePreset,
56
normalizeModelBaseUrl,
67
requestJson,
78
type ApiKeyLoginKind,
@@ -33,7 +34,7 @@ interface ApiKeyLoginProfile {
3334
persistPatch?: AuthPersistPatch;
3435
}
3536

36-
type ApiKeyEndpointKind = Exclude<ApiKeyLoginKind, "unknown">;
37+
type ApiKeyEndpointKind = Exclude<ApiKeyLoginKind, "unknown"> | "minimax";
3738

3839
interface ApiKeyValidationCandidate {
3940
baseUrl: string;
@@ -63,7 +64,7 @@ const ORDINARY_WORKSPACE_REGIONS = [
6364
"us-east-1",
6465
] as const;
6566

66-
const KNOWN_BASE_URLS: Readonly<Record<ApiKeyEndpointKind, readonly string[]>> = {
67+
const KNOWN_BASE_URLS: Readonly<Record<Exclude<ApiKeyLoginKind, "unknown">, readonly string[]>> = {
6768
ordinary: [
6869
"https://dashscope.aliyuncs.com",
6970
"https://dashscope-intl.aliyuncs.com",
@@ -77,6 +78,7 @@ const KNOWN_BASE_URLS: Readonly<Record<ApiKeyEndpointKind, readonly string[]>> =
7778
};
7879

7980
function modelsUrl(baseUrl: string, kind: ApiKeyEndpointKind): string {
81+
if (kind === "minimax") return new URL("/v1/models", baseUrl).toString();
8082
const url = new URL(
8183
kind === "token-plan" ? "/compatible-mode/v1/models" : "/api/v1/models",
8284
`${baseUrl}/`,
@@ -88,7 +90,7 @@ function modelsUrl(baseUrl: string, kind: ApiKeyEndpointKind): string {
8890
return url.toString();
8991
}
9092

91-
function validationKinds(apiKeyKind: ApiKeyLoginKind): ApiKeyEndpointKind[] {
93+
function validationKinds(apiKeyKind: ApiKeyLoginKind): Exclude<ApiKeyLoginKind, "unknown">[] {
9294
if (apiKeyKind === "unknown") return ["ordinary", "token-plan"];
9395
return [apiKeyKind];
9496
}
@@ -119,6 +121,12 @@ export function apiKeyValidationCandidates(
119121
const candidates: ApiKeyValidationCandidate[] = [];
120122
const seenUrls = new Set<string>();
121123

124+
const selectedBaseUrl = options.explicitBaseUrl ?? options.storedBaseUrl;
125+
if (selectedBaseUrl && getMiniMaxProfilePreset(selectedBaseUrl)) {
126+
addCandidate(candidates, seenUrls, selectedBaseUrl, "minimax");
127+
return candidates;
128+
}
129+
122130
if (options.explicitBaseUrl) {
123131
for (const kind of kinds) addCandidate(candidates, seenUrls, options.explicitBaseUrl, kind);
124132
return candidates;

‎packages/commands/src/commands/auth/login.ts‎

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
defineCommand,
33
generateCLIAccessToken,
44
getModelProfilePreset,
5+
getMiniMaxProfilePreset,
56
normalizeModelBaseUrl,
67
} from "bailian-cli-core";
78
import { emitBare } from "bailian-cli-runtime";
@@ -81,8 +82,18 @@ export default defineCommand({
8182
},
8283
},
8384
},
85+
notes: [
86+
{
87+
"en-US":
88+
"Use --config minimax or --config minimax-cn with --api-key for MiniMax global or China text profiles. New profiles default to MiniMax-M3.1-Flash and only claim text.chat; saved models and custom --base-url values are preserved.",
89+
"zh-CN":
90+
"使用 --config minimax 或 --config minimax-cn 配合 --api-key 登录 MiniMax 国际站或中国站文本配置。新配置默认使用 MiniMax-M3.1-Flash,仅声明 text.chat;保留已保存的模型和自定义 --base-url。",
91+
},
92+
],
8493
exampleArgs: [
8594
"--console",
95+
"--config minimax --api-key <key>",
96+
"--config minimax-cn --api-key <key>",
8697
"--api-key sk-xxxxx",
8798
"--open-api --access-key-id LTAIxxxxx --access-key-secret xxxxx",
8899
],
@@ -171,17 +182,23 @@ export default defineCommand({
171182
return;
172183
}
173184
const stored = store.stored();
185+
const namedPreset = getMiniMaxProfilePreset(
186+
getModelProfilePreset(settings.configName)?.baseUrl,
187+
);
174188
const validation = await validateApiKey(deps, key, {
175-
explicitBaseUrl: baseUrl,
189+
explicitBaseUrl: baseUrl ?? (!stored.baseUrl ? namedPreset?.baseUrl : undefined),
176190
storedBaseUrl: stored.baseUrl,
177191
workspaceId: settings.workspaceId,
178192
});
179-
const profilePreset = getModelProfilePreset(
180-
validation.kind === "token-plan" ? "token-plan" : undefined,
181-
);
193+
const profilePreset =
194+
getMiniMaxProfilePreset(validation.baseUrl) ??
195+
getModelProfilePreset(validation.kind === "token-plan" ? "token-plan" : undefined);
182196
await persistApiKey(deps, key, {
183197
persistBaseUrl: validation.baseUrl,
184-
defaultTextModel: profilePreset?.defaultTextModel,
198+
defaultTextModel:
199+
validation.kind === "minimax"
200+
? (settings.defaultTextModel ?? profilePreset?.defaultTextModel)
201+
: profilePreset?.defaultTextModel,
185202
defaultVideoModel: profilePreset?.defaultVideoModel,
186203
defaultImageToVideoModel: profilePreset?.defaultImageToVideoModel,
187204
defaultReferenceToVideoModel: profilePreset?.defaultReferenceToVideoModel,

‎packages/commands/src/commands/text/chat.ts‎

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {
22
defineCommand,
33
chatPath,
44
responsesPath,
5+
getMiniMaxProfilePreset,
6+
UsageError,
57
parseSSE,
68
detectOutputFormat,
79
readTextFromPathOrStdin,
@@ -35,8 +37,9 @@ const CHAT_FLAGS = {
3537
type: "string",
3638
valueHint: "<model>",
3739
description: {
38-
"en-US": "Model ID (default: qwen3.8-max)",
39-
"zh-CN": "模型 ID(默认:qwen3.8-max)",
40+
"en-US":
41+
"Model ID (default: profile model, MiniMax-M3.1-Flash for MiniMax, otherwise qwen3.8-max)",
42+
"zh-CN": "模型 ID(默认:配置模型,MiniMax 使用 MiniMax-M3.1-Flash,其他使用 qwen3.8-max)",
4043
},
4144
},
4245
message: {
@@ -96,8 +99,8 @@ const CHAT_FLAGS = {
9699
enableThinking: {
97100
type: "switch",
98101
description: {
99-
"en-US": "Enable thinking/reasoning mode (for qwen3/qwq models)",
100-
"zh-CN": "启用思考/推理模式(适用于 qwen3/qwq 模型)",
102+
"en-US": "Enable thinking/reasoning mode (Qwen or MiniMax; Flash always reasons)",
103+
"zh-CN": "启用思考/推理模式(Qwen 或 MiniMax;Flash 始终开启推理)",
101104
},
102105
},
103106
thinkingBudget: {
@@ -195,6 +198,12 @@ export default defineCommand({
195198
},
196199
],
197200
notes: [
201+
{
202+
"en-US":
203+
"MiniMax profiles support --api chat and --api responses. Use --model MiniMax-M3, MiniMax-M2.7, or a custom model to override Flash. Flash reasoning is always on; --thinking-budget is unsupported.",
204+
"zh-CN":
205+
"MiniMax 配置支持 --api chat 和 --api responses。使用 --model MiniMax-M3、MiniMax-M2.7 或自定义模型可覆盖 Flash 默认值。Flash 始终开启推理,不支持 --thinking-budget。",
206+
},
198207
{
199208
"en-US":
200209
"Prime mode requires an explicit model and currently supports only the Chat Completions API.",
@@ -226,7 +235,12 @@ export default defineCommand({
226235
const { system, messages } = parseMessages(flags);
227236

228237
const api = flags.api ?? "chat";
229-
const model = flags.model || settings.defaultTextModel || "qwen3.8-max";
238+
const miniMaxPreset = flags.prime ? undefined : getMiniMaxProfilePreset(ctx.client.baseUrl);
239+
const model =
240+
flags.model || settings.defaultTextModel || miniMaxPreset?.defaultTextModel || "qwen3.8-max";
241+
if (miniMaxPreset && flags.thinkingBudget !== undefined) {
242+
throw new UsageError("MiniMax does not support --thinking-budget.");
243+
}
230244
const shouldStream = flags.stream || process.stdout.isTTY;
231245
const format = detectOutputFormat(settings.output);
232246

@@ -258,17 +272,22 @@ export default defineCommand({
258272
if (flags.topP !== undefined) body.top_p = flags.topP;
259273

260274
if (flags.enableThinking) {
261-
body.enable_thinking = true;
262-
if (api === "chat" && "messages" in body && flags.thinkingBudget !== undefined) {
263-
body.thinking_budget = flags.thinkingBudget;
275+
if (miniMaxPreset) {
276+
if ("messages" in body) body.thinking = { type: "adaptive" };
277+
else body.reasoning = { effort: "max" };
278+
} else {
279+
body.enable_thinking = true;
280+
if (api === "chat" && "messages" in body && flags.thinkingBudget !== undefined) {
281+
body.thinking_budget = flags.thinkingBudget;
282+
}
264283
}
265284
}
266285

267286
const requestPath = flags.prime
268287
? resolvePrimeEndpoint(ctx, chatPath())
269288
: api === "responses"
270-
? responsesPath()
271-
: chatPath();
289+
? responsesPath(ctx.client.baseUrl)
290+
: chatPath(ctx.client.baseUrl);
272291

273292
if (flags.tool) {
274293
const tools = flags.tool.map((toolValue) => {
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { afterEach, expect, it, vi } from "vite-plus/test";
2+
import { Client, chatPath, responsesPath, getModelProfilePreset } from "bailian-cli-core";
3+
import { resolveApiKeyLoginConfigName } from "../../core/src/config/profile-presets.ts";
4+
import { apiKeyValidationCandidates } from "../src/commands/auth/login-api-key.ts";
5+
import loginCommand from "../src/commands/auth/login.ts";
6+
import chatCommand from "../src/commands/text/chat.ts";
7+
8+
afterEach(() => vi.restoreAllMocks());
9+
10+
for (const [profile, origin] of [
11+
["minimax", "https://api.minimax.io"],
12+
["minimax-cn", "https://api.minimax.cn"],
13+
]) {
14+
it(`${profile} authenticates only on its selected origin and preserves saved models`, async () => {
15+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
16+
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(
17+
async () =>
18+
new Response(JSON.stringify({ object: "list", data: [] }), {
19+
status: 200,
20+
headers: { "Content-Type": "application/json" },
21+
}),
22+
);
23+
const persist = vi.fn();
24+
for (const saved of [undefined, "MiniMax-M3", "MiniMax-M2.7", "custom-model"]) {
25+
const stored = { baseUrl: undefined, apiKeyCapabilities: undefined };
26+
await loginCommand.run!({
27+
flags: { apiKey: "sk-sp-test-minimax" },
28+
settings: { configName: profile, defaultTextModel: saved, timeout: 5, language: "en-US" },
29+
identity: { productName: "bailian", version: "0.0.0", userAgent: "test" },
30+
authStore: { stored: () => stored, login: persist, path: "test-config" },
31+
} as never);
32+
expect(persist).toHaveBeenLastCalledWith(
33+
expect.objectContaining({
34+
base_url: origin,
35+
default_text_model: saved ?? "MiniMax-M3.1-Flash",
36+
api_key_capabilities: ["text.chat"],
37+
}),
38+
);
39+
expect(persist.mock.lastCall![0].default_speech_model).toBeUndefined();
40+
}
41+
expect(fetchMock.mock.calls.every(([url]) => url === `${origin}/v1/models`)).toBe(true);
42+
expect(apiKeyValidationCandidates("sk-sp-test", { storedBaseUrl: origin })).toEqual([
43+
{ baseUrl: origin, kind: "minimax", modelsUrl: `${origin}/v1/models` },
44+
]);
45+
expect(resolveApiKeyLoginConfigName("sk-sp-test", profile, false, origin)).toBe(profile);
46+
expect(getModelProfilePreset(profile)?.apiKeyCapabilities).toEqual(["text.chat"]);
47+
});
48+
49+
for (const api of ["chat", "responses"]) {
50+
for (const stream of [false, true]) {
51+
it(`${profile} ${api} stream=${stream} sends a real client request with Flash`, async () => {
52+
vi.spyOn(process.stdout, "write").mockImplementation(() => true);
53+
const response =
54+
api === "chat"
55+
? { choices: [{ message: { content: "hello" } }] }
56+
: {
57+
status: "completed",
58+
output: [{ type: "message", content: [{ type: "output_text", text: "hello" }] }],
59+
};
60+
const sse =
61+
api === "chat"
62+
? 'data: {"choices":[{"delta":{"content":"hello"}}]}\n\ndata: [DONE]\n\n'
63+
: 'data: {"type":"response.output_text.delta","delta":"hello"}\n\ndata: {"type":"response.completed","response":{"status":"completed"}}\n\n';
64+
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
65+
new Response(stream ? sse : JSON.stringify(response), {
66+
status: 200,
67+
headers: { "Content-Type": stream ? "text/event-stream" : "application/json" },
68+
}),
69+
);
70+
const settings = { output: "json", timeout: 5, language: "en-US" };
71+
const identity = { productName: "bailian", version: "0.0.0", userAgent: "test" };
72+
const client = new Client({
73+
identity,
74+
settings,
75+
baseUrl: origin,
76+
apiCred: { token: "test-minimax-key", baseUrl: origin },
77+
} as never);
78+
await chatCommand.run!({
79+
client,
80+
settings,
81+
flags: { api, stream, message: ["hello"], enableThinking: true },
82+
} as never);
83+
const [url, request] = fetchMock.mock.calls[0];
84+
expect(url).toBe(`${origin}/v1/${api === "chat" ? "chat/completions" : "responses"}`);
85+
const body = JSON.parse(request!.body as string);
86+
expect(body.model).toBe("MiniMax-M3.1-Flash");
87+
expect(body.enable_thinking).toBeUndefined();
88+
expect(api === "chat" ? body.thinking : body.reasoning).toEqual(
89+
api === "chat" ? { type: "adaptive" } : { effort: "max" },
90+
);
91+
expect(new Headers(request!.headers).get("Authorization")).toBe("Bearer test-minimax-key");
92+
});
93+
}
94+
}
95+
}
96+
97+
it("keeps legacy/custom selections and non-MiniMax routing", async () => {
98+
vi.spyOn(process.stdout, "write").mockImplementation(() => true);
99+
for (const [origin, saved, explicit, expected] of [
100+
["https://api.minimax.io", "MiniMax-M2.7", undefined, "MiniMax-M2.7"],
101+
["https://api.minimax.cn", "MiniMax-M3", "custom-model", "custom-model"],
102+
["https://dashscope.aliyuncs.com", undefined, undefined, "qwen3.8-max"],
103+
["https://gateway.example", "custom-model", undefined, "custom-model"],
104+
]) {
105+
const requestJson = vi.fn().mockResolvedValue({ choices: [{ message: { content: "hello" } }] });
106+
await chatCommand.run!({
107+
client: { baseUrl: origin, requestJson },
108+
settings: { output: "json", defaultTextModel: saved },
109+
flags: { message: ["hello"], model: explicit },
110+
} as never);
111+
expect(requestJson.mock.calls[0][0].body.model).toBe(expected);
112+
expect(requestJson.mock.calls[0][0].path).toBe(chatPath(origin));
113+
}
114+
for (const origin of [
115+
undefined,
116+
"https://gateway.example",
117+
"https://api.minimax.io.example",
118+
"http://api.minimax.io",
119+
]) {
120+
expect(chatPath(origin)).toBe("/compatible-mode/v1/chat/completions");
121+
expect(responsesPath(origin)).toBe("/compatible-mode/v1/responses");
122+
}
123+
expect(getModelProfilePreset("token-plan")?.defaultTextModel).toBe("qwen3.8-max");
124+
expect(getModelProfilePreset("token-plan")?.defaultSpeechModel).toBe("qwen-audio-3.0-tts-plus");
125+
});
126+
127+
it("rejects a MiniMax thinking budget before sending a request", async () => {
128+
const requestJson = vi.fn();
129+
await expect(
130+
chatCommand.run!({
131+
client: { baseUrl: "https://api.minimax.io", requestJson },
132+
settings: { output: "json" },
133+
flags: { message: ["hello"], thinkingBudget: 128 },
134+
} as never),
135+
).rejects.toThrow("MiniMax does not support --thinking-budget");
136+
expect(requestJson).not.toHaveBeenCalled();
137+
});

‎packages/core/src/auth/store.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,12 @@ export function makeAuthStore(sources: ResolutionSources): AuthStore {
9090
resolveBaseUrl: (fallback) => resolveModelBaseUrl(sources, fallback),
9191
async login(patch) {
9292
const targetConfigName = patch.api_key
93-
? resolveApiKeyLoginConfigName(patch.api_key, configName, activateAfterLogin)
93+
? resolveApiKeyLoginConfigName(
94+
patch.api_key,
95+
configName,
96+
activateAfterLogin,
97+
patch.base_url,
98+
)
9499
: configName;
95100
const shouldActivate = activateAfterLogin || targetConfigName !== configName;
96101
const existing = readConfigFile(targetConfigName) as Record<string, unknown>;

‎packages/core/src/client/endpoints.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { UsageError } from "../errors/base.ts";
2+
import { getMiniMaxProfilePreset } from "../config/profile-presets.ts";
23

34
// API path builders — return the path only; the Client prepends the
45
// credential's baseUrl. Commands never see baseUrl.
@@ -22,13 +23,15 @@ function workspaceEndpoint(workspaceId: string, path: string): string {
2223
}
2324

2425
// ---- Chat (OpenAI Compatible) ----
25-
export function chatPath(): string {
26-
return "/compatible-mode/v1/chat/completions";
26+
export function chatPath(baseUrl?: string): string {
27+
return getMiniMaxProfilePreset(baseUrl)
28+
? "/v1/chat/completions"
29+
: "/compatible-mode/v1/chat/completions";
2730
}
2831

2932
// ---- Responses (OpenAI Compatible) ----
30-
export function responsesPath(): string {
31-
return "/compatible-mode/v1/responses";
33+
export function responsesPath(baseUrl?: string): string {
34+
return getMiniMaxProfilePreset(baseUrl) ? "/v1/responses" : "/compatible-mode/v1/responses";
3235
}
3336

3437
// ---- Decision model (TypeSafe System One, workspace-based host) ----

‎packages/core/src/config/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export { ensureConfigDir, getConfigDir, getConfigPath, getCredentialsPath } from
3131
export {
3232
getApiKeyLoginKind,
3333
getModelProfilePreset,
34+
getMiniMaxProfilePreset,
3435
type ApiKeyLoginKind,
3536
} from "./profile-presets.ts";
3637
export { normalizeModelBaseUrl } from "./model-base-url.ts";

0 commit comments

Comments
 (0)