@oh-my-pi/pi-ai 18.4.2 → 18.4.4

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +30 -41
  2. package/dist/types/auth-broker/protocol.d.ts +12 -0
  3. package/dist/types/dialect/rendering.d.ts +4 -0
  4. package/dist/types/images/openai-hosted.d.ts +1 -1
  5. package/dist/types/images/shared.d.ts +5 -2
  6. package/dist/types/providers/anthropic-wire.d.ts +9 -1
  7. package/dist/types/providers/anthropic.d.ts +17 -0
  8. package/dist/types/providers/aws-sigv4.d.ts +5 -0
  9. package/dist/types/providers/bedrock-anthropic.d.ts +9 -0
  10. package/dist/types/providers/bedrock-request-metadata.d.ts +2 -0
  11. package/dist/types/providers/cursor/interaction-query.d.ts +10 -0
  12. package/dist/types/providers/openai-chat-wire.d.ts +2 -2
  13. package/dist/types/providers/openai-codex/request-transformer.d.ts +1 -1
  14. package/dist/types/providers/openai-responses-wire.d.ts +2 -2
  15. package/dist/types/providers/xai-base-url.d.ts +17 -0
  16. package/dist/types/types.d.ts +14 -3
  17. package/dist/types/usage/commandcode.d.ts +4 -0
  18. package/dist/types/usage/shared.d.ts +13 -1
  19. package/dist/types/utils/event-stream.d.ts +7 -0
  20. package/dist/types/utils/openai-http.d.ts +7 -1
  21. package/package.json +6 -6
  22. package/src/auth-broker/client.ts +1 -12
  23. package/src/auth-broker/protocol.ts +32 -0
  24. package/src/auth-broker/remote-store.ts +4 -40
  25. package/src/auth-broker/server.ts +1 -20
  26. package/src/auth-broker/snapshot-cache.ts +1 -9
  27. package/src/dialect/anthropic.ts +3 -25
  28. package/src/dialect/minimax.ts +3 -24
  29. package/src/dialect/rendering.ts +18 -0
  30. package/src/dialect/thinking.ts +24 -6
  31. package/src/dialect/xml.ts +3 -19
  32. package/src/images/openai-hosted.ts +5 -2
  33. package/src/images/openai-images.ts +10 -4
  34. package/src/images/shared.ts +9 -4
  35. package/src/providers/amazon-bedrock.ts +61 -22
  36. package/src/providers/anthropic-compaction.ts +10 -1
  37. package/src/providers/anthropic-wire.ts +12 -1
  38. package/src/providers/anthropic.ts +58 -15
  39. package/src/providers/aws-eventstream.ts +4 -3
  40. package/src/providers/aws-sigv4.ts +1 -1
  41. package/src/providers/azure-openai-responses.ts +3 -4
  42. package/src/providers/bedrock-anthropic.ts +30 -0
  43. package/src/providers/bedrock-request-metadata.ts +6 -0
  44. package/src/providers/connect-error-detail.ts +1 -5
  45. package/src/providers/cursor/interaction-query.ts +4 -2
  46. package/src/providers/cursor.ts +30 -26
  47. package/src/providers/google-gemini-cli.ts +9 -2
  48. package/src/providers/google-shared.ts +16 -6
  49. package/src/providers/openai-chat-wire.ts +2 -2
  50. package/src/providers/openai-codex/request-transformer.ts +1 -1
  51. package/src/providers/openai-codex-responses.ts +27 -16
  52. package/src/providers/openai-completions.ts +51 -36
  53. package/src/providers/openai-responses-wire.ts +2 -2
  54. package/src/providers/openai-responses.ts +3 -4
  55. package/src/providers/openai-shared.ts +130 -18
  56. package/src/providers/xai-base-url.ts +32 -0
  57. package/src/registry/engine/api-key.ts +8 -3
  58. package/src/types.ts +35 -4
  59. package/src/usage/claude.ts +4 -11
  60. package/src/usage/cline-pass.ts +2 -14
  61. package/src/usage/commandcode.ts +209 -0
  62. package/src/usage/cursor.ts +9 -1
  63. package/src/usage/openai-codex.ts +3 -5
  64. package/src/usage/registry.ts +3 -0
  65. package/src/usage/shared.ts +28 -1
  66. package/src/usage/synthetic.ts +4 -40
  67. package/src/usage/umans.ts +8 -36
  68. package/src/usage/zai.ts +10 -38
  69. package/src/utils/event-stream.ts +38 -2
  70. package/src/utils/http-inspector.ts +4 -8
  71. package/src/utils/openai-http.ts +10 -3
  72. package/src/utils/schema/json-schema-validator.ts +23 -26
  73. package/src/utils/schema/meta-validator.ts +4 -7
  74. package/src/utils/schema/wire.ts +17 -21
