@oh-my-pi/pi-ai 16.2.3 → 16.2.5

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.2.5] - 2026-06-28
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Google and Cloud Code Assist streams that end without a finish reason (dropped connections or truncated responses) being treated as fatal; they are now classified as transient so the coding agent automatically retries.
10
+
11
+ ## [16.2.4] - 2026-06-28
12
+
13
+ ### Added
14
+
15
+ - Enabled freeform tool patch support for Azure OpenAI and Codex models
16
+
17
+ ### Fixed
18
+
19
+ - Fixed usage reporting for Antigravity and Z.AI to correctly surface and preserve distinct quota windows (daily, weekly, monthly) instead of collapsing or duplicating them
20
+ - Fixed an issue where `/usage show` returned "No usage data available" when using a custom proxy base URL for Codex
21
+ - Fixed OpenAI stream read errors being incorrectly classified as non-transient, enabling the coding agent to automatically retry after recoverable stream failures
22
+
5
23
  ## [16.2.3] - 2026-06-28
6
24
 
7
25
  ### Changed
@@ -21,6 +21,7 @@ export declare const Flag: {
21
21
  readonly OAuthExpiry: 1073741824;
22
22
  };
23
23
  export type Flag = (typeof Flag)[keyof typeof Flag];
24
+ export declare const STREAM_READ_ERROR_PATTERN: RegExp;
24
25
  export declare const TRANSIENT_TRANSPORT_PATTERN: RegExp;
25
26
  /** Whether an OAuth refresh error message means the grant is definitively dead. */
26
27
  export declare function isOAuthExpiry(errorMessage: string): boolean;
@@ -30,6 +31,7 @@ export declare function retriable(id: number | undefined, opts?: {
30
31
  replayUnsafe?: boolean;
31
32
  }): boolean;
32
33
  export declare function status(error: unknown): number | undefined;
34
+ export declare function isStreamReadErrorText(text: string): boolean;
33
35
  export declare function classify(error: unknown, api?: Api): number;
34
36
  /**
35
37
  * Whether an error (or message string) classifies as an account usage/quota
@@ -1,16 +1,15 @@
1
1
  import type { CredentialRankingStrategy, UsageProvider } from "../usage";
2
2
  export declare const antigravityUsageProvider: UsageProvider;
3
3
  /**
4
- * Antigravity quotas reset daily and are returned per backend counter
5
- * (Anthropic / Google / OpenAI) without a fixed "primary vs secondary"
6
- * split. `fetchAntigravityUsage` already sorts `limits` ascending by
7
- * `remainingFraction`; after model-family scoping, the most-pressured
8
- * relevant counter is index 0.
4
+ * Antigravity quotas are returned per backend counter (Anthropic / Google /
5
+ * OpenAI) and can include both daily and weekly windows. `fetchAntigravityUsage`
6
+ * sorts `limits` ascending by `remainingFraction`; after model-family scoping,
7
+ * the most-pressured relevant counter/window is index 0.
9
8
  *
10
9
  * Leave `secondary` unset: AuthStorage compares secondary metrics before
11
- * primary metrics, which is correct for providers with explicit long-window
12
- * limits but wrong here. Ranking Antigravity by the bottleneck counter first
13
- * avoids preferring an account at 95% Gemini / 0% Claude over one at
14
- * 80% Gemini / 70% Claude.
10
+ * primary metrics, which is correct for providers with a fixed short/long
11
+ * split but wrong here. Ranking Antigravity by the bottleneck counter first
12
+ * avoids preferring an account at 95% Gemini daily / 0% Claude weekly over one
13
+ * with healthier Gemini headroom.
15
14
  */
16
15
  export declare const antigravityRankingStrategy: CredentialRankingStrategy;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "16.2.3",
4
+ "version": "16.2.5",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -38,9 +38,9 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@bufbuild/protobuf": "^2.12.0",
41
- "@oh-my-pi/pi-catalog": "16.2.3",
42
- "@oh-my-pi/pi-utils": "16.2.3",
43
- "@oh-my-pi/pi-wire": "16.2.3",
41
+ "@oh-my-pi/pi-catalog": "16.2.5",
42
+ "@oh-my-pi/pi-utils": "16.2.5",
43
+ "@oh-my-pi/pi-wire": "16.2.5",
44
44
  "arktype": "^2.2.0",
45
45
  "zod": "^4"
46
46
  },
