From 1ba266a8e80e6ef07600305c1e2548648427fcd6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Feb 2026 13:37:04 +0100 Subject: [PATCH] refactor: split minimax-cn provider --- src/commands/auth-choice.apply.minimax.ts | 80 +++++++++---------- src/commands/auth-choice.e2e.test.ts | 8 +- .../auth-choice.preferred-provider.ts | 2 +- src/commands/onboard-auth.config-minimax.ts | 68 ++++++++++------ src/commands/onboard-auth.credentials.ts | 11 ++- ...-non-interactive.provider-auth.e2e.test.ts | 15 ++-- .../local/auth-choice.ts | 13 +-- 7 files changed, 111 insertions(+), 86 deletions(-) diff --git a/src/commands/auth-choice.apply.minimax.ts b/src/commands/auth-choice.apply.minimax.ts index 6282b4821..f28d648c9 100644 --- a/src/commands/auth-choice.apply.minimax.ts +++ b/src/commands/auth-choice.apply.minimax.ts @@ -23,6 +23,30 @@ export async function applyAuthChoiceMiniMax( ): Promise { let nextConfig = params.config; let agentModelOverride: string | undefined; + const ensureMinimaxApiKey = async (opts: { + profileId: string; + promptMessage: string; + }): Promise => { + let hasCredential = false; + const envKey = resolveEnvApiKey("minimax"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing MINIMAX_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setMinimaxApiKey(envKey.apiKey, params.agentDir, opts.profileId); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: opts.promptMessage, + validate: validateApiKeyInput, + }); + await setMinimaxApiKey(normalizeApiKeyInput(String(key)), params.agentDir, opts.profileId); + } + }; const noteAgentModel = async (model: string) => { if (!params.agentId) { return; @@ -58,25 +82,10 @@ export async function applyAuthChoiceMiniMax( ) { const modelId = params.authChoice === "minimax-api-lightning" ? "MiniMax-M2.5-Lightning" : "MiniMax-M2.5"; - let hasCredential = false; - const envKey = resolveEnvApiKey("minimax"); - if (envKey) { - const useExisting = await params.prompter.confirm({ - message: `Use existing MINIMAX_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, - initialValue: true, - }); - if (useExisting) { - await setMinimaxApiKey(envKey.apiKey, params.agentDir); - hasCredential = true; - } - } - if (!hasCredential) { - const key = await params.prompter.text({ - message: "Enter MiniMax API key", - validate: validateApiKeyInput, - }); - await setMinimaxApiKey(normalizeApiKeyInput(String(key)), params.agentDir); - } + await ensureMinimaxApiKey({ + profileId: "minimax:default", + promptMessage: "Enter MiniMax API key", + }); nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "minimax:default", provider: "minimax", @@ -101,38 +110,23 @@ export async function applyAuthChoiceMiniMax( if (params.authChoice === "minimax-api-key-cn") { const modelId = "MiniMax-M2.5"; - let hasCredential = false; - const envKey = resolveEnvApiKey("minimax"); - if (envKey) { - const useExisting = await params.prompter.confirm({ - message: `Use existing MINIMAX_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, - initialValue: true, - }); - if (useExisting) { - await setMinimaxApiKey(envKey.apiKey, params.agentDir); - hasCredential = true; - } - } - if (!hasCredential) { - const key = await params.prompter.text({ - message: "Enter MiniMax China API key", - validate: validateApiKeyInput, - }); - await setMinimaxApiKey(normalizeApiKeyInput(String(key)), params.agentDir); - } + await ensureMinimaxApiKey({ + profileId: "minimax-cn:default", + promptMessage: "Enter MiniMax China API key", + }); nextConfig = applyAuthProfileConfig(nextConfig, { - profileId: "minimax:default", - provider: "minimax", + profileId: "minimax-cn:default", + provider: "minimax-cn", mode: "api_key", }); { - const modelRef = `minimax/${modelId}`; + const modelRef = `minimax-cn/${modelId}`; const applied = await applyDefaultModelChoice({ config: nextConfig, setDefaultModel: params.setDefaultModel, defaultModel: modelRef, - applyDefaultConfig: applyMinimaxApiConfigCn, - applyProviderConfig: applyMinimaxApiProviderConfigCn, + applyDefaultConfig: (config) => applyMinimaxApiConfigCn(config, modelId), + applyProviderConfig: (config) => applyMinimaxApiProviderConfigCn(config, modelId), noteAgentModel, prompter: params.prompter, }); diff --git a/src/commands/auth-choice.e2e.test.ts b/src/commands/auth-choice.e2e.test.ts index 77b20e162..9bdf233f5 100644 --- a/src/commands/auth-choice.e2e.test.ts +++ b/src/commands/auth-choice.e2e.test.ts @@ -253,18 +253,18 @@ describe("applyAuthChoice", () => { expect(text).toHaveBeenCalledWith( expect.objectContaining({ message: "Enter MiniMax China API key" }), ); - expect(result.config.auth?.profiles?.["minimax:default"]).toMatchObject({ - provider: "minimax", + expect(result.config.auth?.profiles?.["minimax-cn:default"]).toMatchObject({ + provider: "minimax-cn", mode: "api_key", }); - expect(result.config.models?.providers?.minimax?.baseUrl).toBe(MINIMAX_CN_API_BASE_URL); + expect(result.config.models?.providers?.["minimax-cn"]?.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"); + expect(parsed.profiles?.["minimax-cn:default"]?.key).toBe("sk-minimax-test"); }); it("prompts and writes Synthetic API key when selecting synthetic-api-key", async () => { diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index f17a9be38..05eaa83ea 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -34,7 +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-key-cn": "minimax-cn", "minimax-api-lightning": "minimax", minimax: "lmstudio", "opencode-zen": "opencode", diff --git a/src/commands/onboard-auth.config-minimax.ts b/src/commands/onboard-auth.config-minimax.ts index b92eb6360..48eed2f8a 100644 --- a/src/commands/onboard-auth.config-minimax.ts +++ b/src/commands/onboard-auth.config-minimax.ts @@ -1,4 +1,5 @@ import type { OpenClawConfig } from "../config/config.js"; +import type { ModelProviderConfig } from "../config/types.models.js"; import { buildMinimaxApiModelDefinition, buildMinimaxModelDefinition, @@ -151,14 +152,22 @@ export function applyMinimaxApiProviderConfig( cfg: OpenClawConfig, modelId: string = "MiniMax-M2.5", ): OpenClawConfig { - return applyMinimaxApiProviderConfigWithBaseUrl(cfg, modelId, MINIMAX_API_BASE_URL); + 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, modelId, MINIMAX_API_BASE_URL); + return applyMinimaxApiConfigWithBaseUrl(cfg, { + providerId: "minimax", + modelId, + baseUrl: MINIMAX_API_BASE_URL, + }); } // MiniMax China API (api.minimaxi.com) @@ -166,44 +175,58 @@ export function applyMinimaxApiProviderConfigCn( cfg: OpenClawConfig, modelId: string = "MiniMax-M2.5", ): OpenClawConfig { - return applyMinimaxApiProviderConfigWithBaseUrl(cfg, modelId, MINIMAX_CN_API_BASE_URL); + 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, modelId, MINIMAX_CN_API_BASE_URL); + return applyMinimaxApiConfigWithBaseUrl(cfg, { + providerId: "minimax-cn", + modelId, + baseUrl: MINIMAX_CN_API_BASE_URL, + }); } +type MinimaxApiProviderConfigParams = { + providerId: string; + modelId: string; + baseUrl: string; +}; + function applyMinimaxApiProviderConfigWithBaseUrl( cfg: OpenClawConfig, - modelId: string, - baseUrl: string, + params: MinimaxApiProviderConfigParams, ): OpenClawConfig { - const providers = { ...cfg.models?.providers }; - const existingProvider = providers.minimax; - const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; - const apiModel = buildMinimaxApiModelDefinition(modelId); - const hasApiModel = existingModels.some((model) => model.id === modelId); + const providers = { ...cfg.models?.providers } as Record; + const existingProvider = providers[params.providerId]; + const existingModels = existingProvider?.models ?? []; + const apiModel = buildMinimaxApiModelDefinition(params.modelId); + const hasApiModel = existingModels.some((model) => model.id === params.modelId); const mergedModels = hasApiModel ? existingModels : [...existingModels, apiModel]; - const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< - string, - unknown - > as { apiKey?: string }; + const { apiKey: existingApiKey, ...existingProviderRest } = existingProvider ?? { + baseUrl: params.baseUrl, + models: [], + }; const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; const normalizedApiKey = resolvedApiKey?.trim() === "minimax" ? "" : resolvedApiKey; - providers.minimax = { + providers[params.providerId] = { ...existingProviderRest, - baseUrl, + baseUrl: params.baseUrl, api: "anthropic-messages", ...(normalizedApiKey?.trim() ? { apiKey: normalizedApiKey } : {}), models: mergedModels.length > 0 ? mergedModels : [apiModel], }; const models = { ...cfg.agents?.defaults?.models }; - models[`minimax/${modelId}`] = { - ...models[`minimax/${modelId}`], + const modelRef = `${params.providerId}/${params.modelId}`; + models[modelRef] = { + ...models[modelRef], alias: "Minimax", }; @@ -222,10 +245,9 @@ function applyMinimaxApiProviderConfigWithBaseUrl( function applyMinimaxApiConfigWithBaseUrl( cfg: OpenClawConfig, - modelId: string, - baseUrl: string, + params: MinimaxApiProviderConfigParams, ): OpenClawConfig { - const next = applyMinimaxApiProviderConfigWithBaseUrl(cfg, modelId, baseUrl); + const next = applyMinimaxApiProviderConfigWithBaseUrl(cfg, params); return { ...next, agents: { @@ -239,7 +261,7 @@ function applyMinimaxApiConfigWithBaseUrl( fallbacks: (next.agents.defaults.model as { fallbacks?: string[] }).fallbacks, } : undefined), - primary: `minimax/${modelId}`, + primary: `${params.providerId}/${params.modelId}`, }, }, }, diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index a78ee4a16..c99b28a5b 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -50,13 +50,18 @@ export async function setGeminiApiKey(key: string, agentDir?: string) { }); } -export async function setMinimaxApiKey(key: string, agentDir?: string) { +export async function setMinimaxApiKey( + key: string, + agentDir?: string, + profileId: string = "minimax:default", +) { + const provider = profileId.split(":")[0] ?? "minimax"; // Write to resolved agent dir so gateway finds credentials on startup. upsertAuthProfile({ - profileId: "minimax:default", + profileId, credential: { type: "api_key", - provider: "minimax", + provider, key, }, agentDir: resolveAuthAgentDir(agentDir), 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 89cc4ebd9..eb1d17f3c 100644 --- a/src/commands/onboard-non-interactive.provider-auth.e2e.test.ts +++ b/src/commands/onboard-non-interactive.provider-auth.e2e.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { setTimeout as delay } from "node:timers/promises"; import { describe, expect, it } from "vitest"; +import { MINIMAX_API_BASE_URL, MINIMAX_CN_API_BASE_URL } from "./onboard-auth.js"; import { OPENAI_DEFAULT_MODEL } from "./openai-model-default.js"; type RuntimeMock = { @@ -178,7 +179,7 @@ describe("onboard (non-interactive): provider auth", () => { 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.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", @@ -209,13 +210,13 @@ describe("onboard (non-interactive): provider auth", () => { 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"); + 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"); await expectApiKeyProfile({ - profileId: "minimax:default", - provider: "minimax", + profileId: "minimax-cn:default", + provider: "minimax-cn", 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 47674996e..9ba6bfd64 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -574,8 +574,11 @@ export async function applyNonInteractiveAuthChoice(params: { 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`; const resolved = await resolveNonInteractiveApiKey({ - provider: "minimax", + provider: providerId, cfg: baseConfig, flagValue: opts.minimaxApiKey, flagName: "--minimax-api-key", @@ -586,16 +589,16 @@ export async function applyNonInteractiveAuthChoice(params: { return null; } if (resolved.source !== "profile") { - await setMinimaxApiKey(resolved.key); + await setMinimaxApiKey(resolved.key, undefined, profileId); } nextConfig = applyAuthProfileConfig(nextConfig, { - profileId: "minimax:default", - provider: "minimax", + profileId, + provider: providerId, mode: "api_key", }); const modelId = authChoice === "minimax-api-lightning" ? "MiniMax-M2.5-Lightning" : "MiniMax-M2.5"; - return authChoice === "minimax-api-key-cn" + return isCn ? applyMinimaxApiConfigCn(nextConfig, modelId) : applyMinimaxApiConfig(nextConfig, modelId); }