From 0cfea46293b351a95184cc002a287d143fc8eda8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Feb 2026 13:16:18 +0100 Subject: [PATCH] fix: wire minimax-api-key-cn onboarding (#15191) (thanks @liuy) --- .gitignore | 2 +- CHANGELOG.md | 1 + src/commands/auth-choice-options.e2e.test.ts | 1 + src/commands/auth-choice-options.ts | 7 +- src/commands/auth-choice.e2e.test.ts | 60 ++++++++++++++++- .../auth-choice.preferred-provider.ts | 1 + ...-non-interactive.provider-auth.e2e.test.ts | 66 +++++++++++++++++++ .../local/auth-choice.ts | 8 ++- 8 files changed, 141 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index dc290f269a..e8c8baf330 100644 --- a/.gitignore +++ b/.gitignore @@ -82,5 +82,5 @@ USER.md /memory/ .agent/*.json !.agent/workflows/ -local/ +/local/ package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md index dbf0587426..05cae16fef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai - Skills: remove duplicate `local-places` Google Places skill/proxy and keep `goplaces` as the single supported Google Places path. - Agents: add pre-prompt context diagnostics (`messages`, `systemPromptChars`, `promptChars`, provider/model, session file) before embedded runner prompt calls to improve overflow debugging. (#8930) Thanks @Glucksberg. - Onboarding/Providers: add first-class Hugging Face Inference provider support (provider wiring, onboarding auth choice/API key flow, and default-model selection), and preserve Hugging Face auth intent in auth-choice remapping (`tokenProvider=huggingface` with `authChoice=apiKey`) while skipping env-override prompts when an explicit token is provided. (#13472) Thanks @Josephrp. +- Onboarding/Providers: add `minimax-api-key-cn` auth choice for the MiniMax China API endpoint. (#15191) Thanks @liuy. ### Breaking diff --git a/src/commands/auth-choice-options.e2e.test.ts b/src/commands/auth-choice-options.e2e.test.ts index c87769507c..86cb6a2e40 100644 --- a/src/commands/auth-choice-options.e2e.test.ts +++ b/src/commands/auth-choice-options.e2e.test.ts @@ -54,6 +54,7 @@ describe("buildAuthChoiceOptions", () => { }); expect(options.some((opt) => opt.value === "minimax-api")).toBe(true); + expect(options.some((opt) => opt.value === "minimax-api-key-cn")).toBe(true); expect(options.some((opt) => opt.value === "minimax-api-lightning")).toBe(true); }); diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 0ad07239ee..424968dfda 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -50,7 +50,7 @@ const AUTH_CHOICE_GROUP_DEFS: { value: "minimax", label: "MiniMax", hint: "M2.5 (recommended)", - choices: ["minimax-portal", "minimax-api", "minimax-api-lightning"], + choices: ["minimax-portal", "minimax-api", "minimax-api-key-cn", "minimax-api-lightning"], }, { value: "moonshot", @@ -286,6 +286,11 @@ const BASE_AUTH_CHOICE_OPTIONS: ReadonlyArray = [ 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 Lightning", diff --git a/src/commands/auth-choice.e2e.test.ts b/src/commands/auth-choice.e2e.test.ts index ca2a8d0e36..77b20e1620 100644 --- a/src/commands/auth-choice.e2e.test.ts +++ b/src/commands/auth-choice.e2e.test.ts @@ -6,7 +6,11 @@ import type { RuntimeEnv } from "../runtime.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import type { AuthChoice } from "./onboard-types.js"; import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.js"; -import { ZAI_CODING_CN_BASE_URL, ZAI_CODING_GLOBAL_BASE_URL } from "./onboard-auth.js"; +import { + MINIMAX_CN_API_BASE_URL, + ZAI_CODING_CN_BASE_URL, + ZAI_CODING_GLOBAL_BASE_URL, +} from "./onboard-auth.js"; vi.mock("../providers/github-copilot-auth.js", () => ({ githubCopilotLoginCommand: vi.fn(async () => {}), @@ -209,6 +213,60 @@ describe("applyAuthChoice", () => { expect(parsed.profiles?.["minimax:default"]?.key).toBe("sk-minimax-test"); }); + it("prompts and writes MiniMax API key when selecting minimax-api-key-cn", async () => { + tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); + process.env.OPENCLAW_STATE_DIR = tempStateDir; + process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); + process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + + const text = vi.fn().mockResolvedValue("sk-minimax-test"); + const select: WizardPrompter["select"] = vi.fn( + async (params) => params.options[0]?.value as never, + ); + const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); + const prompter: WizardPrompter = { + intro: vi.fn(noopAsync), + outro: vi.fn(noopAsync), + note: vi.fn(noopAsync), + select, + multiselect, + text, + confirm: vi.fn(async () => false), + progress: vi.fn(() => ({ update: noop, stop: noop })), + }; + const runtime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn((code: number) => { + throw new Error(`exit:${code}`); + }), + }; + + const result = await applyAuthChoice({ + authChoice: "minimax-api-key-cn", + config: {}, + prompter, + runtime, + setDefaultModel: true, + }); + + expect(text).toHaveBeenCalledWith( + expect.objectContaining({ message: "Enter MiniMax China API key" }), + ); + expect(result.config.auth?.profiles?.["minimax:default"]).toMatchObject({ + provider: "minimax", + mode: "api_key", + }); + expect(result.config.models?.providers?.minimax?.baseUrl).toBe(MINIMAX_CN_API_BASE_URL); + + const authProfilePath = authProfilePathFor(requireAgentDir()); + const raw = await fs.readFile(authProfilePath, "utf8"); + const parsed = JSON.parse(raw) as { + profiles?: Record; + }; + expect(parsed.profiles?.["minimax:default"]?.key).toBe("sk-minimax-test"); + }); + it("prompts and writes Synthetic API key when selecting synthetic-api-key", async () => { tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); process.env.OPENCLAW_STATE_DIR = tempStateDir; diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index 19466971ef..f17a9be381 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -34,6 +34,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { "copilot-proxy": "copilot-proxy", "minimax-cloud": "minimax", "minimax-api": "minimax", + "minimax-api-key-cn": "minimax", "minimax-api-lightning": "minimax", minimax: "lmstudio", "opencode-zen": "opencode", diff --git a/src/commands/onboard-non-interactive.provider-auth.e2e.test.ts b/src/commands/onboard-non-interactive.provider-auth.e2e.test.ts index e4372b7a4c..89cc4ebd94 100644 --- a/src/commands/onboard-non-interactive.provider-auth.e2e.test.ts +++ b/src/commands/onboard-non-interactive.provider-auth.e2e.test.ts @@ -155,6 +155,72 @@ async function expectApiKeyProfile(params: { } describe("onboard (non-interactive): provider auth", () => { + it("stores MiniMax API key and uses global baseUrl by default", async () => { + await withOnboardEnv("openclaw-onboard-minimax-", async ({ configPath, runtime }) => { + await runNonInteractive( + { + nonInteractive: true, + authChoice: "minimax-api", + minimaxApiKey: "sk-minimax-test", + skipHealth: true, + skipChannels: true, + skipSkills: true, + json: true, + }, + runtime, + ); + + const cfg = await readJsonFile<{ + auth?: { profiles?: Record }; + agents?: { defaults?: { model?: { primary?: string } } }; + models?: { providers?: Record }; + }>(configPath); + + expect(cfg.auth?.profiles?.["minimax:default"]?.provider).toBe("minimax"); + expect(cfg.auth?.profiles?.["minimax:default"]?.mode).toBe("api_key"); + expect(cfg.models?.providers?.minimax?.baseUrl).toBe("https://api.minimax.io/anthropic"); + expect(cfg.agents?.defaults?.model?.primary).toBe("minimax/MiniMax-M2.5"); + await expectApiKeyProfile({ + profileId: "minimax:default", + provider: "minimax", + key: "sk-minimax-test", + }); + }); + }, 60_000); + + it("supports MiniMax CN API endpoint auth choice", async () => { + await withOnboardEnv("openclaw-onboard-minimax-cn-", async ({ configPath, runtime }) => { + await runNonInteractive( + { + nonInteractive: true, + authChoice: "minimax-api-key-cn", + minimaxApiKey: "sk-minimax-test", + skipHealth: true, + skipChannels: true, + skipSkills: true, + json: true, + }, + runtime, + ); + + const cfg = await readJsonFile<{ + auth?: { profiles?: Record }; + agents?: { defaults?: { model?: { primary?: string } } }; + models?: { providers?: Record }; + }>(configPath); + + expect(cfg.auth?.profiles?.["minimax:default"]?.provider).toBe("minimax"); + expect(cfg.auth?.profiles?.["minimax:default"]?.mode).toBe("api_key"); + expect(cfg.models?.providers?.minimax?.baseUrl).toBe("https://api.minimaxi.com/anthropic"); + expect(cfg.agents?.defaults?.model?.primary).toBe("minimax/MiniMax-M2.5"); + await expectApiKeyProfile({ + profileId: "minimax:default", + provider: "minimax", + key: "sk-minimax-test", + }); + }); + }, 60_000); + it("stores Z.AI API key and uses global baseUrl by default", async () => { await withOnboardEnv("openclaw-onboard-zai-", async ({ configPath, runtime }) => { await runNonInteractive( diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 962c1e0c7d..47674996eb 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -15,6 +15,7 @@ import { applyQianfanConfig, applyKimiCodeConfig, applyMinimaxApiConfig, + applyMinimaxApiConfigCn, applyMinimaxConfig, applyMoonshotConfig, applyMoonshotConfigCn, @@ -570,6 +571,7 @@ export async function applyNonInteractiveAuthChoice(params: { if ( authChoice === "minimax-cloud" || authChoice === "minimax-api" || + authChoice === "minimax-api-key-cn" || authChoice === "minimax-api-lightning" ) { const resolved = await resolveNonInteractiveApiKey({ @@ -592,8 +594,10 @@ export async function applyNonInteractiveAuthChoice(params: { mode: "api_key", }); const modelId = - authChoice === "minimax-api-lightning" ? "MiniMax-M2.1-lightning" : "MiniMax-M2.1"; - return applyMinimaxApiConfig(nextConfig, modelId); + authChoice === "minimax-api-lightning" ? "MiniMax-M2.5-Lightning" : "MiniMax-M2.5"; + return authChoice === "minimax-api-key-cn" + ? applyMinimaxApiConfigCn(nextConfig, modelId) + : applyMinimaxApiConfig(nextConfig, modelId); } if (authChoice === "minimax") {