@@ -0,0 +1,209 @@
1
+ import { ProviderHttpError } from "../error";
2
+ import type {
3
+ CredentialRankingStrategy,
4
+ UsageFetchContext,
5
+ UsageFetchParams,
6
+ UsageLimit,
7
+ UsageProvider,
8
+ UsageReport,
9
+ } from "../usage";
10
+ import { isRecord } from "../utils";
11
+ import { HOUR_MS, parsePositiveTimestamp, usageStatus, WEEK_MS } from "./shared";
12
+
13
+ const PROVIDER = "commandcode";
14
+ const DEFAULT_ORIGIN = "https://api.commandcode.ai";
15
+ // The CLI also sends `?limits=1` for its org spend-limit panel; omp reads no
16
+ // field from that, so the plain identity route is enough.
17
+ const WHOAMI_PATH = "/alpha/whoami";
18
+ const CREDITS_PATH = "/alpha/billing/credits";
19
+
20
+ /**
21
+ * Command Code's inference base carries `/provider` or `/provider/v1`, while the
22
+ * account routes sit at the host root, so only the configured origin is kept.
23
+ * A blank or unparseable override means "not configured".
24
+ */
25
+ function resolveOrigin(baseUrl: string | undefined): string {
26
+ const trimmed = baseUrl?.trim();
27
+ if (!trimmed) return DEFAULT_ORIGIN;
28
+ try {
29
+ return new URL(trimmed).origin;
30
+ } catch {
31
+ return DEFAULT_ORIGIN;
32
+ }
33
+ }
34
+
35
+ function nonEmptyString(value: unknown): string | undefined {
36
+ return typeof value === "string" && value.length > 0 ? value : undefined;
37
+ }
38
+
39
+ function finiteNumber(value: unknown): number | undefined {
40
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
41
+ }
42
+
43
+ /**
44
+ * Returns the unwrapped JSON body, or `null` for any failure that should read
45
+ * as "no data", 403 included. Only a 401 throws, so a revoked key purges the
46
+ * cached report instead of re-serving it.
47
+ */
48
+ async function getJson(
49
+ url: string,
50
+ apiKey: string,
51
+ signal: AbortSignal | undefined,
52
+ ctx: UsageFetchContext,
53
+ ): Promise<Record<string, unknown> | null> {
54
+ try {
55
+ const response = await ctx.fetch(url, {
56
+ headers: {
57
+ Authorization: `Bearer ${apiKey}`,
58
+ Accept: "application/json",
59
+ },
60
+ signal,
61
+ });
62
+ if (!response.ok) {
63
+ if (response.status === 401) {
64
+ throw new ProviderHttpError(
65
+ `Command Code usage endpoint returned ${response.status} ${response.statusText}`.trim(),
66
+ response.status,
67
+ );
68
+ }
69
+ ctx.logger?.warn("Command Code usage fetch failed", {
70
+ url,
71
+ status: response.status,
72
+ statusText: response.statusText,
73
+ });
74
+ return null;
75
+ }
76
+ const json: unknown = await response.json();
77
+ if (!isRecord(json)) return null;
78
+ return isRecord(json.data) ? json.data : json;
79
+ } catch (error) {
80
+ if (error instanceof ProviderHttpError) throw error;
81
+ ctx.logger?.warn("Command Code usage fetch error", { url, error: String(error) });
82
+ return null;
83
+ }
84
+ }
85
+
86
+ interface WindowSpec {
87
+ key: "fiveHour" | "weekly";
88
+ id: "5h" | "7d";
89
+ limitLabel: string;
90
+ windowLabel: string;
91
+ durationMs: number;
92
+ }
93
+
94
+ const WINDOWS: readonly WindowSpec[] = [
95
+ { key: "fiveHour", id: "5h", limitLabel: "5-hour limit", windowLabel: "5-hour", durationMs: 5 * HOUR_MS },
96
+ { key: "weekly", id: "7d", limitLabel: "Weekly limit", windowLabel: "Weekly", durationMs: WEEK_MS },
97
+ ];
98
+
99
+ function buildWindowLimit(
100
+ spec: WindowSpec,
101
+ raw: unknown,
102
+ accountId: string,
103
+ orgId: string | undefined,
104
+ ): UsageLimit | undefined {
105
+ if (!isRecord(raw)) return undefined;
106
+ const used = finiteNumber(raw.used);
107
+ if (used === undefined) return undefined;
108
+ const cap = finiteNumber(raw.cap);
109
+ const usedFraction = cap !== undefined && cap > 0 ? used / cap : undefined;
110
+ const resetsAt = parsePositiveTimestamp(raw.resetAt);
111
+ // Absent fields are omitted, not set to `undefined`: the usage wire schema
112
+ // rejects an explicit `undefined` for an optional key.
113
+ return {
114
+ id: `${PROVIDER}:${spec.id}`,
115
+ label: spec.limitLabel,
116
+ scope: { provider: PROVIDER, accountId, ...(orgId ? { orgId } : {}), windowId: spec.id, shared: true },
117
+ window: {
118
+ id: spec.id,
119
+ label: spec.windowLabel,
120
+ durationMs: spec.durationMs,
121
+ ...(resetsAt !== undefined ? { resetsAt } : {}),
122
+ },
123
+ amount: {
124
+ used,
125
+ ...(cap !== undefined ? { limit: cap } : {}),
126
+ ...(usedFraction !== undefined ? { usedFraction } : {}),
127
+ unit: "credits",
128
+ },
129
+ status: raw.exceeded === true ? "exhausted" : usageStatus(usedFraction),
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Reads the account routes that the Command Code CLI calls with the same API
135
+ * key; the Provider API documents no usage endpoint. Pay-as-you-go accounts
136
+ * carry no `windowLimits`, so they report only the credit balance. Credits
137
+ * are queried per org, as the CLI does, so each limit records the org that
138
+ * owns the pool alongside the user.
139
+ */
140
+ async function fetchCommandCodeUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
141
+ if (params.provider !== PROVIDER) return null;
142
+ if (params.credential.type !== "api_key" || !params.credential.apiKey) return null;
143
+ const origin = resolveOrigin(params.baseUrl);
144
+
145
+ const whoami = await getJson(`${origin}${WHOAMI_PATH}`, params.credential.apiKey, params.signal, ctx);
146
+ if (!whoami) return null;
147
+ const user = isRecord(whoami.user) ? whoami.user : undefined;
148
+ const org = isRecord(whoami.org) ? whoami.org : undefined;
149
+ const userId = nonEmptyString(user?.id);
150
+ if (!userId) return null;
151
+ const orgId = nonEmptyString(org?.id);
152
+ const orgLogin = nonEmptyString(org?.login);
153
+
154
+ const creditsUrl = `${origin}${CREDITS_PATH}${orgId ? `?orgId=${encodeURIComponent(orgId)}` : ""}`;
155
+ const creditsBody = await getJson(creditsUrl, params.credential.apiKey, params.signal, ctx);
156
+ if (!creditsBody) return null;
157
+ const credits = isRecord(creditsBody.credits) ? creditsBody.credits : undefined;
158
+ if (!credits) return null;
159
+ const monthly = finiteNumber(credits.monthlyCredits);
160
+ const purchased = finiteNumber(credits.purchasedCredits);
161
+ const free = finiteNumber(credits.freeCredits);
162
+ if (monthly === undefined && purchased === undefined && free === undefined) return null;
163
+
164
+ const limits: UsageLimit[] = [];
165
+ const windowLimits = isRecord(creditsBody.windowLimits) ? creditsBody.windowLimits : undefined;
166
+ for (const spec of WINDOWS) {
167
+ const limit = buildWindowLimit(spec, windowLimits?.[spec.key], userId, orgId);
168
+ if (limit) limits.push(limit);
169
+ }
170
+ limits.push({
171
+ id: `${PROVIDER}:balance`,
172
+ label: "Credit balance",
173
+ scope: { provider: PROVIDER, accountId: userId, ...(orgId ? { orgId } : {}), windowId: "balance", shared: true },
174
+ amount: { remaining: (monthly ?? 0) + (purchased ?? 0) + (free ?? 0), unit: "credits" },
175
+ });
176
+
177
+ return {
178
+ provider: PROVIDER,
179
+ fetchedAt: Date.now(),
180
+ limits,
181
+ metadata: {
182
+ accountId: userId,
183
+ email: nonEmptyString(user?.email),
184
+ orgId,
185
+ orgName: orgLogin,
186
+ planType: nonEmptyString(credits.planId),
187
+ },
188
+ raw: { whoami, credits: creditsBody },
189
+ };
190
+ }
191
+
192
+ export const commandCodeUsageProvider: UsageProvider = {
193
+ id: PROVIDER,
194
+ fetchUsage: fetchCommandCodeUsage,
195
+ supports: params => params.provider === PROVIDER && params.credential.type === "api_key",
196
+ validatesCredentials: true,
197
+ };
198
+
199
+ /** Ranks Command Code accounts by the 5-hour and weekly credit windows. */
200
+ export const commandCodeRankingStrategy: CredentialRankingStrategy = {
201
+ findWindowLimits: report => ({
202
+ primary: report.limits.find(limit => limit.window?.id === "5h"),
203
+ secondary: report.limits.find(limit => limit.window?.id === "7d"),
204
+ }),
205
+ windowDefaults: {
206
+ primaryMs: 5 * HOUR_MS,
207
+ secondaryMs: WEEK_MS,
208
+ },
209
+ };
@@ -478,10 +478,18 @@ export const cursorUsageProvider: UsageProvider = {
478
478
  ]);
