fix: resolve MiniMax portal usage auth

This commit is contained in:
Peter Steinberger 2026-04-04 12:40:20 +01:00
parent 9d684e1040
commit fb0d60d7f3
No known key found for this signature in database
9 changed files with 65 additions and 17 deletions

View File

@ -41,6 +41,7 @@ Docs: https://docs.openclaw.ai
- Models/MiniMax: honor `MINIMAX_API_HOST` for implicit bundled MiniMax provider catalogs so China-hosted API-key setups pick `api.minimaxi.com/anthropic` without manual provider config. (#34524) Thanks @caiqinghua.
- Usage/MiniMax: invert remaining-style `usage_percent` fields when MiniMax reports only remaining percentage data, so usage bars stop showing nearly-full remaining quota as nearly-exhausted usage. (#60254) Thanks @jwchmodx.
- Usage/MiniMax: prefer the chat-model `model_remains` entry and derive Coding Plan window labels from MiniMax interval timestamps so MiniMax usage snapshots stop picking zero-budget media rows and misreporting 4h windows as `5h`. (#52349) Thanks @IVY-AI-gif.
- Usage/MiniMax: let usage snapshots treat `minimax-portal` and MiniMax CN aliases as the same MiniMax quota surface, and prefer stored MiniMax OAuth before falling back to Coding Plan keys.
- MiniMax: advertise image input on bundled `MiniMax-M2.7` and `MiniMax-M2.7-highspeed` model definitions so image-capable flows can route through the M2.7 family correctly. (#54843) Thanks @MerlinMiao88888888.
- Agents/exec approvals: let `exec-approvals.json` agent security override stricter gateway tool defaults so approved subagents can use `security: "full"` without falling back to allowlist enforcement again. (#60310) Thanks @lml2468.
- Tasks/maintenance: mark stale cron runs and CLI tasks backed only by long-lived chat sessions as lost again so task cleanup does not keep dead work alive indefinitely. (#60310) Thanks @lml2468.

View File

@ -35,11 +35,10 @@ title: "Usage Tracking"
- JSON usage falls back to `stats`; `stats.cached` is normalized into
`cacheRead`.
- **OpenAI Codex**: OAuth tokens in auth profiles (accountId used when present).
- **MiniMax**: API key (coding plan key; `MINIMAX_CODE_PLAN_KEY`, `MINIMAX_CODING_API_KEY`, or `MINIMAX_API_KEY`); coding-plan window labels come from provider hours/minutes fields when present, then fall back to the `start_time` / `end_time` span. MiniMax's raw `usage_percent` / `usagePercent` fields mean **remaining** quota, so OpenClaw inverts them before display; count-based fields win when present.
- If the coding-plan endpoint returns `model_remains`, OpenClaw prefers the
chat-model entry, derives the window label from timestamps when explicit
`window_hours` / `window_minutes` fields are absent, and includes the model
name in the plan label.
<<<<<<< HEAD
- **MiniMax**: API key or MiniMax OAuth auth profile. OpenClaw treats `minimax`, `minimax-cn`, and `minimax-portal` as the same MiniMax quota surface, prefers stored MiniMax OAuth when present, and otherwise falls back to `MINIMAX_CODE_PLAN_KEY`, `MINIMAX_CODING_API_KEY`, or `MINIMAX_API_KEY`. MiniMax's raw `usage_percent` / `usagePercent` fields mean **remaining** quota, so OpenClaw inverts them before display; count-based fields win when present.
- Coding-plan window labels come from provider hours/minutes fields when present, then fall back to the `start_time` / `end_time` span.
- If the coding-plan endpoint returns `model_remains`, OpenClaw prefers the chat-model entry, derives the window label from timestamps when explicit `window_hours` / `window_minutes` fields are absent, and includes the model name in the plan label.
- **Xiaomi MiMo**: API key via env/config/auth store (`XIAOMI_API_KEY`).
- **z.ai**: API key via env/config/auth store.

View File

@ -237,10 +237,14 @@ Current MiniMax auth choices in the wizard/CLI:
- OpenClaw normalizes MiniMax coding-plan usage to the same `% left` display
used by other providers. MiniMax's raw `usage_percent` / `usagePercent`
fields are remaining quota, not consumed quota, so OpenClaw inverts them.
<<<<<<< HEAD
Count-based fields win when present. When the API returns `model_remains`,
OpenClaw prefers the chat-model entry, derives the window label from
`start_time` / `end_time` when needed, and includes the selected model name
in the plan label so coding-plan windows are easier to distinguish.
- Usage snapshots treat `minimax`, `minimax-cn`, and `minimax-portal` as the
same MiniMax quota surface, and prefer stored MiniMax OAuth before falling
back to Coding Plan key env vars.
- Update pricing values in `models.json` if you need exact cost tracking.
- Referral link for MiniMax Coding Plan (10% off): [https://platform.minimax.io/subscribe/coding-plan?code=DbXJTRClnb&source=link](https://platform.minimax.io/subscribe/coding-plan?code=DbXJTRClnb&source=link)
- See [/concepts/model-providers](/concepts/model-providers) for provider rules.

View File

@ -1,6 +1,6 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import type { Context, Model } from "@mariozechner/pi-ai";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import {
registerProviderPlugin,
requireRegisteredProvider,
@ -150,4 +150,30 @@ describe("minimax provider hooks", () => {
envVars: ["MINIMAX_CODE_PLAN_KEY", "MINIMAX_CODING_API_KEY"],
});
});
it("prefers minimax-portal oauth when resolving MiniMax usage auth", async () => {
const { providers } = await registerProviderPlugin({
plugin: minimaxPlugin,
id: "minimax",
name: "MiniMax Provider",
});
const apiProvider = requireRegisteredProvider(providers, "minimax");
const resolveOAuthToken = vi.fn(async (params?: { provider?: string }) =>
params?.provider === "minimax-portal" ? { token: "portal-oauth-token" } : null,
);
const resolveApiKeyFromConfigAndStore = vi.fn(() => undefined);
await expect(
apiProvider.resolveUsageAuth?.({
provider: "minimax",
config: {},
env: {},
resolveOAuthToken,
resolveApiKeyFromConfigAndStore,
} as never),
).resolves.toEqual({ token: "portal-oauth-token" });
expect(resolveOAuthToken).toHaveBeenCalledWith({ provider: "minimax-portal" });
expect(resolveApiKeyFromConfigAndStore).not.toHaveBeenCalled();
});
});

View File

@ -35,6 +35,12 @@ const PROVIDER_LABEL = "MiniMax";
const DEFAULT_MODEL = MINIMAX_DEFAULT_MODEL_ID;
const DEFAULT_BASE_URL_CN = "https://api.minimaxi.com/anthropic";
const DEFAULT_BASE_URL_GLOBAL = "https://api.minimax.io/anthropic";
const MINIMAX_USAGE_ENV_VAR_KEYS = [
"MINIMAX_OAUTH_TOKEN",
"MINIMAX_CODE_PLAN_KEY",
"MINIMAX_CODING_API_KEY",
"MINIMAX_API_KEY",
] as const;
const HYBRID_ANTHROPIC_OPENAI_REPLAY_HOOKS = buildProviderReplayFamilyHooks({
family: "hybrid-anthropic-openai",
anthropicModelDropThinkingBlocks: true,
@ -238,12 +244,13 @@ export default definePluginEntry({
run: async (ctx) => resolveApiCatalog(ctx),
},
resolveUsageAuth: async (ctx) => {
const portalOauth = await ctx.resolveOAuthToken({ provider: PORTAL_PROVIDER_ID });
if (portalOauth) {
return portalOauth;
}
const apiKey = ctx.resolveApiKeyFromConfigAndStore({
envDirect: [
ctx.env.MINIMAX_CODE_PLAN_KEY,
ctx.env.MINIMAX_CODING_API_KEY,
ctx.env.MINIMAX_API_KEY,
],
providerIds: [API_PROVIDER_ID, PORTAL_PROVIDER_ID],
envDirect: MINIMAX_USAGE_ENV_VAR_KEYS.map((name) => ctx.env[name]),
});
return apiKey ? { token: apiKey } : null;
},

View File

@ -81,7 +81,7 @@ function resolveProviderApiKeyFromConfigAndStore(params: {
async function resolveOAuthToken(params: {
state: UsageAuthState;
provider: UsageProviderId;
provider: string;
}): Promise<ProviderAuth | null> {
const order = resolveAuthProfileOrder({
cfg: params.state.cfg,
@ -108,7 +108,7 @@ async function resolveOAuthToken(params: {
continue;
}
return {
provider: params.provider,
provider: params.provider as UsageProviderId,
token: resolved.apiKey,
accountId:
cred.type === "oauth" && "accountId" in cred
@ -142,10 +142,10 @@ async function resolveProviderUsageAuthViaPlugin(params: {
providerIds: options?.providerIds ?? [params.provider],
envDirect: options?.envDirect,
}),
resolveOAuthToken: async () => {
resolveOAuthToken: async (options) => {
const auth = await resolveOAuthToken({
state: params.state,
provider: params.provider,
provider: options?.provider ?? params.provider,
});
return auth
? {

View File

@ -33,6 +33,9 @@ describe("provider-usage.shared", () => {
it.each([
{ value: "z-ai", expected: "zai" },
{ value: " GOOGLE-GEMINI-CLI ", expected: "google-gemini-cli" },
{ value: "minimax-portal", expected: "minimax" },
{ value: "minimax-cn", expected: "minimax" },
{ value: "minimax-portal-cn", expected: "minimax" },
{ value: "unknown-provider", expected: undefined },
{ value: undefined, expected: undefined },
{ value: null, expected: undefined },

View File

@ -32,6 +32,13 @@ export function resolveUsageProviderId(provider?: string | null): UsageProviderI
return undefined;
}
const normalized = normalizeProviderId(provider);
if (
normalized === "minimax-portal" ||
normalized === "minimax-cn" ||
normalized === "minimax-portal-cn"
) {
return "minimax";
}
return usageProviders.includes(normalized as UsageProviderId)
? (normalized as UsageProviderId)
: undefined;

View File

@ -474,7 +474,8 @@ export type ProviderPreparedRuntimeAuth = {
* The helper methods cover the common OpenClaw auth resolution paths:
*
* - `resolveApiKeyFromConfigAndStore`: env/config/plain token/api_key profiles
* - `resolveOAuthToken`: oauth/token profiles resolved through the auth store
* - `resolveOAuthToken`: oauth/token profiles resolved through the auth store,
* optionally for an explicit provider override
*
* Plugins can still do extra provider-specific work on top (for example parse a
* token blob, read a legacy credential file, or pick between aliases).
@ -489,7 +490,7 @@ export type ProviderResolveUsageAuthContext = {
providerIds?: string[];
envDirect?: Array<string | undefined>;
}) => string | undefined;
resolveOAuthToken: () => Promise<ProviderResolvedUsageAuth | null>;
resolveOAuthToken: (params?: { provider?: string }) => Promise<ProviderResolvedUsageAuth | null>;
};
/**