diff --git a/extensions/minimax-portal-auth/README.md b/extensions/minimax-portal-auth/README.md index d0346f8f5..7fb216612 100644 --- a/extensions/minimax-portal-auth/README.md +++ b/extensions/minimax-portal-auth/README.md @@ -14,11 +14,25 @@ Restart the Gateway after enabling. ## Authenticate +### Global Endpoint (global user) + +Uses `api.minimax.io`: + ```bash clawdbot models auth login --provider minimax-portal --set-default ``` +### China Endpoint + +Uses `api.minimaxi.com`: + +```bash +clawdbot models auth login --provider minimax-portal --auth-id oauth-cn --set-default +``` + ## Notes - MiniMax OAuth uses a device-code login flow. - Tokens auto-refresh; re-run login if refresh fails or access is revoked. +- Global endpoint: `api.minimax.io` (default) +- China endpoint: `api.minimax.chat` (use `--auth-id oauth-cn`) diff --git a/extensions/minimax-portal-auth/index.ts b/extensions/minimax-portal-auth/index.ts index 3929a773c..188d6207a 100644 --- a/extensions/minimax-portal-auth/index.ts +++ b/extensions/minimax-portal-auth/index.ts @@ -1,15 +1,20 @@ import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk"; -import { loginMiniMaxPortalOAuth } from "./oauth.js"; +import { loginMiniMaxPortalOAuth, type MiniMaxRegion } from "./oauth.js"; const PROVIDER_ID = "minimax-portal"; const PROVIDER_LABEL = "MiniMax"; const DEFAULT_MODEL = "MiniMax-M2.1"; -const DEFAULT_BASE_URL = "https://api.minimax.io/anthropic"; +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"; +function getDefaultBaseUrl(region: MiniMaxRegion): string { + return region === "cn" ? DEFAULT_BASE_URL_CN : DEFAULT_BASE_URL_GLOBAL; +} + function buildModelDefinition(params: { id: string; name: string; input: Array<"text" | "image"> }) { return { id: params.id, @@ -22,6 +27,86 @@ function buildModelDefinition(params: { id: string; name: string; input: Array<" }; } +function createOAuthHandler(region: MiniMaxRegion) { + const defaultBaseUrl = getDefaultBaseUrl(region); + const regionLabel = region === "cn" ? "CN" : "Global"; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return async (ctx: any) => { + const progress = ctx.prompter.progress(`Starting MiniMax OAuth (${regionLabel})…`); + try { + const result = await loginMiniMaxPortalOAuth({ + openUrl: ctx.openUrl, + note: ctx.prompter.note, + progress, + region, + }); + + progress.stop("MiniMax OAuth complete"); + + const profileId = `${PROVIDER_ID}:default`; + const baseUrl = result.resourceUrl || defaultBaseUrl; + + return { + profiles: [ + { + profileId, + credential: { + type: "oauth" as const, + provider: PROVIDER_ID, + access: result.access, + refresh: result.refresh, + expires: result.expires, + }, + }, + ], + configPatch: { + models: { + providers: { + [PROVIDER_ID]: { + baseUrl, + apiKey: OAUTH_PLACEHOLDER, + api: "anthropic-messages", + models: [ + buildModelDefinition({ + id: "MiniMax-M2.1", + name: "MiniMax M2.1", + input: ["text"], + }), + buildModelDefinition({ + id: "MiniMax-M2.1-lightning", + name: "MiniMax M2.1 Lightning", + input: ["text"], + }), + ], + }, + }, + }, + agents: { + defaults: { + models: { + "MiniMax-M2.1": { alias: "minimax" }, + }, + }, + }, + }, + defaultModel: 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.`, + ], + }; + } catch (err) { + progress.stop("MiniMax OAuth failed"); + await ctx.prompter.note( + "If OAuth fails, verify your MiniMax account has portal access and try again.", + "MiniMax OAuth", + ); + throw err; + } + }; +} + const minimaxPortalPlugin = { id: "minimax-portal-auth", name: "MiniMax OAuth", @@ -36,81 +121,17 @@ const minimaxPortalPlugin = { auth: [ { id: "oauth", - label: "MiniMax OAuth", - hint: "User code login", + label: "MiniMax OAuth (Global)", + hint: "Global endpoint - api.minimax.io", kind: "user_code", - run: async (ctx) => { - const progress = ctx.prompter.progress("Starting MiniMax OAuth…"); - try { - const result = await loginMiniMaxPortalOAuth({ - openUrl: ctx.openUrl, - note: ctx.prompter.note, - progress, - }); - - progress.stop("MiniMax OAuth complete"); - - const profileId = `${PROVIDER_ID}:default`; - const baseUrl = result.resourceUrl || DEFAULT_BASE_URL; - - return { - profiles: [ - { - profileId, - credential: { - type: "oauth", - provider: PROVIDER_ID, - access: result.access, - refresh: result.refresh, - expires: result.expires, - }, - }, - ], - configPatch: { - models: { - providers: { - [PROVIDER_ID]: { - baseUrl, - apiKey: OAUTH_PLACEHOLDER, - api: "anthropic-messages", - models: [ - buildModelDefinition({ - id: "MiniMax-M2.1", - name: "MiniMax M2.1", - input: ["text"], - }), - buildModelDefinition({ - id: "MiniMax-M2.1-lightning", - name: "MiniMax M2.1 Lightning", - input: ["text"], - }), - ], - }, - }, - }, - agents: { - defaults: { - models: { - "MiniMax-M2.1": { alias: "minimax" }, - }, - }, - }, - }, - defaultModel: DEFAULT_MODEL, - notes: [ - "MiniMax OAuth tokens auto-refresh. Re-run login if refresh fails or access is revoked.", - `Base URL defaults to ${DEFAULT_BASE_URL}. Override models.providers.${PROVIDER_ID}.baseUrl if needed.`, - ], - }; - } catch (err) { - progress.stop("MiniMax OAuth failed"); - await ctx.prompter.note( - "If OAuth fails, verify your MiniMax account has portal access and try again.", - "MiniMax OAuth", - ); - throw err; - } - }, + run: createOAuthHandler("global"), + }, + { + id: "oauth-cn", + label: "MiniMax OAuth (CN)", + hint: "CN endpoint - api.minimax.chat", + kind: "user_code", + run: createOAuthHandler("cn"), }, ], }); diff --git a/extensions/minimax-portal-auth/oauth.ts b/extensions/minimax-portal-auth/oauth.ts index b779dfc4b..6cc060975 100644 --- a/extensions/minimax-portal-auth/oauth.ts +++ b/extensions/minimax-portal-auth/oauth.ts @@ -1,12 +1,31 @@ import { createHash, randomBytes, randomUUID } from "node:crypto"; -const MINIMAX_OAUTH_BASE_URL = "https://api.minimax.io"; -const MINIMAX_OAUTH_CODE_ENDPOINT = `${MINIMAX_OAUTH_BASE_URL}/oauth/code`; -const MINIMAX_OAUTH_TOKEN_ENDPOINT = `${MINIMAX_OAUTH_BASE_URL}/oauth/token`; -const MINIMAX_OAUTH_CLIENT_ID = "78257093-7e40-4613-99e0-527b14b39113"; +export type MiniMaxRegion = "cn" | "global"; + +const MINIMAX_OAUTH_CONFIG = { + cn: { + baseUrl: "https://api.minimaxi.com", + clientId: "78257093-7e40-4613-99e0-527b14b39113", + }, + global: { + baseUrl: "https://api.minimax.io", + clientId: "78257093-7e40-4613-99e0-527b14b39113", + }, +} as const; + const MINIMAX_OAUTH_SCOPE = "group_id profile model.completion"; const MINIMAX_OAUTH_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:user_code"; +function getOAuthEndpoints(region: MiniMaxRegion) { + const config = MINIMAX_OAUTH_CONFIG[region]; + return { + codeEndpoint: `${config.baseUrl}/oauth/code`, + tokenEndpoint: `${config.baseUrl}/oauth/token`, + clientId: config.clientId, + baseUrl: config.baseUrl, + }; +} + export type MiniMaxOAuthAuthorization = { user_code: string; verification_uri: string; @@ -47,8 +66,10 @@ function generatePkce(): { verifier: string; challenge: string; state: string } async function requestOAuthCode(params: { challenge: string; state: string; + region: MiniMaxRegion; }): Promise { - const response = await fetch(MINIMAX_OAUTH_CODE_ENDPOINT, { + const endpoints = getOAuthEndpoints(params.region); + const response = await fetch(endpoints.codeEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", @@ -56,8 +77,8 @@ async function requestOAuthCode(params: { "x-request-id": randomUUID(), }, body: toFormUrlEncoded({ - response_type:"code", - client_id: MINIMAX_OAUTH_CLIENT_ID, + response_type: "code", + client_id: endpoints.clientId, scope: MINIMAX_OAUTH_SCOPE, code_challenge: params.challenge, code_challenge_method: "S256", @@ -88,8 +109,10 @@ async function requestOAuthCode(params: { async function pollOAuthToken(params: { userCode: string; verifier: string; + region: MiniMaxRegion; }): Promise { - const response = await fetch(MINIMAX_OAUTH_TOKEN_ENDPOINT, { + const endpoints = getOAuthEndpoints(params.region); + const response = await fetch(endpoints.tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", @@ -97,7 +120,7 @@ async function pollOAuthToken(params: { }, body: toFormUrlEncoded({ grant_type: MINIMAX_OAUTH_GRANT_TYPE, - client_id: MINIMAX_OAUTH_CLIENT_ID, + client_id: endpoints.clientId, user_code: params.userCode, code_verifier: params.verifier, }), @@ -150,9 +173,11 @@ export async function loginMiniMaxPortalOAuth(params: { openUrl: (url: string) => Promise; note: (message: string, title?: string) => Promise; progress: { update: (message: string) => void; stop: (message?: string) => void }; + region?: MiniMaxRegion; }): Promise { + const region = params.region ?? "global"; const { verifier, challenge, state } = generatePkce(); - const oauth = await requestOAuthCode({ challenge, state }); + const oauth = await requestOAuthCode({ challenge, state, region }); const verificationUrl = oauth.verification_uri; const noteLines = [ @@ -179,6 +204,7 @@ export async function loginMiniMaxPortalOAuth(params: { const result = await pollOAuthToken({ userCode: oauth.user_code, verifier, + region, }); if (result.status === "success") {