@@ -537,6 +537,7 @@ const USAGE_FAILURE_BACKOFF_MS = 10_000;
537
537
  const DEFAULT_USAGE_REQUEST_TIMEOUT_MS = 10_000;
538
538
  const USAGE_REPORT_CACHE_KEY_VERSION_OVERRIDES: Partial<Record<Provider, number>> = {
539
539
  "google-antigravity": 2,
540
+ zai: 2,
540
541
  };
541
542
  const DEFAULT_OAUTH_REFRESH_TIMEOUT_MS = 10_000;
542
543
  /**
@@ -85,6 +85,7 @@ const OVERFLOW_NO_BODY_PATTERN = /\b4(00|13)\s*(status code)?\s*\(no body\)/i;
85
85
  const TIMEOUT_PATTERN = /\b(?:operation\s+)?timed?\s*out\b|\btimeout\b|\bstream stall\b/i;
86
86
  const TRANSIENT_ENVELOPE_PATTERN = /anthropic stream envelope error:/i;
87
87
  const TRANSIENT_ENVELOPE_BEFORE_START_PATTERN = /before message_start/i;
88
+ export const STREAM_READ_ERROR_PATTERN = /stream[_ -]?read[_ -]?error/i;
88
89
  export const TRANSIENT_TRANSPORT_PATTERN =
89
90
  /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)|malformed.?function.?call/i;
90
91
  const AUTH_FAILURE_PATTERN =
@@ -240,9 +241,14 @@ function statusInternal(error: unknown, depth: number): number | undefined {
240
241
  return undefined;
241
242
  }
242
243
 
244
+ export function isStreamReadErrorText(text: string): boolean {
245
+ return STREAM_READ_ERROR_PATTERN.test(text);
246
+ }
247
+
243
248
  function isTransientErrorText(text: string): boolean {
244
249
  return (
245
250
  isUnexpectedSocketCloseMessage(text) ||
251
+ isStreamReadErrorText(text) ||
246
252
  (TRANSIENT_ENVELOPE_PATTERN.test(text) && TRANSIENT_ENVELOPE_BEFORE_START_PATTERN.test(text)) ||
247
253
  TRANSIENT_TRANSPORT_PATTERN.test(text)
248
254
  );
@@ -36,7 +36,13 @@ export class ProviderResponseError extends Error {
36
36
  this.name = "ProviderResponseError";
37
37
  this.provider = options.provider;
38
38
  this.kind = options.kind ?? "output";
39
+ // A safety filter block is a terminal provider finish, not a transient fault.
39
40
  if (this.kind === "content-blocked") attach(this, create(Flag.ProviderFinishError));
41
+ // An incomplete stream (connection dropped / truncated before any terminal
42
+ // event) never produced a finish reason — the request didn't complete, so it
43
+ // is safe to retry. The retry layer's replay-unsafe guard still blocks a
44
+ // retry when partial tool output was already emitted.
45
+ else if (this.kind === "incomplete-stream") attach(this, create(Flag.Transient));
40
46
  }
41
47
  }
42
48
 
@@ -29,7 +29,13 @@ interface AntigravityModelInfo {
29
29
  displayName?: string;
30
30
  quotaInfo?: AntigravityQuotaInfo | AntigravityQuotaInfo[];
31
31
  quotaInfos?: AntigravityQuotaInfo[];
32
+ dailyQuotaInfo?: AntigravityQuotaInfo | AntigravityQuotaInfo[];
33
+ dailyQuotaInfos?: AntigravityQuotaInfo[];
34
+ weeklyQuotaInfo?: AntigravityQuotaInfo | AntigravityQuotaInfo[];
35
+ weeklyQuotaInfos?: AntigravityQuotaInfo[];
32
36
  quotaInfoByTier?: Record<string, AntigravityQuotaInfo | AntigravityQuotaInfo[]>;
37
+ quotaInfoByWindow?: Record<string, AntigravityQuotaInfo | AntigravityQuotaInfo[]>;
38
+ quotaInfosByWindow?: Record<string, AntigravityQuotaInfo | AntigravityQuotaInfo[]>;
33
39
  apiProvider?: string;
34
40
  modelProvider?: string;
35
41
  }
@@ -41,6 +47,39 @@ interface AntigravityUsageResponse {
41
47
  const DEFAULT_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
42
48
  const FETCH_AVAILABLE_MODELS_PATH = "/v1internal:fetchAvailableModels";
43
49
 
50
+ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
51
+ const ONE_WEEK_MS = 7 * ONE_DAY_MS;
52
+
53
+ interface AntigravityWindowDescriptor {
54
+ id: string;
55
+ label: string;
56
+ durationMs?: number;
57
+ }
58
+
59
+ function classifyWindow(id: string | undefined, label: string | undefined): AntigravityWindowDescriptor | undefined {
60
+ const source = `${id ?? ""} ${label ?? ""}`.toLowerCase();
61
+ if (source.includes("week") || source.includes("7d") || /7[\s_-]*day/.test(source)) {
62
+ return { id: "weekly", label: "Weekly", durationMs: ONE_WEEK_MS };
63
+ }
64
+ if (source.includes("day") || source.includes("daily") || source.includes("24h")) {
65
+ return { id: "daily", label: "Daily", durationMs: ONE_DAY_MS };
66
+ }
67
+ if (id || label) return { id: id ?? label ?? "default", label: label ?? id ?? "Default" };
68
+ return undefined;
69
+ }
70
+
71
+ function withWindowDescriptor(
72
+ info: AntigravityQuotaInfo,
73
+ descriptor: AntigravityWindowDescriptor | undefined,
74
+ ): AntigravityQuotaInfo {
75
+ if (!descriptor) return info;
76
+ return {
77
+ ...info,
78
+ windowId: info.windowId ?? descriptor.id,
79
+ windowLabel: info.windowLabel ?? descriptor.label,
80
+ };
81
+ }
82
+
44
83
  function clampFraction(value: number | undefined): number | undefined {
45
84
  if (value === undefined || !Number.isFinite(value)) return undefined;
46
85
  if (value < 0) return 0;
@@ -56,13 +95,15 @@ function getUsageStatus(remainingFraction: number | undefined): UsageStatus | un
56
95
  }
57
96
 
58
97
  function parseWindow(info: AntigravityQuotaInfo): UsageWindow | undefined {
59
- if (!info.resetTime) return undefined;
60
- const resetAt = Date.parse(info.resetTime);
61
- if (!Number.isFinite(resetAt)) return undefined;
98
+ const descriptor = classifyWindow(info.windowId, info.windowLabel);
99
+ const resetAt = info.resetTime ? Date.parse(info.resetTime) : undefined;
100
+ const hasResetAt = resetAt !== undefined && Number.isFinite(resetAt);
101
+ if (!descriptor && !hasResetAt) return undefined;
62
102
  return {
63
- id: info.windowId ?? "default",
64
- label: info.windowLabel ?? "Default",
65
- resetsAt: resetAt,
103
+ id: descriptor?.id ?? info.windowId ?? "default",
104
+ label: info.windowLabel ?? descriptor?.label ?? "Default",
105
+ ...(descriptor?.durationMs !== undefined ? { durationMs: descriptor.durationMs } : {}),
106
+ ...(hasResetAt ? { resetsAt: resetAt } : {}),
66
107
  };
67
108
  }
68
109
 
@@ -106,31 +147,44 @@ function normalizeQuotaInfos(info: AntigravityModelInfo): AntigravityQuotaInfo[]
106
147
  ...(info.apiProvider ? { apiProvider: info.apiProvider } : {}),
107
148
  ...(info.modelProvider ? { modelProvider: info.modelProvider } : {}),
108
149
  };
109
- const addInfo = (value: AntigravityQuotaInfo, tier?: string) => {
110
- results.push({ ...source, ...value, ...(tier ? { tier } : {}) });
150
+ const addInfo = (value: AntigravityQuotaInfo, tier?: string, windowDescriptor?: AntigravityWindowDescriptor) => {
151
+ results.push({ ...source, ...withWindowDescriptor(value, windowDescriptor), ...(tier ? { tier } : {}) });
111
152
  };
112
- const addArray = (values?: AntigravityQuotaInfo[]) => {
113
- if (!values) return;
114
- for (const value of values) addInfo(value);
153
+ const addValue = (
154
+ value: AntigravityQuotaInfo | AntigravityQuotaInfo[] | undefined,
155
+ tier?: string,
156
+ windowDescriptor?: AntigravityWindowDescriptor,
157
+ ) => {
158
+ if (!value) return;
159
+ if (Array.isArray(value)) {
160
+ for (const entry of value) addInfo(entry, tier, windowDescriptor);
161
+ return;
162
+ }
163
+ addInfo(value, tier, windowDescriptor);
115
164
  };
116
165
 
117
- if (Array.isArray(info.quotaInfo)) {
118
- addArray(info.quotaInfo);
119
- } else if (info.quotaInfo) {
120
- addInfo(info.quotaInfo);
121
- }
122
- addArray(info.quotaInfos);
166
+ addValue(info.quotaInfo);
167
+ addValue(info.quotaInfos);
168
+ addValue(info.dailyQuotaInfo, undefined, classifyWindow("daily", "Daily"));
169
+ addValue(info.dailyQuotaInfos, undefined, classifyWindow("daily", "Daily"));
170
+ addValue(info.weeklyQuotaInfo, undefined, classifyWindow("weekly", "Weekly"));
171
+ addValue(info.weeklyQuotaInfos, undefined, classifyWindow("weekly", "Weekly"));
123
172
 
124
173
  if (info.quotaInfoByTier) {
125
174
  for (const [tier, value] of Object.entries(info.quotaInfoByTier)) {
126
- if (Array.isArray(value)) {
127
- for (const entry of value) addInfo(entry, tier);
128
- } else if (value) {
129
- addInfo(value, tier);
130
- }
175
+ addValue(value, tier);
131
176
  }
132
177
  }
133
178
 
179
+ const addWindowMap = (values?: Record<string, AntigravityQuotaInfo | AntigravityQuotaInfo[]>) => {
180
+ if (!values) return;
181
+ for (const [windowId, value] of Object.entries(values)) {
182
+ addValue(value, undefined, classifyWindow(windowId, undefined));
183
+ }
184
+ };
185
+ addWindowMap(info.quotaInfoByWindow);
186
+ addWindowMap(info.quotaInfosByWindow);
187
+
134
188
  return results;
135
189
  }
136
190
 
@@ -231,9 +285,10 @@ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchCo
231
285
  const tierKey = (quotaInfo.tier ?? "default").toLowerCase();
232
286
  const counterName = formatCounterName(quotaInfo);
233
287
  const counterKey = counterName?.toLowerCase() ?? "default";
234
- // Use quotaInfo.windowId even when parseWindow returns undefined
235
- // (no resetTime) separate windows must not collapse to "default".
236
- const windowId = quotaInfo.windowId ?? window?.id ?? "default";
288
+ // Use the parsed window id when available so provider enum names like
289
+ // WINDOW_WEEKLY normalize into the same visible `/usage` group as
290
+ // weeklyQuotaInfo entries.
291
+ const windowId = window?.id ?? quotaInfo.windowId ?? "default";
237
292
  const key = `${counterKey}|${tierKey}|${windowId}`;
238
293
  const existing = deduped.get(key);
239
294
  if (!existing) {
@@ -324,8 +379,6 @@ export const antigravityUsageProvider: UsageProvider = {
324
379
  supports: params => params.provider === "google-antigravity",
325
380
  };
326
381
 
327
- const ONE_DAY_MS = 24 * 60 * 60 * 1000;
328
-
329
382
  function getAntigravityCounterKeyForModel(context: CredentialRankingContext | undefined): string | undefined {
330
383
  const modelId = context?.modelId?.toLowerCase();
331
384
  if (!modelId) return undefined;
@@ -361,17 +414,16 @@ function rankAntigravityLimits(report: UsageReport, context: CredentialRankingCo
361
414
  }
362
415
 
363
416
  /**
364
- * Antigravity quotas reset daily and are returned per backend counter
365
- * (Anthropic / Google / OpenAI) without a fixed "primary vs secondary"
366
- * split. `fetchAntigravityUsage` already sorts `limits` ascending by
367
- * `remainingFraction`; after model-family scoping, the most-pressured
368
- * relevant counter is index 0.
417
+ * Antigravity quotas are returned per backend counter (Anthropic / Google /
418
+ * OpenAI) and can include both daily and weekly windows. `fetchAntigravityUsage`
419
+ * sorts `limits` ascending by `remainingFraction`; after model-family scoping,
420
+ * the most-pressured relevant counter/window is index 0.
369
421
  *
370
422
  * Leave `secondary` unset: AuthStorage compares secondary metrics before
371
- * primary metrics, which is correct for providers with explicit long-window
372
- * limits but wrong here. Ranking Antigravity by the bottleneck counter first
373
- * avoids preferring an account at 95% Gemini / 0% Claude over one at
374
- * 80% Gemini / 70% Claude.
423
+ * primary metrics, which is correct for providers with a fixed short/long
424
+ * split but wrong here. Ranking Antigravity by the bottleneck counter first
425
+ * avoids preferring an account at 95% Gemini daily / 0% Claude weekly over one
426
+ * with healthier Gemini headroom.
375
427
  */
