@narumitw/pi-usage 0.54.0 → 0.58.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/src/usage.ts CHANGED
@@ -56,6 +56,7 @@ import {
56
56
  import { showUsageSettings } from "./usage-settings-ui.js";
57
57
 
58
58
  const CACHE_TTL_MS = 5 * 60 * 1000;
59
+ const STATUS_COUNTDOWN_REFRESH_MS = 60 * 1000;
59
60
  const DEFAULT_TIMEOUT_MS = 15_000;
60
61
  const ALL_PROVIDER_CONCURRENCY = 2;
61
62
  const FAILURE_BACKOFF_MS = 30_000;
@@ -103,23 +104,26 @@ export default function usageExtension(
103
104
  let sessionActive = false;
104
105
  let statusGeneration = 0;
105
106
  let sessionGeneration = 0;
106
- let xaiSettingsGeneration = 0;
107
107
  let statusRefreshTimer: ReturnType<typeof setTimeout> | undefined;
108
+ let statusCountdownTimer: ReturnType<typeof setTimeout> | undefined;
108
109
  let statusController: AbortController | undefined;
109
110
  let fastRuntime: ReturnType<typeof registerCodexFastMode>;
110
111
 
111
- const xaiUsageEnabled = () => {
112
- const state = settingsRuntime.get();
113
- return state.kind !== "invalid" && state.settings.xaiUsage;
114
- };
115
- const activeAdapterForProvider = (providerId: string | undefined) =>
116
- adapterForProvider(providerId, xaiUsageEnabled());
117
-
118
- const clearStatusTimer = () => {
112
+ const clearStatusRefreshTimer = () => {
119
113
  if (statusRefreshTimer) clearTimeout(statusRefreshTimer);
120
114
  statusRefreshTimer = undefined;
121
115
  };
122
116
 
117
+ const clearStatusCountdownTimer = () => {
118
+ if (statusCountdownTimer) clearTimeout(statusCountdownTimer);
119
+ statusCountdownTimer = undefined;
120
+ };
121
+
122
+ const clearStatusTimers = () => {
123
+ clearStatusRefreshTimer();
124
+ clearStatusCountdownTimer();
125
+ };
126
+
123
127
  const safeSetStatus = (ctx: ExtensionContext, value: string | undefined): boolean => {
124
128
  try {
125
129
  ctx.ui.setStatus(STATUS_KEY, value);
@@ -134,12 +138,12 @@ export default function usageExtension(
134
138
  statusGeneration += 1;
135
139
  statusController?.abort();
136
140
  statusController = undefined;
137
- clearStatusTimer();
141
+ clearStatusTimers();
138
142
  safeSetStatus(ctx, undefined);
139
143
  };
140
144
 
141
145
  const scheduleStatusRefresh = (ctx: ExtensionContext, model: PiModel) => {
142
- clearStatusTimer();
146
+ clearStatusRefreshTimer();
143
147
  const generation = statusGeneration;
144
148
  statusRefreshTimer = setTimeout(() => {
145
149
  statusRefreshTimer = undefined;
@@ -155,13 +159,14 @@ export default function usageExtension(
155
159
  model: PiModel,
156
160
  shouldSchedule: boolean,
157
161
  ) => {
158
- if (activeAdapterForProvider(model.provider)?.publishesStatusline === false) {
159
- clearStatusTimer();
162
+ clearStatusCountdownTimer();
163
+ if (adapterForProvider(model.provider)?.publishesStatusline === false) {
164
+ clearStatusRefreshTimer();
160
165
  safeSetStatus(ctx, undefined);
161
166
  return;
162
167
  }
163
168
  if (outcome.state.status === "unsupported") {
164
- clearStatusTimer();
169
+ clearStatusRefreshTimer();
165
170
  safeSetStatus(ctx, undefined);
166
171
  return;
167
172
  }
@@ -176,10 +181,43 @@ export default function usageExtension(
176
181
  }
177
182
  return;
178
183
  }
179
- const rawValue = formatUsageStatusline(outcome.state.report, model);
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
+ );
180
194
  const value = rawValue ? fastRuntime.decorateStatus(model, rawValue) : undefined;
181
195
  if (!safeSetStatus(ctx, value)) return;
182
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
+ }
183
221
  };
184
222
 
185
223
  const invalidateProviderState = (providerId: string) => {
@@ -210,18 +248,22 @@ export default function usageExtension(
210
248
  displayState: UsageDisplayState,
211
249
  force: boolean,
212
250
  signal: AbortSignal,
251
+ authRetry = 0,
252
+ deadlineAt = Date.now() + DEFAULT_TIMEOUT_MS,
213
253
  ): Promise<QueryOutcome> => {
214
- const startedAt = Date.now();
215
254
  const expectedSessionGeneration = sessionGeneration;
216
- const expectedXaiSettingsGeneration = xaiSettingsGeneration;
217
255
  const expectedSessionId = ctx.sessionManager.getSessionId();
218
256
  const expectedModelIdentity = modelIdentity(ctx.model);
257
+ const expectedFireworksAccountId =
258
+ adapter.id === "fireworks" ? settingsRuntime.get().settings.fireworksAccountId : undefined;
259
+ const querySettings =
260
+ adapter.id === "fireworks" ? { fireworksAccountId: expectedFireworksAccountId } : undefined;
219
261
  let auth: ResolvedUsageAuth | undefined;
220
262
  try {
221
263
  auth = await awaitWithDeadline(
222
264
  resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
223
265
  signal,
224
- DEFAULT_TIMEOUT_MS,
266
+ Math.max(1, deadlineAt - Date.now()),
225
267
  `resolving ${adapter.displayName} runtime auth`,
226
268
  );
227
269
  } catch (error) {
@@ -239,16 +281,14 @@ export default function usageExtension(
239
281
  },
240
282
  };
241
283
  }
242
- if (
243
- adapter.id === "xai" &&
244
- (expectedSessionGeneration !== sessionGeneration ||
245
- expectedXaiSettingsGeneration !== xaiSettingsGeneration ||
246
- !xaiUsageEnabled() ||
247
- ctx.sessionManager.getSessionId() !== expectedSessionId ||
248
- modelIdentity(ctx.model) !== expectedModelIdentity)
249
- ) {
250
- throw abortError();
251
- }
284
+ const requiresRequestBoundaryGuard = ["deepseek", "fireworks", "xai"].includes(adapter.id);
285
+ const requestContextChanged = () =>
286
+ expectedSessionGeneration !== sessionGeneration ||
287
+ ctx.sessionManager.getSessionId() !== expectedSessionId ||
288
+ modelIdentity(ctx.model) !== expectedModelIdentity ||
289
+ (adapter.id === "fireworks" &&
290
+ settingsRuntime.get().settings.fireworksAccountId !== expectedFireworksAccountId);
291
+ if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
252
292
  if (!auth) {
253
293
  if (displayState === "current") {
254
294
  transitionCurrentIdentity(`${adapter.id}:unavailable`, adapter.id);
@@ -264,11 +304,15 @@ export default function usageExtension(
264
304
  authState: "unavailable",
265
305
  };
266
306
  }
307
+ const queryFingerprint =
308
+ adapter.id === "fireworks"
309
+ ? `${auth.fingerprint}:account:${expectedFireworksAccountId ?? "auto"}`
310
+ : auth.fingerprint;
267
311
  if (displayState === "current") {
268
- transitionCurrentIdentity(`${adapter.id}:${auth.fingerprint}`, adapter.id);
312
+ transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
269
313
  }
270
314
 
271
- const cached = !force ? cache.get(adapter.id, auth.fingerprint) : undefined;
315
+ const cached = !force ? cache.get(adapter.id, queryFingerprint) : undefined;
272
316
  if (cached) {
273
317
  return {
274
318
  state: {
@@ -282,7 +326,7 @@ export default function usageExtension(
282
326
  };
283
327
  }
284
328
 
285
- const failureKey = `${adapter.id}:${auth.fingerprint}`;
329
+ const failureKey = `${adapter.id}:${queryFingerprint}`;
286
330
  const previousFailure = failureBackoff.get(failureKey);
287
331
  if (!force && previousFailure && previousFailure.until > Date.now()) {
288
332
  return {
@@ -301,44 +345,39 @@ export default function usageExtension(
301
345
  const queryId = querySequence;
302
346
  setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
303
347
 
348
+ let deepSeekAuthChanged = false;
304
349
  try {
305
- const remainingMs = Math.max(1, DEFAULT_TIMEOUT_MS - (Date.now() - startedAt));
306
- const guard =
307
- adapter.id === "xai"
308
- ? async () => {
309
- if (
310
- signal.aborted ||
311
- expectedSessionGeneration !== sessionGeneration ||
312
- expectedXaiSettingsGeneration !== xaiSettingsGeneration ||
313
- !xaiUsageEnabled() ||
314
- ctx.sessionManager.getSessionId() !== expectedSessionId ||
315
- modelIdentity(ctx.model) !== expectedModelIdentity
316
- ) {
317
- throw abortError();
318
- }
319
- const revalidated = await awaitWithDeadline(
320
- resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
321
- signal,
322
- Math.max(1, DEFAULT_TIMEOUT_MS - (Date.now() - startedAt)),
323
- "revalidating xAI runtime auth",
324
- );
325
- if (
326
- signal.aborted ||
327
- expectedSessionGeneration !== sessionGeneration ||
328
- expectedXaiSettingsGeneration !== xaiSettingsGeneration ||
329
- !xaiUsageEnabled() ||
330
- ctx.sessionManager.getSessionId() !== expectedSessionId ||
331
- modelIdentity(ctx.model) !== expectedModelIdentity ||
332
- revalidated?.fingerprint !== auth.fingerprint
333
- ) {
334
- throw abortError();
350
+ const remainingMs = Math.max(1, deadlineAt - Date.now());
351
+ const guard = requiresRequestBoundaryGuard
352
+ ? async () => {
353
+ if (signal.aborted || requestContextChanged()) throw abortError();
354
+ const revalidated = await awaitWithDeadline(
355
+ resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
356
+ signal,
357
+ Math.max(1, deadlineAt - Date.now()),
358
+ `revalidating ${adapter.displayName} runtime auth`,
359
+ );
360
+ if (signal.aborted || requestContextChanged()) throw abortError();
361
+ if (revalidated?.fingerprint !== auth.fingerprint) {
362
+ if (adapter.id === "deepseek") {
363
+ deepSeekAuthChanged = true;
364
+ throw new Error("DeepSeek runtime credential changed during the balance query.");
335
365
  }
366
+ throw abortError();
336
367
  }
337
- : undefined;
338
- const report = await queryProviderUsage(adapter, auth, signal, remainingMs, guard);
368
+ }
369
+ : undefined;
370
+ const report = await queryProviderUsage(
371
+ adapter,
372
+ auth,
373
+ signal,
374
+ remainingMs,
375
+ guard,
376
+ querySettings,
377
+ );
339
378
  if (guard) await guard();
340
379
  if (latestQueries.get(failureKey) === queryId) {
341
- cache.set(adapter.id, auth.fingerprint, report);
380
+ cache.set(adapter.id, queryFingerprint, report);
342
381
  failureBackoff.delete(failureKey);
343
382
  }
344
383
  return {
@@ -353,6 +392,24 @@ export default function usageExtension(
353
392
  };
354
393
  } catch (error) {
355
394
  if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
395
+ if (
396
+ deepSeekAuthChanged &&
397
+ authRetry === 0 &&
398
+ !signal.aborted &&
399
+ !requestContextChanged() &&
400
+ Date.now() < deadlineAt
401
+ ) {
402
+ if (latestQueries.get(failureKey) === queryId) latestQueries.delete(failureKey);
403
+ return queryAdapterState(
404
+ ctx,
405
+ adapter,
406
+ displayState,
407
+ true,
408
+ signal,
409
+ authRetry + 1,
410
+ deadlineAt,
411
+ );
412
+ }
356
413
  const message = errorMessage(error);
357
414
  const now = Date.now();
358
415
  for (const [key, failure] of failureBackoff) {
@@ -385,7 +442,7 @@ export default function usageExtension(
385
442
  force: boolean,
386
443
  signal: AbortSignal,
387
444
  ): Promise<QueryOutcome> => {
388
- const adapter = activeAdapterForProvider(model?.provider);
445
+ const adapter = adapterForProvider(model?.provider);
389
446
  if (!adapter) {
390
447
  const providerId = model?.provider ?? "none";
391
448
  transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
@@ -395,12 +452,9 @@ export default function usageExtension(
395
452
  providerName: providerDisplayName(ctx, providerId),
396
453
  displayState: "current",
397
454
  status: "unsupported",
398
- message:
399
- providerId === "xai" && !xaiUsageEnabled()
400
- ? "xAI usage is disabled. Open Settings to enable it."
401
- : model
402
- ? `Usage reporting is not supported for ${providerDisplayName(ctx, providerId)}.`
403
- : "No model is selected.",
455
+ message: model
456
+ ? `Usage reporting is not supported for ${providerDisplayName(ctx, providerId)}.`
457
+ : "No model is selected.",
404
458
  },
405
459
  };
406
460
  }
@@ -412,7 +466,7 @@ export default function usageExtension(
412
466
  model: PiModel | undefined,
413
467
  force: boolean,
414
468
  ) => {
415
- const adapter = activeAdapterForProvider(model?.provider);
469
+ const adapter = adapterForProvider(model?.provider);
416
470
  if (!adapter || !model) {
417
471
  const providerId = model?.provider ?? "none";
418
472
  transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
@@ -425,6 +479,7 @@ export default function usageExtension(
425
479
  }
426
480
  statusGeneration += 1;
427
481
  const generation = statusGeneration;
482
+ clearStatusCountdownTimer();
428
483
  statusController?.abort();
429
484
  const controller = new AbortController();
430
485
  statusController = controller;
@@ -494,7 +549,7 @@ export default function usageExtension(
494
549
  if (generation !== statusGeneration || modelIdentity(ctx.model) !== modelIdentity(model)) {
495
550
  return false;
496
551
  }
497
- const adapter = activeAdapterForProvider(model?.provider);
552
+ const adapter = adapterForProvider(model?.provider);
498
553
  if (outcome.authState === "unavailable") {
499
554
  if (!adapter) return false;
500
555
  try {
@@ -569,7 +624,7 @@ export default function usageExtension(
569
624
  const menuGeneration = statusGeneration;
570
625
  statusController?.abort();
571
626
  statusController = undefined;
572
- clearStatusTimer();
627
+ clearStatusTimers();
573
628
  const controller = new AbortController();
574
629
  activeControllers.add(controller);
575
630
  try {
@@ -668,7 +723,7 @@ export default function usageExtension(
668
723
  providers: () => ({
669
724
  kind: "actions",
670
725
  title: "Select a configured provider",
671
- items: configuredAdapters(ctx, xaiUsageEnabled())
726
+ items: configuredAdapters(ctx)
672
727
  .filter((adapter) => adapter.id !== ctx.model?.provider)
673
728
  .map((adapter) => ({
674
729
  id: adapter.id,
@@ -742,14 +797,14 @@ export default function usageExtension(
742
797
  settingsRuntime,
743
798
  controller.signal,
744
799
  () => statusGeneration === menuGeneration && !controller.signal.aborted,
745
- (id, _previous, next) => {
746
- if (id !== "xaiUsage") return;
747
- xaiSettingsGeneration += 1;
748
- invalidateProviderState("xai");
749
- if (!next) {
750
- for (const active of activeControllers) {
751
- if (active !== controller) active.abort();
752
- }
800
+ (id) => {
801
+ if (
802
+ id === "codexStatusResetCountdown" &&
803
+ stableCurrent &&
804
+ statusGeneration === menuGeneration &&
805
+ !controller.signal.aborted
806
+ ) {
807
+ publishStableCurrent(ctx, stableCurrent);
753
808
  }
754
809
  },
755
810
  );
@@ -946,7 +1001,7 @@ export default function usageExtension(
946
1001
  return { kind: "stay" };
947
1002
  },
948
1003
  another: async () => {
949
- const others = configuredAdapters(ctx, xaiUsageEnabled()).filter(
1004
+ const others = configuredAdapters(ctx).filter(
950
1005
  (adapter) => adapter.id !== ctx.model?.provider,
951
1006
  );
952
1007
  if (others.length === 0) {
@@ -956,7 +1011,7 @@ export default function usageExtension(
956
1011
  return { kind: "to", screen: "providers" };
957
1012
  },
958
1013
  provider: async ({ itemId }) => {
959
- const adapter = configuredAdapters(ctx, xaiUsageEnabled()).find(
1014
+ const adapter = configuredAdapters(ctx).find(
960
1015
  (candidate) => candidate.id === itemId && candidate.id !== ctx.model?.provider,
961
1016
  );
962
1017
  if (!adapter) return { kind: "back" };
@@ -984,7 +1039,7 @@ export default function usageExtension(
984
1039
  return { kind: "back" };
985
1040
  },
986
1041
  all: async () => {
987
- const adapters = configuredAdapters(ctx, xaiUsageEnabled());
1042
+ const adapters = configuredAdapters(ctx);
988
1043
  const currentProviderId = ctx.model?.provider;
989
1044
  const settled = await runMenuOperation(
990
1045
  ctx,
@@ -1065,16 +1120,21 @@ export default function usageExtension(
1065
1120
  }
1066
1121
  },
1067
1122
  });
1068
- pi.on("session_start", (_event, ctx) => {
1123
+ pi.on("session_start", async (_event, ctx) => {
1069
1124
  sessionGeneration += 1;
1070
- xaiSettingsGeneration += 1;
1071
1125
  statusGeneration += 1;
1072
- clearStatusTimer();
1126
+ clearStatusTimers();
1073
1127
  for (const controller of activeControllers) controller.abort();
1074
1128
  activeControllers.clear();
1075
1129
  statusController = undefined;
1076
1130
  sessionActive = true;
1077
- startStatusRefresh(ctx, ctx.model, false);
1131
+ const ownerGeneration = sessionGeneration;
1132
+ try {
1133
+ await fastRuntime.prepareSession(ctx);
1134
+ } catch (error) {
1135
+ if (isStaleExtensionContextError(error) || ownerGeneration !== sessionGeneration) return;
1136
+ throw error;
1137
+ }
1078
1138
  });
1079
1139
  pi.on("session_tree", (_event, ctx) => {
1080
1140
  startStatusRefresh(ctx, ctx.model, false);
@@ -1088,9 +1148,8 @@ export default function usageExtension(
1088
1148
  pi.on("session_shutdown", (_event, ctx) => {
1089
1149
  sessionActive = false;
1090
1150
  sessionGeneration += 1;
1091
- xaiSettingsGeneration += 1;
1092
1151
  statusGeneration += 1;
1093
- clearStatusTimer();
1152
+ clearStatusTimers();
1094
1153
  for (const controller of activeControllers) controller.abort();
1095
1154
  activeControllers.clear();
1096
1155
  statusController = undefined;
@@ -1101,7 +1160,10 @@ export default function usageExtension(
1101
1160
  safeSetStatus(ctx, undefined);
1102
1161
  });
1103
1162
 
1104
- fastRuntime = registerCodexFastMode(pi, settingsRuntime, (ctx) =>
1105
- startStatusRefresh(ctx, ctx.model, false),
1163
+ fastRuntime = registerCodexFastMode(
1164
+ pi,
1165
+ settingsRuntime,
1166
+ (ctx) => startStatusRefresh(ctx, ctx.model, false),
1167
+ { registerSessionStart: false },
1106
1168
  );
1107
1169
  }