@ai-devkit/agent-manager 0.33.1 → 0.34.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.
Files changed (81) hide show
  1. package/dist/__tests__/capacity/codex.test.js +2 -1
  2. package/dist/__tests__/capacity/codex.test.js.map +1 -1
  3. package/dist/__tests__/capacity/index.test.js +6 -3
  4. package/dist/__tests__/capacity/index.test.js.map +1 -1
  5. package/dist/__tests__/capacity/zai.test.js +213 -0
  6. package/dist/__tests__/capacity/zai.test.js.map +1 -0
  7. package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js +189 -0
  8. package/dist/__tests__/providers/opencode/OpenCodeAdapter.test.js.map +1 -0
  9. package/dist/__tests__/providers/opencode/OpenCodeAgentMapper.test.js +89 -0
  10. package/dist/__tests__/providers/opencode/OpenCodeAgentMapper.test.js.map +1 -0
  11. package/dist/__tests__/providers/opencode/OpenCodeSessionLocator.test.js +94 -0
  12. package/dist/__tests__/providers/opencode/OpenCodeSessionLocator.test.js.map +1 -0
  13. package/dist/__tests__/providers/opencode/OpenCodeSessionParser.test.js +147 -0
  14. package/dist/__tests__/providers/opencode/OpenCodeSessionParser.test.js.map +1 -0
  15. package/dist/adapters/index.d.ts +1 -1
  16. package/dist/adapters/index.d.ts.map +1 -1
  17. package/dist/adapters/index.js +1 -1
  18. package/dist/adapters/index.js.map +1 -1
  19. package/dist/capacity/codex.d.ts +1 -0
  20. package/dist/capacity/codex.d.ts.map +1 -1
  21. package/dist/capacity/codex.js +9 -4
  22. package/dist/capacity/codex.js.map +1 -1
  23. package/dist/capacity/index.d.ts +8 -0
  24. package/dist/capacity/index.d.ts.map +1 -1
  25. package/dist/capacity/index.js +10 -9
  26. package/dist/capacity/index.js.map +1 -1
  27. package/dist/capacity/types.d.ts +8 -0
  28. package/dist/capacity/types.d.ts.map +1 -1
  29. package/dist/capacity/types.js.map +1 -1
  30. package/dist/capacity/zai.d.ts +14 -0
  31. package/dist/capacity/zai.d.ts.map +1 -0
  32. package/dist/capacity/zai.js +203 -0
  33. package/dist/capacity/zai.js.map +1 -0
  34. package/dist/index.d.ts +3 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/{adapters → providers/opencode}/OpenCodeAdapter.d.ts +5 -12
  39. package/dist/providers/opencode/OpenCodeAdapter.d.ts.map +1 -0
  40. package/dist/providers/opencode/OpenCodeAdapter.js +86 -0
  41. package/dist/providers/opencode/OpenCodeAdapter.js.map +1 -0
  42. package/dist/providers/opencode/OpenCodeAgentMapper.d.ts +11 -0
  43. package/dist/providers/opencode/OpenCodeAgentMapper.d.ts.map +1 -0
  44. package/dist/providers/opencode/OpenCodeAgentMapper.js +50 -0
  45. package/dist/providers/opencode/OpenCodeAgentMapper.js.map +1 -0
  46. package/dist/providers/opencode/OpenCodeSessionLocator.d.ts +21 -0
  47. package/dist/providers/opencode/OpenCodeSessionLocator.d.ts.map +1 -0
  48. package/dist/providers/opencode/OpenCodeSessionLocator.js +99 -0
  49. package/dist/providers/opencode/OpenCodeSessionLocator.js.map +1 -0
  50. package/dist/providers/opencode/OpenCodeSessionParser.d.ts +18 -0
  51. package/dist/providers/opencode/OpenCodeSessionParser.d.ts.map +1 -0
  52. package/dist/providers/opencode/OpenCodeSessionParser.js +104 -0
  53. package/dist/providers/opencode/OpenCodeSessionParser.js.map +1 -0
  54. package/package.json +1 -1
  55. package/src/__tests__/capacity/codex.test.ts +94 -26
  56. package/src/__tests__/capacity/fixtures/zai-auth.json +6 -0
  57. package/src/__tests__/capacity/fixtures/zai-quota-invalid.json +7 -0
  58. package/src/__tests__/capacity/fixtures/zai-quota.json +38 -0
  59. package/src/__tests__/capacity/index.test.ts +10 -3
  60. package/src/__tests__/capacity/zai.test.ts +186 -0
  61. package/src/__tests__/providers/opencode/OpenCodeAdapter.test.ts +197 -0
  62. package/src/__tests__/providers/opencode/OpenCodeAgentMapper.test.ts +83 -0
  63. package/src/__tests__/providers/opencode/OpenCodeSessionLocator.test.ts +84 -0
  64. package/src/__tests__/providers/opencode/OpenCodeSessionParser.test.ts +148 -0
  65. package/src/adapters/index.ts +1 -1
  66. package/src/capacity/codex.ts +96 -28
  67. package/src/capacity/index.ts +29 -11
  68. package/src/capacity/types.ts +8 -0
  69. package/src/capacity/zai.ts +237 -0
  70. package/src/index.ts +8 -3
  71. package/src/providers/opencode/OpenCodeAdapter.ts +112 -0
  72. package/src/providers/opencode/OpenCodeAgentMapper.ts +61 -0
  73. package/src/providers/opencode/OpenCodeSessionLocator.ts +123 -0
  74. package/src/providers/opencode/OpenCodeSessionParser.ts +145 -0
  75. package/dist/__tests__/adapters/OpenCodeAdapter.test.js +0 -636
  76. package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +0 -1
  77. package/dist/adapters/OpenCodeAdapter.d.ts.map +0 -1
  78. package/dist/adapters/OpenCodeAdapter.js +0 -289
  79. package/dist/adapters/OpenCodeAdapter.js.map +0 -1
  80. package/src/__tests__/adapters/OpenCodeAdapter.test.ts +0 -544
  81. package/src/adapters/OpenCodeAdapter.ts +0 -373
