onboard(minimax): flatten auth to 4 direct choices, unify CN/Global under single provider (#44284)

Replace the multi-step MiniMax onboarding wizard with 4 flat options:
- MiniMax Global — OAuth (minimax.io)
- MiniMax Global — API Key (minimax.io)
- MiniMax CN — OAuth (minimaxi.com)
- MiniMax CN — API Key (minimaxi.com)

Storage changes:
- Unify CN and Global under provider "minimax" (baseUrl distinguishes region)
- Profiles: minimax:global / minimax:cn (both regions can coexist)
- Model ref: minimax/MiniMax-M2.5 (no more minimax-cn/ prefix)
- Remove LM Studio local mode and Lightning/Highspeed choice

Backward compatibility:
- Keep minimax-cn in provider-env-vars for existing configs
- Accept minimax-cn as legacy tokenProvider in CI pipelines
- Error with migration hint for removed auth choices in non-interactive mode
- Warn when dual-profile overwrites shared provider baseUrl

Made-with: Cursor
This commit is contained in:
liyuan97
2026-03-13 02:23:42 +08:00
committed by GitHub
parent 1492ad20a9
commit 55f47e5ce6
16 changed files with 274 additions and 435 deletions

View File

@@ -2198,7 +2198,7 @@ Anthropic-compatible, built-in provider. Shortcut: `openclaw onboard --auth-choi
{
id: "hf:MiniMaxAI/MiniMax-M2.5",
name: "MiniMax M2.5",
reasoning: false,
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 192000,
@@ -2238,7 +2238,7 @@ Base URL should omit `/v1` (Anthropic client appends it). Shortcut: `openclaw on
{
id: "MiniMax-M2.5",
name: "MiniMax M2.5",
reasoning: false,
reasoning: true,
input: ["text"],
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
contextWindow: 200000,

View File

@@ -151,7 +151,7 @@ Configure manually via `openclaw.json`:
{
id: "minimax-m2.5-gs32",
name: "MiniMax M2.5 GS32",
reasoning: false,
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 196608,

View File

@@ -9,20 +9,6 @@ import type { ProviderConfig } from "./models-config.providers.js";
describe("models-config merge helpers", () => {
const preservedApiKey = "AGENT_KEY"; // pragma: allowlist secret
const kimiModel: ProviderConfig["models"][number] = {
id: "k2p5",
name: "Kimi for Coding",
input: ["text", "image"],
reasoning: true,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128_000,
maxTokens: 8_000,
};
it("refreshes implicit model metadata while preserving explicit reasoning overrides", () => {
const merged = mergeProviderModels(
@@ -83,17 +69,31 @@ describe("models-config merge helpers", () => {
it("preserves implicit provider headers when explicit config adds extra headers", () => {
const merged = mergeProviderModels(
{
baseUrl: "https://api.example.com",
api: "anthropic-messages",
baseUrl: "https://api.anthropic.com",
headers: { "User-Agent": "claude-code/0.1.0" },
models: [kimiModel],
} as ProviderConfig,
models: [
{
id: "k2p5",
name: "Kimi for Coding",
input: ["text", "image"],
reasoning: true,
},
],
} as unknown as ProviderConfig,
{
baseUrl: "https://api.example.com",
api: "anthropic-messages",
baseUrl: "https://api.anthropic.com",
headers: { "X-Kimi-Tenant": "tenant-a" },
models: [kimiModel],
} as ProviderConfig,
models: [
{
id: "k2p5",
name: "Kimi for Coding",
input: ["text", "image"],
reasoning: true,
},
],
} as unknown as ProviderConfig,
);
expect(merged.headers).toEqual({

View File

@@ -187,7 +187,7 @@ const MODELSTUDIO_MODEL_CATALOG: ReadonlyArray<ProviderModelConfig> = [
{
id: "MiniMax-M2.5",
name: "MiniMax-M2.5",
reasoning: false,
reasoning: true,
input: ["text"],
cost: MODELSTUDIO_DEFAULT_COST,
contextWindow: 1_000_000,

View File

@@ -5,8 +5,6 @@ export const AUTH_CHOICE_LEGACY_ALIASES_FOR_CLI: ReadonlyArray<AuthChoice> = [
"oauth",
"claude-cli",
"codex-cli",
"minimax-cloud",
"minimax",
];
export function normalizeLegacyOnboardAuthChoice(

View File

@@ -57,7 +57,7 @@ const AUTH_CHOICE_GROUP_DEFS: {
value: "minimax",
label: "MiniMax",
hint: "M2.5 (recommended)",
choices: ["minimax-portal", "minimax-api", "minimax-api-key-cn", "minimax-api-lightning"],
choices: ["minimax-global-oauth", "minimax-global-api", "minimax-cn-oauth", "minimax-cn-api"],
},
{
value: "moonshot",
@@ -291,9 +291,24 @@ const BASE_AUTH_CHOICE_OPTIONS: ReadonlyArray<AuthChoiceOption> = [
label: "Xiaomi API key",
},
{
value: "minimax-portal",
label: "MiniMax OAuth",
hint: "Oauth plugin for MiniMax",
value: "minimax-global-oauth",
label: "MiniMax Global — OAuth (minimax.io)",
hint: "Only supports OAuth for the coding plan",
},
{
value: "minimax-global-api",
label: "MiniMax Global — API Key (minimax.io)",
hint: "sk-api- or sk-cp- keys supported",
},
{
value: "minimax-cn-oauth",
label: "MiniMax CN — OAuth (minimaxi.com)",
hint: "Only supports OAuth for the coding plan",
},
{
value: "minimax-cn-api",
label: "MiniMax CN — API Key (minimaxi.com)",
hint: "sk-api- or sk-cp- keys supported",
},
{ value: "qwen-portal", label: "Qwen OAuth" },
{
@@ -307,17 +322,6 @@ const BASE_AUTH_CHOICE_OPTIONS: ReadonlyArray<AuthChoiceOption> = [
label: "OpenCode Zen catalog",
hint: "Claude, GPT, Gemini via opencode.ai/zen",
},
{ value: "minimax-api", label: "MiniMax M2.5" },
{
value: "minimax-api-key-cn",
label: "MiniMax M2.5 (CN)",
hint: "China endpoint (api.minimaxi.com)",
},
{
value: "minimax-api-lightning",
label: "MiniMax M2.5 Highspeed",
hint: "Official fast tier (legacy: Lightning)",
},
{ value: "qianfan-api-key", label: "Qianfan API key" },
{
value: "modelstudio-api-key-cn",

View File

@@ -1,6 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import { applyAuthChoiceMiniMax } from "./auth-choice.apply.minimax.js";
import {
createAuthTestLifecycle,
@@ -10,23 +9,6 @@ import {
setupAuthTestEnv,
} from "./test-wizard-helpers.js";
function createMinimaxPrompter(
params: {
text?: WizardPrompter["text"];
confirm?: WizardPrompter["confirm"];
select?: WizardPrompter["select"];
} = {},
): WizardPrompter {
return createWizardPrompter(
{
text: params.text,
confirm: params.confirm,
select: params.select,
},
{ defaultSelect: "oauth" },
);
}
describe("applyAuthChoiceMiniMax", () => {
const lifecycle = createAuthTestLifecycle([
"OPENCLAW_STATE_DIR",
@@ -56,27 +38,25 @@ describe("applyAuthChoiceMiniMax", () => {
async function runMiniMaxChoice(params: {
authChoice: Parameters<typeof applyAuthChoiceMiniMax>[0]["authChoice"];
opts?: Parameters<typeof applyAuthChoiceMiniMax>[0]["opts"];
env?: { apiKey?: string; oauthToken?: string };
prompter?: Parameters<typeof createMinimaxPrompter>[0];
env?: { apiKey?: string };
prompterText?: () => Promise<string>;
}) {
const agentDir = await setupTempState();
resetMiniMaxEnv();
if (params.env?.apiKey !== undefined) {
process.env.MINIMAX_API_KEY = params.env.apiKey;
}
if (params.env?.oauthToken !== undefined) {
process.env.MINIMAX_OAUTH_TOKEN = params.env.oauthToken;
}
const text = vi.fn(async () => "should-not-be-used");
const confirm = vi.fn(async () => true);
const result = await applyAuthChoiceMiniMax({
authChoice: params.authChoice,
config: {},
prompter: createMinimaxPrompter({
text,
// Pass select: undefined so ref-mode uses the non-interactive fallback (same as old test behavior).
prompter: createWizardPrompter({
text: params.prompterText ?? text,
confirm,
...params.prompter,
select: undefined,
}),
runtime: createExitThrowingRuntime(),
setDefaultModel: true,
@@ -94,7 +74,7 @@ describe("applyAuthChoiceMiniMax", () => {
const result = await applyAuthChoiceMiniMax({
authChoice: "openrouter-api-key",
config: {},
prompter: createMinimaxPrompter(),
prompter: createWizardPrompter({}),
runtime: createExitThrowingRuntime(),
setDefaultModel: true,
});
@@ -104,61 +84,52 @@ describe("applyAuthChoiceMiniMax", () => {
it.each([
{
caseName: "uses opts token for minimax-api without prompt",
authChoice: "minimax-api" as const,
caseName: "uses opts token for minimax-global-api without prompt",
authChoice: "minimax-global-api" as const,
tokenProvider: "minimax",
token: "mm-opts-token",
profileId: "minimax:default",
provider: "minimax",
profileId: "minimax:global",
expectedModel: "minimax/MiniMax-M2.5",
},
{
caseName:
"uses opts token for minimax-api-key-cn with trimmed/case-insensitive tokenProvider",
authChoice: "minimax-api-key-cn" as const,
tokenProvider: " MINIMAX-CN ",
caseName: "uses opts token for minimax-cn-api with trimmed/case-insensitive tokenProvider",
authChoice: "minimax-cn-api" as const,
tokenProvider: " MINIMAX ",
token: "mm-cn-opts-token",
profileId: "minimax-cn:default",
provider: "minimax-cn",
expectedModel: "minimax-cn/MiniMax-M2.5",
profileId: "minimax:cn",
expectedModel: "minimax/MiniMax-M2.5",
},
])(
"$caseName",
async ({ authChoice, tokenProvider, token, profileId, provider, expectedModel }) => {
const { agentDir, result, text, confirm } = await runMiniMaxChoice({
authChoice,
opts: {
tokenProvider,
token,
},
});
])("$caseName", async ({ authChoice, tokenProvider, token, profileId, expectedModel }) => {
const { agentDir, result, text, confirm } = await runMiniMaxChoice({
authChoice,
opts: { tokenProvider, token },
});
expect(result).not.toBeNull();
expect(result?.config.auth?.profiles?.[profileId]).toMatchObject({
provider,
mode: "api_key",
});
expect(resolveAgentModelPrimaryValue(result?.config.agents?.defaults?.model)).toBe(
expectedModel,
);
expect(text).not.toHaveBeenCalled();
expect(confirm).not.toHaveBeenCalled();
expect(result).not.toBeNull();
expect(result?.config.auth?.profiles?.[profileId]).toMatchObject({
provider: "minimax",
mode: "api_key",
});
expect(resolveAgentModelPrimaryValue(result?.config.agents?.defaults?.model)).toBe(
expectedModel,
);
expect(text).not.toHaveBeenCalled();
expect(confirm).not.toHaveBeenCalled();
const parsed = await readAuthProfiles(agentDir);
expect(parsed.profiles?.[profileId]?.key).toBe(token);
},
);
const parsed = await readAuthProfiles(agentDir);
expect(parsed.profiles?.[profileId]?.key).toBe(token);
});
it.each([
{
name: "uses env token for minimax-api-key-cn as plaintext by default",
name: "uses env token for minimax-cn-api as plaintext by default",
opts: undefined,
expectKey: "mm-env-token",
expectKeyRef: undefined,
expectConfirmCalls: 1,
},
{
name: "uses env token for minimax-api-key-cn as keyRef in ref mode",
name: "uses env token for minimax-cn-api as keyRef in ref mode",
opts: { secretInputMode: "ref" as const }, // pragma: allowlist secret
expectKey: undefined,
expectKeyRef: {
@@ -170,54 +141,68 @@ describe("applyAuthChoiceMiniMax", () => {
},
])("$name", async ({ opts, expectKey, expectKeyRef, expectConfirmCalls }) => {
const { agentDir, result, text, confirm } = await runMiniMaxChoice({
authChoice: "minimax-api-key-cn",
authChoice: "minimax-cn-api",
opts,
env: { apiKey: "mm-env-token" }, // pragma: allowlist secret
});
expect(result).not.toBeNull();
if (!opts) {
expect(result?.config.auth?.profiles?.["minimax-cn:default"]).toMatchObject({
provider: "minimax-cn",
expect(result?.config.auth?.profiles?.["minimax:cn"]).toMatchObject({
provider: "minimax",
mode: "api_key",
});
expect(resolveAgentModelPrimaryValue(result?.config.agents?.defaults?.model)).toBe(
"minimax-cn/MiniMax-M2.5",
"minimax/MiniMax-M2.5",
);
}
expect(text).not.toHaveBeenCalled();
expect(confirm).toHaveBeenCalledTimes(expectConfirmCalls);
const parsed = await readAuthProfiles(agentDir);
expect(parsed.profiles?.["minimax-cn:default"]?.key).toBe(expectKey);
expect(parsed.profiles?.["minimax:cn"]?.key).toBe(expectKey);
if (expectKeyRef) {
expect(parsed.profiles?.["minimax-cn:default"]?.keyRef).toEqual(expectKeyRef);
expect(parsed.profiles?.["minimax:cn"]?.keyRef).toEqual(expectKeyRef);
} else {
expect(parsed.profiles?.["minimax-cn:default"]?.keyRef).toBeUndefined();
expect(parsed.profiles?.["minimax:cn"]?.keyRef).toBeUndefined();
}
});
it("uses minimax-api-lightning default model", async () => {
it("minimax-global-api uses minimax:global profile and minimax/MiniMax-M2.5 model", async () => {
const { agentDir, result, text, confirm } = await runMiniMaxChoice({
authChoice: "minimax-api-lightning",
authChoice: "minimax-global-api",
opts: {
tokenProvider: "minimax",
token: "mm-lightning-token",
token: "mm-global-token",
},
});
expect(result).not.toBeNull();
expect(result?.config.auth?.profiles?.["minimax:default"]).toMatchObject({
expect(result?.config.auth?.profiles?.["minimax:global"]).toMatchObject({
provider: "minimax",
mode: "api_key",
});
expect(resolveAgentModelPrimaryValue(result?.config.agents?.defaults?.model)).toBe(
"minimax/MiniMax-M2.5-highspeed",
"minimax/MiniMax-M2.5",
);
expect(result?.config.models?.providers?.minimax?.baseUrl).toContain("minimax.io");
expect(text).not.toHaveBeenCalled();
expect(confirm).not.toHaveBeenCalled();
const parsed = await readAuthProfiles(agentDir);
expect(parsed.profiles?.["minimax:default"]?.key).toBe("mm-lightning-token");
expect(parsed.profiles?.["minimax:global"]?.key).toBe("mm-global-token");
});
it("minimax-cn-api sets CN baseUrl", async () => {
const { result } = await runMiniMaxChoice({
authChoice: "minimax-cn-api",
opts: {
tokenProvider: "minimax",
token: "mm-cn-token",
},
});
expect(result).not.toBeNull();
expect(result?.config.models?.providers?.minimax?.baseUrl).toContain("minimaxi.com");
});
});

View File

@@ -12,130 +12,93 @@ import {
applyMinimaxApiConfigCn,
applyMinimaxApiProviderConfig,
applyMinimaxApiProviderConfigCn,
applyMinimaxConfig,
applyMinimaxProviderConfig,
setMinimaxApiKey,
} from "./onboard-auth.js";
export async function applyAuthChoiceMiniMax(
params: ApplyAuthChoiceParams,
): Promise<ApplyAuthChoiceResult | null> {
let nextConfig = params.config;
let agentModelOverride: string | undefined;
const applyProviderDefaultModel = createAuthChoiceDefaultModelApplierForMutableState(
params,
() => nextConfig,
(config) => (nextConfig = config),
() => agentModelOverride,
(model) => (agentModelOverride = model),
);
const requestedSecretInputMode = normalizeSecretInputModeInput(params.opts?.secretInputMode);
const ensureMinimaxApiKey = async (opts: {
profileId: string;
promptMessage: string;
}): Promise<void> => {
// OAuth paths — delegate to plugin, no API key needed
if (params.authChoice === "minimax-global-oauth") {
return await applyAuthChoicePluginProvider(params, {
authChoice: "minimax-global-oauth",
pluginId: "minimax-portal-auth",
providerId: "minimax-portal",
methodId: "oauth",
label: "MiniMax",
});
}
if (params.authChoice === "minimax-cn-oauth") {
return await applyAuthChoicePluginProvider(params, {
authChoice: "minimax-cn-oauth",
pluginId: "minimax-portal-auth",
providerId: "minimax-portal",
methodId: "oauth-cn",
label: "MiniMax CN",
});
}
// API key paths
if (params.authChoice === "minimax-global-api" || params.authChoice === "minimax-cn-api") {
const isCn = params.authChoice === "minimax-cn-api";
const profileId = isCn ? "minimax:cn" : "minimax:global";
const keyLink = isCn
? "https://platform.minimaxi.com/user-center/basic-information/interface-key"
: "https://platform.minimax.io/user-center/basic-information/interface-key";
const promptMessage = `Enter MiniMax ${isCn ? "CN " : ""}API key (sk-api- or sk-cp-)\n${keyLink}`;
let nextConfig = params.config;
let agentModelOverride: string | undefined;
const applyProviderDefaultModel = createAuthChoiceDefaultModelApplierForMutableState(
params,
() => nextConfig,
(config) => (nextConfig = config),
() => agentModelOverride,
(model) => (agentModelOverride = model),
);
const requestedSecretInputMode = normalizeSecretInputModeInput(params.opts?.secretInputMode);
// Warn when both Global and CN share the same `minimax` provider entry — configuring one
// overwrites the other's baseUrl. Only show when the other profile is already present.
const otherProfileId = isCn ? "minimax:global" : "minimax:cn";
const hasOtherProfile = Boolean(nextConfig.auth?.profiles?.[otherProfileId]);
const noteMessage = hasOtherProfile
? `Note: Global and CN both use the "minimax" provider entry. Saving this key will overwrite the existing ${isCn ? "Global" : "CN"} endpoint (${otherProfileId}).`
: undefined;
await ensureApiKeyFromOptionEnvOrPrompt({
token: params.opts?.token,
tokenProvider: params.opts?.tokenProvider,
secretInputMode: requestedSecretInputMode,
config: nextConfig,
expectedProviders: ["minimax", "minimax-cn"],
// Accept "minimax-cn" as a legacy tokenProvider alias for the CN path.
expectedProviders: isCn ? ["minimax", "minimax-cn"] : ["minimax"],
provider: "minimax",
envLabel: "MINIMAX_API_KEY",
promptMessage: opts.promptMessage,
promptMessage,
normalize: normalizeApiKeyInput,
validate: validateApiKeyInput,
prompter: params.prompter,
noteMessage,
setCredential: async (apiKey, mode) =>
setMinimaxApiKey(apiKey, params.agentDir, opts.profileId, { secretInputMode: mode }),
});
};
const applyMinimaxApiVariant = async (opts: {
profileId: string;
provider: "minimax" | "minimax-cn";
promptMessage: string;
modelRefPrefix: "minimax" | "minimax-cn";
modelId: string;
applyDefaultConfig: (
config: ApplyAuthChoiceParams["config"],
modelId: string,
) => ApplyAuthChoiceParams["config"];
applyProviderConfig: (
config: ApplyAuthChoiceParams["config"],
modelId: string,
) => ApplyAuthChoiceParams["config"];
}): Promise<ApplyAuthChoiceResult> => {
await ensureMinimaxApiKey({
profileId: opts.profileId,
promptMessage: opts.promptMessage,
setMinimaxApiKey(apiKey, params.agentDir, profileId, { secretInputMode: mode }),
});
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: opts.profileId,
provider: opts.provider,
profileId,
provider: "minimax",
mode: "api_key",
});
const modelRef = `${opts.modelRefPrefix}/${opts.modelId}`;
await applyProviderDefaultModel({
defaultModel: modelRef,
applyDefaultConfig: (config) => opts.applyDefaultConfig(config, opts.modelId),
applyProviderConfig: (config) => opts.applyProviderConfig(config, opts.modelId),
});
return { config: nextConfig, agentModelOverride };
};
if (params.authChoice === "minimax-portal") {
// Let user choose between Global/CN endpoints
const endpoint = await params.prompter.select({
message: "Select MiniMax endpoint",
options: [
{ value: "oauth", label: "Global", hint: "OAuth for international users" },
{ value: "oauth-cn", label: "CN", hint: "OAuth for users in China" },
],
defaultModel: "minimax/MiniMax-M2.5",
applyDefaultConfig: (config) =>
isCn ? applyMinimaxApiConfigCn(config) : applyMinimaxApiConfig(config),
applyProviderConfig: (config) =>
isCn ? applyMinimaxApiProviderConfigCn(config) : applyMinimaxApiProviderConfig(config),
});
return await applyAuthChoicePluginProvider(params, {
authChoice: "minimax-portal",
pluginId: "minimax-portal-auth",
providerId: "minimax-portal",
methodId: endpoint,
label: "MiniMax",
});
}
if (
params.authChoice === "minimax-cloud" ||
params.authChoice === "minimax-api" ||
params.authChoice === "minimax-api-lightning"
) {
return await applyMinimaxApiVariant({
profileId: "minimax:default",
provider: "minimax",
promptMessage: "Enter MiniMax API key",
modelRefPrefix: "minimax",
modelId:
params.authChoice === "minimax-api-lightning" ? "MiniMax-M2.5-highspeed" : "MiniMax-M2.5",
applyDefaultConfig: applyMinimaxApiConfig,
applyProviderConfig: applyMinimaxApiProviderConfig,
});
}
if (params.authChoice === "minimax-api-key-cn") {
return await applyMinimaxApiVariant({
profileId: "minimax-cn:default",
provider: "minimax-cn",
promptMessage: "Enter MiniMax China API key",
modelRefPrefix: "minimax-cn",
modelId: "MiniMax-M2.5",
applyDefaultConfig: applyMinimaxApiConfigCn,
applyProviderConfig: applyMinimaxApiProviderConfigCn,
});
}
if (params.authChoice === "minimax") {
await applyProviderDefaultModel({
defaultModel: "lmstudio/minimax-m2.5-gs32",
applyDefaultConfig: applyMinimaxConfig,
applyProviderConfig: applyMinimaxProviderConfig,
});
return { config: nextConfig, agentModelOverride };
}

View File

@@ -34,11 +34,10 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
"huggingface-api-key": "huggingface",
"github-copilot": "github-copilot",
"copilot-proxy": "copilot-proxy",
"minimax-cloud": "minimax",
"minimax-api": "minimax",
"minimax-api-key-cn": "minimax-cn",
"minimax-api-lightning": "minimax",
minimax: "lmstudio",
"minimax-global-oauth": "minimax-portal",
"minimax-global-api": "minimax",
"minimax-cn-oauth": "minimax-portal",
"minimax-cn-api": "minimax",
"opencode-zen": "opencode",
"opencode-go": "opencode-go",
"xai-api-key": "xai",
@@ -46,7 +45,6 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
"qwen-portal": "qwen-portal",
"volcengine-api-key": "volcengine",
"byteplus-api-key": "byteplus",
"minimax-portal": "minimax-portal",
"qianfan-api-key": "qianfan",
"custom-api-key": "custom",
};

View File

@@ -208,8 +208,8 @@ describe("applyAuthChoice", () => {
it("prompts and writes provider API key for common providers", async () => {
const scenarios: Array<{
authChoice:
| "minimax-api"
| "minimax-api-key-cn"
| "minimax-global-api"
| "minimax-cn-api"
| "synthetic-api-key"
| "huggingface-api-key";
promptContains: string;
@@ -220,17 +220,17 @@ describe("applyAuthChoice", () => {
expectedModelPrefix?: string;
}> = [
{
authChoice: "minimax-api" as const,
authChoice: "minimax-global-api" as const,
promptContains: "Enter MiniMax API key",
profileId: "minimax:default",
profileId: "minimax:global",
provider: "minimax",
token: "sk-minimax-test",
},
{
authChoice: "minimax-api-key-cn" as const,
promptContains: "Enter MiniMax China API key",
profileId: "minimax-cn:default",
provider: "minimax-cn",
authChoice: "minimax-cn-api" as const,
promptContains: "Enter MiniMax CN API key",
profileId: "minimax:cn",
provider: "minimax",
token: "sk-minimax-test",
expectedBaseUrl: MINIMAX_CN_API_BASE_URL,
},
@@ -1243,7 +1243,7 @@ describe("applyAuthChoice", () => {
it("writes portal OAuth credentials for plugin providers", async () => {
const scenarios: Array<{
authChoice: "qwen-portal" | "minimax-portal";
authChoice: "qwen-portal" | "minimax-global-oauth";
label: string;
authId: string;
authLabel: string;
@@ -1268,7 +1268,7 @@ describe("applyAuthChoice", () => {
apiKey: "qwen-oauth", // pragma: allowlist secret
},
{
authChoice: "minimax-portal",
authChoice: "minimax-global-oauth",
label: "MiniMax",
authId: "oauth",
authLabel: "MiniMax OAuth (Global)",
@@ -1278,7 +1278,6 @@ describe("applyAuthChoice", () => {
api: "anthropic-messages",
defaultModel: "minimax-portal/MiniMax-M2.5",
apiKey: "minimax-oauth", // pragma: allowlist secret
selectValue: "oauth",
},
];
for (const scenario of scenarios) {

View File

@@ -1,5 +1,4 @@
import type { OpenClawConfig } from "../config/config.js";
import { toAgentModelListLike } from "../config/model-input.js";
import type { ModelProviderConfig } from "../config/types.models.js";
import {
applyAgentDefaultModelPrimary,
@@ -7,154 +6,10 @@ import {
} from "./onboard-auth.config-shared.js";
import {
buildMinimaxApiModelDefinition,
buildMinimaxModelDefinition,
DEFAULT_MINIMAX_BASE_URL,
DEFAULT_MINIMAX_CONTEXT_WINDOW,
DEFAULT_MINIMAX_MAX_TOKENS,
MINIMAX_API_BASE_URL,
MINIMAX_CN_API_BASE_URL,
MINIMAX_HOSTED_COST,
MINIMAX_HOSTED_MODEL_ID,
MINIMAX_HOSTED_MODEL_REF,
MINIMAX_LM_STUDIO_COST,
} from "./onboard-auth.models.js";
export function applyMinimaxProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
const models = { ...cfg.agents?.defaults?.models };
models["anthropic/claude-opus-4-6"] = {
...models["anthropic/claude-opus-4-6"],
alias: models["anthropic/claude-opus-4-6"]?.alias ?? "Opus",
};
models["lmstudio/minimax-m2.5-gs32"] = {
...models["lmstudio/minimax-m2.5-gs32"],
alias: models["lmstudio/minimax-m2.5-gs32"]?.alias ?? "Minimax",
};
const providers = { ...cfg.models?.providers };
if (!providers.lmstudio) {
providers.lmstudio = {
baseUrl: "http://127.0.0.1:1234/v1",
apiKey: "lmstudio",
api: "openai-responses",
models: [
buildMinimaxModelDefinition({
id: "minimax-m2.5-gs32",
name: "MiniMax M2.5 GS32",
reasoning: false,
cost: MINIMAX_LM_STUDIO_COST,
contextWindow: 196608,
maxTokens: 8192,
}),
],
};
}
return applyOnboardAuthAgentModelsAndProviders(cfg, { agentModels: models, providers });
}
export function applyMinimaxHostedProviderConfig(
cfg: OpenClawConfig,
params?: { baseUrl?: string },
): OpenClawConfig {
const models = { ...cfg.agents?.defaults?.models };
models[MINIMAX_HOSTED_MODEL_REF] = {
...models[MINIMAX_HOSTED_MODEL_REF],
alias: models[MINIMAX_HOSTED_MODEL_REF]?.alias ?? "Minimax",
};
const providers = { ...cfg.models?.providers };
const hostedModel = buildMinimaxModelDefinition({
id: MINIMAX_HOSTED_MODEL_ID,
cost: MINIMAX_HOSTED_COST,
contextWindow: DEFAULT_MINIMAX_CONTEXT_WINDOW,
maxTokens: DEFAULT_MINIMAX_MAX_TOKENS,
});
const existingProvider = providers.minimax;
const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : [];
const hasHostedModel = existingModels.some((model) => model.id === MINIMAX_HOSTED_MODEL_ID);
const mergedModels = hasHostedModel ? existingModels : [...existingModels, hostedModel];
providers.minimax = {
...existingProvider,
baseUrl: params?.baseUrl?.trim() || DEFAULT_MINIMAX_BASE_URL,
apiKey: "minimax",
api: "openai-completions",
models: mergedModels.length > 0 ? mergedModels : [hostedModel],
};
return applyOnboardAuthAgentModelsAndProviders(cfg, { agentModels: models, providers });
}
export function applyMinimaxConfig(cfg: OpenClawConfig): OpenClawConfig {
const next = applyMinimaxProviderConfig(cfg);
return applyAgentDefaultModelPrimary(next, "lmstudio/minimax-m2.5-gs32");
}
export function applyMinimaxHostedConfig(
cfg: OpenClawConfig,
params?: { baseUrl?: string },
): OpenClawConfig {
const next = applyMinimaxHostedProviderConfig(cfg, params);
return {
...next,
agents: {
...next.agents,
defaults: {
...next.agents?.defaults,
model: {
...toAgentModelListLike(next.agents?.defaults?.model),
primary: MINIMAX_HOSTED_MODEL_REF,
},
},
},
};
}
// MiniMax Anthropic-compatible API (platform.minimax.io/anthropic)
export function applyMinimaxApiProviderConfig(
cfg: OpenClawConfig,
modelId: string = "MiniMax-M2.5",
): OpenClawConfig {
return applyMinimaxApiProviderConfigWithBaseUrl(cfg, {
providerId: "minimax",
modelId,
baseUrl: MINIMAX_API_BASE_URL,
});
}
export function applyMinimaxApiConfig(
cfg: OpenClawConfig,
modelId: string = "MiniMax-M2.5",
): OpenClawConfig {
return applyMinimaxApiConfigWithBaseUrl(cfg, {
providerId: "minimax",
modelId,
baseUrl: MINIMAX_API_BASE_URL,
});
}
// MiniMax China API (api.minimaxi.com)
export function applyMinimaxApiProviderConfigCn(
cfg: OpenClawConfig,
modelId: string = "MiniMax-M2.5",
): OpenClawConfig {
return applyMinimaxApiProviderConfigWithBaseUrl(cfg, {
providerId: "minimax-cn",
modelId,
baseUrl: MINIMAX_CN_API_BASE_URL,
});
}
export function applyMinimaxApiConfigCn(
cfg: OpenClawConfig,
modelId: string = "MiniMax-M2.5",
): OpenClawConfig {
return applyMinimaxApiConfigWithBaseUrl(cfg, {
providerId: "minimax-cn",
modelId,
baseUrl: MINIMAX_CN_API_BASE_URL,
});
}
type MinimaxApiProviderConfigParams = {
providerId: string;
modelId: string;
@@ -193,17 +48,7 @@ function applyMinimaxApiProviderConfigWithBaseUrl(
alias: "Minimax",
};
return {
...cfg,
agents: {
...cfg.agents,
defaults: {
...cfg.agents?.defaults,
models,
},
},
models: { mode: cfg.models?.mode ?? "merge", providers },
};
return applyOnboardAuthAgentModelsAndProviders(cfg, { agentModels: models, providers });
}
function applyMinimaxApiConfigWithBaseUrl(
@@ -213,3 +58,49 @@ function applyMinimaxApiConfigWithBaseUrl(
const next = applyMinimaxApiProviderConfigWithBaseUrl(cfg, params);
return applyAgentDefaultModelPrimary(next, `${params.providerId}/${params.modelId}`);
}
// MiniMax Global API (platform.minimax.io/anthropic)
export function applyMinimaxApiProviderConfig(
cfg: OpenClawConfig,
modelId: string = "MiniMax-M2.5",
): OpenClawConfig {
return applyMinimaxApiProviderConfigWithBaseUrl(cfg, {
providerId: "minimax",
modelId,
baseUrl: MINIMAX_API_BASE_URL,
});
}
export function applyMinimaxApiConfig(
cfg: OpenClawConfig,
modelId: string = "MiniMax-M2.5",
): OpenClawConfig {
return applyMinimaxApiConfigWithBaseUrl(cfg, {
providerId: "minimax",
modelId,
baseUrl: MINIMAX_API_BASE_URL,
});
}
// MiniMax CN API (api.minimaxi.com/anthropic) — same provider id, different baseUrl
export function applyMinimaxApiProviderConfigCn(
cfg: OpenClawConfig,
modelId: string = "MiniMax-M2.5",
): OpenClawConfig {
return applyMinimaxApiProviderConfigWithBaseUrl(cfg, {
providerId: "minimax",
modelId,
baseUrl: MINIMAX_CN_API_BASE_URL,
});
}
export function applyMinimaxApiConfigCn(
cfg: OpenClawConfig,
modelId: string = "MiniMax-M2.5",
): OpenClawConfig {
return applyMinimaxApiConfigWithBaseUrl(cfg, {
providerId: "minimax",
modelId,
baseUrl: MINIMAX_CN_API_BASE_URL,
});
}

View File

@@ -50,10 +50,6 @@ export {
applyMinimaxApiConfigCn,
applyMinimaxApiProviderConfig,
applyMinimaxApiProviderConfigCn,
applyMinimaxConfig,
applyMinimaxHostedConfig,
applyMinimaxHostedProviderConfig,
applyMinimaxProviderConfig,
} from "./onboard-auth.config-minimax.js";
export {

View File

@@ -183,16 +183,16 @@ describe("onboard (non-interactive): provider auth", () => {
it("stores MiniMax API key and uses global baseUrl by default", async () => {
await withOnboardEnv("openclaw-onboard-minimax-", async (env) => {
const cfg = await runOnboardingAndReadConfig(env, {
authChoice: "minimax-api",
authChoice: "minimax-global-api",
minimaxApiKey: "sk-minimax-test", // pragma: allowlist secret
});
expect(cfg.auth?.profiles?.["minimax:default"]?.provider).toBe("minimax");
expect(cfg.auth?.profiles?.["minimax:default"]?.mode).toBe("api_key");
expect(cfg.auth?.profiles?.["minimax:global"]?.provider).toBe("minimax");
expect(cfg.auth?.profiles?.["minimax:global"]?.mode).toBe("api_key");
expect(cfg.models?.providers?.minimax?.baseUrl).toBe(MINIMAX_API_BASE_URL);
expect(cfg.agents?.defaults?.model?.primary).toBe("minimax/MiniMax-M2.5");
await expectApiKeyProfile({
profileId: "minimax:default",
profileId: "minimax:global",
provider: "minimax",
key: "sk-minimax-test",
});
@@ -202,17 +202,17 @@ describe("onboard (non-interactive): provider auth", () => {
it("supports MiniMax CN API endpoint auth choice", async () => {
await withOnboardEnv("openclaw-onboard-minimax-cn-", async (env) => {
const cfg = await runOnboardingAndReadConfig(env, {
authChoice: "minimax-api-key-cn",
authChoice: "minimax-cn-api",
minimaxApiKey: "sk-minimax-test", // pragma: allowlist secret
});
expect(cfg.auth?.profiles?.["minimax-cn:default"]?.provider).toBe("minimax-cn");
expect(cfg.auth?.profiles?.["minimax-cn:default"]?.mode).toBe("api_key");
expect(cfg.models?.providers?.["minimax-cn"]?.baseUrl).toBe(MINIMAX_CN_API_BASE_URL);
expect(cfg.agents?.defaults?.model?.primary).toBe("minimax-cn/MiniMax-M2.5");
expect(cfg.auth?.profiles?.["minimax:cn"]?.provider).toBe("minimax");
expect(cfg.auth?.profiles?.["minimax:cn"]?.mode).toBe("api_key");
expect(cfg.models?.providers?.minimax?.baseUrl).toBe(MINIMAX_CN_API_BASE_URL);
expect(cfg.agents?.defaults?.model?.primary).toBe("minimax/MiniMax-M2.5");
await expectApiKeyProfile({
profileId: "minimax-cn:default",
provider: "minimax-cn",
profileId: "minimax:cn",
provider: "minimax",
key: "sk-minimax-test",
});
});

View File

@@ -21,7 +21,6 @@ import {
applyKimiCodeConfig,
applyMinimaxApiConfig,
applyMinimaxApiConfigCn,
applyMinimaxConfig,
applyMoonshotConfig,
applyMoonshotConfigCn,
applyOpencodeGoConfig,
@@ -863,22 +862,37 @@ export async function applyNonInteractiveAuthChoice(params: {
return applyVeniceConfig(nextConfig);
}
if (
authChoice === "minimax-cloud" ||
authChoice === "minimax-api" ||
authChoice === "minimax-api-key-cn" ||
authChoice === "minimax-api-lightning"
) {
const isCn = authChoice === "minimax-api-key-cn";
const providerId = isCn ? "minimax-cn" : "minimax";
const profileId = `${providerId}:default`;
// Legacy aliases: these choice values were removed; fail with an actionable message so
// existing CI automation gets a clear error instead of silently exiting 0 with no auth.
const REMOVED_MINIMAX_CHOICES: Record<string, string> = {
minimax: "minimax-global-api",
"minimax-api": "minimax-global-api",
"minimax-cloud": "minimax-global-api",
"minimax-api-lightning": "minimax-global-api",
"minimax-api-key-cn": "minimax-cn-api",
};
if (Object.prototype.hasOwnProperty.call(REMOVED_MINIMAX_CHOICES, authChoice as string)) {
const replacement = REMOVED_MINIMAX_CHOICES[authChoice as string];
runtime.error(
`"${authChoice as string}" is no longer supported. Use --auth-choice ${replacement} instead.`,
);
runtime.exit(1);
return null;
}
if (authChoice === "minimax-global-api" || authChoice === "minimax-cn-api") {
const isCn = authChoice === "minimax-cn-api";
const profileId = isCn ? "minimax:cn" : "minimax:global";
const resolved = await resolveApiKey({
provider: providerId,
provider: "minimax",
cfg: baseConfig,
flagValue: opts.minimaxApiKey,
flagName: "--minimax-api-key",
envVar: "MINIMAX_API_KEY",
runtime,
// Disable profile fallback: both regions share provider "minimax", so an existing
// Global profile key must not be silently reused when configuring CN (and vice versa).
allowProfile: false,
});
if (!resolved) {
return null;
@@ -892,18 +906,10 @@ export async function applyNonInteractiveAuthChoice(params: {
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId,
provider: providerId,
provider: "minimax",
mode: "api_key",
});
const modelId =
authChoice === "minimax-api-lightning" ? "MiniMax-M2.5-highspeed" : "MiniMax-M2.5";
return isCn
? applyMinimaxApiConfigCn(nextConfig, modelId)
: applyMinimaxApiConfig(nextConfig, modelId);
}
if (authChoice === "minimax") {
return applyMinimaxConfig(nextConfig);
return isCn ? applyMinimaxApiConfigCn(nextConfig) : applyMinimaxApiConfig(nextConfig);
}
if (authChoice === "opencode-zen") {
@@ -1091,7 +1097,8 @@ export async function applyNonInteractiveAuthChoice(params: {
authChoice === "chutes" ||
authChoice === "openai-codex" ||
authChoice === "qwen-portal" ||
authChoice === "minimax-portal"
authChoice === "minimax-global-oauth" ||
authChoice === "minimax-cn-oauth"
) {
runtime.error("OAuth requires interactive mode.");
runtime.exit(1);

View File

@@ -126,7 +126,7 @@ export const ONBOARD_PROVIDER_AUTH_FLAGS: ReadonlyArray<OnboardProviderAuthFlag>
},
{
optionKey: "minimaxApiKey",
authChoice: "minimax-api",
authChoice: "minimax-global-api",
cliFlag: "--minimax-api-key",
cliOption: "--minimax-api-key <key>",
description: "MiniMax API key",

View File

@@ -35,12 +35,10 @@ export type AuthChoice =
| "zai-global"
| "zai-cn"
| "xiaomi-api-key"
| "minimax-cloud"
| "minimax"
| "minimax-api"
| "minimax-api-key-cn"
| "minimax-api-lightning"
| "minimax-portal"
| "minimax-global-oauth"
| "minimax-global-api"
| "minimax-cn-oauth"
| "minimax-cn-api"
| "opencode-zen"
| "opencode-go"
| "github-copilot"