@narumitw/pi-usage 0.53.0 → 0.54.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
@@ -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 { awaitWithDeadline, errorMessage, runWithConcurrency, UsageCache } from "./core.js";
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,6 +53,7 @@ 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;
50
59
  const DEFAULT_TIMEOUT_MS = 15_000;
@@ -57,6 +66,7 @@ const REFRESH_CURRENT = "Refresh current usage";
57
66
  const VIEW_ANOTHER = "View another configured provider…";
58
67
  const VIEW_ALL = "View all configured providers…";
59
68
  const CLOSE = "Close";
69
+ const SETTINGS = "Settings";
60
70
  const REDEEM_CODEX_RESET = "Redeem usage limit reset…";
61
71
 
62
72
  type UsageExtensionDependencies = {
@@ -92,10 +102,19 @@ export default function usageExtension(
92
102
  let activeCurrentIdentity: string | undefined;
93
103
  let sessionActive = false;
94
104
  let statusGeneration = 0;
105
+ let sessionGeneration = 0;
106
+ let xaiSettingsGeneration = 0;
95
107
  let statusRefreshTimer: ReturnType<typeof setTimeout> | undefined;
96
108
  let statusController: AbortController | undefined;
97
109
  let fastRuntime: ReturnType<typeof registerCodexFastMode>;
98
110
 
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
+
99
118
  const clearStatusTimer = () => {
100
119
  if (statusRefreshTimer) clearTimeout(statusRefreshTimer);
101
120
  statusRefreshTimer = undefined;
@@ -136,7 +155,7 @@ export default function usageExtension(
136
155
  model: PiModel,
137
156
  shouldSchedule: boolean,
138
157
  ) => {
139
- if (adapterForProvider(model.provider)?.publishesStatusline === false) {
158
+ if (activeAdapterForProvider(model.provider)?.publishesStatusline === false) {
140
159
  clearStatusTimer();
141
160
  safeSetStatus(ctx, undefined);
142
161
  return;
@@ -193,6 +212,10 @@ export default function usageExtension(
193
212
  signal: AbortSignal,
194
213
  ): Promise<QueryOutcome> => {
195
214
  const startedAt = Date.now();
215
+ const expectedSessionGeneration = sessionGeneration;
216
+ const expectedXaiSettingsGeneration = xaiSettingsGeneration;
217
+ const expectedSessionId = ctx.sessionManager.getSessionId();
218
+ const expectedModelIdentity = modelIdentity(ctx.model);
196
219
  let auth: ResolvedUsageAuth | undefined;
197
220
  try {
198
221
  auth = await awaitWithDeadline(
@@ -216,6 +239,16 @@ export default function usageExtension(
216
239
  },
217
240
  };
218
241
  }
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
+ }
219
252
  if (!auth) {
220
253
  if (displayState === "current") {
221
254
  transitionCurrentIdentity(`${adapter.id}:unavailable`, adapter.id);
@@ -270,7 +303,40 @@ export default function usageExtension(
270
303
 
271
304
  try {
272
305
  const remainingMs = Math.max(1, DEFAULT_TIMEOUT_MS - (Date.now() - startedAt));
273
- const report = await queryProviderUsage(adapter, auth, signal, remainingMs);
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();
335
+ }
336
+ }
337
+ : undefined;
338
+ const report = await queryProviderUsage(adapter, auth, signal, remainingMs, guard);
339
+ if (guard) await guard();
274
340
  if (latestQueries.get(failureKey) === queryId) {
275
341
  cache.set(adapter.id, auth.fingerprint, report);
276
342
  failureBackoff.delete(failureKey);
@@ -319,7 +385,7 @@ export default function usageExtension(
319
385
  force: boolean,
320
386
  signal: AbortSignal,
321
387
  ): Promise<QueryOutcome> => {
322
- const adapter = adapterForProvider(model?.provider);
388
+ const adapter = activeAdapterForProvider(model?.provider);
323
389
  if (!adapter) {
324
390
  const providerId = model?.provider ?? "none";
325
391
  transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
@@ -329,9 +395,12 @@ export default function usageExtension(
329
395
  providerName: providerDisplayName(ctx, providerId),
330
396
  displayState: "current",
331
397
  status: "unsupported",
332
- message: model
333
- ? `Usage reporting is not supported for ${providerDisplayName(ctx, providerId)}.`
334
- : "No model is selected.",
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.",
335
404
  },
336
405
  };
337
406
  }
@@ -343,7 +412,7 @@ export default function usageExtension(
343
412
  model: PiModel | undefined,
344
413
  force: boolean,
345
414
  ) => {
346
- const adapter = adapterForProvider(model?.provider);
415
+ const adapter = activeAdapterForProvider(model?.provider);
347
416
  if (!adapter || !model) {
348
417
  const providerId = model?.provider ?? "none";
349
418
  transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
@@ -425,7 +494,7 @@ export default function usageExtension(
425
494
  if (generation !== statusGeneration || modelIdentity(ctx.model) !== modelIdentity(model)) {
426
495
  return false;
427
496
  }
428
- const adapter = adapterForProvider(model?.provider);
497
+ const adapter = activeAdapterForProvider(model?.provider);
429
498
  if (outcome.authState === "unavailable") {
430
499
  if (!adapter) return false;
431
500
  try {
@@ -533,6 +602,7 @@ export default function usageExtension(
533
602
  | "reset-error";
534
603
  type Action =
535
604
  | "refresh"
605
+ | "settings"
536
606
  | "toggle-fast"
537
607
  | "another"
538
608
  | "all"
@@ -560,6 +630,7 @@ export default function usageExtension(
560
630
  lines: [...formatProviderStates(visibleStates).split("\n"), ...fastLines],
561
631
  items: [
562
632
  { id: "refresh", label: REFRESH_CURRENT, action: "refresh" },
633
+ { id: "settings", label: SETTINGS, action: "settings" },
563
634
  ...(fastAvailability.kind === "available"
564
635
  ? [
565
636
  {
@@ -597,7 +668,7 @@ export default function usageExtension(
597
668
  providers: () => ({
598
669
  kind: "actions",
599
670
  title: "Select a configured provider",
600
- items: configuredAdapters(ctx)
671
+ items: configuredAdapters(ctx, xaiUsageEnabled())
601
672
  .filter((adapter) => adapter.id !== ctx.model?.provider)
602
673
  .map((adapter) => ({
603
674
  id: adapter.id,
@@ -665,6 +736,37 @@ export default function usageExtension(
665
736
  }),
666
737
  },
667
738
  actions: {
739
+ settings: async () => {
740
+ await showUsageSettings(
741
+ ctx,
742
+ settingsRuntime,
743
+ controller.signal,
744
+ () => 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
+ }
753
+ }
754
+ },
755
+ );
756
+ fastState = settingsRuntime.get();
757
+ const revalidated = await queryStableCurrent(
758
+ ctx,
759
+ false,
760
+ controller,
761
+ "Applying usage settings…",
762
+ );
763
+ if (!revalidated) return { kind: "stay" };
764
+ stableCurrent = revalidated;
765
+ current = revalidated.outcome;
766
+ visibleStates = [current.state];
767
+ publishStableCurrent(ctx, revalidated);
768
+ return { kind: "stay" };
769
+ },
668
770
  "toggle-fast": async () => {
669
771
  const availability = fastRuntime.availability(ctx.model);
670
772
  if (availability.kind !== "available" || fastState.kind === "invalid") {
@@ -844,7 +946,7 @@ export default function usageExtension(
844
946
  return { kind: "stay" };
845
947
  },
846
948
  another: async () => {
847
- const others = configuredAdapters(ctx).filter(
949
+ const others = configuredAdapters(ctx, xaiUsageEnabled()).filter(
848
950
  (adapter) => adapter.id !== ctx.model?.provider,
849
951
  );
850
952
  if (others.length === 0) {
@@ -854,7 +956,7 @@ export default function usageExtension(
854
956
  return { kind: "to", screen: "providers" };
855
957
  },
856
958
  provider: async ({ itemId }) => {
857
- const adapter = configuredAdapters(ctx).find(
959
+ const adapter = configuredAdapters(ctx, xaiUsageEnabled()).find(
858
960
  (candidate) => candidate.id === itemId && candidate.id !== ctx.model?.provider,
859
961
  );
860
962
  if (!adapter) return { kind: "back" };
@@ -882,7 +984,7 @@ export default function usageExtension(
882
984
  return { kind: "back" };
883
985
  },
884
986
  all: async () => {
885
- const adapters = configuredAdapters(ctx);
987
+ const adapters = configuredAdapters(ctx, xaiUsageEnabled());
886
988
  const currentProviderId = ctx.model?.provider;
887
989
  const settled = await runMenuOperation(
888
990
  ctx,
@@ -964,6 +1066,8 @@ export default function usageExtension(
964
1066
  },
965
1067
  });
966
1068
  pi.on("session_start", (_event, ctx) => {
1069
+ sessionGeneration += 1;
1070
+ xaiSettingsGeneration += 1;
967
1071
  statusGeneration += 1;
968
1072
  clearStatusTimer();
969
1073
  for (const controller of activeControllers) controller.abort();
@@ -983,6 +1087,8 @@ export default function usageExtension(
983
1087
  });
984
1088
  pi.on("session_shutdown", (_event, ctx) => {
985
1089
  sessionActive = false;
1090
+ sessionGeneration += 1;
1091
+ xaiSettingsGeneration += 1;
986
1092
  statusGeneration += 1;
987
1093
  clearStatusTimer();
988
1094
  for (const controller of activeControllers) controller.abort();