@@ -0,0 +1,148 @@
1
+ import Database from "better-sqlite3";
2
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
+ import { OpenCodeSessionParser } from "../../../providers/opencode/OpenCodeSessionParser.js";
4
+
5
+ describe("OpenCodeSessionParser", () => {
6
+ let db: Database.Database;
7
+ let parser: OpenCodeSessionParser;
8
+
9
+ beforeEach(() => {
10
+ db = new Database(":memory:");
11
+ db.exec(`
12
+ CREATE TABLE message (id TEXT, session_id TEXT, data TEXT, time_created INTEGER, time_updated INTEGER);
13
+ CREATE TABLE part (message_id TEXT, session_id TEXT, data TEXT, time_created INTEGER);
14
+ `);
15
+ parser = new OpenCodeSessionParser();
16
+ });
17
+
18
+ afterEach(() => {
19
+ db.close();
20
+ });
21
+
22
+ it("extracts session stats from OpenCode messages and parts", () => {
23
+ insertMessage(db, {
24
+ id: "msg-user",
25
+ sessionId: "sess-1",
26
+ role: "user",
27
+ timeCreated: 1000,
28
+ timeUpdated: 1000,
29
+ });
30
+ insertMessage(db, {
31
+ id: "msg-assistant",
32
+ sessionId: "sess-1",
33
+ role: "assistant",
34
+ timeCreated: 2000,
35
+ timeUpdated: 2500,
36
+ completed: 2400,
37
+ });
38
+ insertPart(db, {
39
+ messageId: "msg-user",
40
+ sessionId: "sess-1",
41
+ data: { type: "text", text: "Plan the migration" },
42
+ timeCreated: 1000,
43
+ });
44
+
45
+ expect(parser.getSessionStats(db, "sess-1")).toEqual({
46
+ lastRole: "assistant",
47
+ lastTimeUpdated: 2500,
48
+ lastAssistantCompleted: true,
49
+ lastAssistantErrored: false,
50
+ summary: "Plan the migration",
51
+ });
52
+ });
53
+
54
+ it("orders stats by message creation time, not metadata update time", () => {
55
+ insertMessage(db, {
56
+ id: "msg-user",
57
+ sessionId: "sess-1",
58
+ role: "user",
59
+ timeCreated: 1000,
60
+ timeUpdated: 3000,
61
+ });
62
+ insertMessage(db, {
63
+ id: "msg-assistant",
64
+ sessionId: "sess-1",
65
+ role: "assistant",
66
+ timeCreated: 2000,
67
+ timeUpdated: 2100,
68
+ completed: 2200,
69
+ });
70
+
71
+ expect(parser.getSessionStats(db, "sess-1").lastRole).toBe("assistant");
72
+ });
73
+
74
+ it("reads visible conversation text and hides verbose-only parts by default", () => {
75
+ insertMessage(db, {
76
+ id: "msg-user",
77
+ sessionId: "sess-1",
78
+ role: "user",
79
+ timeCreated: 1000,
80
+ timeUpdated: 1000,
81
+ });
82
+ insertMessage(db, {
83
+ id: "msg-assistant",
84
+ sessionId: "sess-1",
85
+ role: "assistant",
86
+ timeCreated: 2000,
87
+ timeUpdated: 2000,
88
+ });
89
+ insertPart(db, {
90
+ messageId: "msg-user",
91
+ sessionId: "sess-1",
92
+ data: { type: "text", text: "Hello" },
93
+ timeCreated: 1000,
94
+ });
95
+ insertPart(db, {
96
+ messageId: "msg-assistant",
97
+ sessionId: "sess-1",
98
+ data: { type: "reasoning", reasoning: "private thought" },
99
+ timeCreated: 2000,
100
+ });
101
+ insertPart(db, {
102
+ messageId: "msg-assistant",
103
+ sessionId: "sess-1",
104
+ data: { type: "tool", tool: "read_file" },
105
+ timeCreated: 3000,
106
+ });
107
+
108
+ expect(parser.getConversation(db, "sess-1")).toEqual([{ role: "user", content: "Hello" }]);
109
+ expect(parser.getConversation(db, "sess-1", { verbose: true })).toEqual([
110
+ { role: "user", content: "Hello" },
111
+ { role: "assistant", content: "[thinking] private thought" },
112
+ { role: "assistant", content: "[tool: read_file]" },
113
+ ]);
114
+ });
115
+ });
116
+
117
+ function insertMessage(
118
+ db: Database.Database,
119
+ input: {
120
+ id: string;
121
+ sessionId: string;
122
+ role: string;
123
+ timeCreated: number;
124
+ timeUpdated: number;
125
+ completed?: number;
126
+ },
127
+ ): void {
128
+ db.prepare(
129
+ "INSERT INTO message (id, session_id, data, time_created, time_updated) VALUES (?, ?, ?, ?, ?)",
130
+ ).run(
131
+ input.id,
132
+ input.sessionId,
133
+ JSON.stringify({
134
+ role: input.role,
135
+ time: input.completed === undefined ? {} : { completed: input.completed },
136
+ }),
137
+ input.timeCreated,
138
+ input.timeUpdated,
139
+ );
140
+ }
141
+
142
+ function insertPart(
143
+ db: Database.Database,
144
+ input: { messageId: string; sessionId: string; data: unknown; timeCreated: number },
145
+ ): void {
146
+ db.prepare("INSERT INTO part (message_id, session_id, data, time_created) VALUES (?, ?, ?, ?)")
147
+ .run(input.messageId, input.sessionId, JSON.stringify(input.data), input.timeCreated);
148
+ }
@@ -3,7 +3,7 @@ export { CodexAdapter } from "../providers/codex/CodexAdapter.js";
3
3
  export { CopilotAdapter } from "../providers/copilot/CopilotAdapter.js";
