From 444db8c527f534c40fbc37c4fdefe2c55afb87c1 Mon Sep 17 00:00:00 2001 From: xiaose Date: Thu, 29 Jan 2026 17:17:06 +0800 Subject: [PATCH] feat: code --- extensions/minimax-portal-auth/index.ts | 12 ++-- src/agents/auth-profiles/constants.ts | 1 + src/agents/auth-profiles/external-cli-sync.ts | 56 ++++++++++++++++- src/agents/cli-credentials.ts | 60 +++++++++++++++++++ src/agents/models-config.providers.ts | 28 +++++++++ src/commands/auth-choice-options.ts | 8 ++- .../auth-choice.preferred-provider.ts | 1 + .../local/auth-choice.ts | 3 +- src/config/plugin-auto-enable.ts | 1 + 9 files changed, 160 insertions(+), 10 deletions(-) diff --git a/extensions/minimax-portal-auth/index.ts b/extensions/minimax-portal-auth/index.ts index f36b26b24..c0692fdbf 100644 --- a/extensions/minimax-portal-auth/index.ts +++ b/extensions/minimax-portal-auth/index.ts @@ -9,12 +9,16 @@ const DEFAULT_BASE_URL_CN = "https://api.minimaxi.com/anthropic"; const DEFAULT_BASE_URL_GLOBAL = "https://api.minimax.io/anthropic"; const DEFAULT_CONTEXT_WINDOW = 200000; const DEFAULT_MAX_TOKENS = 8192; -const OAUTH_PLACEHOLDER = "minimax-portal-oauth"; +const OAUTH_PLACEHOLDER = "minimax-oauth"; function getDefaultBaseUrl(region: MiniMaxRegion): string { return region === "cn" ? DEFAULT_BASE_URL_CN : DEFAULT_BASE_URL_GLOBAL; } +function modelRef(modelId: string): string { + return `${PROVIDER_ID}/${modelId}`; +} + function buildModelDefinition(params: { id: string; name: string; input: Array<"text" | "image"> }) { return { id: params.id, @@ -89,13 +93,13 @@ function createOAuthHandler(region: MiniMaxRegion) { agents: { defaults: { models: { - "MiniMax-M2.1": { alias: "minimax-m2.1" }, - "MiniMax-M2.1-lightning": { alias: "minimax-m2.1-lightning" }, + [modelRef("MiniMax-M2.1")]: { alias: "minimax-m2.1" }, + [modelRef("MiniMax-M2.1-lightning")]: { alias: "minimax-m2.1-lightning" }, }, }, }, }, - defaultModel: DEFAULT_MODEL, + defaultModel: modelRef(DEFAULT_MODEL), notes: [ "MiniMax OAuth tokens auto-refresh. Re-run login if refresh fails or access is revoked.", `Base URL defaults to ${defaultBaseUrl}. Override models.providers.${PROVIDER_ID}.baseUrl if needed.`, diff --git a/src/agents/auth-profiles/constants.ts b/src/agents/auth-profiles/constants.ts index 65c2f7a54..83946ac7a 100644 --- a/src/agents/auth-profiles/constants.ts +++ b/src/agents/auth-profiles/constants.ts @@ -7,6 +7,7 @@ export const LEGACY_AUTH_FILENAME = "auth.json"; export const CLAUDE_CLI_PROFILE_ID = "anthropic:claude-cli"; export const CODEX_CLI_PROFILE_ID = "openai-codex:codex-cli"; export const QWEN_CLI_PROFILE_ID = "qwen-portal:qwen-cli"; +export const MINIMAX_CLI_PROFILE_ID = "minimax-portal:minimax-cli"; export const AUTH_STORE_LOCK_OPTIONS = { retries: { diff --git a/src/agents/auth-profiles/external-cli-sync.ts b/src/agents/auth-profiles/external-cli-sync.ts index d1fa31f23..361ed67e9 100644 --- a/src/agents/auth-profiles/external-cli-sync.ts +++ b/src/agents/auth-profiles/external-cli-sync.ts @@ -1,8 +1,12 @@ -import { readQwenCliCredentialsCached } from "../cli-credentials.js"; +import { + readQwenCliCredentialsCached, + readMiniMaxCliCredentialsCached, +} from "../cli-credentials.js"; import { EXTERNAL_CLI_NEAR_EXPIRY_MS, EXTERNAL_CLI_SYNC_TTL_MS, QWEN_CLI_PROFILE_ID, + MINIMAX_CLI_PROFILE_ID, log, } from "./constants.js"; import type { AuthProfileCredential, AuthProfileStore, OAuthCredential } from "./types.js"; @@ -25,15 +29,48 @@ function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCr function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: number): boolean { if (!cred) return false; if (cred.type !== "oauth" && cred.type !== "token") return false; - if (cred.provider !== "qwen-portal") { + if (cred.provider !== "qwen-portal" && cred.provider !== "minimax-portal") { return false; } if (typeof cred.expires !== "number") return true; return cred.expires > now + EXTERNAL_CLI_NEAR_EXPIRY_MS; } +/** Sync external CLI credentials into the store for a given provider. */ +function syncExternalCliCredentialsForProvider( + store: AuthProfileStore, + profileId: string, + provider: string, + readCredentials: () => OAuthCredential | null, + now: number, +): boolean { + const existing = store.profiles[profileId]; + const shouldSync = + !existing || existing.provider !== provider || !isExternalProfileFresh(existing, now); + const creds = shouldSync ? readCredentials() : null; + if (!creds) return false; + + const existingOAuth = existing?.type === "oauth" ? existing : undefined; + const shouldUpdate = + !existingOAuth || + existingOAuth.provider !== provider || + existingOAuth.expires <= now || + creds.expires > existingOAuth.expires; + + if (shouldUpdate && !shallowEqualOAuthCredentials(existingOAuth, creds)) { + store.profiles[profileId] = creds; + log.info(`synced ${provider} credentials from external cli`, { + profileId, + expires: new Date(creds.expires).toISOString(), + }); + return true; + } + + return false; +} + /** - * Sync OAuth credentials from external CLI tools (Qwen Code CLI) into the store. + * Sync OAuth credentials from external CLI tools (Qwen Code CLI, MiniMax CLI) into the store. * * Returns true if any credentials were updated. */ @@ -69,5 +106,18 @@ export function syncExternalCliCredentials(store: AuthProfileStore): boolean { } } + // Sync from MiniMax Portal CLI + if ( + syncExternalCliCredentialsForProvider( + store, + MINIMAX_CLI_PROFILE_ID, + "minimax-portal", + () => readMiniMaxCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }), + now, + ) + ) { + mutated = true; + } + return mutated; } diff --git a/src/agents/cli-credentials.ts b/src/agents/cli-credentials.ts index 5ca5629ff..18e73893b 100644 --- a/src/agents/cli-credentials.ts +++ b/src/agents/cli-credentials.ts @@ -14,6 +14,7 @@ const log = createSubsystemLogger("agents/auth-profiles"); const CLAUDE_CLI_CREDENTIALS_RELATIVE_PATH = ".claude/.credentials.json"; const CODEX_CLI_AUTH_FILENAME = "auth.json"; const QWEN_CLI_CREDENTIALS_RELATIVE_PATH = ".qwen/oauth_creds.json"; +const MINIMax_CLI_CREDENTIALS_RELATIVE_PATH = ".minimax/oauth_creds.json"; const CLAUDE_CLI_KEYCHAIN_SERVICE = "Claude Code-credentials"; const CLAUDE_CLI_KEYCHAIN_ACCOUNT = "Claude Code"; @@ -27,11 +28,13 @@ type CachedValue = { let claudeCliCache: CachedValue | null = null; let codexCliCache: CachedValue | null = null; let qwenCliCache: CachedValue | null = null; +let minimaxCliCache: CachedValue | null = null; export function resetCliCredentialCachesForTest(): void { claudeCliCache = null; codexCliCache = null; qwenCliCache = null; + minimaxCliCache = null; } export type ClaudeCliCredential = @@ -66,6 +69,14 @@ export type QwenCliCredential = { expires: number; }; +export type MiniMaxCliCredential = { + type: "oauth"; + provider: "minimax-portal"; + access: string; + refresh: string; + expires: number; +}; + type ClaudeCliFileOptions = { homeDir?: string; }; @@ -102,6 +113,11 @@ function resolveQwenCliCredentialsPath(homeDir?: string) { return path.join(baseDir, QWEN_CLI_CREDENTIALS_RELATIVE_PATH); } +function resolveMiniMaxCliCredentialsPath(homeDir?: string) { + const baseDir = homeDir ?? resolveUserPath("~"); + return path.join(baseDir, MINIMax_CLI_CREDENTIALS_RELATIVE_PATH); +} + function computeCodexKeychainAccount(codexHome: string) { const hash = createHash("sha256").update(codexHome).digest("hex"); return `cli|${hash.slice(0, 16)}`; @@ -186,6 +202,28 @@ function readQwenCliCredentials(options?: { homeDir?: string }): QwenCliCredenti }; } +function readMiniMaxCliCredentials(options?: { homeDir?: string }): MiniMaxCliCredential | null { + const credPath = resolveMiniMaxCliCredentialsPath(options?.homeDir); + const raw = loadJsonFile(credPath); + if (!raw || typeof raw !== "object") return null; + const data = raw as Record; + const accessToken = data.access_token; + const refreshToken = data.refresh_token; + const expiresAt = data.expiry_date; + + if (typeof accessToken !== "string" || !accessToken) return null; + if (typeof refreshToken !== "string" || !refreshToken) return null; + if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return null; + + return { + type: "oauth", + provider: "minimax-portal", + access: accessToken, + refresh: refreshToken, + expires: expiresAt, + }; +} + function readClaudeCliKeychainCredentials( execSyncImpl: ExecSyncFn = execSync, ): ClaudeCliCredential | null { @@ -497,3 +535,25 @@ export function readQwenCliCredentialsCached(options?: { } return value; } + +export function readMiniMaxCliCredentialsCached(options?: { + ttlMs?: number; + homeDir?: string; +}): MiniMaxCliCredential | null { + const ttlMs = options?.ttlMs ?? 0; + const now = Date.now(); + const cacheKey = resolveMiniMaxCliCredentialsPath(options?.homeDir); + if ( + ttlMs > 0 && + minimaxCliCache && + minimaxCliCache.cacheKey === cacheKey && + now - minimaxCliCache.readAt < ttlMs + ) { + return minimaxCliCache.value; + } + const value = readMiniMaxCliCredentials({ homeDir: options?.homeDir }); + if (ttlMs > 0) { + minimaxCliCache = { value, readAt: now, cacheKey }; + } + return value; +} diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index a176dac8a..c19846253 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -18,10 +18,12 @@ type ModelsConfig = NonNullable; export type ProviderConfig = NonNullable[string]; const MINIMAX_API_BASE_URL = "https://api.minimax.chat/v1"; +const MINIMAX_PORTAL_BASE_URL = "https://api.minimax.io/anthropic"; const MINIMAX_DEFAULT_MODEL_ID = "MiniMax-M2.1"; const MINIMAX_DEFAULT_VISION_MODEL_ID = "MiniMax-VL-01"; const MINIMAX_DEFAULT_CONTEXT_WINDOW = 200000; const MINIMAX_DEFAULT_MAX_TOKENS = 8192; +const MINIMAX_OAUTH_PLACEHOLDER = "minimax-oauth"; // Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs. const MINIMAX_API_COST = { input: 15, @@ -268,6 +270,24 @@ function buildMinimaxProvider(): ProviderConfig { }; } +function buildMinimaxPortalProvider(): ProviderConfig { + return { + baseUrl: MINIMAX_PORTAL_BASE_URL, + api: "anthropic-messages", + models: [ + { + id: MINIMAX_DEFAULT_MODEL_ID, + name: "MiniMax M2.1", + reasoning: false, + input: ["text"], + cost: MINIMAX_API_COST, + contextWindow: MINIMAX_DEFAULT_CONTEXT_WINDOW, + maxTokens: MINIMAX_DEFAULT_MAX_TOKENS, + }, + ], + }; +} + function buildMoonshotProvider(): ProviderConfig { return { baseUrl: MOONSHOT_BASE_URL, @@ -374,6 +394,14 @@ export async function resolveImplicitProviders(params: { providers.minimax = { ...buildMinimaxProvider(), apiKey: minimaxKey }; } + const minimaxOauthProfile = listProfilesForProvider(authStore, "minimax-portal"); + if (minimaxOauthProfile.length > 0) { + providers["minimax-portal"] = { + ...buildMinimaxPortalProvider(), + apiKey: MINIMAX_OAUTH_PLACEHOLDER, + }; + } + const moonshotKey = resolveEnvApiKeyVarName("moonshot") ?? resolveApiKeyFromProfiles({ provider: "moonshot", store: authStore }); diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index d5c59a781..3b75b536b 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -51,7 +51,7 @@ const AUTH_CHOICE_GROUP_DEFS: { value: "minimax", label: "MiniMax", hint: "M2.1 (recommended)", - choices: ["minimax-api", "minimax-api-lightning","minimax-portal"], + choices: ["minimax-portal", "minimax-api", "minimax-api-lightning"], }, { value: "qwen", @@ -164,7 +164,11 @@ export function buildAuthChoiceOptions(params: { hint: "Uses the bundled Gemini CLI auth plugin", }); options.push({ value: "zai-api-key", label: "Z.AI (GLM 4.7) API key" }); - options.push({ value: "minimax-portal", label: "MiniMax OAuth" }); + options.push({ + value: "minimax-portal", + label: "MiniMax OAuth", + hint: "OAuth new users enjoy a 3-day free trial of the MiniMax Coding Plan!", + }); options.push({ value: "qwen-portal", label: "Qwen OAuth" }); options.push({ value: "copilot-proxy", diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index 6fe26b59a..a8d193dcb 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -28,6 +28,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { minimax: "lmstudio", "opencode-zen": "opencode", "qwen-portal": "qwen-portal", + "minimax-portal": "minimax-portal", }; export function resolvePreferredProviderForAuthChoice(choice: AuthChoice): string | undefined { diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 7d952730c..20eee8a2c 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -359,7 +359,8 @@ export async function applyNonInteractiveAuthChoice(params: { authChoice === "oauth" || authChoice === "chutes" || authChoice === "openai-codex" || - authChoice === "qwen-portal" + authChoice === "qwen-portal" || + authChoice === "minimax-portal" ) { runtime.error("OAuth requires interactive mode."); runtime.exit(1); diff --git a/src/config/plugin-auto-enable.ts b/src/config/plugin-auto-enable.ts index a7632e41f..1de355893 100644 --- a/src/config/plugin-auto-enable.ts +++ b/src/config/plugin-auto-enable.ts @@ -33,6 +33,7 @@ const PROVIDER_PLUGIN_IDS: Array<{ pluginId: string; providerId: string }> = [ { pluginId: "google-gemini-cli-auth", providerId: "google-gemini-cli" }, { pluginId: "qwen-portal-auth", providerId: "qwen-portal" }, { pluginId: "copilot-proxy", providerId: "copilot-proxy" }, + { pluginId: "minimax-portal-auth", providerId: "minimax-portal" }, ]; function isRecord(value: unknown): value is Record {