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
### 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.
@ -143,6 +160,7 @@ Use the interactive config wizard to set MiniMax without editing JSON:
3) Choose **MiniMax M2.1**.
4) Pick your default model when prompted.
## 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.

View File

@ -29,5 +29,5 @@ You will be prompted to select an endpoint:
## Notes
- MiniMax OAuth uses a device-code login flow.
- Tokens auto-refresh; re-run login if refresh fails or access is revoked.
- MiniMax OAuth uses a user-code login flow.
- 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");
if (result.notification_message) {
await ctx.prompter.note(result.notification_message, "MiniMax OAuth");
}
const profileId = `${PROVIDER_ID}:default`;
const baseUrl = result.resourceUrl || defaultBaseUrl;
@ -85,7 +89,8 @@ function createOAuthHandler(region: MiniMaxRegion) {
agents: {
defaults: {
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: [
"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.`,
...(result.notification_message ? [result.notification_message] : []),
],
};
} 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(
"If OAuth fails, verify your MiniMax account has portal access and try again.",
"MiniMax OAuth",

View File

@ -29,10 +29,8 @@ function getOAuthEndpoints(region: MiniMaxRegion) {
export type MiniMaxOAuthAuthorization = {
user_code: string;
verification_uri: string;
expires_in: number;
expired_in: number;
interval?: number;
has_benefit: boolean;
benefit_message: string;
state: string;
};
@ -41,6 +39,7 @@ export type MiniMaxOAuthToken = {
refresh: string;
expires: number;
resourceUrl?: string;
notification_message?: string;
};
type TokenPending = { status: "pending"; message?: string };
@ -127,6 +126,7 @@ async function pollOAuthToken(params: {
});
if (!response.ok) {
const text = await response.text();
let payload: {
status?: string;
base_resp?: { status_code?: number; status_msg?: string };
@ -134,11 +134,11 @@ async function pollOAuthToken(params: {
try {
payload = (await response.json()) as typeof payload;
} catch {
return { status: "error", message: response.statusText };
return { status: "error", message: text || "MiniMax OAuth failed to parse response.",};
}
return {
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;
token_type?: 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") {
return { status: "pending", message: "current user code is not authorized" };
}
@ -163,8 +169,9 @@ async function pollOAuthToken(params: {
token: {
access: tokenPayload.access_token,
refresh: tokenPayload.refresh_token,
expires: Date.now() + tokenPayload.expired_in * 1000,
expires: tokenPayload.expired_in,
resourceUrl: tokenPayload.resource_url,
notification_message: tokenPayload.notification_message,
},
};
}
@ -183,10 +190,8 @@ export async function loginMiniMaxPortalOAuth(params: {
const noteLines = [
`Open ${verificationUrl} to approve access.`,
`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");
try {
@ -195,11 +200,11 @@ export async function loginMiniMaxPortalOAuth(params: {
// Fall back to manual copy/paste if browser open fails.
}
const start = Date.now();
let pollIntervalMs = oauth.interval ? oauth.interval * 1000 : 2000;
const timeoutMs = oauth.expires_in * 1000;
let pollIntervalMs = oauth.interval ? oauth.interval : 2000;
const expireTimeMs = oauth.expired_in;
while (Date.now() - start < timeoutMs) {
while (Date.now() < expireTimeMs) {
params.progress.update("Waiting for MiniMax OAuth approval…");
const result = await pollOAuthToken({
userCode: oauth.user_code,
@ -207,6 +212,15 @@ export async function loginMiniMaxPortalOAuth(params: {
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") {
return result.token;
}

View File

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