feat(ai): land the Token Plan usage provider for the international host only
Registering a second default provider fetch was flagged as needing maintainer sign-off, and `api.minimaxi.com` cannot be exercised by anyone on this change, so the mainland id is left exactly as it was: `minimax-code-cn` is not registered, its base URL and provider export are gone, and `supports()` no longer claims an id that resolves to nothing. The parser, the not-in-plan handling and the shared scope are unchanged and keep their tests; the suite now asserts that the CN id resolves to no usage provider, so wiring it later is a deliberate change rather than a silent one.
This commit is contained in:
@@ -4,7 +4,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` and `minimax-code-cn` surface real remaining percentages instead of empty reports. Both ids are registered in the default usage provider set — the China id previously resolved to nothing, so it never reached a fetcher at all.
|
||||
- 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,7 +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 { minimaxCodeCnUsageProvider, minimaxCodeUsageProvider } from "./usage/minimax-code";
|
||||
import { minimaxCodeUsageProvider } from "./usage/minimax-code";
|
||||
import { ollamaCloudUsageProvider, ollamaUsageProvider } from "./usage/ollama";
|
||||
import { codexRankingStrategy, openaiCodexUsageProvider } from "./usage/openai-codex";
|
||||
import {
|
||||
@@ -645,7 +645,6 @@ const DEFAULT_USAGE_PROVIDERS: UsageProvider[] = [
|
||||
openaiCodexUsageProvider,
|
||||
kimiUsageProvider,
|
||||
minimaxCodeUsageProvider,
|
||||
minimaxCodeCnUsageProvider,
|
||||
antigravityUsageProvider,
|
||||
googleGeminiCliUsageProvider,
|
||||
ollamaUsageProvider,
|
||||
|
||||
@@ -10,9 +10,7 @@ import { isRecord } from "../utils";
|
||||
import { toNumber } from "./shared";
|
||||
|
||||
const INTL_PROVIDER = "minimax-code";
|
||||
const CN_PROVIDER = "minimax-code-cn";
|
||||
const INTL_BASE_URL = "https://api.minimax.io";
|
||||
const CN_BASE_URL = "https://api.minimaxi.com";
|
||||
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. */
|
||||
@@ -203,7 +201,7 @@ function buildBucketLimits(provider: string, bucket: TokenPlanBucket, accountId:
|
||||
}
|
||||
|
||||
/**
|
||||
* MiniMax Token Plan usage provider.
|
||||
* 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
|
||||
@@ -211,17 +209,13 @@ function buildBucketLimits(provider: string, bucket: TokenPlanBucket, accountId:
|
||||
* 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 && params.provider !== CN_PROVIDER) return 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$/, "")
|
||||
: params.provider === CN_PROVIDER
|
||||
? CN_BASE_URL
|
||||
: INTL_BASE_URL;
|
||||
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,
|
||||
@@ -289,11 +283,3 @@ export const minimaxCodeUsageProvider: UsageProvider = {
|
||||
supports: params =>
|
||||
params.provider === INTL_PROVIDER && params.credential.type === "api_key" && Boolean(params.credential.apiKey),
|
||||
};
|
||||
|
||||
/** MiniMax Token Plan (China, `api.minimaxi.com`). */
|
||||
export const minimaxCodeCnUsageProvider: UsageProvider = {
|
||||
id: CN_PROVIDER,
|
||||
fetchUsage: fetchMiniMaxCodeUsage,
|
||||
supports: params =>
|
||||
params.provider === CN_PROVIDER && params.credential.type === "api_key" && Boolean(params.credential.apiKey),
|
||||
};
|
||||
|
||||
@@ -2,14 +2,14 @@ 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 { minimaxCodeCnUsageProvider, minimaxCodeUsageProvider } from "@oh-my-pi/pi-ai/usage/minimax-code";
|
||||
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-cn", apiKey = "sk-cp-test"): UsageFetchParams {
|
||||
function params(provider: "minimax-code" = "minimax-code", apiKey = "sk-cp-test"): UsageFetchParams {
|
||||
return { provider, credential: { type: "api_key", apiKey }, accountKey: "account-1" };
|
||||
}
|
||||
|
||||
@@ -176,19 +176,6 @@ describe("MiniMax Token Plan usage", () => {
|
||||
expect(videoWeekly?.notes).toEqual(["Requests: 1/21"]);
|
||||
});
|
||||
|
||||
test("routes the China provider to the mainland endpoint", async () => {
|
||||
let requestedUrl = "";
|
||||
const fetchMock: FetchImpl = input => {
|
||||
requestedUrl = String(input);
|
||||
return Promise.resolve(Response.json(remainsPayload()));
|
||||
};
|
||||
|
||||
const report = await minimaxCodeCnUsageProvider.fetchUsage(params("minimax-code-cn"), { fetch: fetchMock });
|
||||
|
||||
expect(requestedUrl).toBe("https://api.minimaxi.com/v1/token_plan/remains");
|
||||
expect(report?.provider).toBe("minimax-code-cn");
|
||||
});
|
||||
|
||||
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/"]) {
|
||||
@@ -266,7 +253,7 @@ describe("MiniMax Token Plan usage", () => {
|
||||
const fetchMock: FetchImpl = () =>
|
||||
Promise.resolve(Response.json(payloadOf(generalBucket(), notInPlanBucket("video"))));
|
||||
|
||||
const report = await minimaxCodeCnUsageProvider.fetchUsage(params("minimax-code-cn"), { fetch: fetchMock });
|
||||
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"] });
|
||||
@@ -276,7 +263,7 @@ describe("MiniMax Token Plan usage", () => {
|
||||
const fetchMock: FetchImpl = () =>
|
||||
Promise.resolve(Response.json(payloadOf(notInPlanBucket("general"), notInPlanBucket("video"))));
|
||||
|
||||
expect(await minimaxCodeCnUsageProvider.fetchUsage(params("minimax-code-cn"), { fetch: fetchMock })).toBeNull();
|
||||
expect(await minimaxCodeUsageProvider.fetchUsage(params("minimax-code"), { fetch: fetchMock })).toBeNull();
|
||||
});
|
||||
|
||||
test("keeps a bucket whose windows disagree about being in the plan", async () => {
|
||||
@@ -298,7 +285,7 @@ describe("MiniMax Token Plan usage", () => {
|
||||
for (const bucket of [meteredWeekly, meteredInterval]) {
|
||||
const fetchMock: FetchImpl = () => Promise.resolve(Response.json(payloadOf(bucket)));
|
||||
|
||||
const report = await minimaxCodeCnUsageProvider.fetchUsage(params("minimax-code-cn"), { fetch: fetchMock });
|
||||
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");
|
||||
@@ -312,12 +299,12 @@ describe("MiniMax Token Plan usage", () => {
|
||||
expect(await minimaxCodeUsageProvider.fetchUsage(params("minimax-code"), { fetch: fetchMock })).toBeNull();
|
||||
});
|
||||
|
||||
test("registers both Token Plan ids in AuthStorage's default usage resolver", async () => {
|
||||
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")).toBe(minimaxCodeCnUsageProvider);
|
||||
expect(storage.usageProviderFor("minimax-code-cn")).toBeUndefined();
|
||||
} finally {
|
||||
storage.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user