feat(minimax-portal-auth): add regional endpoint selection and improve OAuth flow

- Support Global (api.minimax.io) and CN (api.minimaxi.com) endpoints
- Add endpoint selection prompt in onboard flow
- Add notification_message display after successful login
- Update documentation with Coding Plan details
- Fix API response field parsing (expired_in, base_resp.status_msg)
- Add debug logging for OAuth response and poll results
This commit is contained in:
xiaose 2026-01-28 22:42:31 +08:00
parent cdd03f465d
commit 2219152ccf
5 changed files with 67 additions and 19 deletions

View File

@ -35,7 +35,24 @@ MiniMax highlights these improvements in M2.1:
## Choose a setup ## Choose a setup
### MiniMax M2.1 — recommended ### MiniMax OAuth (Coding Plan) — recommended
**Best for:** quick setup with MiniMax Coding Plan via OAuth, no API key required.
Enable the bundled OAuth plugin and authenticate:
```bash
moltbot plugins enable minimax-portal-auth
moltbot gateway restart
moltbot onboard --auth-choice minimax-portal
```
You will be prompted to select an endpoint:
- **Global** - International users (`api.minimax.io`)
- **CN** - Users in China (`api.minimaxi.com`)
See [MiniMax OAuth plugin README](https://github.com/moltbot/moltbot/tree/main/extensions/minimax-portal-auth) for details.
### MiniMax M2.1 (API key)
**Best for:** hosted MiniMax with Anthropic-compatible API. **Best for:** hosted MiniMax with Anthropic-compatible API.
@ -143,6 +160,7 @@ Use the interactive config wizard to set MiniMax without editing JSON:
3) Choose **MiniMax M2.1**. 3) Choose **MiniMax M2.1**.
4) Pick your default model when prompted. 4) Pick your default model when prompted.
## Configuration options ## Configuration options
- `models.providers.minimax.baseUrl`: prefer `https://api.minimax.io/anthropic` (Anthropic-compatible); `https://api.minimax.io/v1` is optional for OpenAI-compatible payloads. - `models.providers.minimax.baseUrl`: prefer `https://api.minimax.io/anthropic` (Anthropic-compatible); `https://api.minimax.io/v1` is optional for OpenAI-compatible payloads.

View File

@ -29,5 +29,5 @@ You will be prompted to select an endpoint:
## Notes ## Notes
- MiniMax OAuth uses a device-code login flow. - MiniMax OAuth uses a user-code login flow.
- Tokens auto-refresh; re-run login if refresh fails or access is revoked. - Currently, OAuth login is supported only for the Coding plan

View File

