@maxiaochao/pi-toolkit 0.11.8 → 0.11.10

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 (35) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +15 -28
  3. package/package.json +2 -4
  4. package/packages/openai-account-pool/CHANGELOG.md +0 -11
  5. package/packages/openai-account-pool/README.md +0 -67
  6. package/packages/openai-account-pool/TESTING.md +0 -28
  7. package/packages/openai-account-pool/docs/demo.gif +0 -0
  8. package/packages/openai-account-pool/docs/screenshot.png +0 -0
  9. package/packages/openai-account-pool/extensions/openai-account-pool.ts +0 -368
  10. package/packages/openai-account-pool/package.json +0 -45
  11. package/packages/openai-account-pool/src/auth.ts +0 -418
  12. package/packages/openai-account-pool/src/availability.ts +0 -230
  13. package/packages/openai-account-pool/src/command-completions.ts +0 -18
  14. package/packages/openai-account-pool/src/enrollment.ts +0 -339
  15. package/packages/openai-account-pool/src/lease.ts +0 -128
  16. package/packages/openai-account-pool/src/lock.ts +0 -171
  17. package/packages/openai-account-pool/src/login-slot.ts +0 -158
  18. package/packages/openai-account-pool/src/policy.ts +0 -176
  19. package/packages/openai-account-pool/src/provider.ts +0 -511
  20. package/packages/openai-account-pool/src/status-widget.ts +0 -62
  21. package/packages/openai-account-pool/src/store.ts +0 -259
  22. package/packages/openai-account-pool/src/usage.ts +0 -291
  23. package/packages/openai-account-pool/tests/command-completions.mjs +0 -10
  24. package/packages/openai-account-pool/tests/enrollment.mjs +0 -322
  25. package/packages/openai-account-pool/tests/failover-stream.mjs +0 -554
  26. package/packages/openai-account-pool/tests/helpers/fixtures.mjs +0 -40
  27. package/packages/openai-account-pool/tests/helpers/native-credential-store.mjs +0 -58
  28. package/packages/openai-account-pool/tests/helpers/pi-runtime.mjs +0 -24
  29. package/packages/openai-account-pool/tests/helpers/refresh-child.mjs +0 -29
  30. package/packages/openai-account-pool/tests/integration-login-ownership.mjs +0 -299
  31. package/packages/openai-account-pool/tests/login-slot.mjs +0 -131
  32. package/packages/openai-account-pool/tests/multiprocess.mjs +0 -73
  33. package/packages/openai-account-pool/tests/selection.mjs +0 -286
  34. package/packages/openai-account-pool/tests/store-ownership.mjs +0 -369
  35. package/packages/openai-account-pool/tests/usage.mjs +0 -428