4
4
  export { GeminiCliAdapter } from "../providers/gemini/GeminiCliAdapter.js";
5
5
  export { GrokCliAdapter } from "./GrokCliAdapter.js";
6
- export { OpenCodeAdapter } from "./OpenCodeAdapter.js";
6
+ export { OpenCodeAdapter } from "../providers/opencode/OpenCodeAdapter.js";
7
7
  export { PiAdapter } from "../providers/pi/PiAdapter.js";
8
8
  export { AgentStatus } from "./AgentAdapter.js";
9
9
  export type {
@@ -15,7 +15,13 @@ type RpcMessage = { id?: number; method: string; params?: UnknownRecord };
15
15
  type CliResponses = { rateLimits: unknown; account: unknown };
16
16
  type CodexRpc = (messages: RpcMessage[]) => Promise<CliResponses>;
17
17
 
18
- export const CODEX_APP_SERVER_ARGS = ["-s", "read-only", "-a", "untrusted", "app-server"] as const;
18
+ export const CODEX_APP_SERVER_ARGS = [
19
+ "-s",
20
+ "read-only",
21
+ "-a",
22
+ "untrusted",
23
+ "app-server",
24
+ ] as const;
19
25
 
20
26
  type CodexProbeOptions = {
21
27
  installed: boolean;
@@ -57,12 +63,18 @@ function safeIdentifier(value: unknown): string | null {
57
63
  return candidate;
58
64
  }
59
65
 
60
- export function resolveCodexAuthPath(env: NodeJS.ProcessEnv = process.env): string {
66
+ export function resolveCodexAuthPath(
67
+ env: NodeJS.ProcessEnv = process.env,
68
+ ): string {
61
69
  const root = env.CODEX_HOME || join(env.HOME || "", ".codex");
62
70
  return join(root, "auth.json");
63
71
  }
64
72
 
65
- export function toRateWindow(value: unknown, id: string, label: string): CapacityWindow | null {
73
+ export function toRateWindow(
74
+ value: unknown,
75
+ id: string,
76
+ label: string,
77
+ ): CapacityWindow | null {
66
78
  const input = record(value);
67
79
  if (!input) return null;
68
80
  const used = finiteNumber(input.used_percent);
@@ -84,13 +96,24 @@ function extraWindows(value: unknown): CapacityWindow[] {
84
96
  const scope = safeIdentifier(limit.limit_name) ?? `extra-${index + 1}`;
85
97
  const windows = record(limit.rate_limit) ?? limit;
86
98
  return [
87
- toRateWindow(windows.primary_window, `${scope}:primary`, `${scope} primary`),
88
- toRateWindow(windows.secondary_window, `${scope}:secondary`, `${scope} secondary`),
99
+ toRateWindow(
100
+ windows.primary_window,
101
+ `${scope}:primary`,
102
+ `${scope} primary`,
103
+ ),
104
+ toRateWindow(
105
+ windows.secondary_window,
106
+ `${scope}:secondary`,
107
+ `${scope} secondary`,
108
+ ),
89
109
  ].filter((window): window is CapacityWindow => window !== null);
90
110
  });
91
111
  }
92
112
 
93
- export function parseUsage(raw: unknown, source: "pat" | "oauth"): UsageSnapshot {
113
+ export function parseUsage(
114
+ raw: unknown,
115
+ source: "pat" | "oauth",
116
+ ): UsageSnapshot {
94
117
  const response = record(raw) ?? {};
95
118
  const limits = record(response.rate_limit) ?? {};
96
119
  const credits = record(response.credits) ?? {};
@@ -105,7 +128,11 @@ export function parseUsage(raw: unknown, source: "pat" | "oauth"): UsageSnapshot
105
128
  };
106
129
  }
107
130
 
108
- function cliWindow(value: unknown, id: string, label: string): CapacityWindow | null {
131
+ function cliWindow(
132
+ value: unknown,
133
+ id: string,
134
+ label: string,
135
+ ): CapacityWindow | null {
109
136
  const input = record(value);
110
137
  if (!input) return null;
111
138
  return {
@@ -117,10 +144,14 @@ function cliWindow(value: unknown, id: string, label: string): CapacityWindow |
117
144
  };
118
145
  }
119
146
 
120
- function cliSnapshotWindows(value: unknown, fallbackId: string): CapacityWindow[] {
147
+ function cliSnapshotWindows(
148
+ value: unknown,
149
+ fallbackId: string,
150
+ ): CapacityWindow[] {
121
151
  const snapshot = record(value);
122
152
  if (!snapshot) return [];
123
- const scope = safeIdentifier(snapshot.limitId) ?? safeIdentifier(fallbackId) ?? "codex";
153
+ const scope =
154
+ safeIdentifier(snapshot.limitId) ?? safeIdentifier(fallbackId) ?? "codex";
124
155
  return [
125
156
  cliWindow(snapshot.primary, `${scope}:primary`, `${scope} primary`),
126
157
  cliWindow(snapshot.secondary, `${scope}:secondary`, `${scope} secondary`),
@@ -136,7 +167,9 @@ export function parseCliUsage(raw: unknown): UsageSnapshot {
136
167
  for (const [id, snapshot] of Object.entries(buckets))
137
168
  windows.push(...cliSnapshotWindows(snapshot, id));
138
169
  }
139
- const unique = [...new Map(windows.map((window) => [window.id, window])).values()];
170
+ const unique = [
171
+ ...new Map(windows.map((window) => [window.id, window])).values(),
172
+ ];
140
173
  return { windows: unique, creditsRemaining: null, source: "cli" };
141
174
  }
142
175
 
@@ -145,18 +178,23 @@ function capacityFromSnapshot(
145
178
  context: CodexProbeOptions,
146
179
  raw?: unknown,
147
180
  ): CapacityReport {
148
- const hasUsage = snapshot.windows.some((window) => window.usedPercent !== null);
181
+ const hasUsage = snapshot.windows.some(
182
+ (window) => window.usedPercent !== null,
183
+ );
149
184
  const rateLimits = record(record(raw)?.rateLimits);
150
185
  const reached = nonEmptyText(rateLimits?.rateLimitReachedType);
151
186
  const resetCredits =
152
- record(record(raw)?.rateLimitResetCredits) ?? record(record(raw)?.usageLimitResetCredits);
187
+ record(record(raw)?.rateLimitResetCredits) ??
188
+ record(record(raw)?.usageLimitResetCredits);
153
189
  return {
154
- provider: "codex",
190
+ harness: "codex",
191
+ provider: "openai",
155
192
  generatedAt: context.checkedAt,
156
193
  authenticated: true,
157
194
  available: reached ? "no" : hasUsage ? "yes" : "unknown",
158
195
  windows: snapshot.windows,
159
- creditsRemaining: snapshot.creditsRemaining ?? finiteNumber(resetCredits?.availableCount),
196
+ creditsRemaining:
197
+ snapshot.creditsRemaining ?? finiteNumber(resetCredits?.availableCount),
160
198
  };
161
199
  }
162
200
 
@@ -164,7 +202,9 @@ function jwtExpiry(token: string): number | null {
164
202
  const part = token.split(".")[1];
165
203
  if (!part) return null;
166
204
  try {
167
- return finiteNumber(record(JSON.parse(Buffer.from(part, "base64url").toString("utf8")))?.exp);
205
+ return finiteNumber(
206
+ record(JSON.parse(Buffer.from(part, "base64url").toString("utf8")))?.exp,
207
+ );
168
208
  } catch {
169
209
  return null;
170
210
  }
@@ -191,7 +231,10 @@ async function fetchJson(
191
231
  const timer = setTimeout(() => controller.abort(), timeoutMs);
192
232
  try {
193
233
  const response = await fetcher(url, { ...init, signal: controller.signal });
194
- if (!response.ok) throw new Error(response.status === 401 ? "unauthorized" : "request failed");
234
+ if (!response.ok)
235
+ throw new Error(
236
+ response.status === 401 ? "unauthorized" : "request failed",
237
+ );
195
238
  return await response.json();
196
239
  } finally {
197
240
  clearTimeout(timer);
@@ -209,14 +252,20 @@ async function apiSnapshot(
209
252
  fetcher,
210
253
  "https://chatgpt.com/backend-api/wham/usage",
211
254
  {
212
- headers: { Authorization: `Bearer ${token}`, "ChatGPT-Account-Id": accountId },
255
+ headers: {
256
+ Authorization: `Bearer ${token}`,
257
+ "ChatGPT-Account-Id": accountId,
258
+ },
213
259
  },
214
260
  options.timeoutMs ?? 5000,
215
261
  );
216
262
  return parseUsage(raw, source);
217
263
  }
218
264
 
219
- function appServerRpc(messages: RpcMessage[], timeoutMs = 5000): Promise<CliResponses> {
265
+ function appServerRpc(
266
+ messages: RpcMessage[],
267
+ timeoutMs = 5000,
268
+ ): Promise<CliResponses> {
220
269
  return new Promise((resolve, reject) => {
221
270
  const child = spawn("codex", CODEX_APP_SERVER_ARGS, {
222
271
  stdio: ["pipe", "pipe", "ignore"],
@@ -232,8 +281,13 @@ function appServerRpc(messages: RpcMessage[], timeoutMs = 5000): Promise<CliResp
232
281
  if (error) reject(error);
233
282
  else resolve(results as CliResponses);
234
283
  };
235
- const timer = setTimeout(() => finish(new Error("codex probe timed out")), timeoutMs);
236
- child.once("error", () => finish(new Error("codex app-server unavailable")));
284
+ const timer = setTimeout(
285
+ () => finish(new Error("codex probe timed out")),
286
+ timeoutMs,
287
+ );
288
+ child.once("error", () =>
289
+ finish(new Error("codex app-server unavailable")),
290
+ );
237
291
  child.once("exit", () => {
238
292
  if (!settled) finish(new Error("codex app-server exited"));
239
293
  });
@@ -256,7 +310,8 @@ function appServerRpc(messages: RpcMessage[], timeoutMs = 5000): Promise<CliResp
256
310
  for (const request of messages.slice(1))
257
311
  child.stdin.write(`${JSON.stringify(request)}\n`);
258
312
  } else if (message.id === 2) {
259
- if (message.error) finish(new Error("codex rate-limit method failed"));
313
+ if (message.error)
314
+ finish(new Error("codex rate-limit method failed"));
260
315
  else results.rateLimits = message.result;
261
316
  } else if (message.id === 3) {
262
317
  if (message.error) finish(new Error("codex account method failed"));
@@ -269,10 +324,11 @@ function appServerRpc(messages: RpcMessage[], timeoutMs = 5000): Promise<CliResp
269
324
  });
270
325
  }
271
326
 
272
- function unavailable(options: CodexProbeOptions): CapacityReport {
327
+ export function codexUnavailableReport(checkedAt: string): CapacityReport {
273
328
  return {
274
- provider: "codex",
275
- generatedAt: options.checkedAt,
329
+ harness: "codex",
330
+ provider: "openai",
331
+ generatedAt: checkedAt,
276
332
  authenticated: null,
277
333
  available: "unknown",
278
334
  windows: [],
@@ -280,7 +336,13 @@ function unavailable(options: CodexProbeOptions): CapacityReport {
280
336
  };
281
337
  }
282
338
 
283
- async function cliFallback(options: CodexProbeOptions): Promise<CapacityReport> {
339
+ function unavailable(options: CodexProbeOptions): CapacityReport {
340
+ return codexUnavailableReport(options.checkedAt);
341
+ }
342
+
343
+ async function cliFallback(
344
+ options: CodexProbeOptions,
345
+ ): Promise<CapacityReport> {
284
346
  if (!options.installed) return unavailable(options);
285
347
  const messages: RpcMessage[] = [
286
348
  {
@@ -296,7 +358,8 @@ async function cliFallback(options: CodexProbeOptions): Promise<CapacityReport>
296
358
  { id: 3, method: "account/read" },
297
359
  ];
298
360
  try {
299
- const rpc = options.rpc ?? ((requests) => appServerRpc(requests, options.timeoutMs));
361
+ const rpc =
362
+ options.rpc ?? ((requests) => appServerRpc(requests, options.timeoutMs));
300
363
  const response = await rpc(messages);
301
364
  const result = capacityFromSnapshot(
302
365
  parseCliUsage(response.rateLimits),
@@ -318,7 +381,9 @@ async function cliFallback(options: CodexProbeOptions): Promise<CapacityReport>
318
381
  }
319
382
  }
320
383
 
321
- export async function probeCodexCapacity(options: CodexProbeOptions): Promise<CapacityReport> {
384
+ export async function probeCodexCapacity(
385
+ options: CodexProbeOptions,
386
+ ): Promise<CapacityReport> {
322
387
  let parsed: UnknownRecord | null = null;
323
388
  try {
324
389
  const contents = await (options.readFile ?? readFile)(
@@ -345,7 +410,10 @@ export async function probeCodexCapacity(options: CodexProbeOptions): Promise<Ca
345
410
  );
346
411
  const accountId = nonEmptyText(whoami?.chatgpt_account_id);
347
412
  if (!accountId) throw new Error("account unavailable");
348
- return capacityFromSnapshot(await apiSnapshot(pat, accountId, "pat", options), options);
413
+ return capacityFromSnapshot(
414
+ await apiSnapshot(pat, accountId, "pat", options),
415
+ options,
416
+ );
349
417
  } catch {
350
418
  // Continue to a separately available OAuth credential before using the CLI.
351
419
  }
@@ -1,7 +1,8 @@
1
1
  import { constants } from "node:fs";
2
2
  import { access as fsAccess } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { probeCodexCapacity } from "./codex.js";
4
+ import { codexUnavailableReport, probeCodexCapacity } from "./codex.js";
5
+ import { probeZaiCapacity } from "./zai.js";
5
6
  import type { CapacityReport } from "./types.js";
6
7
 
7
8
  export type { CapacityReport, CapacityWindow } from "./types.js";
@@ -13,6 +14,14 @@ export type CapacityProbeOptions = {
13
14
  probe?: typeof probeCodexCapacity;
14
15
  };
15
16
 
17
+ export type ZaiCapacityOptions = {
18
+ now?: () => Date;
19
+ env?: NodeJS.ProcessEnv;
20
+ readFile?: (path: string, encoding: BufferEncoding) => Promise<string>;
21
+ fetch?: typeof globalThis.fetch;
22
+ timeoutMs?: number;
23
+ };
24
+
16
25
  async function canAccess(target: string, mode: number): Promise<boolean> {
17
26
  try {
18
27
  await fsAccess(target, mode);
@@ -46,17 +55,26 @@ export async function getCodexCapacityReport(
46
55
  options: CapacityProbeOptions = {},
47
56
  ): Promise<CapacityReport> {
48
57
  const generatedAt = (options.now?.() ?? new Date()).toISOString();
49
- const installed = await isCodexInstalled(options.path ?? process.env.PATH ?? "", options.access);
58
+ const installed = await isCodexInstalled(
59
+ options.path ?? process.env.PATH ?? "",
60
+ options.access,
61
+ );
50
62
  try {
51
- return await (options.probe ?? probeCodexCapacity)({ installed, checkedAt: generatedAt });
63
+ return await (options.probe ?? probeCodexCapacity)({
64
+ installed,
65
+ checkedAt: generatedAt,
66
+ });
52
67
  } catch {
53
- return {
54
- provider: "codex",
55
- generatedAt,
56
- authenticated: null,
57
- available: "unknown",
58
- windows: [],
59
- creditsRemaining: null,
60
- };
68
+ return codexUnavailableReport(generatedAt);
61
69
  }
62
70
  }
71
+
72
+ export async function getZaiCapacityReport(
73
+ options: ZaiCapacityOptions = {},
74
+ ): Promise<CapacityReport> {
75
+ const { now, ...probeOptions } = options;
76
+ return probeZaiCapacity({
77
+ ...probeOptions,
78
+ checkedAt: (now?.() ?? new Date()).toISOString(),
79
+ });
80
+ }
@@ -1,4 +1,5 @@
1
1
  export type Availability = "yes" | "no" | "unknown";
2
+ export type CapacityLimitType = "TOKENS_LIMIT" | "CREDIT_LIMIT" | "TIME_LIMIT";
2
3
 
3
4
  export interface CapacityWindow {
4
5
  id: string;
@@ -6,9 +7,16 @@ export interface CapacityWindow {
6
7
  durationMinutes: number | null;
7
8
  usedPercent: number | null;
8
9
  resetsAt: string | null;
10
+ limitType?: CapacityLimitType;
11
+ total?: number | null;
12
+ current?: number | null;
13
+ remaining?: number | null;
9
14
  }
10
15
 
11
16
  export interface CapacityReport {
17
+ /** Harness whose quota pool is measured, e.g. "pi" or "codex". */
18
+ harness: string;
19
+ /** Model provider backing the harness, e.g. "zai" or "openai". */
12
20
  provider: string;
13
21
  generatedAt: string;
14
22
  authenticated: boolean | null;