@ -44,6 +44,10 @@ function createOAuthHandler(region: MiniMaxRegion) {
progress.stop("MiniMax OAuth complete"); progress.stop("MiniMax OAuth complete");
if (result.notification_message) {
await ctx.prompter.note(result.notification_message, "MiniMax OAuth");
}
const profileId = `${PROVIDER_ID}:default`; const profileId = `${PROVIDER_ID}:default`;
const baseUrl = result.resourceUrl || defaultBaseUrl; const baseUrl = result.resourceUrl || defaultBaseUrl;
@ -85,7 +89,8 @@ function createOAuthHandler(region: MiniMaxRegion) {
agents: { agents: {
defaults: { defaults: {
models: { models: {
"MiniMax-M2.1": { alias: "minimax" }, "MiniMax-M2.1": { alias: "minimax-m2.1" },
"MiniMax-M2.1-lightning": { alias: "minimax-m2.1-lightning" },
}, },
}, },
}, },
@ -94,10 +99,12 @@ function createOAuthHandler(region: MiniMaxRegion) {
notes: [ notes: [
"MiniMax OAuth tokens auto-refresh. Re-run login if refresh fails or access is revoked.", "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.`, `Base URL defaults to ${defaultBaseUrl}. Override models.providers.${PROVIDER_ID}.baseUrl if needed.`,
...(result.notification_message ? [result.notification_message] : []),
], ],
}; };
} catch (err) { } catch (err) {
progress.stop("MiniMax OAuth failed"); const errorMsg = err instanceof Error ? err.message : String(err);
progress.stop(`MiniMax OAuth failed: ${errorMsg}`);
await ctx.prompter.note( await ctx.prompter.note(
"If OAuth fails, verify your MiniMax account has portal access and try again.", "If OAuth fails, verify your MiniMax account has portal access and try again.",
"MiniMax OAuth", "MiniMax OAuth",

View File

@ -29,10 +29,8 @@ function getOAuthEndpoints(region: MiniMaxRegion) {
export type MiniMaxOAuthAuthorization = { export type MiniMaxOAuthAuthorization = {
user_code: string; user_code: string;
verification_uri: string; verification_uri: string;
expires_in: number; expired_in: number;
interval?: number; interval?: number;
has_benefit: boolean;
benefit_message: string;
state: string; state: string;
}; };
@ -41,6 +39,7 @@ export type MiniMaxOAuthToken = {
refresh: string; refresh: string;
expires: number; expires: number;
resourceUrl?: string; resourceUrl?: string;
notification_message?: string;
}; };
type TokenPending = { status: "pending"; message?: string }; type TokenPending = { status: "pending"; message?: string };
@ -127,6 +126,7 @@ async function pollOAuthToken(params: {
}); });
if (!response.ok) { if (!response.ok) {
const text = await response.text();
let payload: { let payload: {
status?: string; status?: string;
base_resp?: { status_code?: number; status_msg?: string }; base_resp?: { status_code?: number; status_msg?: string };
@ -134,11 +134,11 @@ async function pollOAuthToken(params: {
try { try {
payload = (await response.json()) as typeof payload; payload = (await response.json()) as typeof payload;
} catch { } catch {
return { status: "error", message: response.statusText }; return { status: "error", message: text || "MiniMax OAuth failed to parse response.",};
} }
return { return {
status: "error", status: "error",
message: payload?.base_resp?.status_msg ?? response.statusText, message: text || "MiniMax OAuth failed to parse response.",
}; };
} }
@ -149,7 +149,13 @@ async function pollOAuthToken(params: {
expired_in?: number | null; expired_in?: number | null;
token_type?: string; token_type?: string;
resource_url?: string; resource_url?: string;
notification_message?: string;
}; };
if (tokenPayload.status === "error") {
return { status: "error", message: "An error occurred. Please try again later"};
}
if (tokenPayload.status != "success") { if (tokenPayload.status != "success") {
return { status: "pending", message: "current user code is not authorized" }; return { status: "pending", message: "current user code is not authorized" };
} }
@ -163,8 +169,9 @@ async function pollOAuthToken(params: {
token: { token: {
access: tokenPayload.access_token, access: tokenPayload.access_token,
refresh: tokenPayload.refresh_token, refresh: tokenPayload.refresh_token,
expires: Date.now() + tokenPayload.expired_in * 1000, expires: tokenPayload.expired_in,
resourceUrl: tokenPayload.resource_url, resourceUrl: tokenPayload.resource_url,
notification_message: tokenPayload.notification_message,
}, },
}; };
} }
@ -183,10 +190,8 @@ export async function loginMiniMaxPortalOAuth(params: {
const noteLines = [ const noteLines = [
`Open ${verificationUrl} to approve access.`, `Open ${verificationUrl} to approve access.`,
`If prompted, enter the code ${oauth.user_code}.`, `If prompted, enter the code ${oauth.user_code}.`,
`Interval: ${oauth.interval ?? "default (2000ms)"}, Expires in: ${oauth.expired_in}ms`,
]; ];
if (oauth.has_benefit && oauth.benefit_message) {
noteLines.push("", oauth.benefit_message);
}
await params.note(noteLines.join("\n"), "MiniMax OAuth"); await params.note(noteLines.join("\n"), "MiniMax OAuth");
try { try {
@ -195,11 +200,11 @@ export async function loginMiniMaxPortalOAuth(params: {
// Fall back to manual copy/paste if browser open fails. // Fall back to manual copy/paste if browser open fails.
} }
const start = Date.now(); let pollIntervalMs = oauth.interval ? oauth.interval : 2000;
let pollIntervalMs = oauth.interval ? oauth.interval * 1000 : 2000; const expireTimeMs = oauth.expired_in;
const timeoutMs = oauth.expires_in * 1000;
while (Date.now() - start < timeoutMs) {
while (Date.now() < expireTimeMs) {
params.progress.update("Waiting for MiniMax OAuth approval…"); params.progress.update("Waiting for MiniMax OAuth approval…");
const result = await pollOAuthToken({ const result = await pollOAuthToken({
userCode: oauth.user_code, userCode: oauth.user_code,
@ -207,6 +212,15 @@ export async function loginMiniMaxPortalOAuth(params: {
region, region,
}); });
// // Debug: print poll result
// await params.note(
// `status: ${result.status}` +
// (result.status === "success" ? `\ntoken: ${JSON.stringify(result.token, null, 2)}` : "") +
// (result.status === "error" ? `\nmessage: ${result.message}` : "") +
// (result.status === "pending" && result.message ? `\nmessage: ${result.message}` : ""),
// "MiniMax OAuth Poll Result",
// );
if (result.status === "success") { if (result.status === "success") {
return result.token; return result.token;
} }

View File

@ -29,11 +29,20 @@ export async function applyAuthChoiceMiniMax(
); );
}; };
if (params.authChoice === "minimax-portal") { 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" },
],
});
return await applyAuthChoicePluginProvider(params, { return await applyAuthChoicePluginProvider(params, {
authChoice: "minimax-portal", authChoice: "minimax-portal",
pluginId: "minimax-portal-auth", pluginId: "minimax-portal-auth",
providerId: "minimax-portal", providerId: "minimax-portal",
methodId: "device", methodId: endpoint as string,
label: "MiniMax", label: "MiniMax",
}); });
} }