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:
parent
cdd03f465d
commit
2219152ccf
@ -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.
|
||||||
|
|||||||
@ -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
|
||||||
@ -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",
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user