diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 7c682fd6b..f722d397c 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -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. diff --git a/packages/ai/src/auth-storage.ts b/packages/ai/src/auth-storage.ts index cd00041aa..2886e5f4b 100644 --- a/packages/ai/src/auth-storage.ts +++ b/packages/ai/src/auth-storage.ts @@ -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, diff --git a/packages/ai/src/usage/minimax-code.ts b/packages/ai/src/usage/minimax-code.ts index 7dd9a9d02..90f68ac70 100644 --- a/packages/ai/src/usage/minimax-code.ts +++ b/packages/ai/src/usage/minimax-code.ts @@ -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 { - 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), -}; diff --git a/packages/ai/test/minimax-token-plan-usage.test.ts b/packages/ai/test/minimax-token-plan-usage.test.ts index cd62819a9..463729b0d 100644 --- a/packages/ai/test/minimax-token-plan-usage.test.ts +++ b/packages/ai/test/minimax-token-plan-usage.test.ts @@ -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(); }