@narumitw/pi-usage 0.53.0 → 0.57.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.
- package/README.md +171 -45
- package/dist/index.ts +1082 -67
- package/dist/index.ts.map +4 -4
- package/package.json +16 -11
- package/src/format.ts +184 -7
- package/src/index.ts +9 -0
- package/src/providers/deepseek.ts +85 -0
- package/src/providers/kimi-coding.ts +276 -0
- package/src/providers/xai.ts +186 -0
- package/src/query.ts +250 -9
- package/src/settings.ts +12 -0
- package/src/types.ts +28 -2
- package/src/usage-helpers.ts +2 -2
- package/src/usage-settings-ui.ts +127 -0
- package/src/usage.ts +156 -14
package/src/usage.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// This orchestrator intentionally remains over 1,000 lines because its menu, query generations,
|
|
2
|
+
// account cache, cancellation, and session lifecycle share one consistency boundary.
|
|
1
3
|
import { randomUUID } from "node:crypto";
|
|
2
4
|
import type {
|
|
3
5
|
ExtensionAPI,
|
|
@@ -20,7 +22,13 @@ import {
|
|
|
20
22
|
resetOptionExpiration,
|
|
21
23
|
resolveCodexResetAuth,
|
|
22
24
|
} from "./codex-resets.js";
|
|
23
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
abortError,
|
|
27
|
+
awaitWithDeadline,
|
|
28
|
+
errorMessage,
|
|
29
|
+
runWithConcurrency,
|
|
30
|
+
UsageCache,
|
|
31
|
+
} from "./core.js";
|
|
24
32
|
import { formatProviderStates, formatUsageStatusline } from "./format.js";
|
|
25
33
|
import { createOAuthCredentialCandidateReader } from "./oauth-credential-source.js";
|
|
26
34
|
import {
|
|
@@ -45,8 +53,10 @@ import {
|
|
|
45
53
|
providerDisplayName,
|
|
46
54
|
setBoundedMap,
|
|
47
55
|
} from "./usage-helpers.js";
|
|
56
|
+
import { showUsageSettings } from "./usage-settings-ui.js";
|
|
48
57
|
|
|
49
58
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
59
|
+
const STATUS_COUNTDOWN_REFRESH_MS = 60 * 1000;
|
|
50
60
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
51
61
|
const ALL_PROVIDER_CONCURRENCY = 2;
|
|
52
62
|
const FAILURE_BACKOFF_MS = 30_000;
|
|
@@ -57,6 +67,7 @@ const REFRESH_CURRENT = "Refresh current usage";
|
|
|
57
67
|
const VIEW_ANOTHER = "View another configured provider…";
|
|
58
68
|
const VIEW_ALL = "View all configured providers…";
|
|
59
69
|
const CLOSE = "Close";
|
|
70
|
+
const SETTINGS = "Settings";
|
|
60
71
|
const REDEEM_CODEX_RESET = "Redeem usage limit reset…";
|
|
61
72
|
|
|
62
73
|
type UsageExtensionDependencies = {
|
|
@@ -92,15 +103,27 @@ export default function usageExtension(
|
|
|
92
103
|
let activeCurrentIdentity: string | undefined;
|
|
93
104
|
let sessionActive = false;
|
|
94
105
|
let statusGeneration = 0;
|
|
106
|
+
let sessionGeneration = 0;
|
|
95
107
|
let statusRefreshTimer: ReturnType<typeof setTimeout> | undefined;
|
|
108
|
+
let statusCountdownTimer: ReturnType<typeof setTimeout> | undefined;
|
|
96
109
|
let statusController: AbortController | undefined;
|
|
97
110
|
let fastRuntime: ReturnType<typeof registerCodexFastMode>;
|
|
98
111
|
|
|
99
|
-
const
|
|
112
|
+
const clearStatusRefreshTimer = () => {
|
|
100
113
|
if (statusRefreshTimer) clearTimeout(statusRefreshTimer);
|
|
101
114
|
statusRefreshTimer = undefined;
|
|
102
115
|
};
|
|
103
116
|
|
|
117
|
+
const clearStatusCountdownTimer = () => {
|
|
118
|
+
if (statusCountdownTimer) clearTimeout(statusCountdownTimer);
|
|
119
|
+
statusCountdownTimer = undefined;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const clearStatusTimers = () => {
|
|
123
|
+
clearStatusRefreshTimer();
|
|
124
|
+
clearStatusCountdownTimer();
|
|
125
|
+
};
|
|
126
|
+
|
|
104
127
|
const safeSetStatus = (ctx: ExtensionContext, value: string | undefined): boolean => {
|
|
105
128
|
try {
|
|
106
129
|
ctx.ui.setStatus(STATUS_KEY, value);
|
|
@@ -115,12 +138,12 @@ export default function usageExtension(
|
|
|
115
138
|
statusGeneration += 1;
|
|
116
139
|
statusController?.abort();
|
|
117
140
|
statusController = undefined;
|
|
118
|
-
|
|
141
|
+
clearStatusTimers();
|
|
119
142
|
safeSetStatus(ctx, undefined);
|
|
120
143
|
};
|
|
121
144
|
|
|
122
145
|
const scheduleStatusRefresh = (ctx: ExtensionContext, model: PiModel) => {
|
|
123
|
-
|
|
146
|
+
clearStatusRefreshTimer();
|
|
124
147
|
const generation = statusGeneration;
|
|
125
148
|
statusRefreshTimer = setTimeout(() => {
|
|
126
149
|
statusRefreshTimer = undefined;
|
|
@@ -136,13 +159,14 @@ export default function usageExtension(
|
|
|
136
159
|
model: PiModel,
|
|
137
160
|
shouldSchedule: boolean,
|
|
138
161
|
) => {
|
|
162
|
+
clearStatusCountdownTimer();
|
|
139
163
|
if (adapterForProvider(model.provider)?.publishesStatusline === false) {
|
|
140
|
-
|
|
164
|
+
clearStatusRefreshTimer();
|
|
141
165
|
safeSetStatus(ctx, undefined);
|
|
142
166
|
return;
|
|
143
167
|
}
|
|
144
168
|
if (outcome.state.status === "unsupported") {
|
|
145
|
-
|
|
169
|
+
clearStatusRefreshTimer();
|
|
146
170
|
safeSetStatus(ctx, undefined);
|
|
147
171
|
return;
|
|
148
172
|
}
|
|
@@ -157,10 +181,43 @@ export default function usageExtension(
|
|
|
157
181
|
}
|
|
158
182
|
return;
|
|
159
183
|
}
|
|
160
|
-
const
|
|
184
|
+
const showCodexResetCountdown =
|
|
185
|
+
outcome.state.report.providerId === "openai-codex" &&
|
|
186
|
+
settingsRuntime.get().settings.codexStatusResetCountdown;
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
const rawValue = formatUsageStatusline(
|
|
189
|
+
outcome.state.report,
|
|
190
|
+
model,
|
|
191
|
+
now,
|
|
192
|
+
showCodexResetCountdown,
|
|
193
|
+
);
|
|
161
194
|
const value = rawValue ? fastRuntime.decorateStatus(model, rawValue) : undefined;
|
|
162
195
|
if (!safeSetStatus(ctx, value)) return;
|
|
163
196
|
if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
|
|
197
|
+
if (
|
|
198
|
+
sessionActive &&
|
|
199
|
+
showCodexResetCountdown &&
|
|
200
|
+
outcome.state.report.buckets.some(
|
|
201
|
+
(bucket) =>
|
|
202
|
+
bucket.resetsAt !== undefined &&
|
|
203
|
+
Number.isFinite(bucket.resetsAt) &&
|
|
204
|
+
bucket.resetsAt * 1_000 > now,
|
|
205
|
+
)
|
|
206
|
+
) {
|
|
207
|
+
const generation = statusGeneration;
|
|
208
|
+
statusCountdownTimer = setTimeout(() => {
|
|
209
|
+
statusCountdownTimer = undefined;
|
|
210
|
+
if (
|
|
211
|
+
!sessionActive ||
|
|
212
|
+
generation !== statusGeneration ||
|
|
213
|
+
modelIdentity(ctx.model) !== modelIdentity(model)
|
|
214
|
+
) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
publishStatus(ctx, outcome, model, false);
|
|
218
|
+
}, STATUS_COUNTDOWN_REFRESH_MS);
|
|
219
|
+
statusCountdownTimer.unref?.();
|
|
220
|
+
}
|
|
164
221
|
};
|
|
165
222
|
|
|
166
223
|
const invalidateProviderState = (providerId: string) => {
|
|
@@ -191,14 +248,18 @@ export default function usageExtension(
|
|
|
191
248
|
displayState: UsageDisplayState,
|
|
192
249
|
force: boolean,
|
|
193
250
|
signal: AbortSignal,
|
|
251
|
+
authRetry = 0,
|
|
252
|
+
deadlineAt = Date.now() + DEFAULT_TIMEOUT_MS,
|
|
194
253
|
): Promise<QueryOutcome> => {
|
|
195
|
-
const
|
|
254
|
+
const expectedSessionGeneration = sessionGeneration;
|
|
255
|
+
const expectedSessionId = ctx.sessionManager.getSessionId();
|
|
256
|
+
const expectedModelIdentity = modelIdentity(ctx.model);
|
|
196
257
|
let auth: ResolvedUsageAuth | undefined;
|
|
197
258
|
try {
|
|
198
259
|
auth = await awaitWithDeadline(
|
|
199
260
|
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
200
261
|
signal,
|
|
201
|
-
|
|
262
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
202
263
|
`resolving ${adapter.displayName} runtime auth`,
|
|
203
264
|
);
|
|
204
265
|
} catch (error) {
|
|
@@ -216,6 +277,12 @@ export default function usageExtension(
|
|
|
216
277
|
},
|
|
217
278
|
};
|
|
218
279
|
}
|
|
280
|
+
const requiresRequestBoundaryGuard = adapter.id === "deepseek" || adapter.id === "xai";
|
|
281
|
+
const requestContextChanged = () =>
|
|
282
|
+
expectedSessionGeneration !== sessionGeneration ||
|
|
283
|
+
ctx.sessionManager.getSessionId() !== expectedSessionId ||
|
|
284
|
+
modelIdentity(ctx.model) !== expectedModelIdentity;
|
|
285
|
+
if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
|
|
219
286
|
if (!auth) {
|
|
220
287
|
if (displayState === "current") {
|
|
221
288
|
transitionCurrentIdentity(`${adapter.id}:unavailable`, adapter.id);
|
|
@@ -268,9 +335,30 @@ export default function usageExtension(
|
|
|
268
335
|
const queryId = querySequence;
|
|
269
336
|
setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
|
|
270
337
|
|
|
338
|
+
let deepSeekAuthChanged = false;
|
|
271
339
|
try {
|
|
272
|
-
const remainingMs = Math.max(1,
|
|
273
|
-
const
|
|
340
|
+
const remainingMs = Math.max(1, deadlineAt - Date.now());
|
|
341
|
+
const guard = requiresRequestBoundaryGuard
|
|
342
|
+
? async () => {
|
|
343
|
+
if (signal.aborted || requestContextChanged()) throw abortError();
|
|
344
|
+
const revalidated = await awaitWithDeadline(
|
|
345
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
346
|
+
signal,
|
|
347
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
348
|
+
`revalidating ${adapter.displayName} runtime auth`,
|
|
349
|
+
);
|
|
350
|
+
if (signal.aborted || requestContextChanged()) throw abortError();
|
|
351
|
+
if (revalidated?.fingerprint !== auth.fingerprint) {
|
|
352
|
+
if (adapter.id === "deepseek") {
|
|
353
|
+
deepSeekAuthChanged = true;
|
|
354
|
+
throw new Error("DeepSeek runtime credential changed during the balance query.");
|
|
355
|
+
}
|
|
356
|
+
throw abortError();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
: undefined;
|
|
360
|
+
const report = await queryProviderUsage(adapter, auth, signal, remainingMs, guard);
|
|
361
|
+
if (guard) await guard();
|
|
274
362
|
if (latestQueries.get(failureKey) === queryId) {
|
|
275
363
|
cache.set(adapter.id, auth.fingerprint, report);
|
|
276
364
|
failureBackoff.delete(failureKey);
|
|
@@ -287,6 +375,24 @@ export default function usageExtension(
|
|
|
287
375
|
};
|
|
288
376
|
} catch (error) {
|
|
289
377
|
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
378
|
+
if (
|
|
379
|
+
deepSeekAuthChanged &&
|
|
380
|
+
authRetry === 0 &&
|
|
381
|
+
!signal.aborted &&
|
|
382
|
+
!requestContextChanged() &&
|
|
383
|
+
Date.now() < deadlineAt
|
|
384
|
+
) {
|
|
385
|
+
if (latestQueries.get(failureKey) === queryId) latestQueries.delete(failureKey);
|
|
386
|
+
return queryAdapterState(
|
|
387
|
+
ctx,
|
|
388
|
+
adapter,
|
|
389
|
+
displayState,
|
|
390
|
+
true,
|
|
391
|
+
signal,
|
|
392
|
+
authRetry + 1,
|
|
393
|
+
deadlineAt,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
290
396
|
const message = errorMessage(error);
|
|
291
397
|
const now = Date.now();
|
|
292
398
|
for (const [key, failure] of failureBackoff) {
|
|
@@ -356,6 +462,7 @@ export default function usageExtension(
|
|
|
356
462
|
}
|
|
357
463
|
statusGeneration += 1;
|
|
358
464
|
const generation = statusGeneration;
|
|
465
|
+
clearStatusCountdownTimer();
|
|
359
466
|
statusController?.abort();
|
|
360
467
|
const controller = new AbortController();
|
|
361
468
|
statusController = controller;
|
|
@@ -500,7 +607,7 @@ export default function usageExtension(
|
|
|
500
607
|
const menuGeneration = statusGeneration;
|
|
501
608
|
statusController?.abort();
|
|
502
609
|
statusController = undefined;
|
|
503
|
-
|
|
610
|
+
clearStatusTimers();
|
|
504
611
|
const controller = new AbortController();
|
|
505
612
|
activeControllers.add(controller);
|
|
506
613
|
try {
|
|
@@ -533,6 +640,7 @@ export default function usageExtension(
|
|
|
533
640
|
| "reset-error";
|
|
534
641
|
type Action =
|
|
535
642
|
| "refresh"
|
|
643
|
+
| "settings"
|
|
536
644
|
| "toggle-fast"
|
|
537
645
|
| "another"
|
|
538
646
|
| "all"
|
|
@@ -560,6 +668,7 @@ export default function usageExtension(
|
|
|
560
668
|
lines: [...formatProviderStates(visibleStates).split("\n"), ...fastLines],
|
|
561
669
|
items: [
|
|
562
670
|
{ id: "refresh", label: REFRESH_CURRENT, action: "refresh" },
|
|
671
|
+
{ id: "settings", label: SETTINGS, action: "settings" },
|
|
563
672
|
...(fastAvailability.kind === "available"
|
|
564
673
|
? [
|
|
565
674
|
{
|
|
@@ -665,6 +774,37 @@ export default function usageExtension(
|
|
|
665
774
|
}),
|
|
666
775
|
},
|
|
667
776
|
actions: {
|
|
777
|
+
settings: async () => {
|
|
778
|
+
await showUsageSettings(
|
|
779
|
+
ctx,
|
|
780
|
+
settingsRuntime,
|
|
781
|
+
controller.signal,
|
|
782
|
+
() => statusGeneration === menuGeneration && !controller.signal.aborted,
|
|
783
|
+
(id) => {
|
|
784
|
+
if (
|
|
785
|
+
id === "codexStatusResetCountdown" &&
|
|
786
|
+
stableCurrent &&
|
|
787
|
+
statusGeneration === menuGeneration &&
|
|
788
|
+
!controller.signal.aborted
|
|
789
|
+
) {
|
|
790
|
+
publishStableCurrent(ctx, stableCurrent);
|
|
791
|
+
}
|
|
792
|
+
},
|
|
793
|
+
);
|
|
794
|
+
fastState = settingsRuntime.get();
|
|
795
|
+
const revalidated = await queryStableCurrent(
|
|
796
|
+
ctx,
|
|
797
|
+
false,
|
|
798
|
+
controller,
|
|
799
|
+
"Applying usage settings…",
|
|
800
|
+
);
|
|
801
|
+
if (!revalidated) return { kind: "stay" };
|
|
802
|
+
stableCurrent = revalidated;
|
|
803
|
+
current = revalidated.outcome;
|
|
804
|
+
visibleStates = [current.state];
|
|
805
|
+
publishStableCurrent(ctx, revalidated);
|
|
806
|
+
return { kind: "stay" };
|
|
807
|
+
},
|
|
668
808
|
"toggle-fast": async () => {
|
|
669
809
|
const availability = fastRuntime.availability(ctx.model);
|
|
670
810
|
if (availability.kind !== "available" || fastState.kind === "invalid") {
|
|
@@ -964,8 +1104,9 @@ export default function usageExtension(
|
|
|
964
1104
|
},
|
|
965
1105
|
});
|
|
966
1106
|
pi.on("session_start", (_event, ctx) => {
|
|
1107
|
+
sessionGeneration += 1;
|
|
967
1108
|
statusGeneration += 1;
|
|
968
|
-
|
|
1109
|
+
clearStatusTimers();
|
|
969
1110
|
for (const controller of activeControllers) controller.abort();
|
|
970
1111
|
activeControllers.clear();
|
|
971
1112
|
statusController = undefined;
|
|
@@ -983,8 +1124,9 @@ export default function usageExtension(
|
|
|
983
1124
|
});
|
|
984
1125
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
985
1126
|
sessionActive = false;
|
|
1127
|
+
sessionGeneration += 1;
|
|
986
1128
|
statusGeneration += 1;
|
|
987
|
-
|
|
1129
|
+
clearStatusTimers();
|
|
988
1130
|
for (const controller of activeControllers) controller.abort();
|
|
989
1131
|
activeControllers.clear();
|
|
990
1132
|
statusController = undefined;
|