479
479
  let report: UsageReport | null;
480
480
  if (legacyReport && summaryReport) {
481
+ // `/auth/usage` is Cursor's request-count API from before usage-based
482
+ // plans. Current plans answer it with an uncapped, always-zero `gpt-4`
483
+ // bucket; beside the summary's dollar rails an uncapped, unused bucket
484
+ // carries nothing, so it is dropped. Capped buckets and uncapped buckets
485
+ // with recorded requests stay. Without a summary it stays the fallback.
486
+ const legacyLimits = legacyReport.limits.filter(
487
+ limit => limit.amount.limit !== undefined || (limit.amount.used ?? 0) > 0,
488
+ );
481
489
  report = {
482
490
  provider: "cursor",
483
491
  fetchedAt,
484
- limits: [...legacyReport.limits, ...summaryReport.limits],
492
+ limits: [...legacyLimits, ...summaryReport.limits],
485
493
  raw: {
486
494
  authUsage: legacyReport.raw,
487
495
  usageSummary: summaryReport.raw,
@@ -18,7 +18,7 @@ import type {
18
18
  import { isRecord } from "../utils";
19
19
  import { normalizeCodexBaseUrl } from "./openai-codex-base-url";
20
20
  import { listCodexResetCredits } from "./openai-codex-reset";
21
- import { HOUR_MS } from "./shared";
21
+ import { HOUR_MS, usageStatus } from "./shared";
22
22
 
23
23
  const CODEX_USAGE_PATH = "wham/usage";
24
24
  const CODEX_VERIFIED_ACCESS_PATH = "accounts/verified_access";
@@ -341,10 +341,8 @@ function buildUsageAmount(window: ParsedUsageWindow): UsageAmount {
341
341
  }
342
342
 
343
343
  function buildUsageStatus(args: { usedFraction?: number; explicitlyAllowed: boolean }): UsageLimit["status"] {
344
- if (args.usedFraction === undefined) return "unknown";
345
- if (args.usedFraction >= 1) return args.explicitlyAllowed ? "warning" : "exhausted";
346
- if (args.usedFraction >= 0.9) return "warning";
347
- return "ok";
344
+ const status = usageStatus(args.usedFraction);
345
+ return status === "exhausted" && args.explicitlyAllowed ? "warning" : status;
348
346
  }
349
347
 
350
348
  /**
@@ -4,6 +4,7 @@ import { alibabaTokenPlanRankingStrategy, alibabaTokenPlanUsageProvider } from "
4
4
  import { charmHyperUsageProvider } from "./charm-hyper";
5
5
  import { claudeRankingStrategy, claudeUsageProvider } from "./claude";
6
6
  import { clinePassUsageProvider } from "./cline-pass";
7
+ import { commandCodeRankingStrategy, commandCodeUsageProvider } from "./commandcode";
7
8
  import { cursorRankingStrategy, cursorUsageProvider } from "./cursor";
8
9
  import { devinUsageProvider } from "./devin";
9
10
  import { googleGeminiCliUsageProvider } from "./gemini";
@@ -45,6 +46,7 @@ export const DEFAULT_USAGE_PROVIDERS: readonly UsageProvider[] = [
45
46
  xaiOauthUsageProvider,
46
47
  devinUsageProvider,
47
48
  charmHyperUsageProvider,
49
+ commandCodeUsageProvider,
48
50
  ];
49
51
 
50
52
  const DEFAULT_USAGE_PROVIDER_MAP = new Map<Provider, UsageProvider>(
@@ -66,6 +68,7 @@ const DEFAULT_RANKING_STRATEGIES = new Map<Provider, CredentialRankingStrategy>(
66
68
  ["zai", zaiRankingStrategy],
67
69
  ["opencode-go", opencodeGoRankingStrategy],
68
70
  ["xai-oauth", xaiOauthRankingStrategy],
71
+ ["commandcode", commandCodeRankingStrategy],
69
72
  ]);
70
73
 
71
74
  /** Built-in ranking strategy for `provider`. */
@@ -1,5 +1,5 @@
1
1
  import { toNumber } from "@oh-my-pi/pi-catalog/utils";
2
- import type { UsageStatus } from "../usage";
2
+ import type { UsageAmount, UsageStatus } from "../usage";
3
3
 
4
4
  /** Milliseconds in one hour. */
5
5
  export const HOUR_MS = 60 * 60 * 1000;
@@ -31,3 +31,30 @@ export function usageStatus(usedFraction: number | undefined): UsageStatus {
31
31
  if (usedFraction >= 0.9) return "warning";
32
32
  return "ok";
33
33
  }
34
+
35
+ /**
36
+ * Builds an amount from absolute counters. Without an authoritative
37
+ * `usedFraction`, it derives one from `used / limit` (capped at 1).
38
+ * Undefined fields are omitted rather than emitted as `undefined` keys.
39
+ */
40
+ export function buildUsageAmount(args: {
41
+ used: number | undefined;
42
+ limit: number | undefined;
43
+ remaining: number | undefined;
44
+ usedFraction?: number;
45
+ unit: UsageAmount["unit"];
46
+ }): UsageAmount {
47
+ let usedFraction = args.usedFraction;
48
+ if (usedFraction === undefined && args.used !== undefined && args.limit !== undefined && args.limit > 0) {
49
+ usedFraction = Math.min(args.used / args.limit, 1);
50
+ }
51
+ const remainingFraction = usedFraction !== undefined ? Math.max(1 - usedFraction, 0) : undefined;
52
+ return {
53
+ ...(args.used !== undefined ? { used: args.used } : {}),
54
+ ...(args.limit !== undefined ? { limit: args.limit } : {}),
55
+ ...(args.remaining !== undefined ? { remaining: args.remaining } : {}),
56
+ ...(usedFraction !== undefined ? { usedFraction } : {}),
57
+ ...(remainingFraction !== undefined ? { remainingFraction } : {}),
58
+ unit: args.unit,
59
+ };
60
+ }
@@ -1,6 +1,5 @@
1
1
  import { isRecord } from "@oh-my-pi/pi-utils/type-guards";
2
2
  import type {
3
- UsageAmount,
4
3
  UsageFetchContext,
5
4
  UsageFetchParams,
6
5
  UsageLimit,
@@ -9,7 +8,7 @@ import type {
9
8
  UsageStatus,
10
9
  UsageWindow,
11
10
  } from "../usage";
12
- import { HOUR_MS, parseIsoTimestamp, WEEK_MS } from "./shared";
11
+ import { buildUsageAmount, HOUR_MS, parseIsoTimestamp, usageStatus, WEEK_MS } from "./shared";
13
12
 
14
13
  const QUOTAS_URL = "https://api.synthetic.new/v2/quotas";
15
14
 
@@ -20,35 +19,6 @@ function parseDollarAmount(value: unknown): number | undefined {
20
19
  return Number.isFinite(parsed) ? parsed : undefined;
21
20
  }
22
21
 
23
- function buildUsageAmount(args: {
24
- used: number | undefined;
25
- limit: number | undefined;
26
- remaining: number | undefined;
27
- usedFraction: number | undefined;
28
- unit: UsageAmount["unit"];
29
- }): UsageAmount {
30
- let usedFraction = args.usedFraction;
31
- if (usedFraction === undefined && args.used !== undefined && args.limit !== undefined && args.limit > 0) {
32
- usedFraction = Math.min(args.used / args.limit, 1);
33
- }
34
- const remainingFraction = usedFraction !== undefined ? Math.max(1 - usedFraction, 0) : undefined;
35
- return {
36
- ...(args.used !== undefined ? { used: args.used } : {}),
37
- ...(args.limit !== undefined ? { limit: args.limit } : {}),
38
- ...(args.remaining !== undefined ? { remaining: args.remaining } : {}),
39
- ...(usedFraction !== undefined ? { usedFraction } : {}),
40
- ...(remainingFraction !== undefined ? { remainingFraction } : {}),
41
- unit: args.unit,
42
- };
43
- }
44
-
45
- function getUsageStatus(usedFraction: number | undefined): UsageStatus | undefined {
46
- if (usedFraction === undefined) return undefined;
47
- if (usedFraction >= 1) return "exhausted";
48
- if (usedFraction >= 0.9) return "warning";
49
- return "ok";
50
- }
51
-
52
22
  function parseRollingFiveHourLimit(raw: unknown, provider: UsageFetchParams["provider"]): UsageLimit | null {
53
23
  if (!isRecord(raw)) return null;
54
24
  const remaining = typeof raw.remaining === "number" ? raw.remaining : undefined;
@@ -67,14 +37,8 @@ function parseRollingFiveHourLimit(raw: unknown, provider: UsageFetchParams["pro
67
37
  durationMs: 5 * HOUR_MS,
68
38
  ...(nextTickAt !== undefined ? { resetsAt: nextTickAt, resetLabel: "tick" } : {}),
69
39
  };
70
- const amount = buildUsageAmount({
71
- used,
72
- limit: max,
73
- remaining,
74
- usedFraction: undefined,
75
- unit: "requests",
76
- });
77
- const status: UsageStatus = limited ? "exhausted" : (getUsageStatus(amount.usedFraction) ?? "ok");
40
+ const amount = buildUsageAmount({ used, limit: max, remaining, unit: "requests" });
41
+ const status: UsageStatus = limited ? "exhausted" : usageStatus(amount.usedFraction ?? 0);
78
42
  return {
79
43
  id: "synthetic:requests:5h",
80
44
  label: "Synthetic Requests",
@@ -117,7 +81,7 @@ function parseWeeklyTokenLimit(raw: unknown, provider: UsageFetchParams["provide
117
81
  scope: { provider, windowId: "7d", shared: true },
118
82
  window,
119
83
  amount,
120
- status: getUsageStatus(amount.usedFraction),
84
+ status: amount.usedFraction === undefined ? undefined : usageStatus(amount.usedFraction),
121
85
  };
122
86
  }
123
87
 
@@ -1,6 +1,5 @@
1
1
  import { ProviderHttpError } from "../error";
2
2
  import type {
3
- UsageAmount,
4
3
  UsageFetchContext,
5
4
  UsageFetchParams,
6
5
  UsageLimit,
@@ -10,7 +9,7 @@ import type {
10
9
  UsageWindow,
11
10
  } from "../usage";
12
11
  import { isRecord } from "../utils";
13
- import { HOUR_MS } from "./shared";
12
+ import { buildUsageAmount, HOUR_MS, usageStatus } from "./shared";
14
13
 
15
14
  const UMANS_PROVIDER = "umans";
16
15
  const DEFAULT_ENDPOINT = "https://api.code.umans.ai";
@@ -53,13 +52,6 @@ function toFiniteNumber(value: unknown): number | undefined {
53
52
  return value;
54
53
  }
55
54
 
56
- function resolveStatus(usedFraction: number | undefined): UsageStatus | undefined {
57
- if (usedFraction === undefined) return undefined;
58
- if (usedFraction >= 1) return "exhausted";
59
- if (usedFraction >= 0.9) return "warning";
60
- return "ok";
61
- }
62
-
63
55
  /**
64
56
  * Soft-cap status never reaches `exhausted`: hitting the effective-request
65
57
  * limit only means burst headroom is being consumed — Umans throttles (429)
@@ -72,26 +64,6 @@ function softCapStatus(usedFraction: number | undefined): UsageStatus | undefine
72
64
  return "ok";
73
65
  }
74
66
 
75
- function buildAmount(args: {
76
- used: number | undefined;
77
- limit: number | undefined;
78
- remaining: number | undefined;
79
- unit: UsageAmount["unit"];
80
- }): UsageAmount {
81
- const used = args.used;
82
- const limit = args.limit;
83
- const usedFraction = used !== undefined && limit !== undefined && limit > 0 ? Math.min(used / limit, 1) : undefined;
84
- const remainingFraction = usedFraction !== undefined ? Math.max(1 - usedFraction, 0) : undefined;
85
- return {
86
- used,
87
- limit,
88
- remaining: args.remaining,
89
- usedFraction,
90
- remainingFraction,
91
- unit: args.unit,
92
- };
93
- }
94
-
95
67
  function buildRequestsLimits(payload: UmansUsagePayload, provider: string): UsageLimit[] {
96
68
  const limit = toFiniteNumber(payload.limits?.requests?.limit);
97
69
  const hardCap = toFiniteNumber(payload.limits?.requests?.hard_cap);
@@ -127,7 +99,7 @@ function buildRequestsLimits(payload: UmansUsagePayload, provider: string): Usag
127
99
  // limit still never drives exhaustion on its own: weighted headroom stays
128
100
  // decisive (https://github.com/can1357/oh-my-pi/issues/7858).
129
101
  if (weightedUsed === undefined || hardCap === undefined) {
130
- const amount = buildAmount({
102
+ const amount = buildUsageAmount({
131
103
  used: weightedUsed ?? rawUsed,
132
104
  limit,
133
105
  remaining: weightedUsed !== undefined ? weightedRemaining : rawRemaining,
@@ -140,7 +112,7 @@ function buildRequestsLimits(payload: UmansUsagePayload, provider: string): Usag
140
112
  scope: { provider, windowId: window.id, shared: true },
141
113
  window,
142
114
  amount,
143
- status: resolveStatus(amount.usedFraction),
115
+ status: amount.usedFraction === undefined ? undefined : usageStatus(amount.usedFraction),
144
116
  },
145
117
  ];
146
118
  }
@@ -151,7 +123,7 @@ function buildRequestsLimits(payload: UmansUsagePayload, provider: string): Usag
151
123
  // read as exhausted mid-window while the account still has weighted
152
124
  // headroom (https://github.com/can1357/oh-my-pi/issues/7858). Soft cap hits
153
125
  // warn; only the burst ceiling (`hard_cap`, raw counts) can exhaust.
154
- const softAmount = buildAmount({ used: weightedUsed, limit, remaining: weightedRemaining, unit: "requests" });
126
+ const softAmount = buildUsageAmount({ used: weightedUsed, limit, remaining: weightedRemaining, unit: "requests" });
155
127
  const limits: UsageLimit[] = [
156
128
  {
157
129
  id: "umans:requests:soft",
@@ -163,14 +135,14 @@ function buildRequestsLimits(payload: UmansUsagePayload, provider: string): Usag
163
135
  },
164
136
  ];
165
137
  if (hardCap !== undefined && rawUsed !== undefined) {
166
- const hardAmount = buildAmount({ used: rawUsed, limit: hardCap, remaining: undefined, unit: "requests" });
138
+ const hardAmount = buildUsageAmount({ used: rawUsed, limit: hardCap, remaining: undefined, unit: "requests" });
167
139
  limits.push({
168
140
  id: "umans:requests:hard",
169
141
  label: "Requests (burst ceiling)",
170
142
  scope: { provider, windowId: window.id, shared: true },
171
143
  window,
172
144
  amount: hardAmount,
173
- status: resolveStatus(hardAmount.usedFraction),
145
+ status: hardAmount.usedFraction === undefined ? undefined : usageStatus(hardAmount.usedFraction),
174
146
  });
175
147
  }
176
148
  return limits;
@@ -180,14 +152,14 @@ function buildConcurrencyLimit(payload: UmansUsagePayload, provider: string): Us
180
152
  const limit = toFiniteNumber(payload.limits?.concurrency?.limit);
181
153
  const used = toFiniteNumber(payload.usage?.concurrent_sessions);
182
154
  if (limit === undefined && used === undefined) return null;
183
- const amount = buildAmount({ used, limit, remaining: undefined, unit: "requests" });
155
+ const amount = buildUsageAmount({ used, limit, remaining: undefined, unit: "requests" });
184
156
  return {
185
157
  id: "umans:concurrency",
186
158
  label: "Concurrency",
187
159
  // Concurrency is instantaneous, not windowed.
188
160
  scope: { provider, windowId: "concurrency" },
189
161
  amount,
190
- status: resolveStatus(amount.usedFraction),
162
+ status: amount.usedFraction === undefined ? undefined : usageStatus(amount.usedFraction),
191
163
  };
192
164
  }
193
165
 
package/src/usage/zai.ts CHANGED
@@ -2,17 +2,15 @@ import { toNumber } from "@oh-my-pi/pi-catalog/utils";
2
2
  import { USER_AGENT } from "@oh-my-pi/pi-utils";
3
3
  import type {
4
4
  CredentialRankingStrategy,
5
- UsageAmount,
6
5
  UsageFetchContext,
7
6
  UsageFetchParams,
8
7
  UsageLimit,
9
8
  UsageProvider,
10
9
  UsageReport,
11
- UsageStatus,
12
10
  UsageWindow,
13
11
  } from "../usage";
14
12
  import { isRecord } from "../utils";
15
- import { DAY_MS, HOUR_MS, WEEK_MS } from "./shared";
13
+ import { buildUsageAmount, DAY_MS, HOUR_MS, usageStatus, WEEK_MS } from "./shared";
16
14
 
17
15
  const DEFAULT_ENDPOINT = "https://api.z.ai";
18
16
  const QUOTA_PATH = "/api/monitor/usage/quota/limit";
@@ -94,35 +92,9 @@ function parseLimitItem(value: unknown): ZaiUsageLimitItem | null {
94
92
  };
95
93
  }
96
94
 
97
- function buildUsageAmount(args: {
98
- used: number | undefined;
99
- limit: number | undefined;
100
- remaining: number | undefined;
101
- unit: UsageAmount["unit"];
102
- percentage?: number;
103
- }): UsageAmount {
104
- const usedFraction =
105
- args.percentage !== undefined
106
- ? Math.min(Math.max(args.percentage / 100, 0), 1)
107
- : args.used !== undefined && args.limit !== undefined && args.limit > 0
108
- ? Math.min(args.used / args.limit, 1)
109
- : undefined;
110
- const remainingFraction = usedFraction !== undefined ? Math.max(1 - usedFraction, 0) : undefined;
111
- return {
112
- used: args.used,
113
- limit: args.limit,
114
- remaining: args.remaining,
115
- usedFraction,
116
- remainingFraction,
117
- unit: args.unit,
118
- };
119
- }
120
-
121
- function getUsageStatus(usedFraction: number | undefined): UsageStatus | undefined {
122
- if (usedFraction === undefined) return undefined;
123
- if (usedFraction >= 1) return "exhausted";
124
- if (usedFraction >= 0.9) return "warning";
125
- return "ok";
95
+ /** Z.AI `percentage` is 0-100 and may overshoot; clamp it to a used fraction. */
96
+ function percentageFraction(percentage: number | undefined): number | undefined {
97
+ return percentage === undefined ? undefined : Math.min(Math.max(percentage / 100, 0), 1);
126
98
  }
127
99
 
128
100
  function formatDate(value: Date): string {
@@ -283,7 +255,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
283
255
  used: parsed.currentValue,
284
256
  limit: parsed.usage,
285
257
  remaining: parsed.remaining,
286
- percentage: parsed.percentage,
258
+ usedFraction: percentageFraction(parsed.percentage),
287
259
  unit: "tokens",
288
260
  });
289
261
  const window = buildZaiWindow(parsed);
@@ -297,7 +269,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
297
269
  },
298
270
  window,
299
271
  amount,
300
- status: getUsageStatus(amount.usedFraction),
272
+ status: amount.usedFraction === undefined ? undefined : usageStatus(amount.usedFraction),
301
273
  });
302
274
  }
303
275
  if (parsed.type === "TIME_LIMIT") {
@@ -306,7 +278,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
306
278
  used: parsed.currentValue,
307
279
  limit: parsed.usage,
308
280
  remaining: parsed.remaining,
309
- percentage: parsed.percentage,
281
+ usedFraction: percentageFraction(parsed.percentage),
310
282
  unit: "requests",
311
283
  });
312
284
  const featureLimit = isZaiFeatureRequestLimit(parsed);
@@ -321,7 +293,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
321
293
  },
322
294
  window,
323
295
  amount,
324
- status: getUsageStatus(amount.usedFraction),
296
+ status: amount.usedFraction === undefined ? undefined : usageStatus(amount.usedFraction),
325
297
  });