@@ -1,428 +0,0 @@
1
- import assert from "node:assert/strict";
2
-
3
- import { commitAccount, loadPoolSnapshot } from "../src/auth.ts";
4
- import { availabilityFor, claimProbeLease, recordFailure, recordSuccess } from "../src/availability.ts";
5
- import { poolStatus } from "../src/enrollment.ts";
6
- import { CodexPoolStatusWidget, formatAccountStatusLine } from "../src/status-widget.ts";
7
- import { poolStatePathFor, readPoolState } from "../src/store.ts";
8
- import {
9
- CODEX_USAGE_ENDPOINT,
10
- accountReadyAtMs,
11
- fetchAccountUsage,
12
- normalizeUsageResponse,
13
- refreshPoolUsage,
14
- soonestReady,
15
- } from "../src/usage.ts";
16
- import { fakeAccessToken, fakeCredential, makeAgentDir } from "./helpers/fixtures.mjs";
17
-
18
- const NOW = Date.UTC(2026, 9, 13, 12, 0, 0);
19
- const ACCOUNT_A = "account-aaaa";
20
- const ACCOUNT_B = "account-bbbb";
21
-
22
- function usagePayload({ primary = 20, secondary = 30, primaryReset = NOW + 60_000, secondaryReset = NOW + 120_000, allowed = true } = {}) {
23
- return {
24
- plan_type: "plus",
25
- rate_limit: {
26
- allowed,
27
- limit_reached: !allowed,
28
- primary_window: { used_percent: primary, limit_window_seconds: 18_000, reset_at: Math.floor(primaryReset / 1_000) },
29
- secondary_window: { used_percent: secondary, limit_window_seconds: 604_800, reset_at: Math.floor(secondaryReset / 1_000) },
30
- },
31
- };
32
- }
33
-
34
- function response(payload, status = 200) {
35
- return new Response(typeof payload === "string" ? payload : JSON.stringify(payload), {
36
- status,
37
- headers: { "content-type": "application/json" },
38
- });
39
- }
40
-
41
- async function setupPool(credentials = [
42
- fakeCredential(ACCOUNT_A, { now: NOW, email: "a@example.com", plan: "plus" }),
43
- fakeCredential(ACCOUNT_B, { now: NOW, email: "b@example.com", plan: "plus" }),
44
- ]) {
45
- const agent = makeAgentDir();
46
- for (const credential of credentials) await commitAccount({ authPath: agent.authPath, credential, now: NOW });
47
- return { agent, statePath: poolStatePathFor(agent.directory), snapshot: await loadPoolSnapshot(agent.authPath) };
48
- }
49
-
50
- // Normalization recognizes the real 5-hour and weekly windows, clamps percentages, and converts epoch seconds.
51
- {
52
- const usage = normalizeUsageResponse(usagePayload({ primary: -4, secondary: 140 }), NOW);
53
- assert.deepEqual(
54
- { primary: usage.primary, secondary: usage.secondary },
55
- {
56
- primary: { usedPercent: 0, windowSeconds: 18_000, resetAtMs: NOW + 60_000 },
57
- secondary: { usedPercent: 100, windowSeconds: 604_800, resetAtMs: NOW + 120_000 },
58
- },
59
- );
60
- assert.equal(usage.plan, "plus");
61
- assert.equal(usage.observedAtMs, NOW);
62
- }
63
-
64
- // The private endpoint receives exactly the bearer token and account selector it requires.
65
- {
66
- const token = fakeAccessToken(ACCOUNT_A);
67
- let call;
68
- const usage = await fetchAccountUsage({
69
- credential: { access: token },
70
- accountId: ACCOUNT_A,
71
- now: () => NOW,
72
- fetch: async (input, init) => {
73
- call = { input: String(input), init };
74
- return response(usagePayload());
75
- },
76
- });
77
- assert.equal(call.input, CODEX_USAGE_ENDPOINT);
78
- assert.equal(call.init.method, "GET");
79
- assert.equal(call.init.headers.Authorization, `Bearer ${token}`);
80
- assert.equal(call.init.headers["ChatGPT-Account-Id"], ACCOUNT_A);
81
- assert.equal(usage.primary.windowSeconds, 18_000);
82
- }
83
-
84
- // Bad status, malformed JSON/data and oversized bodies fail without putting credentials in errors.
85
- {
86
- const secret = "never-print-this-token";
87
- for (const fetch of [
88
- async () => response({ error: "nope" }, 503),
89
- async () => response("not-json"),
90
- async () => response({ unrelated: true }),
91
- async () => response("x".repeat(256 * 1024 + 1)),
92
- ]) {
93
- await assert.rejects(
94
- fetchAccountUsage({ credential: { access: secret }, accountId: ACCOUNT_A, fetch, now: () => NOW }),
95
- (error) => error instanceof Error && !error.message.includes(secret),
96
- );
97
- }
98
- }
99
-
100
- // The body limit is enforced while streaming and the remaining body is cancelled.
101
- {
102
- let pulls = 0;
103
- let cancelled = false;
104
- const encoder = new TextEncoder();
105
- const chunks = [
106
- encoder.encode(`${JSON.stringify(usagePayload())}${" ".repeat(200_000)}`),
107
- encoder.encode(" ".repeat(100_000)),
108
- ...Array.from({ length: 8 }, () => encoder.encode("should-not-be-read")),
109
- ];
110
- const body = new ReadableStream({
111
- pull(controller) {
112
- controller.enqueue(chunks[pulls++]);
113
- if (pulls === chunks.length) controller.close();
114
- },
115
- cancel() { cancelled = true; },
116
- });
117
- await assert.rejects(
118
- fetchAccountUsage({ credential: { access: "fixture-token" }, accountId: ACCOUNT_A, fetch: async () => new Response(body), now: () => NOW }),
119
- /too large/,
120
- );
121
- assert.equal(cancelled, true);
122
- assert.ok(pulls < chunks.length);
123
- }
124
-
125
- // A private-endpoint stall is locally bounded even without a caller cancellation signal.
126
- {
127
- await assert.rejects(
128
- fetchAccountUsage({
129
- credential: { access: "fixture-token" },
130
- accountId: ACCOUNT_A,
131
- timeoutMs: 5,
132
- fetch: async (_input, init) => new Promise((_resolve, reject) => {
133
- init.signal.addEventListener("abort", () => reject(Object.assign(new Error("aborted"), { name: "AbortError" })), { once: true });
134
- }),
135
- }),
136
- (error) => error instanceof Error && error.name === "AbortError",
137
- );
138
- }
139
-
140
- // Every account is queried sequentially; a failure does not prevent later accounts from updating.
141
- {
142
- const { agent, statePath, snapshot } = await setupPool();
143
- try {
144
- const order = [];
145
- let releaseFirst;
146
- const firstHeld = new Promise((resolve) => { releaseFirst = resolve; });
147
- let firstStarted;
148
- const started = new Promise((resolve) => { firstStarted = resolve; });
149
- const pending = refreshPoolUsage({
150
- authPath: agent.authPath,
151
- statePath,
152
- accounts: snapshot.accounts,
153
- refresh: async (credential) => credential,
154
- now: () => NOW,
155
- fetch: async (_input, init) => {
156
- const id = init.headers["ChatGPT-Account-Id"];
157
- order.push(id);
158
- if (id === ACCOUNT_A) {
159
- firstStarted();
160
- await firstHeld;
161
- }
162
- return id === ACCOUNT_A ? response({}, 500) : response(usagePayload());
163
- },
164
- });
165
- await started;
166
- assert.deepEqual(order, [ACCOUNT_A]);
167
- releaseFirst();
168
- const results = await pending;
169
- assert.deepEqual(order, [ACCOUNT_A, ACCOUNT_B]);
170
- assert.ok(results[0].error);
171
- assert.equal(results[1].usage.primary.usedPercent, 20);
172
- assert.equal((await readPoolState(statePath)).accounts[ACCOUNT_B].usage.observedAtMs, NOW);
173
- } finally {
174
- agent.dispose();
175
- }
176
- }
177
-
178
- // Credential freshness uses the injected clock; a valid credential is never refreshed.
179
- {
180
- const { agent, statePath, snapshot } = await setupPool([fakeCredential(ACCOUNT_A, { now: NOW, expiresInMs: 60 * 60_000 })]);
181
- try {
182
- let refreshes = 0;
183
- await refreshPoolUsage({
184
- authPath: agent.authPath,
185
- statePath,
186
- accounts: snapshot.accounts,
187
- refresh: async (credential) => { refreshes += 1; return credential; },
188
- now: () => NOW,
189
- fetch: async () => response(usagePayload()),
190
- });
191
- assert.equal(refreshes, 0);
192
- } finally {
193
- agent.dispose();
194
- }
195
- }
196
-
197
- // Expired OAuth credentials are refreshed first and the rotated access token is used for usage lookup.
198
- {
199
- const old = fakeCredential(ACCOUNT_A, { now: NOW, expiresInMs: 0 });
200
- const { agent, statePath, snapshot } = await setupPool([old]);
201
- try {
202
- const rotated = fakeCredential(ACCOUNT_A, { now: NOW, marker: "rotated" });
203
- let refreshes = 0;
204
- let authorization;
205
- await refreshPoolUsage({
206
- authPath: agent.authPath,
207
- statePath,
208
- accounts: snapshot.accounts,
209
- refresh: async () => { refreshes += 1; return rotated; },
210
- now: () => NOW,
211
- fetch: async (_input, init) => { authorization = init.headers.Authorization; return response(usagePayload()); },
212
- });
213
- assert.equal(refreshes, 1);
214
- assert.equal(authorization, `Bearer ${rotated.access}`);
215
- } finally {
216
- agent.dispose();
217
- }
218
- }
219
-
220
- // Refresh and parser failures collapse to a fixed non-secret result code.
221
- {
222
- const { agent, statePath, snapshot } = await setupPool([fakeCredential(ACCOUNT_A, { now: NOW, expiresInMs: 0 })]);
223
- try {
224
- const results = await refreshPoolUsage({
225
- authPath: agent.authPath,
226
- statePath,
227
- accounts: snapshot.accounts,
228
- refresh: async () => { throw new Error("refresh-token-secret-value-123456789"); },
229
- now: () => NOW,
230
- fetch: async () => response(usagePayload()),
231
- });
232
- assert.equal(results[0].error, "usage_unavailable");
233
- assert.doesNotMatch(JSON.stringify(results), /refresh-token-secret/);
234
- } finally {
235
- agent.dispose();
236
- }
237
- }
238
-
239
- // An older available observation cannot erase newer usage or quota-failure evidence.
240
- {
241
- const { agent, statePath, snapshot } = await setupPool([fakeCredential(ACCOUNT_A, { now: NOW })]);
242
- try {
243
- const account = snapshot.accounts[0];
244
- await refreshPoolUsage({
245
- authPath: agent.authPath,
246
- statePath,
247
- accounts: [account],
248
- refresh: async (credential) => credential,
249
- now: () => NOW + 1_000,
250
- fetch: async () => response(usagePayload({ primary: 100, allowed: false, primaryReset: NOW + 300_000 })),
251
- });
252
- await recordFailure({ statePath, account, now: NOW + 1_000, classification: { kind: "quota", reason: "usage_limit_reached", scope: "account", serverResetAtMs: NOW + 300_000 } });
253
- await refreshPoolUsage({
254
- authPath: agent.authPath,
255
- statePath,
256
- accounts: [account],
257
- refresh: async (credential) => credential,
258
- now: () => NOW,
259
- fetch: async () => response(usagePayload({ allowed: true })),
260
- });
261
- const state = await readPoolState(statePath);
262
- assert.equal(state.accounts[ACCOUNT_A].usage.allowed, false);
263
- assert.ok(state.accounts[ACCOUNT_A].cooldowns.account);
264
- } finally {
265
- agent.dispose();
266
- }
267
- }
268
-
269
- // Clearing recovered quota evidence does not bypass another process's active probe lease.
270
- {
271
- const { agent, statePath, snapshot } = await setupPool([fakeCredential(ACCOUNT_A, { now: NOW })]);
272
- try {
273
- const account = snapshot.accounts[0];
274
- await recordFailure({ statePath, account, now: NOW - 120_000, classification: { kind: "quota", reason: "usage_limit_reached", scope: "account", serverResetAtMs: NOW - 60_000 } });
275
- assert.equal(await claimProbeLease({ statePath, accountId: account.id, owner: "other", now: NOW }), true);
276
- await refreshPoolUsage({
277
- authPath: agent.authPath,
278
- statePath,
279
- accounts: [account],
280
- refresh: async (credential) => credential,
281
- now: () => NOW,
282
- fetch: async () => response(usagePayload({ allowed: true })),
283
- });
284
- const state = await readPoolState(statePath);
285
- const [availability] = availabilityFor({ accounts: [account], state, now: NOW, probeOwner: "this-process" });
286
- assert.equal(availability.unavailable.reason, "probe_in_progress");
287
- } finally {
288
- agent.dispose();
289
- }
290
- }
291
-
292
- // Live availability clears quota evidence only; auth-invalid evidence survives. Exhaustion retains cooldowns.
293
- {
294
- const { agent, statePath, snapshot } = await setupPool();
295
- try {
296
- const [accountA, accountB] = snapshot.accounts;
297
- await recordFailure({ statePath, account: accountA, now: NOW, classification: { kind: "quota", reason: "usage_limit_reached", scope: "account", serverResetAtMs: NOW + 60_000 } });
298
- await recordFailure({ statePath, account: accountA, now: NOW, classification: { kind: "auth_invalid", reason: "authorization_failed" } });
299
- await recordFailure({ statePath, account: accountB, now: NOW, classification: { kind: "quota", reason: "usage_limit_reached", scope: "account", serverResetAtMs: NOW + 120_000 } });
300
- await refreshPoolUsage({
301
- authPath: agent.authPath,
302
- statePath,
303
- accounts: snapshot.accounts,
304
- refresh: async (credential) => credential,
305
- now: () => NOW,
306
- fetch: async (_input, init) => init.headers["ChatGPT-Account-Id"] === ACCOUNT_A
307
- ? response(usagePayload({ allowed: true }))
308
- : response(usagePayload({ primary: 100, allowed: false })),
309
- });
310
- const state = await readPoolState(statePath);
311
- assert.deepEqual(state.accounts[ACCOUNT_A].cooldowns, {});
312
- assert.equal(state.accounts[ACCOUNT_A].authInvalidGeneration, accountA.generation);
313
- assert.ok(state.accounts[ACCOUNT_B].cooldowns.account);
314
- } finally {
315
- agent.dispose();
316
- }
317
- }
318
-
319
- // Readiness waits for every blocking window and chooses the earliest account, not the earliest limit.
320
- {
321
- const accounts = [
322
- { id: ACCOUNT_A, key: "a", ordinal: 1, addedAt: NOW, generation: 1 },
323
- { id: ACCOUNT_B, key: "b", ordinal: 2, addedAt: NOW, generation: 1 },
324
- ];
325
- const state = {
326
- v: 1,
327
- accounts: {
328
- [ACCOUNT_A]: {
329
- cooldowns: { account: { reason: "usage_limit_reached", generation: 1, serverResetAtMs: NOW + 60_000, probeAfterMs: null, recordedAtMs: NOW } },
330
- usage: { primary: { usedPercent: 100, resetAtMs: NOW + 300_000 }, secondary: { usedPercent: 100, resetAtMs: NOW + 180_000 }, observedAtMs: NOW },
331
- },
332
- [ACCOUNT_B]: {
333
- cooldowns: { account: { reason: "usage_limit_reached", generation: 1, serverResetAtMs: NOW + 240_000, probeAfterMs: null, recordedAtMs: NOW } },
334
- },
335
- },
336
- };
337
- assert.equal(accountReadyAtMs({ account: accounts[0], record: state.accounts[ACCOUNT_A], now: NOW }), NOW + 300_000);
338
- assert.deepEqual(soonestReady({ accounts, state, now: NOW }), { accountId: ACCOUNT_B, readyAtMs: NOW + 240_000 });
339
- }
340
-
341
- // Recovery ignores unrelated model cooldowns and refuses partial estimates with unknown blockers.
342
- {
343
- const account = { id: ACCOUNT_A, key: "a", ordinal: 1, addedAt: NOW, generation: 1 };
344
- const record = {
345
- cooldowns: {
346
- account: { reason: "usage_limit_reached", generation: 1, serverResetAtMs: NOW + 60_000, probeAfterMs: null, recordedAtMs: NOW },
347
- "model:other": { reason: "usage_limit_reached", generation: 1, serverResetAtMs: NOW + 7 * 86_400_000, probeAfterMs: null, recordedAtMs: NOW },
348
- },
349
- };
350
- assert.equal(accountReadyAtMs({ account, record, now: NOW, modelId: "wanted" }), NOW + 60_000);
351
- record.usage = {
352
- allowed: false,
353
- limitReached: true,
354
- primary: { usedPercent: 100 },
355
- secondary: { usedPercent: 100, resetAtMs: NOW + 600_000 },
356
- observedAtMs: NOW,
357
- };
358
- assert.equal(accountReadyAtMs({ account, record, now: NOW, modelId: "wanted" }), undefined);
359
- }
360
-
361
- // Status queries all accounts, shows email and usage, and marks only the last successful account.
362
- {
363
- const { agent, statePath, snapshot } = await setupPool();
364
- try {
365
- await recordSuccess({ statePath, account: snapshot.accounts[1], requestStartedAtMs: NOW });
366
- const queried = [];
367
- const status = await poolStatus({
368
- authPath: agent.authPath,
369
- statePath,
370
- now: NOW,
371
- refresh: async (credential) => credential,
372
- fetch: async (_input, init) => { queried.push(init.headers["ChatGPT-Account-Id"]); return response(usagePayload()); },
373
- });
374
- assert.deepEqual(queried, [ACCOUNT_A, ACCOUNT_B]);
375
- assert.equal(status.accounts.length, 2);
376
- assert.match(status.accounts[0].label, /a@example\.com/);
377
- assert.doesNotMatch(status.accounts[0].label, /当前|默认/);
378
- assert.match(status.accounts[1].label, /b@example\.com/);
379
- assert.equal(status.accounts[0].isCurrent, false);
380
- assert.equal(status.accounts[1].isCurrent, true);
381
- assert.match(formatAccountStatusLine(status.accounts[1]), /●当前$/);
382
- assert.doesNotMatch(status.accounts[1].label, /★默认/);
383
- const theme = { fg: (_color, text) => text };
384
- const widget = new CodexPoolStatusWidget(theme);
385
- assert.match(widget.render(96).join("\n"), /正在读取账号池状态/);
386
- widget.update(status, { loading: true });
387
- const rendered = widget.render(160);
388
- assert.match(rendered.join("\n"), /正在更新官网额度/);
389
- const currentLine = rendered.find((line) => line.includes("●当前"));
390
- assert.ok(currentLine, JSON.stringify(rendered));
391
- assert.match(currentLine, /●当前$/);
392
- for (const line of status.accounts) {
393
- assert.match(line.detail, /5 小时已用 20%/);
394
- assert.match(line.detail, /7 天已用 30%/);
395
- assert.match(line.detail, /重置/);
396
- }
397
- } finally {
398
- agent.dispose();
399
- }
400
- }
401
-
402
- // A failed live status lookup retains stored exhaustion and does not count the account as ready.
403
- {
404
- const { agent, statePath } = await setupPool([fakeCredential(ACCOUNT_A, { now: NOW })]);
405
- try {
406
- await poolStatus({
407
- authPath: agent.authPath,
408
- statePath,
409
- now: NOW,
410
- refresh: async (credential) => credential,
411
- fetch: async () => response(usagePayload({ primary: 100, allowed: false, primaryReset: NOW + 600_000 })),
412
- });
413
- const status = await poolStatus({
414
- authPath: agent.authPath,
415
- statePath,
416
- now: NOW,
417
- refresh: async (credential) => credential,
418
- fetch: async () => response({}, 503),
419
- });
420
- assert.equal(status.accounts[0].state, "cooling");
421
- assert.match(status.accounts[0].detail, /用量未知/);
422
- assert.doesNotMatch(status.summary, /1 个现在可用/);
423
- } finally {
424
- agent.dispose();
425
- }
426
- }
427
-
428
- console.log("Usage endpoint tests passed");