diff --git a/CHANGELOG.md b/CHANGELOG.md index dbfde0c872f..83c818ba70e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/concepts/usage-tracking.md b/docs/concepts/usage-tracking.md index 7d19cbe91fe..32569b8b940 100644 --- a/docs/concepts/usage-tracking.md +++ b/docs/concepts/usage-tracking.md @@ -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. diff --git a/docs/providers/minimax.md b/docs/providers/minimax.md index d4816e93d42..a2450bd09fe 100644 --- a/docs/providers/minimax.md +++ b/docs/providers/minimax.md @@ -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. diff --git a/extensions/minimax/index.test.ts b/extensions/minimax/index.test.ts index e53ea4fa5ac..fb03191f2a9 100644 --- a/extensions/minimax/index.test.ts +++ b/extensions/minimax/index.test.ts @@ -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(); + }); }); diff --git a/extensions/minimax/index.ts b/extensions/minimax/index.ts index 85429fe87e0..899e8ded529 100644 --- a/extensions/minimax/index.ts +++ b/extensions/minimax/index.ts @@ -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; }, diff --git a/src/infra/provider-usage.auth.ts b/src/infra/provider-usage.auth.ts index 832f624d54a..4e8389f77a4 100644 --- a/src/infra/provider-usage.auth.ts +++ b/src/infra/provider-usage.auth.ts @@ -81,7 +81,7 @@ function resolveProviderApiKeyFromConfigAndStore(params: { async function resolveOAuthToken(params: { state: UsageAuthState; - provider: UsageProviderId; + provider: string; }): Promise { 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 ? { diff --git a/src/infra/provider-usage.shared.test.ts b/src/infra/provider-usage.shared.test.ts index 912b8b55d42..a35d64d22a0 100644 --- a/src/infra/provider-usage.shared.test.ts +++ b/src/infra/provider-usage.shared.test.ts @@ -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 }, diff --git a/src/infra/provider-usage.shared.ts b/src/infra/provider-usage.shared.ts index 2a8526888cd..5f38ad9f30d 100644 --- a/src/infra/provider-usage.shared.ts +++ b/src/infra/provider-usage.shared.ts @@ -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; diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 811e7076cc7..1875af0b9ca 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -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; - resolveOAuthToken: () => Promise; + resolveOAuthToken: (params?: { provider?: string }) => Promise; }; /**