@oh-my-pi/pi-ai 6.9.69 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,393 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { CODEX_BASE_URL } from "$ai/providers/openai-codex/constants";
3
+ import type {
4
+ UsageAmount,
5
+ UsageCache,
6
+ UsageFetchContext,
7
+ UsageFetchParams,
8
+ UsageLimit,
9
+ UsageProvider,
10
+ UsageReport,
11
+ UsageWindow,
12
+ } from "$ai/usage";
13
+
14
+ const CODEX_USAGE_PATH = "wham/usage";
15
+ const DEFAULT_CACHE_TTL_MS = 60_000;
16
+ const JWT_AUTH_CLAIM = "https://api.openai.com/auth";
17
+ const JWT_PROFILE_CLAIM = "https://api.openai.com/profile";
18
+
19
+ interface CodexUsageWindowPayload {
20
+ used_percent?: number;
21
+ limit_window_seconds?: number;
22
+ reset_after_seconds?: number;
23
+ reset_at?: number;
24
+ }
25
+
26
+ interface CodexUsageRateLimitPayload {
27
+ allowed?: boolean;
28
+ limit_reached?: boolean;
29
+ primary_window?: CodexUsageWindowPayload | null;
30
+ secondary_window?: CodexUsageWindowPayload | null;
31
+ }
32
+
33
+ interface CodexUsagePayload {
34
+ plan_type?: string;
35
+ rate_limit?: CodexUsageRateLimitPayload | null;
36
+ }
37
+
38
+ interface ParsedUsageWindow {
39
+ usedPercent?: number;
40
+ limitWindowSeconds?: number;
41
+ resetAfterSeconds?: number;
42
+ resetAt?: number;
43
+ }
44
+
45
+ interface ParsedUsage {
46
+ planType?: string;
47
+ allowed?: boolean;
48
+ limitReached?: boolean;
49
+ primary?: ParsedUsageWindow;
50
+ secondary?: ParsedUsageWindow;
51
+ raw: CodexUsagePayload;
52
+ }
53
+
54
+ interface JwtPayload {
55
+ [JWT_AUTH_CLAIM]?: {
56
+ chatgpt_account_id?: string;
57
+ };
58
+ [JWT_PROFILE_CLAIM]?: {
59
+ email?: string;
60
+ };
61
+ }
62
+
63
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
64
+ typeof value === "object" && value !== null && !Array.isArray(value);
65
+
66
+ const toNumber = (value: unknown): number | undefined => {
67
+ if (typeof value === "number" && Number.isFinite(value)) return value;
68
+ if (typeof value === "string") {
69
+ const trimmed = value.trim();
70
+ if (!trimmed) return undefined;
71
+ const parsed = Number(trimmed);
72
+ if (Number.isFinite(parsed)) return parsed;
73
+ }
74
+ return undefined;
75
+ };
76
+
77
+ const toBoolean = (value: unknown): boolean | undefined => {
78
+ if (typeof value === "boolean") return value;
79
+ return undefined;
80
+ };
81
+
82
+ function base64UrlDecode(input: string): string {
83
+ const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
84
+ const padLen = (4 - (base64.length % 4)) % 4;
85
+ const padded = base64 + "=".repeat(padLen);
86
+ return Buffer.from(padded, "base64").toString("utf8");
87
+ }
88
+
89
+ function parseJwt(token: string): JwtPayload | null {
90
+ const parts = token.split(".");
91
+ if (parts.length !== 3) return null;
92
+ try {
93
+ const payloadJson = base64UrlDecode(parts[1]);
94
+ return JSON.parse(payloadJson) as JwtPayload;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ function extractAccountId(token: string | undefined): string | undefined {
101
+ if (!token) return undefined;
102
+ const payload = parseJwt(token);
103
+ return payload?.[JWT_AUTH_CLAIM]?.chatgpt_account_id ?? undefined;
104
+ }
105
+
106
+ function extractEmail(token: string | undefined): string | undefined {
107
+ if (!token) return undefined;
108
+ const payload = parseJwt(token);
109
+ return payload?.[JWT_PROFILE_CLAIM]?.email ?? undefined;
110
+ }
111
+
112
+ function parseUsageWindow(payload: unknown): ParsedUsageWindow | undefined {
113
+ if (!isRecord(payload)) return undefined;
114
+ const usedPercent = toNumber(payload.used_percent);
115
+ const limitWindowSeconds = toNumber(payload.limit_window_seconds);
116
+ const resetAfterSeconds = toNumber(payload.reset_after_seconds);
117
+ const resetAt = toNumber(payload.reset_at);
118
+ if (
119
+ usedPercent === undefined &&
120
+ limitWindowSeconds === undefined &&
121
+ resetAfterSeconds === undefined &&
122
+ resetAt === undefined
123
+ ) {
124
+ return undefined;
125
+ }
126
+ return {
127
+ usedPercent,
128
+ limitWindowSeconds,
129
+ resetAfterSeconds,
130
+ resetAt,
131
+ };
132
+ }
133
+
134
+ function parseUsagePayload(payload: unknown): ParsedUsage | null {
135
+ if (!isRecord(payload)) return null;
136
+ const planType = typeof payload.plan_type === "string" ? payload.plan_type : undefined;
137
+ const rateLimit = isRecord(payload.rate_limit) ? payload.rate_limit : undefined;
138
+ if (!rateLimit) return null;
139
+ const parsed: ParsedUsage = {
140
+ planType,
141
+ allowed: toBoolean(rateLimit.allowed),
142
+ limitReached: toBoolean(rateLimit.limit_reached),
143
+ primary: parseUsageWindow(rateLimit.primary_window),
144
+ secondary: parseUsageWindow(rateLimit.secondary_window),
145
+ raw: payload as CodexUsagePayload,
146
+ };
147
+ if (!parsed.primary && !parsed.secondary && parsed.allowed === undefined && parsed.limitReached === undefined) {
148
+ return null;
149
+ }
150
+ return parsed;
151
+ }
152
+
153
+ function normalizeCodexBaseUrl(baseUrl?: string): string {
154
+ const fallback = CODEX_BASE_URL;
155
+ const trimmed = baseUrl?.trim() ? baseUrl.trim() : fallback;
156
+ const base = trimmed.replace(/\/+$/, "");
157
+ const lower = base.toLowerCase();
158
+ if (
159
+ (lower.startsWith("https://chatgpt.com") || lower.startsWith("https://chat.openai.com")) &&
160
+ !lower.includes("/backend-api")
161
+ ) {
162
+ return `${base}/backend-api`;
163
+ }
164
+ return base;
165
+ }
166
+
167
+ function buildCodexUsageUrl(baseUrl: string): string {
168
+ const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
169
+ return `${normalized}${CODEX_USAGE_PATH}`;
170
+ }
171
+
172
+ function formatWindowLabel(value: number, unit: "hour" | "day"): string {
173
+ const rounded = Math.round(value);
174
+ const suffix = rounded === 1 ? unit : `${unit}s`;
175
+ return `${rounded} ${suffix}`;
176
+ }
177
+
178
+ function buildWindowLabel(seconds: number): { id: string; label: string } {
179
+ const daySeconds = 86_400;
180
+ if (seconds >= daySeconds) {
181
+ const days = Math.round(seconds / daySeconds);
182
+ return { id: `${days}d`, label: formatWindowLabel(days, "day") };
183
+ }
184
+ const hours = Math.max(1, Math.round(seconds / 3600));
185
+ return { id: `${hours}h`, label: formatWindowLabel(hours, "hour") };
186
+ }
187
+
188
+ function resolveResetTimes(window: ParsedUsageWindow, nowMs: number): Pick<UsageWindow, "resetsAt" | "resetInMs"> {
189
+ const resetAt = window.resetAt;
190
+ if (resetAt !== undefined) {
191
+ const resetAtMs = resetAt > 1_000_000_000_000 ? resetAt : resetAt * 1000;
192
+ if (Number.isFinite(resetAtMs)) {
193
+ return { resetsAt: resetAtMs, resetInMs: resetAtMs - nowMs };
194
+ }
195
+ }
196
+ if (window.resetAfterSeconds !== undefined) {
197
+ const resetInMs = window.resetAfterSeconds * 1000;
198
+ return { resetsAt: nowMs + resetInMs, resetInMs };
199
+ }
200
+ return {};
201
+ }
202
+
203
+ function buildUsageWindow(window: ParsedUsageWindow, key: string, nowMs: number): UsageWindow {
204
+ if (window.limitWindowSeconds !== undefined) {
205
+ const { id, label } = buildWindowLabel(window.limitWindowSeconds);
206
+ const durationMs = window.limitWindowSeconds * 1000;
207
+ return { id, label, durationMs, ...resolveResetTimes(window, nowMs) };
208
+ }
209
+ const fallbackLabel = key === "primary" ? "Primary window" : "Secondary window";
210
+ return { id: key, label: fallbackLabel, ...resolveResetTimes(window, nowMs) };
211
+ }
212
+
213
+ function buildUsageAmount(window: ParsedUsageWindow): UsageAmount {
214
+ const usedPercent = window.usedPercent;
215
+ if (usedPercent === undefined) {
216
+ return { unit: "percent" };
217
+ }
218
+ const clamped = Math.min(Math.max(usedPercent, 0), 100);
219
+ const usedFraction = clamped / 100;
220
+ return {
221
+ used: clamped,
222
+ limit: 100,
223
+ remaining: Math.max(0, 100 - clamped),
224
+ usedFraction,
225
+ remainingFraction: Math.max(0, 1 - usedFraction),
226
+ unit: "percent",
227
+ };
228
+ }
229
+
230
+ function buildUsageStatus(usedFraction?: number, limitReached?: boolean): UsageLimit["status"] {
231
+ if (limitReached) return "exhausted";
232
+ if (usedFraction === undefined) return "unknown";
233
+ if (usedFraction >= 1) return "exhausted";
234
+ if (usedFraction >= 0.9) return "warning";
235
+ return "ok";
236
+ }
237
+
238
+ function buildUsageLimit(args: {
239
+ key: "primary" | "secondary";
240
+ window: ParsedUsageWindow;
241
+ accountId?: string;
242
+ planType?: string;
243
+ limitReached?: boolean;
244
+ nowMs: number;
245
+ }): UsageLimit {
246
+ const usageWindow = buildUsageWindow(args.window, args.key, args.nowMs);
247
+ const amount = buildUsageAmount(args.window);
248
+ return {
249
+ id: `openai-codex:${args.key}`,
250
+ label: usageWindow.label,
251
+ scope: {
252
+ provider: "openai-codex",
253
+ accountId: args.accountId,
254
+ tier: args.planType,
255
+ windowId: usageWindow.id,
256
+ shared: true,
257
+ },
258
+ window: usageWindow,
259
+ amount,
260
+ status: buildUsageStatus(amount.usedFraction, args.limitReached),
261
+ };
262
+ }
263
+
264
+ function resolveCacheExpiry(args: { report: UsageReport | null; nowMs: number }): number {
265
+ const { report, nowMs } = args;
266
+ if (!report) return nowMs + DEFAULT_CACHE_TTL_MS;
267
+ const exhausted = report.limits.some((limit) => limit.status === "exhausted");
268
+ const resetCandidates = report.limits
269
+ .map((limit) => limit.window?.resetsAt)
270
+ .filter((value): value is number => typeof value === "number" && Number.isFinite(value));
271
+ const earliestReset = resetCandidates.length > 0 ? Math.min(...resetCandidates) : undefined;
272
+ if (exhausted && earliestReset) return earliestReset;
273
+ if (earliestReset) return Math.min(nowMs + DEFAULT_CACHE_TTL_MS, earliestReset);
274
+ return nowMs + DEFAULT_CACHE_TTL_MS;
275
+ }
276
+
277
+ async function getCachedReport(
278
+ cache: UsageCache,
279
+ cacheKey: string,
280
+ nowMs: number,
281
+ ): Promise<UsageReport | null | undefined> {
282
+ const cached = await cache.get(cacheKey);
283
+ if (!cached) return undefined;
284
+ if (cached.expiresAt <= nowMs) return undefined;
285
+ return cached.value;
286
+ }
287
+
288
+ async function setCachedReport(
289
+ cache: UsageCache,
290
+ cacheKey: string,
291
+ report: UsageReport | null,
292
+ expiresAt: number,
293
+ ): Promise<void> {
294
+ await cache.set(cacheKey, { value: report, expiresAt });
295
+ }
296
+
297
+ export const openaiCodexUsageProvider: UsageProvider = {
298
+ id: "openai-codex",
299
+ supports(params: UsageFetchParams): boolean {
300
+ return params.provider === "openai-codex" && params.credential.type === "oauth";
301
+ },
302
+ async fetchUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
303
+ if (params.provider !== "openai-codex") return null;
304
+ const { credential } = params;
305
+ if (credential.type !== "oauth") return null;
306
+
307
+ const accessToken = credential.accessToken;
308
+ if (!accessToken) return null;
309
+
310
+ const nowMs = ctx.now();
311
+ if (credential.expiresAt !== undefined && credential.expiresAt <= nowMs) {
312
+ ctx.logger?.warn("Codex usage token expired", { provider: params.provider });
313
+ return null;
314
+ }
315
+
316
+ const baseUrl = normalizeCodexBaseUrl(params.baseUrl);
317
+ const accountId = credential.accountId ?? extractAccountId(accessToken);
318
+ const cacheKey = `usage:openai-codex:${accountId ?? "unknown"}:${baseUrl}`;
319
+ const cached = await getCachedReport(ctx.cache, cacheKey, nowMs);
320
+ if (cached !== undefined) return cached;
321
+
322
+ const headers: Record<string, string> = {
323
+ Authorization: `Bearer ${accessToken}`,
324
+ "User-Agent": "OpenCode-Status-Plugin/1.0",
325
+ };
326
+ if (accountId) {
327
+ headers["ChatGPT-Account-Id"] = accountId;
328
+ }
329
+
330
+ const url = buildCodexUsageUrl(baseUrl);
331
+ let payload: unknown;
332
+ try {
333
+ const response = await ctx.fetch(url, { headers, signal: params.signal });
334
+ if (!response.ok) {
335
+ ctx.logger?.warn("Codex usage request failed", { status: response.status, provider: params.provider });
336
+ return null;
337
+ }
338
+ payload = await response.json();
339
+ } catch (error) {
340
+ ctx.logger?.warn("Codex usage request error", { provider: params.provider, error: String(error) });
341
+ return null;
342
+ }
343
+
344
+ const parsed = parseUsagePayload(payload);
345
+ if (!parsed) {
346
+ ctx.logger?.warn("Codex usage response invalid", { provider: params.provider });
347
+ return null;
348
+ }
349
+
350
+ const limits: UsageLimit[] = [];
351
+ if (parsed.primary) {
352
+ limits.push(
353
+ buildUsageLimit({
354
+ key: "primary",
355
+ window: parsed.primary,
356
+ accountId,
357
+ planType: parsed.planType,
358
+ limitReached: parsed.limitReached,
359
+ nowMs,
360
+ }),
361
+ );
362
+ }
363
+ if (parsed.secondary) {
364
+ limits.push(
365
+ buildUsageLimit({
366
+ key: "secondary",
367
+ window: parsed.secondary,
368
+ accountId,
369
+ planType: parsed.planType,
370
+ limitReached: parsed.limitReached,
371
+ nowMs,
372
+ }),
373
+ );
374
+ }
375
+
376
+ const report: UsageReport = {
377
+ provider: "openai-codex",
378
+ fetchedAt: nowMs,
379
+ limits,
380
+ metadata: {
381
+ planType: parsed.planType,
382
+ allowed: parsed.allowed,
383
+ limitReached: parsed.limitReached,
384
+ email: credential.email ?? extractEmail(accessToken),
385
+ },
386
+ raw: parsed.raw,
387
+ };
388
+
389
+ const expiresAt = resolveCacheExpiry({ report, nowMs });
390
+ await setCachedReport(ctx.cache, cacheKey, report, expiresAt);
391
+ return report;
392
+ },
393
+ };
@@ -0,0 +1,292 @@
1
+ import type {
2
+ UsageAmount,
3
+ UsageFetchContext,
4
+ UsageFetchParams,
5
+ UsageLimit,
6
+ UsageProvider,
7
+ UsageReport,
8
+ UsageStatus,
9
+ UsageWindow,
10
+ } from "$ai/usage";
11
+
12
+ const DEFAULT_ENDPOINT = "https://api.z.ai";
13
+ const QUOTA_PATH = "/api/monitor/usage/quota/limit";
14
+ const MODEL_USAGE_PATH = "/api/monitor/usage/model-usage";
15
+ const DEFAULT_CACHE_TTL_MS = 60_000;
16
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
17
+
18
+ function normalizeZaiBaseUrl(baseUrl?: string): string {
19
+ if (!baseUrl || !baseUrl.trim()) return DEFAULT_ENDPOINT;
20
+ try {
21
+ return new URL(baseUrl.trim()).origin;
22
+ } catch {
23
+ return DEFAULT_ENDPOINT;
24
+ }
25
+ }
26
+
27
+ interface ZaiUsageLimitItem {
28
+ type?: string;
29
+ usage?: number;
30
+ currentValue?: number;
31
+ percentage?: number;
32
+ remaining?: number;
33
+ nextResetTime?: number;
34
+ }
35
+
36
+ interface ZaiQuotaPayload {
37
+ success?: boolean;
38
+ code?: number;
39
+ msg?: string;
40
+ data?: {
41
+ limits?: ZaiUsageLimitItem[];
42
+ };
43
+ }
44
+
45
+ function isRecord(value: unknown): value is Record<string, unknown> {
46
+ return typeof value === "object" && value !== null && !Array.isArray(value);
47
+ }
48
+
49
+ function toNumber(value: unknown): number | undefined {
50
+ if (typeof value === "number" && Number.isFinite(value)) return value;
51
+ if (typeof value === "string" && value.trim()) {
52
+ const parsed = Number(value);
53
+ return Number.isFinite(parsed) ? parsed : undefined;
54
+ }
55
+ return undefined;
56
+ }
57
+
58
+ function parseMillis(value: unknown): number | undefined {
59
+ const parsed = toNumber(value);
60
+ if (parsed === undefined) return undefined;
61
+ return parsed > 1_000_000_000_000 ? parsed : parsed * 1000;
62
+ }
63
+
64
+ function parseLimitItem(value: unknown): ZaiUsageLimitItem | null {
65
+ if (!isRecord(value)) return null;
66
+ const type = typeof value.type === "string" ? value.type : undefined;
67
+ if (!type) return null;
68
+ return {
69
+ type,
70
+ usage: toNumber(value.usage),
71
+ currentValue: toNumber(value.currentValue),
72
+ percentage: toNumber(value.percentage),
73
+ remaining: toNumber(value.remaining),
74
+ nextResetTime: parseMillis(value.nextResetTime),
75
+ };
76
+ }
77
+
78
+ function buildUsageAmount(args: {
79
+ used: number | undefined;
80
+ limit: number | undefined;
81
+ remaining: number | undefined;
82
+ unit: UsageAmount["unit"];
83
+ percentage?: number;
84
+ }): UsageAmount {
85
+ const usedFraction =
86
+ args.percentage !== undefined
87
+ ? Math.min(Math.max(args.percentage / 100, 0), 1)
88
+ : args.used !== undefined && args.limit !== undefined && args.limit > 0
89
+ ? Math.min(args.used / args.limit, 1)
90
+ : undefined;
91
+ const remainingFraction = usedFraction !== undefined ? Math.max(1 - usedFraction, 0) : undefined;
92
+ return {
93
+ used: args.used,
94
+ limit: args.limit,
95
+ remaining: args.remaining,
96
+ usedFraction,
97
+ remainingFraction,
98
+ unit: args.unit,
99
+ };
100
+ }
101
+
102
+ function buildUsageWindow(
103
+ id: string,
104
+ label: string,
105
+ resetsAt: number | undefined,
106
+ now: number,
107
+ ): UsageWindow | undefined {
108
+ if (!resetsAt) return { id, label };
109
+ const resetInMs = Math.max(0, resetsAt - now);
110
+ return {
111
+ id,
112
+ label,
113
+ resetsAt,
114
+ resetInMs,
115
+ };
116
+ }
117
+
118
+ function getUsageStatus(usedFraction: number | undefined): UsageStatus | undefined {
119
+ if (usedFraction === undefined) return undefined;
120
+ if (usedFraction >= 1) return "exhausted";
121
+ if (usedFraction >= 0.9) return "warning";
122
+ return "ok";
123
+ }
124
+
125
+ function buildCacheKey(params: UsageFetchParams): string {
126
+ const credential = params.credential;
127
+ const account = credential.accountId ?? credential.email ?? "unknown";
128
+ const token = credential.apiKey ?? credential.accessToken;
129
+ const fingerprint = token && typeof token === "string" ? Bun.hash(token).toString(16) : "anonymous";
130
+ const baseUrl = params.baseUrl ?? DEFAULT_ENDPOINT;
131
+ return `usage:${params.provider}:${account}:${fingerprint}:${baseUrl}`;
132
+ }
133
+
134
+ function resolveCacheExpiry(now: number, limits: UsageLimit[]): number {
135
+ const earliestReset = limits
136
+ .map((limit) => limit.window?.resetsAt)
137
+ .filter((value): value is number => typeof value === "number" && Number.isFinite(value))
138
+ .reduce((min, value) => (min === undefined ? value : Math.min(min, value)), undefined as number | undefined);
139
+ if (!earliestReset) return now + DEFAULT_CACHE_TTL_MS;
140
+ return Math.min(earliestReset, now + DEFAULT_CACHE_TTL_MS);
141
+ }
142
+
143
+ function formatDate(value: Date): string {
144
+ const pad = (input: number) => String(input).padStart(2, "0");
145
+ return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}+${pad(value.getHours())}:${pad(
146
+ value.getMinutes(),
147
+ )}:${pad(value.getSeconds())}`;
148
+ }
149
+
150
+ function buildModelUsageUrl(baseUrl: string, now: Date): string {
151
+ const start = new Date(now.getTime() - SEVEN_DAYS_MS);
152
+ const startTime = formatDate(start);
153
+ const endTime = formatDate(now);
154
+ return `${baseUrl}${MODEL_USAGE_PATH}?startTime=${encodeURIComponent(startTime)}&endTime=${encodeURIComponent(endTime)}`;
155
+ }
156
+
157
+ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
158
+ if (params.provider !== "zai") return null;
159
+ const credential = params.credential;
160
+ if (credential.type !== "api_key" || !credential.apiKey) return null;
161
+
162
+ const cacheKey = buildCacheKey(params);
163
+ const cachedEntry = await ctx.cache.get(cacheKey);
164
+ const now = ctx.now();
165
+ if (cachedEntry && cachedEntry.expiresAt > now) return cachedEntry.value;
166
+
167
+ const baseUrl = normalizeZaiBaseUrl(params.baseUrl);
168
+ const url = `${baseUrl}${QUOTA_PATH}`;
169
+ const headers: Record<string, string> = {
170
+ Authorization: credential.apiKey,
171
+ "Content-Type": "application/json",
172
+ "User-Agent": "OpenCode-Status-Plugin/1.0",
173
+ };
174
+
175
+ let payload: ZaiQuotaPayload | null = null;
176
+ try {
177
+ const response = await ctx.fetch(url, {
178
+ headers,
179
+ signal: params.signal,
180
+ });
181
+ if (!response.ok) {
182
+ ctx.logger?.warn("ZAI usage fetch failed", { status: response.status, statusText: response.statusText });
183
+ return null;
184
+ }
185
+ payload = (await response.json()) as ZaiQuotaPayload;
186
+ } catch (error) {
187
+ ctx.logger?.warn("ZAI usage fetch error", { error: String(error) });
188
+ return null;
189
+ }
190
+
191
+ if (!payload) return null;
192
+ if (payload.success !== true) {
193
+ ctx.logger?.warn("ZAI usage response invalid", { code: payload.code, message: payload.msg });
194
+ return null;
195
+ }
196
+
197
+ const limitsPayload = Array.isArray(payload.data?.limits) ? payload.data?.limits : [];
198
+ const limits: UsageLimit[] = [];
199
+
200
+ for (const rawLimit of limitsPayload) {
201
+ const parsed = parseLimitItem(rawLimit);
202
+ if (!parsed) continue;
203
+ if (parsed.type === "TOKENS_LIMIT") {
204
+ const amount = buildUsageAmount({
205
+ used: parsed.currentValue,
206
+ limit: parsed.usage,
207
+ remaining: parsed.remaining,
208
+ percentage: parsed.percentage,
209
+ unit: "tokens",
210
+ });
211
+ const window = buildUsageWindow("quota", "Quota", parsed.nextResetTime, now);
212
+ limits.push({
213
+ id: "zai:tokens",
214
+ label: "ZAI Token Quota",
215
+ scope: {
216
+ provider: params.provider,
217
+ windowId: window?.id ?? "quota",
218
+ shared: true,
219
+ },
220
+ window,
221
+ amount,
222
+ status: getUsageStatus(amount.usedFraction),
223
+ });
224
+ }
225
+ if (parsed.type === "TIME_LIMIT") {
226
+ const window = buildUsageWindow("quota", "Quota", undefined, now);
227
+ const amount = buildUsageAmount({
228
+ used: parsed.currentValue,
229
+ limit: parsed.usage,
230
+ remaining: parsed.remaining,
231
+ percentage: parsed.percentage,
232
+ unit: "requests",
233
+ });
234
+ limits.push({
235
+ id: "zai:requests",
236
+ label: "ZAI Request Quota",
237
+ scope: {
238
+ provider: params.provider,
239
+ windowId: "quota",
240
+ shared: true,
241
+ },
242
+ window,
243
+ amount,
244
+ status: getUsageStatus(amount.usedFraction),
245
+ });
246
+ }
247
+ }
248
+
249
+ if (limits.length === 0) return null;
250
+
251
+ const report: UsageReport = {
252
+ provider: params.provider,
253
+ fetchedAt: now,
254
+ limits,
255
+ metadata: {
256
+ endpoint: url,
257
+ accountId: credential.accountId,
258
+ email: credential.email,
259
+ },
260
+ raw: payload,
261
+ };
262
+
263
+ const expiresAt = resolveCacheExpiry(now, limits);
264
+ await ctx.cache.set(cacheKey, { value: report, expiresAt });
265
+
266
+ const modelUsageUrl = buildModelUsageUrl(baseUrl, new Date(now));
267
+ try {
268
+ const response = await ctx.fetch(modelUsageUrl, {
269
+ headers,
270
+ signal: params.signal,
271
+ });
272
+ if (response.ok) {
273
+ const modelUsagePayload = (await response.json()) as unknown;
274
+ if (isRecord(modelUsagePayload)) {
275
+ report.metadata = {
276
+ ...report.metadata,
277
+ modelUsage: modelUsagePayload,
278
+ };
279
+ }
280
+ }
281
+ } catch (error) {
282
+ ctx.logger?.debug("ZAI model usage fetch failed", { error: String(error) });
283
+ }
284
+
285
+ return report;
286
+ }
287
+
288
+ export const zaiUsageProvider: UsageProvider = {
289
+ id: "zai",
290
+ fetchUsage: fetchZaiUsage,
291
+ supports: (params) => params.provider === "zai" && params.credential.type === "api_key",
292
+ };