376
428
  export const antigravityRankingStrategy: CredentialRankingStrategy = {
377
429
  findWindowLimits(report, context) {
@@ -384,8 +436,8 @@ export const antigravityRankingStrategy: CredentialRankingStrategy = {
384
436
  const counterKey = getAntigravityCounterKeyForModel(context);
385
437
  return `counter:${counterKey ?? "unknown"}`;
386
438
  },
387
- // Antigravity windows omit `durationMs`; the endpoint is
388
- // `daily-cloudcode-pa.googleapis.com`, so fall back to 24h when computing
389
- // drain rate.
439
+ // Antigravity windows carry `durationMs` when the response identifies them
440
+ // as daily/weekly. Fall back to daily for legacy unlabelled quotaInfo
441
+ // entries from `daily-cloudcode-pa.googleapis.com`.
390
442
  windowDefaults: { primaryMs: ONE_DAY_MS, secondaryMs: ONE_DAY_MS },
391
443
  };
package/src/usage/zai.ts CHANGED
@@ -15,6 +15,15 @@ const DEFAULT_ENDPOINT = "https://api.z.ai";
15
15
  const QUOTA_PATH = "/api/monitor/usage/quota/limit";
16
16
  const MODEL_USAGE_PATH = "/api/monitor/usage/model-usage";
17
17
  const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
18
+ const HOUR_MS = 60 * 60 * 1000;
19
+ const DAY_MS = 24 * HOUR_MS;
20
+ const WEEK_MS = 7 * DAY_MS;
21
+ const MONTH_MS = 30 * DAY_MS;
22
+
23
+ interface ZaiUsageDetail {
24
+ modelCode?: string;
25
+ usage?: number;
26
+ }
18
27
 
19
28
  function normalizeZaiBaseUrl(baseUrl?: string): string {
20
29
  if (!baseUrl?.trim()) return DEFAULT_ENDPOINT;
@@ -32,6 +41,9 @@ interface ZaiUsageLimitItem {
32
41
  percentage?: number;
33
42
  remaining?: number;
34
43
  nextResetTime?: number;
44
+ unit?: number;
45
+ number?: number;
46
+ usageDetails?: ZaiUsageDetail[];
35
47
  }
36
48
 
37
49
  interface ZaiQuotaPayload {
@@ -49,6 +61,21 @@ function parseMillis(value: unknown): number | undefined {
49
61
  return parsed > 1_000_000_000_000 ? parsed : parsed * 1000;
50
62
  }
51
63
 
64
+ function parseUsageDetails(value: unknown): ZaiUsageDetail[] | undefined {
65
+ if (!Array.isArray(value)) return undefined;
66
+ const details: ZaiUsageDetail[] = [];
67
+ for (const item of value) {
68
+ if (!isRecord(item)) continue;
69
+ const modelCode = typeof item.modelCode === "string" && item.modelCode ? item.modelCode : undefined;
70
+ const usage = toNumber(item.usage);
71
+ details.push({
72
+ ...(modelCode !== undefined ? { modelCode } : {}),
73
+ ...(usage !== undefined ? { usage } : {}),
74
+ });
75
+ }
76
+ return details.length > 0 ? details : undefined;
77
+ }
78
+
52
79
  function parseLimitItem(value: unknown): ZaiUsageLimitItem | null {
53
80
  if (!isRecord(value)) return null;
54
81
  const type = typeof value.type === "string" ? value.type : undefined;
@@ -60,6 +87,9 @@ function parseLimitItem(value: unknown): ZaiUsageLimitItem | null {
60
87
  percentage: toNumber(value.percentage),
61
88
  remaining: toNumber(value.remaining),
62
89
  nextResetTime: parseMillis(value.nextResetTime),
90
+ unit: toNumber(value.unit),
91
+ number: toNumber(value.number),
92
+ usageDetails: parseUsageDetails(value.usageDetails),
63
93
  };
64
94
  }
65
95
 
@@ -101,6 +131,59 @@ function formatDate(value: Date): string {
101
131
  )}:${pad(value.getSeconds())}`;
102
132
  }
103
133
 
134
+ function formatCountedUnit(count: number, singular: string): string {
135
+ const suffix = count === 1 ? "" : "s";
136
+ return `${count} ${singular}${suffix}`;
137
+ }
138
+
139
+ function buildZaiWindow(parsed: ZaiUsageLimitItem): UsageWindow {
140
+ const count = parsed.number !== undefined && parsed.number > 0 ? parsed.number : 1;
141
+ let id: string;
142
+ let label: string;
143
+ let durationMs: number | undefined;
144
+ switch (parsed.unit) {
145
+ case 3:
146
+ id = `${count}h`;
147
+ label = formatCountedUnit(count, "Hour");
148
+ durationMs = count * HOUR_MS;
149
+ break;
150
+ case 4:
151
+ id = `${count}d`;
152
+ label = formatCountedUnit(count, "Day");
153
+ durationMs = count * DAY_MS;
154
+ break;
155
+ case 5:
156
+ id = `${count}mo`;
157
+ label = count === 1 ? "Monthly" : formatCountedUnit(count, "Month");
158
+ durationMs = count * MONTH_MS;
159
+ break;
160
+ case 6:
161
+ id = "1w";
162
+ label = "Weekly";
163
+ durationMs = WEEK_MS;
164
+ break;
165
+ default:
166
+ id = parsed.unit !== undefined ? `${count}u${parsed.unit}` : "quota";
167
+ label = "Quota";
168
+ break;
169
+ }
170
+ return {
171
+ id,
172
+ label,
173
+ ...(durationMs !== undefined ? { durationMs } : {}),
174
+ ...(parsed.nextResetTime !== undefined ? { resetsAt: parsed.nextResetTime } : {}),
175
+ };
176
+ }
177
+
178
+ function requestQuotaLabel(parsed: ZaiUsageLimitItem): string {
179
+ const detailCodes =
180
+ parsed.usageDetails?.map(detail => detail.modelCode).filter((code): code is string => !!code) ?? [];
181
+ if (detailCodes.includes("search-prime") && detailCodes.includes("web-reader") && detailCodes.includes("zread")) {
182
+ return "ZAI Web Search / Reader / Zread Quota";
183
+ }
184
+ return "ZAI Request Quota";
185
+ }
186
+
104
187
  function buildModelUsageUrl(baseUrl: string, now: Date): string {
105
188
  const start = new Date(now.getTime() - SEVEN_DAYS_MS);
106
189
  const startTime = formatDate(start);
@@ -157,18 +240,13 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
157
240
  percentage: parsed.percentage,
158
241
  unit: "tokens",
159
242
  });
160
- const window: UsageWindow = {
161
- id: "quota",
162
- label: "Quota",
163
- durationMs: SEVEN_DAYS_MS,
164
- resetsAt: parsed.nextResetTime,
165
- };
243
+ const window = buildZaiWindow(parsed);
166
244
  limits.push({
167
- id: "zai:tokens",
168
- label: "ZAI Token Quota",
245
+ id: `zai:tokens:${window.id}`,
246
+ label: `ZAI ${window.label} Token Quota`,
169
247
  scope: {
170
248
  provider: params.provider,
171
- windowId: window?.id ?? "quota",
249
+ windowId: window.id,
172
250
  shared: true,
173
251
  },
174
252
  window,
@@ -177,12 +255,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
177
255
  });
178
256
  }
179
257
  if (parsed.type === "TIME_LIMIT") {
180
- const window: UsageWindow = {
181
- id: "quota",
182
- label: "Quota",
183
- durationMs: SEVEN_DAYS_MS,
184
- resetsAt: parsed.nextResetTime,
185
- };
258
+ const window = buildZaiWindow(parsed);
186
259
  const amount = buildUsageAmount({
187
260
  used: parsed.currentValue,
188
261
  limit: parsed.usage,
@@ -191,11 +264,11 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
191
264
  unit: "requests",
192
265
  });
193
266
  limits.push({
194
- id: "zai:requests",
195
- label: "ZAI Request Quota",
267
+ id: `zai:requests:${window.id}`,
268
+ label: requestQuotaLabel(parsed),
196
269
  scope: {
197
270
  provider: params.provider,
198
- windowId: "quota",
271
+ windowId: window.id,
199
272
  shared: true,
200
273
  },
201
274
  window,