diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index b26cefeb11c..b4a697d5a5a 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -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, diff --git a/docs/providers/minimax.md b/docs/providers/minimax.md index f060c637de8..8cdc5b028f6 100644 --- a/docs/providers/minimax.md +++ b/docs/providers/minimax.md @@ -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, diff --git a/src/agents/models-config.merge.test.ts b/src/agents/models-config.merge.test.ts index 9d7786af524..b84d4e363d6 100644 --- a/src/agents/models-config.merge.test.ts +++ b/src/agents/models-config.merge.test.ts @@ -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({ diff --git a/src/agents/models-config.providers.static.ts b/src/agents/models-config.providers.static.ts index c0361d02762..a0aa879c727 100644 --- a/src/agents/models-config.providers.static.ts +++ b/src/agents/models-config.providers.static.ts @@ -187,7 +187,7 @@ const MODELSTUDIO_MODEL_CATALOG: ReadonlyArray = [ { id: "MiniMax-M2.5", name: "MiniMax-M2.5", - reasoning: false, + reasoning: true, input: ["text"], cost: MODELSTUDIO_DEFAULT_COST, contextWindow: 1_000_000, diff --git a/src/commands/auth-choice-legacy.ts b/src/commands/auth-choice-legacy.ts index e93e920503f..d14ab4c6322 100644 --- a/src/commands/auth-choice-legacy.ts +++ b/src/commands/auth-choice-legacy.ts @@ -5,8 +5,6 @@ export const AUTH_CHOICE_LEGACY_ALIASES_FOR_CLI: ReadonlyArray = [ "oauth", "claude-cli", "codex-cli", - "minimax-cloud", - "minimax", ]; export function normalizeLegacyOnboardAuthChoice( diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 077fee024b9..e5bab0e29fe 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -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 = [ 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 = [ 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", diff --git a/src/commands/auth-choice.apply.minimax.test.ts b/src/commands/auth-choice.apply.minimax.test.ts index 5998fde9484..9b5442b108c 100644 --- a/src/commands/auth-choice.apply.minimax.test.ts +++ b/src/commands/auth-choice.apply.minimax.test.ts @@ -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[0]["authChoice"]; opts?: Parameters[0]["opts"]; - env?: { apiKey?: string; oauthToken?: string }; - prompter?: Parameters[0]; + env?: { apiKey?: string }; + prompterText?: () => Promise; }) { 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"); }); }); diff --git a/src/commands/auth-choice.apply.minimax.ts b/src/commands/auth-choice.apply.minimax.ts index 86e5a485afd..1a381b908b8 100644 --- a/src/commands/auth-choice.apply.minimax.ts +++ b/src/commands/auth-choice.apply.minimax.ts @@ -12,130 +12,93 @@ import { applyMinimaxApiConfigCn, applyMinimaxApiProviderConfig, applyMinimaxApiProviderConfigCn, - applyMinimaxConfig, - applyMinimaxProviderConfig, setMinimaxApiKey, } from "./onboard-auth.js"; export async function applyAuthChoiceMiniMax( params: ApplyAuthChoiceParams, ): Promise { - 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 => { + // 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 => { - 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 }; } diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index 7ebc0b24ea1..8adf42f3984 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -34,11 +34,10 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { "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> = { "qwen-portal": "qwen-portal", "volcengine-api-key": "volcengine", "byteplus-api-key": "byteplus", - "minimax-portal": "minimax-portal", "qianfan-api-key": "qianfan", "custom-api-key": "custom", }; diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index 6cdf32fa1d2..8651d5d024d 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -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) { diff --git a/src/commands/onboard-auth.config-minimax.ts b/src/commands/onboard-auth.config-minimax.ts index 04c109f7e56..14ec734592b 100644 --- a/src/commands/onboard-auth.config-minimax.ts +++ b/src/commands/onboard-auth.config-minimax.ts @@ -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, + }); +} diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index cda460b6c19..f51e61a8cee 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -50,10 +50,6 @@ export { applyMinimaxApiConfigCn, applyMinimaxApiProviderConfig, applyMinimaxApiProviderConfigCn, - applyMinimaxConfig, - applyMinimaxHostedConfig, - applyMinimaxHostedProviderConfig, - applyMinimaxProviderConfig, } from "./onboard-auth.config-minimax.js"; export { diff --git a/src/commands/onboard-non-interactive.provider-auth.test.ts b/src/commands/onboard-non-interactive.provider-auth.test.ts index 9606b70259f..0c0e2f38fad 100644 --- a/src/commands/onboard-non-interactive.provider-auth.test.ts +++ b/src/commands/onboard-non-interactive.provider-auth.test.ts @@ -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", }); }); diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index af119c12efe..a4a5cc6e4c4 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -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 = { + 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); diff --git a/src/commands/onboard-provider-auth-flags.ts b/src/commands/onboard-provider-auth-flags.ts index 7610727097f..53df8cdc4c8 100644 --- a/src/commands/onboard-provider-auth-flags.ts +++ b/src/commands/onboard-provider-auth-flags.ts @@ -126,7 +126,7 @@ export const ONBOARD_PROVIDER_AUTH_FLAGS: ReadonlyArray }, { optionKey: "minimaxApiKey", - authChoice: "minimax-api", + authChoice: "minimax-global-api", cliFlag: "--minimax-api-key", cliOption: "--minimax-api-key ", description: "MiniMax API key", diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index 40a02e85c15..ef92d5ba02f 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -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"