326
298
  }
327
299
  if (parsed.type === "CREDIT_LIMIT") {
@@ -335,7 +307,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
335
307
  used: parsed.currentValue,
336
308
  limit: parsed.usage,
337
309
  remaining: parsed.remaining,
338
- percentage: hasAbsoluteMeter ? undefined : parsed.percentage,
310
+ usedFraction: hasAbsoluteMeter ? undefined : percentageFraction(parsed.percentage),
339
311
  unit: "credits",
340
312
  });
341
313
  limits.push({
@@ -348,7 +320,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
348
320
  },
349
321
  window,
350
322
  amount,
351
- status: getUsageStatus(amount.usedFraction),
323
+ status: amount.usedFraction === undefined ? undefined : usageStatus(amount.usedFraction),
352
324
  });
353
325
  }
354
326
  }
@@ -6,9 +6,21 @@ export interface LocalWorkSource {
6
6
  readonly hasPendingLocalWork: boolean;
7
7
  }
8
8
 
9
+ /** Consumed head slots tolerated before the backlog is compacted (see {@link EventStream.queue}). */
10
+ const QUEUE_COMPACT_MIN_HEAD = 64;
11
+
9
12
  // Generic event stream class for async iteration
10
13
  export class EventStream<T, R = T> implements AsyncIterable<T> {
14
+ /**
15
+ * Events pushed while no consumer was waiting. The iterator dequeues by
16
+ * advancing {@link #queueHead} instead of `shift()` — O(remaining) per event,
17
+ * quadratic for a consumer draining a backlog — so while it drains, the
18
+ * slots before the head are consumed (cleared) placeholders. Do not mutate
19
+ * the array while the stream is being iterated.
20
+ */
11
21
  queue: T[] = [];
22
+ /** Index of the next undelivered event in {@link queue}; 0 whenever the queue is empty. */
23
+ #queueHead = 0;
12
24
  waiting: Array<{ resolve: (value: IteratorResult<T>) => void; reject: (err: unknown) => void }> = [];
13
25
  done = false;
14
26
  /** True once finalResultPromise has been resolved or rejected. */
@@ -115,10 +127,34 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
115
127
  }
