@narumitw/pi-usage 0.54.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/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,10 +248,10 @@ 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);
219
257
  let auth: ResolvedUsageAuth | undefined;
@@ -221,7 +259,7 @@ export default function usageExtension(
221
259
  auth = await awaitWithDeadline(
222
260
  resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
223
261
  signal,
224
- DEFAULT_TIMEOUT_MS,
262
+ Math.max(1, deadlineAt - Date.now()),
225
263
  `resolving ${adapter.displayName} runtime auth`,
226
264
  );
227
265
  } catch (error) {
@@ -239,16 +277,12 @@ export default function usageExtension(
239
277
  },
240
278
  };
241
279
  }
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
- }
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();
252
286
  if (!auth) {
253
287
  if (displayState === "current") {
254
288
  transitionCurrentIdentity(`${adapter.id}:unavailable`, adapter.id);
@@ -301,40 +335,28 @@ export default function usageExtension(
301
335
  const queryId = querySequence;
302
336
  setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
303
337
 
338
+ let deepSeekAuthChanged = false;
304
339
  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();
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.");
335
355
  }
356
+ throw abortError();
336
357
  }
337
- : undefined;
358
+ }
359
+ : undefined;
338
360
  const report = await queryProviderUsage(adapter, auth, signal, remainingMs, guard);
339
361
  if (guard) await guard();
340
362
  if (latestQueries.get(failureKey) === queryId) {
@@ -353,6 +375,24 @@ export default function usageExtension(
353
375
  };
354
376
  } catch (error) {
355
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
+ }
356
396
  const message = errorMessage(error);
357
397
  const now = Date.now();
358
398
  for (const [key, failure] of failureBackoff) {
@@ -385,7 +425,7 @@ export default function usageExtension(
385
425
  force: boolean,
386
426
  signal: AbortSignal,
387
427
  ): Promise<QueryOutcome> => {
388
- const adapter = activeAdapterForProvider(model?.provider);
428
+ const adapter = adapterForProvider(model?.provider);
389
429
  if (!adapter) {
390
430
  const providerId = model?.provider ?? "none";
391
431
  transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
@@ -395,12 +435,9 @@ export default function usageExtension(
395
435
  providerName: providerDisplayName(ctx, providerId),
396
436
  displayState: "current",
397
437
  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.",
438
+ message: model
439
+ ? `Usage reporting is not supported for ${providerDisplayName(ctx, providerId)}.`
440
+ : "No model is selected.",
404
441
  },
405
442
  };
406
443
  }
@@ -412,7 +449,7 @@ export default function usageExtension(
412
449
  model: PiModel | undefined,
413
450
  force: boolean,
414
451
  ) => {
415
- const adapter = activeAdapterForProvider(model?.provider);
452
+ const adapter = adapterForProvider(model?.provider);
416
453
  if (!adapter || !model) {
417
454
  const providerId = model?.provider ?? "none";
418
455
  transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
@@ -425,6 +462,7 @@ export default function usageExtension(
425
462
  }
426
463
  statusGeneration += 1;
427
464
  const generation = statusGeneration;
465
+ clearStatusCountdownTimer();
428
466
  statusController?.abort();
429
467
  const controller = new AbortController();
430
468
  statusController = controller;
@@ -494,7 +532,7 @@ export default function usageExtension(
494
532
  if (generation !== statusGeneration || modelIdentity(ctx.model) !== modelIdentity(model)) {
495
533
  return false;
496
534
  }
497
- const adapter = activeAdapterForProvider(model?.provider);
535
+ const adapter = adapterForProvider(model?.provider);
498
536
  if (outcome.authState === "unavailable") {
499
537
  if (!adapter) return false;
500
538
  try {
@@ -569,7 +607,7 @@ export default function usageExtension(
569
607
  const menuGeneration = statusGeneration;
570
608
  statusController?.abort();
571
609
  statusController = undefined;
572
- clearStatusTimer();
610
+ clearStatusTimers();
573
611
  const controller = new AbortController();
574
612
  activeControllers.add(controller);
575
613
  try {
@@ -668,7 +706,7 @@ export default function usageExtension(
668
706
  providers: () => ({
669
707
  kind: "actions",
670
708
  title: "Select a configured provider",
671
- items: configuredAdapters(ctx, xaiUsageEnabled())
709
+ items: configuredAdapters(ctx)
672
710
  .filter((adapter) => adapter.id !== ctx.model?.provider)
673
711
  .map((adapter) => ({
674
712
  id: adapter.id,
@@ -742,14 +780,14 @@ export default function usageExtension(
742
780
  settingsRuntime,
743
781
  controller.signal,
744
782
  () => 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
- }
783
+ (id) => {
784
+ if (
785
+ id === "codexStatusResetCountdown" &&
786
+ stableCurrent &&
787
+ statusGeneration === menuGeneration &&
788
+ !controller.signal.aborted
789
+ ) {
790
+ publishStableCurrent(ctx, stableCurrent);
753
791
  }
754
792
  },
755
793
  );
@@ -946,7 +984,7 @@ export default function usageExtension(
946
984
  return { kind: "stay" };
947
985
  },
948
986
  another: async () => {
949
- const others = configuredAdapters(ctx, xaiUsageEnabled()).filter(
987
+ const others = configuredAdapters(ctx).filter(
950
988
  (adapter) => adapter.id !== ctx.model?.provider,
951
989
  );
952
990
  if (others.length === 0) {
@@ -956,7 +994,7 @@ export default function usageExtension(
956
994
  return { kind: "to", screen: "providers" };
957
995
  },
958
996
  provider: async ({ itemId }) => {
959
- const adapter = configuredAdapters(ctx, xaiUsageEnabled()).find(
997
+ const adapter = configuredAdapters(ctx).find(
960
998
  (candidate) => candidate.id === itemId && candidate.id !== ctx.model?.provider,
961
999
  );
962
1000
  if (!adapter) return { kind: "back" };
@@ -984,7 +1022,7 @@ export default function usageExtension(
984
1022
  return { kind: "back" };
985
1023
  },
986
1024
  all: async () => {
987
- const adapters = configuredAdapters(ctx, xaiUsageEnabled());
1025
+ const adapters = configuredAdapters(ctx);
988
1026
  const currentProviderId = ctx.model?.provider;
989
1027
  const settled = await runMenuOperation(
990
1028
  ctx,
@@ -1067,9 +1105,8 @@ export default function usageExtension(
1067
1105
  });
1068
1106
  pi.on("session_start", (_event, ctx) => {
1069
1107
  sessionGeneration += 1;
1070
- xaiSettingsGeneration += 1;
1071
1108
  statusGeneration += 1;
1072
- clearStatusTimer();
1109
+ clearStatusTimers();
1073
1110
  for (const controller of activeControllers) controller.abort();
1074
1111
  activeControllers.clear();
1075
1112
  statusController = undefined;
@@ -1088,9 +1125,8 @@ export default function usageExtension(
1088
1125
  pi.on("session_shutdown", (_event, ctx) => {
1089
1126
  sessionActive = false;
1090
1127
  sessionGeneration += 1;
1091
- xaiSettingsGeneration += 1;
1092
1128
  statusGeneration += 1;
1093
- clearStatusTimer();
1129
+ clearStatusTimers();
1094
1130
  for (const controller of activeControllers) controller.abort();
1095
1131
  activeControllers.clear();
1096
1132
  statusController = undefined;