Merge PR #6650: feat(ai): report MiniMax Token Plan quota in usage (@everton-dgn)
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
|
||||
### Added
|
||||
|
||||
- MiniMax Token Plan accounts now report quota in `omp usage`. `GET /v1/token_plan/remains` returns one bucket per plan quota, each carrying a rolling interval window and a weekly window, so `minimax-code` surfaces real remaining percentages instead of an empty report. A model the plan does not include comes back looking like an untouched quota; those buckets are dropped from the report and named in its metadata. The mainland id `minimax-code-cn` is untouched.
|
||||
- OAuth logins now stamp `authorizedAt` (epoch ms of the interactive login) on the stored credential, and every refresh-persist path preserves it. Anthropic expires the whole OAuth grant family ~30 days after authorization regardless of refresh-token rotation (observed as `invalid_grant: "Refresh token expired"` on the latest rotated token, exactly 30 days after login, across four production accounts), so the login anchor is what makes re-login deadlines computable. Exported `ANTHROPIC_OAUTH_GRANT_TTL_MS` alongside the anthropic OAuth flow.
|
||||
- Added `GET /v1/credentials/disabled` to the auth broker and `AuthBrokerClient.listDisabledCredentials`: disabled-credential tombstones (`DisabledCredentialSummary` — identity, verbatim disable cause, disable timestamp; never token material) so auto-disabled accounts stay visible to clients instead of silently vanishing from the snapshot. `AuthStorage.listDisabledCredentials` serves the same data locally from SQLite; clients of brokers predating the endpoint get an empty list (404 mapped, no error).
|
||||
- Added `AuthStorage.revalidateCredentials()` and the optional `AuthCredentialStore.refreshSnapshot` hook: remote broker stores re-fetch `GET /v1/snapshot` on demand so callers pairing live per-credential data with stored identities (`omp usage`) never render against the up-to-an-hour-stale disk-cached snapshot; local SQLite stores are always current and only reload.
|
||||
|
||||
@@ -54,6 +54,7 @@ import { googleGeminiCliUsageProvider } from "./usage/gemini";
|
||||
import { githubCopilotUsageProvider } from "./usage/github-copilot";
|
||||
import { antigravityRankingStrategy, antigravityUsageProvider } from "./usage/google-antigravity";
|
||||
import { kimiUsageProvider } from "./usage/kimi";
|
||||
import { minimaxCodeUsageProvider } from "./usage/minimax-code";
|
||||
import { ollamaCloudUsageProvider, ollamaUsageProvider } from "./usage/ollama";
|
||||
import { codexRankingStrategy, openaiCodexUsageProvider } from "./usage/openai-codex";
|
||||
import {
|
||||
@@ -645,6 +646,7 @@ const DEFAULT_USAGE_PROVIDERS: UsageProvider[] = [
|
||||
alibabaTokenPlanUsageProvider,
|
||||
openaiCodexUsageProvider,
|
||||
kimiUsageProvider,
|
||||
minimaxCodeUsageProvider,
|
||||
antigravityUsageProvider,
|
||||
googleGeminiCliUsageProvider,
|
||||
ollamaUsageProvider,
|
||||
|
||||
@@ -1,30 +1,291 @@
|
||||
import type { UsageFetchContext, UsageFetchParams, UsageProvider, UsageReport } from "../usage";
|
||||
import type {
|
||||
UsageFetchContext,
|
||||
UsageFetchParams,
|
||||
UsageLimit,
|
||||
UsageProvider,
|
||||
UsageReport,
|
||||
UsageStatus,
|
||||
} from "../usage";
|
||||
import { isRecord } from "../utils";
|
||||
import { toNumber } from "./shared";
|
||||
|
||||
const INTL_PROVIDER = "minimax-code";
|
||||
const INTL_BASE_URL = "https://api.minimax.io";
|
||||
const REMAINS_PATH = "/v1/token_plan/remains";
|
||||
const HOUR_MS = 60 * 60 * 1000;
|
||||
/** `current_*_status` enum reported per window: 1 normal, 2 exhausted, 3 unlimited. */
|
||||
const STATUS_EXHAUSTED = 2;
|
||||
const STATUS_UNLIMITED = 3;
|
||||
/**
|
||||
* MiniMax Token Plan usage provider.
|
||||
*
|
||||
* MiniMax Token Plan is a subscription-based service with a rolling quota system.
|
||||
*
|
||||
* Currently, MiniMax does not expose a usage/quota API endpoint for the Token Plan.
|
||||
* Usage is tracked via the web dashboard at https://platform.minimax.io/user-center/payment/token-plan
|
||||
*
|
||||
* This provider exists to register support for the minimax-code provider in the
|
||||
* usage system. When MiniMax adds a usage API, this can be implemented.
|
||||
* The plan-wide token quota every chat model draws from. It is a quota category,
|
||||
* not a catalog model id, so its limits are scoped `shared`: `AuthStorage` has no
|
||||
* MiniMax ranking strategy and would otherwise match `scope.modelId` against ids
|
||||
* like `MiniMax-M3` and drop the "models with usage data" mapping. Category
|
||||
* buckets such as `video` meter a separate quota and keep their own model scope.
|
||||
*/
|
||||
async function fetchMiniMaxCodeUsage(params: UsageFetchParams, _ctx: UsageFetchContext): Promise<UsageReport | null> {
|
||||
if (params.provider !== "minimax-code" && params.provider !== "minimax-code-cn") {
|
||||
return null;
|
||||
}
|
||||
const SHARED_BUCKET = "general";
|
||||
|
||||
// MiniMax Token Plan does not currently expose a usage API
|
||||
// Users can check their usage via the web dashboard
|
||||
return null;
|
||||
/** One `model_remains[]` bucket: a plan quota tracked over a rolling interval plus a weekly window. */
|
||||
interface TokenPlanBucket {
|
||||
modelName: string;
|
||||
intervalStart?: number;
|
||||
intervalEnd?: number;
|
||||
intervalRemainingPercent?: number;
|
||||
intervalTotalCount?: number;
|
||||
intervalUsageCount?: number;
|
||||
intervalStatus?: number;
|
||||
weeklyStart?: number;
|
||||
weeklyEnd?: number;
|
||||
weeklyRemainingPercent?: number;
|
||||
weeklyTotalCount?: number;
|
||||
weeklyUsageCount?: number;
|
||||
weeklyStatus?: number;
|
||||
}
|
||||
|
||||
/** MiniMax reports epoch milliseconds; tolerate seconds in case a deployment differs. */
|
||||
function parseTimestamp(value: unknown): number | undefined {
|
||||
const parsed = toNumber(value);
|
||||
if (parsed === undefined || parsed <= 0) return undefined;
|
||||
return parsed < 1_000_000_000_000 ? parsed * 1000 : parsed;
|
||||
}
|
||||
|
||||
/** `current_*_remaining_percent` is 0..100 remaining; usage fractions are 0..1 used. */
|
||||
function usedFractionFromRemainingPercent(value: unknown): number | undefined {
|
||||
const parsed = toNumber(value);
|
||||
if (parsed === undefined || !Number.isFinite(parsed)) return undefined;
|
||||
// (100 - p) / 100 keeps whole percentages exact; 1 - p / 100 does not (90 → 0.09999999999999998).
|
||||
return Math.min(1, Math.max(0, (100 - parsed) / 100));
|
||||
}
|
||||
|
||||
function usageStatus(usedFraction: number): UsageStatus {
|
||||
if (usedFraction >= 1) return "exhausted";
|
||||
if (usedFraction >= 0.9) return "warning";
|
||||
return "ok";
|
||||
}
|
||||
|
||||
function parseBucket(value: unknown): TokenPlanBucket | null {
|
||||
if (!isRecord(value)) return null;
|
||||
const modelName = typeof value.model_name === "string" ? value.model_name.trim() : "";
|
||||
if (!modelName) return null;
|
||||
return {
|
||||
modelName,
|
||||
intervalStart: parseTimestamp(value.start_time),
|
||||
intervalEnd: parseTimestamp(value.end_time),
|
||||
intervalRemainingPercent: toNumber(value.current_interval_remaining_percent),
|
||||
intervalTotalCount: toNumber(value.current_interval_total_count),
|
||||
intervalUsageCount: toNumber(value.current_interval_usage_count),
|
||||
intervalStatus: toNumber(value.current_interval_status),
|
||||
weeklyStart: parseTimestamp(value.weekly_start_time),
|
||||
weeklyEnd: parseTimestamp(value.weekly_end_time),
|
||||
weeklyRemainingPercent: toNumber(value.current_weekly_remaining_percent),
|
||||
weeklyTotalCount: toNumber(value.current_weekly_total_count),
|
||||
weeklyUsageCount: toNumber(value.current_weekly_usage_count),
|
||||
weeklyStatus: toNumber(value.current_weekly_status),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A model outside the current plan is reported as both windows "unlimited"
|
||||
* with zero totals and 100% remaining, which would otherwise read as a pristine
|
||||
* quota. MiniMax's own CLI treats exactly this shape as "not in plan"
|
||||
* ([MiniMax-AI/cli#173](https://github.com/MiniMax-AI/cli/issues/173)), so the
|
||||
* bucket is kept out of the limits and named in `metadata.unavailableModels`.
|
||||
* Zero totals alone are not enough: a live plan reports `0/0` with status 1 and
|
||||
* a real remaining percentage.
|
||||
*/
|
||||
function isUnavailablePlan(bucket: TokenPlanBucket): boolean {
|
||||
return (
|
||||
bucket.intervalTotalCount === 0 &&
|
||||
bucket.weeklyTotalCount === 0 &&
|
||||
bucket.intervalStatus === STATUS_UNLIMITED &&
|
||||
bucket.weeklyStatus === STATUS_UNLIMITED
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interval length varies per bucket (text quotas roll every few hours, media
|
||||
* quotas daily), so the window id follows the reported span instead of a
|
||||
* hardcoded tier. Spans that are not whole hours are labelled in minutes
|
||||
* rather than rounded into a wrong hour count.
|
||||
*/
|
||||
function intervalWindowId(durationMs: number | undefined): { id: string; label: string } {
|
||||
if (durationMs === undefined || durationMs <= 0) return { id: "interval", label: "Interval" };
|
||||
if (durationMs % HOUR_MS === 0) {
|
||||
const hours = durationMs / HOUR_MS;
|
||||
return { id: `${hours}h`, label: `${hours} Hour` };
|
||||
}
|
||||
const minutes = Math.round(durationMs / 60_000);
|
||||
if (minutes <= 0) return { id: "interval", label: "Interval" };
|
||||
return { id: `${minutes}m`, label: `${minutes} Minute` };
|
||||
}
|
||||
|
||||
function buildLimit(args: {
|
||||
provider: string;
|
||||
bucket: TokenPlanBucket;
|
||||
windowId: string;
|
||||
windowLabel: string;
|
||||
durationMs?: number;
|
||||
resetsAt?: number;
|
||||
usedFraction: number | undefined;
|
||||
windowStatus?: number;
|
||||
usageCount?: number;
|
||||
totalCount?: number;
|
||||
accountId?: string;
|
||||
}): UsageLimit | undefined {
|
||||
// The endpoint's own status outranks the percentage: an exhausted window may
|
||||
// omit it, or keep a stale one that would otherwise render as healthy quota.
|
||||
const usedFraction = args.windowStatus === STATUS_EXHAUSTED ? 1 : args.usedFraction;
|
||||
if (usedFraction === undefined) return undefined;
|
||||
const totalCount = args.totalCount;
|
||||
return {
|
||||
id: `${args.bucket.modelName}:${args.windowId}`,
|
||||
label: `${args.bucket.modelName.charAt(0).toUpperCase()}${args.bucket.modelName.slice(1)} ${args.windowLabel}`,
|
||||
scope: {
|
||||
provider: args.provider,
|
||||
...(args.accountId ? { accountId: args.accountId } : {}),
|
||||
...(args.bucket.modelName === SHARED_BUCKET ? { shared: true as const } : { modelId: args.bucket.modelName }),
|
||||
windowId: args.windowId,
|
||||
},
|
||||
window: {
|
||||
id: args.windowId,
|
||||
label: args.windowLabel,
|
||||
...(args.durationMs !== undefined && args.durationMs > 0 ? { durationMs: args.durationMs } : {}),
|
||||
...(args.resetsAt ? { resetsAt: args.resetsAt } : {}),
|
||||
},
|
||||
amount: {
|
||||
used: usedFraction * 100,
|
||||
usedFraction,
|
||||
remaining: 100 - usedFraction * 100,
|
||||
remainingFraction: 1 - usedFraction,
|
||||
unit: "percent",
|
||||
},
|
||||
status: usageStatus(usedFraction),
|
||||
...(totalCount !== undefined && totalCount > 0
|
||||
? { notes: [`Requests: ${args.usageCount ?? 0}/${totalCount}`] }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildBucketLimits(provider: string, bucket: TokenPlanBucket, accountId: string | undefined): UsageLimit[] {
|
||||
const intervalDuration =
|
||||
bucket.intervalStart !== undefined && bucket.intervalEnd !== undefined
|
||||
? bucket.intervalEnd - bucket.intervalStart
|
||||
: undefined;
|
||||
const weeklyDuration =
|
||||
bucket.weeklyStart !== undefined && bucket.weeklyEnd !== undefined
|
||||
? bucket.weeklyEnd - bucket.weeklyStart
|
||||
: undefined;
|
||||
const interval = intervalWindowId(intervalDuration);
|
||||
return [
|
||||
buildLimit({
|
||||
provider,
|
||||
bucket,
|
||||
windowId: interval.id,
|
||||
windowLabel: interval.label,
|
||||
durationMs: intervalDuration,
|
||||
resetsAt: bucket.intervalEnd,
|
||||
usedFraction: usedFractionFromRemainingPercent(bucket.intervalRemainingPercent),
|
||||
windowStatus: bucket.intervalStatus,
|
||||
usageCount: bucket.intervalUsageCount,
|
||||
totalCount: bucket.intervalTotalCount,
|
||||
accountId,
|
||||
}),
|
||||
buildLimit({
|
||||
provider,
|
||||
bucket,
|
||||
windowId: "7d",
|
||||
windowLabel: "7 Day",
|
||||
durationMs: weeklyDuration,
|
||||
resetsAt: bucket.weeklyEnd,
|
||||
usedFraction: usedFractionFromRemainingPercent(bucket.weeklyRemainingPercent),
|
||||
windowStatus: bucket.weeklyStatus,
|
||||
usageCount: bucket.weeklyUsageCount,
|
||||
totalCount: bucket.weeklyTotalCount,
|
||||
accountId,
|
||||
}),
|
||||
].filter((limit): limit is UsageLimit => limit !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* MiniMax Token Plan usage provider (international, `api.minimax.io`).
|
||||
*
|
||||
* `GET /v1/token_plan/remains` returns one `model_remains[]` bucket per plan
|
||||
* quota (text, media, …), each carrying a rolling interval window and a weekly
|
||||
* window with the remaining percentage. MiniMax answers HTTP 200 even for
|
||||
* rejected credentials, so `base_resp.status_code` is the real success signal.
|
||||
*/
|
||||
async function fetchMiniMaxCodeUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
|
||||
if (params.provider !== INTL_PROVIDER) return null;
|
||||
const apiKey = params.credential.apiKey;
|
||||
if (params.credential.type !== "api_key" || !apiKey) return null;
|
||||
|
||||
try {
|
||||
const configuredBaseUrl = params.baseUrl?.trim();
|
||||
const baseUrl = configuredBaseUrl ? configuredBaseUrl.replace(/\/+$/, "").replace(/\/v1$/, "") : INTL_BASE_URL;
|
||||
const response = await ctx.fetch(`${baseUrl}${REMAINS_PATH}`, {
|
||||
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
||||
signal: params.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
ctx.logger?.warn("MiniMax Token Plan usage fetch failed", {
|
||||
provider: params.provider,
|
||||
status: response.status,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const payload: unknown = await response.json();
|
||||
if (!isRecord(payload)) return null;
|
||||
const statusCode = isRecord(payload.base_resp) ? toNumber(payload.base_resp.status_code) : undefined;
|
||||
if (statusCode !== 0) {
|
||||
ctx.logger?.warn("MiniMax Token Plan usage response rejected", {
|
||||
provider: params.provider,
|
||||
statusCode: statusCode ?? "missing",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
if (!Array.isArray(payload.model_remains)) return null;
|
||||
|
||||
const accountId = params.credential.accountId;
|
||||
const limits: UsageLimit[] = [];
|
||||
const models: string[] = [];
|
||||
const unavailableModels: string[] = [];
|
||||
for (const entry of payload.model_remains) {
|
||||
const bucket = parseBucket(entry);
|
||||
if (!bucket) continue;
|
||||
models.push(bucket.modelName);
|
||||
if (isUnavailablePlan(bucket)) {
|
||||
unavailableModels.push(bucket.modelName);
|
||||
continue;
|
||||
}
|
||||
limits.push(...buildBucketLimits(params.provider, bucket, accountId));
|
||||
}
|
||||
if (limits.length === 0) return null;
|
||||
|
||||
return {
|
||||
provider: params.provider,
|
||||
fetchedAt: Date.now(),
|
||||
limits,
|
||||
metadata: {
|
||||
source: "minimax-token-plan",
|
||||
models,
|
||||
...(unavailableModels.length > 0 ? { unavailableModels } : {}),
|
||||
...(accountId ? { accountId } : {}),
|
||||
},
|
||||
raw: payload,
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.logger?.warn("MiniMax Token Plan usage request failed", {
|
||||
provider: params.provider,
|
||||
error: error instanceof Error ? error.name : "unknown",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** MiniMax Token Plan (international, `api.minimax.io`). */
|
||||
export const minimaxCodeUsageProvider: UsageProvider = {
|
||||
id: "minimax-code",
|
||||
id: INTL_PROVIDER,
|
||||
fetchUsage: fetchMiniMaxCodeUsage,
|
||||
supports: (params: UsageFetchParams) =>
|
||||
(params.provider === "minimax-code" || params.provider === "minimax-code-cn") &&
|
||||
params.credential.type === "api_key",
|
||||
supports: params =>
|
||||
params.provider === INTL_PROVIDER && params.credential.type === "api_key" && Boolean(params.credential.apiKey),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type AuthCredentialStore, AuthStorage } from "@oh-my-pi/pi-ai/auth-storage";
|
||||
import type { FetchImpl } from "@oh-my-pi/pi-ai/types";
|
||||
import type { UsageFetchParams } from "@oh-my-pi/pi-ai/usage";
|
||||
import { minimaxCodeUsageProvider } from "@oh-my-pi/pi-ai/usage/minimax-code";
|
||||
|
||||
const INTERVAL_START = 1_785_009_600_000;
|
||||
const INTERVAL_END = 1_785_024_000_000;
|
||||
const WEEKLY_START = 1_784_505_600_000;
|
||||
const WEEKLY_END = 1_785_110_400_000;
|
||||
|
||||
function params(provider: "minimax-code" = "minimax-code", apiKey = "sk-cp-test"): UsageFetchParams {
|
||||
return { provider, credential: { type: "api_key", apiKey }, accountKey: "account-1" };
|
||||
}
|
||||
|
||||
function emptyStore(): AuthCredentialStore {
|
||||
return {
|
||||
close() {},
|
||||
listAuthCredentials() {
|
||||
return [];
|
||||
},
|
||||
updateAuthCredential() {},
|
||||
deleteAuthCredential() {},
|
||||
tryDisableAuthCredentialIfMatches() {
|
||||
return false;
|
||||
},
|
||||
replaceAuthCredentialsForProvider() {
|
||||
return [];
|
||||
},
|
||||
upsertAuthCredentialForProvider() {
|
||||
return [];
|
||||
},
|
||||
deleteAuthCredentialsForProvider() {},
|
||||
getCache() {
|
||||
return null;
|
||||
},
|
||||
setCache() {},
|
||||
cleanExpiredCache() {},
|
||||
};
|
||||
}
|
||||
|
||||
/** One `model_remains[]` entry: percentages and statuses are optional because the endpoint omits them. */
|
||||
interface RemainsBucket {
|
||||
model_name: string;
|
||||
start_time: number;
|
||||
end_time: number;
|
||||
current_interval_total_count: number;
|
||||
current_interval_usage_count: number;
|
||||
current_interval_remaining_percent?: number;
|
||||
current_interval_status?: number;
|
||||
weekly_start_time: number;
|
||||
weekly_end_time: number;
|
||||
current_weekly_total_count: number;
|
||||
current_weekly_usage_count: number;
|
||||
current_weekly_remaining_percent?: number;
|
||||
current_weekly_status?: number;
|
||||
}
|
||||
|
||||
interface RemainsPayload {
|
||||
model_remains: RemainsBucket[];
|
||||
base_resp: { status_code: number; status_msg: string };
|
||||
}
|
||||
|
||||
/** A live plan bucket: zero totals with status 1 still carry a real remaining percentage. */
|
||||
function generalBucket(): RemainsBucket {
|
||||
return {
|
||||
model_name: "general",
|
||||
start_time: INTERVAL_START,
|
||||
end_time: INTERVAL_END,
|
||||
current_interval_total_count: 0,
|
||||
current_interval_usage_count: 0,
|
||||
current_interval_remaining_percent: 90,
|
||||
current_interval_status: 1,
|
||||
weekly_start_time: WEEKLY_START,
|
||||
weekly_end_time: WEEKLY_END,
|
||||
current_weekly_total_count: 0,
|
||||
current_weekly_usage_count: 0,
|
||||
current_weekly_remaining_percent: 78,
|
||||
current_weekly_status: 1,
|
||||
};
|
||||
}
|
||||
|
||||
/** A metered bucket: request counts on both windows. */
|
||||
function videoBucket(): RemainsBucket {
|
||||
return {
|
||||
model_name: "video",
|
||||
start_time: INTERVAL_END - 86_400_000,
|
||||
end_time: INTERVAL_END,
|
||||
current_interval_total_count: 3,
|
||||
current_interval_usage_count: 1,
|
||||
current_interval_remaining_percent: 100,
|
||||
current_interval_status: 1,
|
||||
weekly_start_time: WEEKLY_START,
|
||||
weekly_end_time: WEEKLY_END,
|
||||
current_weekly_total_count: 21,
|
||||
current_weekly_usage_count: 1,
|
||||
current_weekly_remaining_percent: 100,
|
||||
current_weekly_status: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function payloadOf(...buckets: RemainsBucket[]): RemainsPayload {
|
||||
return { model_remains: buckets, base_resp: { status_code: 0, status_msg: "success" } };
|
||||
}
|
||||
|
||||
function remainsPayload(): RemainsPayload {
|
||||
return payloadOf(generalBucket(), videoBucket());
|
||||
}
|
||||
|
||||
/** The bucket shape MiniMax returns for a model the plan does not include (MiniMax-AI/cli#173). */
|
||||
function notInPlanBucket(modelName: string): RemainsBucket {
|
||||
return {
|
||||
model_name: modelName,
|
||||
start_time: INTERVAL_START,
|
||||
end_time: INTERVAL_END,
|
||||
current_interval_total_count: 0,
|
||||
current_interval_usage_count: 0,
|
||||
current_interval_remaining_percent: 100,
|
||||
current_interval_status: 3,
|
||||
weekly_start_time: WEEKLY_START,
|
||||
weekly_end_time: WEEKLY_END,
|
||||
current_weekly_total_count: 0,
|
||||
current_weekly_usage_count: 0,
|
||||
current_weekly_remaining_percent: 100,
|
||||
current_weekly_status: 3,
|
||||
};
|
||||
}
|
||||
|
||||
describe("MiniMax Token Plan usage", () => {
|
||||
test("maps each quota bucket to its rolling and weekly windows", async () => {
|
||||
const requests: { url: string; init?: RequestInit }[] = [];
|
||||
const fetchMock: FetchImpl = (input, init) => {
|
||||
requests.push({ url: String(input), init });
|
||||
return Promise.resolve(Response.json(remainsPayload()));
|
||||
};
|
||||
|
||||
const report = await minimaxCodeUsageProvider.fetchUsage(params("minimax-code"), { fetch: fetchMock });
|
||||
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(requests[0]?.url).toBe("https://api.minimax.io/v1/token_plan/remains");
|
||||
expect(new Headers(requests[0]?.init?.headers).get("Authorization")).toBe("Bearer sk-cp-test");
|
||||
expect(report?.provider).toBe("minimax-code");
|
||||
expect(report?.metadata).toMatchObject({ source: "minimax-token-plan", models: ["general", "video"] });
|
||||
expect(report?.limits.map(limit => limit.id)).toEqual(["general:4h", "general:7d", "video:24h", "video:7d"]);
|
||||
|
||||
const [intervalLimit, weeklyLimit, videoInterval, videoWeekly] = report?.limits ?? [];
|
||||
expect(intervalLimit?.label).toBe("General 4 Hour");
|
||||
expect(intervalLimit?.window).toEqual({
|
||||
id: "4h",
|
||||
label: "4 Hour",
|
||||
durationMs: 14_400_000,
|
||||
resetsAt: INTERVAL_END,
|
||||
});
|
||||
expect(intervalLimit?.amount).toEqual({
|
||||
used: 10,
|
||||
usedFraction: 0.1,
|
||||
remaining: 90,
|
||||
remainingFraction: 0.9,
|
||||
unit: "percent",
|
||||
});
|
||||
expect(intervalLimit?.status).toBe("ok");
|
||||
expect(intervalLimit?.notes).toBeUndefined();
|
||||
|
||||
expect(weeklyLimit?.window).toEqual({ id: "7d", label: "7 Day", durationMs: 604_800_000, resetsAt: WEEKLY_END });
|
||||
expect(weeklyLimit?.amount).toEqual({
|
||||
used: 22,
|
||||
usedFraction: 0.22,
|
||||
remaining: 78,
|
||||
remainingFraction: 0.78,
|
||||
unit: "percent",
|
||||
});
|
||||
|
||||
expect(videoInterval?.label).toBe("Video 24 Hour");
|
||||
expect(videoInterval?.amount.usedFraction).toBe(0);
|
||||
expect(videoInterval?.notes).toEqual(["Requests: 1/3"]);
|
||||
expect(videoWeekly?.notes).toEqual(["Requests: 1/21"]);
|
||||
});
|
||||
|
||||
test("honors a configured base URL for the quota request", async () => {
|
||||
// One case per trim the provider applies: trailing slash, trailing `/v1`, and both together.
|
||||
for (const configured of ["https://proxy.example", "https://proxy.example/", "https://proxy.example/v1/"]) {
|
||||
let requestedUrl = "";
|
||||
const fetchMock: FetchImpl = input => {
|
||||
requestedUrl = String(input);
|
||||
return Promise.resolve(Response.json(remainsPayload()));
|
||||
};
|
||||
const request: UsageFetchParams = { ...params("minimax-code"), baseUrl: configured };
|
||||
|
||||
const report = await minimaxCodeUsageProvider.fetchUsage(request, { fetch: fetchMock });
|
||||
|
||||
expect(requestedUrl).toBe("https://proxy.example/v1/token_plan/remains");
|
||||
expect(report?.provider).toBe("minimax-code");
|
||||
}
|
||||
});
|
||||
|
||||
test("fails closed when MiniMax rejects the key inside a 200 response", async () => {
|
||||
const fetchMock: FetchImpl = () =>
|
||||
Promise.resolve(
|
||||
Response.json({
|
||||
base_resp: { status_code: 1004, status_msg: "login fail: Please carry the API secret key" },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(await minimaxCodeUsageProvider.fetchUsage(params("minimax-code"), { fetch: fetchMock })).toBeNull();
|
||||
});
|
||||
|
||||
test("does not fetch without an API key credential", async () => {
|
||||
let fetched = false;
|
||||
const fetchMock: FetchImpl = () => {
|
||||
fetched = true;
|
||||
return Promise.resolve(Response.json(remainsPayload()));
|
||||
};
|
||||
const request: UsageFetchParams = { provider: "minimax-code", credential: { type: "oauth" } };
|
||||
|
||||
expect(minimaxCodeUsageProvider.supports?.(request)).toBe(false);
|
||||
expect(await minimaxCodeUsageProvider.fetchUsage(request, { fetch: fetchMock })).toBeNull();
|
||||
expect(fetched).toBe(false);
|
||||
});
|
||||
|
||||
test("marks a spent window exhausted and drops buckets with no percentage", async () => {
|
||||
const spentGeneral: RemainsBucket = { ...generalBucket(), current_interval_remaining_percent: 0 };
|
||||
const videoWithoutPercentages: RemainsBucket = {
|
||||
model_name: "video",
|
||||
start_time: INTERVAL_END - 86_400_000,
|
||||
end_time: INTERVAL_END,
|
||||
current_interval_total_count: 3,
|
||||
current_interval_usage_count: 1,
|
||||
current_interval_status: 1,
|
||||
weekly_start_time: WEEKLY_START,
|
||||
weekly_end_time: WEEKLY_END,
|
||||
current_weekly_total_count: 21,
|
||||
current_weekly_usage_count: 1,
|
||||
current_weekly_status: 1,
|
||||
};
|
||||
const fetchMock: FetchImpl = () =>
|
||||
Promise.resolve(Response.json(payloadOf(spentGeneral, videoWithoutPercentages)));
|
||||
|
||||
const report = await minimaxCodeUsageProvider.fetchUsage(params("minimax-code"), { fetch: fetchMock });
|
||||
|
||||
expect(report?.limits.map(limit => limit.id)).toEqual(["general:4h", "general:7d"]);
|
||||
expect(report?.limits[0]?.status).toBe("exhausted");
|
||||
expect(report?.limits[0]?.amount.usedFraction).toBe(1);
|
||||
});
|
||||
|
||||
test("returns null when the plan reports no quota buckets", async () => {
|
||||
const fetchMock: FetchImpl = () =>
|
||||
Promise.resolve(Response.json({ model_remains: [], base_resp: { status_code: 0 } }));
|
||||
|
||||
expect(await minimaxCodeUsageProvider.fetchUsage(params("minimax-code"), { fetch: fetchMock })).toBeNull();
|
||||
});
|
||||
|
||||
test("reports a window the endpoint marks exhausted, percentage or not", async () => {
|
||||
const noPercentage: RemainsBucket = {
|
||||
...generalBucket(),
|
||||
current_interval_status: 2,
|
||||
current_interval_remaining_percent: undefined,
|
||||
};
|
||||
const stalePercentage: RemainsBucket = { ...generalBucket(), current_interval_status: 2 };
|
||||
|
||||
for (const bucket of [noPercentage, stalePercentage]) {
|
||||
const fetchMock: FetchImpl = () => Promise.resolve(Response.json(payloadOf(bucket)));
|
||||
|
||||
const report = await minimaxCodeUsageProvider.fetchUsage(params("minimax-code"), { fetch: fetchMock });
|
||||
|
||||
const interval = report?.limits.find(limit => limit.id === "general:4h");
|
||||
expect(interval?.status).toBe("exhausted");
|
||||
expect(interval?.amount).toMatchObject({ usedFraction: 1, remainingFraction: 0 });
|
||||
expect(report?.limits.find(limit => limit.id === "general:7d")?.status).toBe("ok");
|
||||
}
|
||||
});
|
||||
|
||||
test("keeps a model that is not in the plan out of the reported quota", async () => {
|
||||
const fetchMock: FetchImpl = () =>
|
||||
Promise.resolve(Response.json(payloadOf(generalBucket(), notInPlanBucket("video"))));
|
||||
|
||||
const report = await minimaxCodeUsageProvider.fetchUsage(params("minimax-code"), { fetch: fetchMock });
|
||||
|
||||
expect(report?.limits.map(limit => limit.id)).toEqual(["general:4h", "general:7d"]);
|
||||
expect(report?.metadata).toMatchObject({ models: ["general", "video"], unavailableModels: ["video"] });
|
||||
});
|
||||
|
||||
test("returns null when every model is outside the plan", async () => {
|
||||
const fetchMock: FetchImpl = () =>
|
||||
Promise.resolve(Response.json(payloadOf(notInPlanBucket("general"), notInPlanBucket("video"))));
|
||||
|
||||
expect(await minimaxCodeUsageProvider.fetchUsage(params("minimax-code"), { fetch: fetchMock })).toBeNull();
|
||||
});
|
||||
|
||||
test("keeps a bucket whose windows disagree about being in the plan", async () => {
|
||||
const meteredWeekly: RemainsBucket = {
|
||||
...notInPlanBucket("video"),
|
||||
current_weekly_status: 1,
|
||||
current_weekly_total_count: 21,
|
||||
current_weekly_usage_count: 1,
|
||||
current_weekly_remaining_percent: 40,
|
||||
};
|
||||
const meteredInterval: RemainsBucket = {
|
||||
...notInPlanBucket("video"),
|
||||
current_interval_status: 1,
|
||||
current_interval_total_count: 3,
|
||||
current_interval_usage_count: 1,
|
||||
current_interval_remaining_percent: 40,
|
||||
};
|
||||
|
||||
for (const bucket of [meteredWeekly, meteredInterval]) {
|
||||
const fetchMock: FetchImpl = () => Promise.resolve(Response.json(payloadOf(bucket)));
|
||||
|
||||
const report = await minimaxCodeUsageProvider.fetchUsage(params("minimax-code"), { fetch: fetchMock });
|
||||
|
||||
expect(report?.limits.map(limit => limit.id)).toEqual(["video:4h", "video:7d"]);
|
||||
expect(report?.metadata).not.toHaveProperty("unavailableModels");
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects a payload with no base_resp envelope", async () => {
|
||||
const fetchMock: FetchImpl = () =>
|
||||
Promise.resolve(Response.json({ model_remains: remainsPayload().model_remains }));
|
||||
|
||||
expect(await minimaxCodeUsageProvider.fetchUsage(params("minimax-code"), { fetch: fetchMock })).toBeNull();
|
||||
});
|
||||
|
||||
test("registers the Token Plan id in AuthStorage's default usage resolver", async () => {
|
||||
const storage = new AuthStorage(emptyStore());
|
||||
await storage.reload();
|
||||
try {
|
||||
expect(storage.usageProviderFor("minimax-code")).toBe(minimaxCodeUsageProvider);
|
||||
expect(storage.usageProviderFor("minimax-code-cn")).toBeUndefined();
|
||||
} finally {
|
||||
storage.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("reports the shared plan quota against catalog model ids", async () => {
|
||||
const fetchMock: FetchImpl = () => Promise.resolve(Response.json(remainsPayload()));
|
||||
|
||||
const report = await minimaxCodeUsageProvider.fetchUsage(params("minimax-code"), { fetch: fetchMock });
|
||||
if (!report) throw new Error("expected a usage report");
|
||||
|
||||
const general = report.limits.find(limit => limit.id === "general:4h");
|
||||
expect(general?.scope).toEqual({ provider: "minimax-code", shared: true, windowId: "4h" });
|
||||
const video = report.limits.find(limit => limit.id === "video:24h");
|
||||
expect(video?.scope).toEqual({ provider: "minimax-code", modelId: "video", windowId: "24h" });
|
||||
|
||||
// Without a MiniMax ranking strategy AuthStorage matches `shared` or an exact
|
||||
// catalog id, so a bucket-name scope would report no models at all.
|
||||
const storage = new AuthStorage(emptyStore());
|
||||
await storage.reload();
|
||||
try {
|
||||
expect(storage.getUsageReportingModelIds("minimax-code", ["MiniMax-M3", "MiniMax-M2"], [report])).toEqual([
|
||||
"MiniMax-M3",
|
||||
"MiniMax-M2",
|
||||
]);
|
||||
} finally {
|
||||
storage.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user