116
128
  }
117
129
 
130
+ /**
131
+ * Take the event at the queue head. Clears the consumed slot so it stops
132
+ * retaining the event, resets the queue once drained, and compacts it once
133
+ * consumed slots are at least half of it (amortized O(1) per event).
134
+ */
135
+ #dequeue(): T {
136
+ const queue = this.queue;
137
+ const head = this.#queueHead;
138
+ const event = queue[head];
139
+ if (head + 1 === queue.length) {
140
+ queue.length = 0;
141
+ this.#queueHead = 0;
142
+ return event;
143
+ }
144
+ // The slot is dead once the head moves past it; `undefined` only drops the reference.
145
+ queue[head] = undefined as T;
146
+ this.#queueHead = head + 1;
147
+ if (this.#queueHead >= QUEUE_COMPACT_MIN_HEAD && this.#queueHead * 2 >= queue.length) {
148
+ queue.splice(0, this.#queueHead);
149
+ this.#queueHead = 0;
150
+ }
151
+ return event;
152
+ }
153
+
118
154
  async *[Symbol.asyncIterator](): AsyncIterator<T> {
119
155
  while (true) {
120
- if (this.queue.length > 0) {
121
- yield this.queue.shift()!;
156
+ if (this.#queueHead < this.queue.length) {
157
+ yield this.#dequeue();
122
158
  } else if (this.#failed) {
123
159
  throw this.#error;
124
160
  } else if (this.done) {