@narumitw/pi-usage 0.57.0 → 0.59.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.
@@ -11,12 +11,16 @@ import {
11
11
  Text,
12
12
  } from "@earendil-works/pi-tui";
13
13
  import { errorMessage } from "./core.js";
14
+ import { isFireworksAccountId } from "./providers/fireworks.js";
14
15
  import type { UsageSettings, UsageSettingsRuntime } from "./settings.js";
15
16
 
17
+ const AUTO = "Auto";
18
+ const EDIT = "Edit…";
16
19
  const OFF = "Off";
17
20
  const ON = "On";
18
21
 
19
22
  type UsageSettingId = keyof UsageSettings;
23
+ type SettingsScreenResult = { changed: boolean; editFireworksAccount: boolean };
20
24
 
21
25
  export async function showUsageSettings(
22
26
  ctx: ExtensionCommandContext,
@@ -29,15 +33,38 @@ export async function showUsageSettings(
29
33
  if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${settingsRuntime.get().path}`, "info");
30
34
  return false;
31
35
  }
32
- if (parentSignal.aborted || !isCurrent()) return false;
36
+ let changed = false;
37
+ while (!parentSignal.aborted && isCurrent()) {
38
+ const result = await showSettingsList(ctx, settingsRuntime, parentSignal, isCurrent, onApplied);
39
+ if (!result) return changed;
40
+ changed ||= result.changed;
41
+ if (!result.editFireworksAccount) return changed;
42
+ changed ||= await editFireworksAccount(
43
+ ctx,
44
+ settingsRuntime,
45
+ parentSignal,
46
+ isCurrent,
47
+ onApplied,
48
+ );
49
+ }
50
+ return changed;
51
+ }
33
52
 
34
- return ctx.ui.custom<boolean>((tui, theme, _keybindings, done) => {
53
+ async function showSettingsList(
54
+ ctx: ExtensionCommandContext,
55
+ settingsRuntime: UsageSettingsRuntime,
56
+ parentSignal: AbortSignal,
57
+ isCurrent: () => boolean,
58
+ onApplied: (id: UsageSettingId) => void,
59
+ ): Promise<SettingsScreenResult | undefined> {
60
+ return ctx.ui.custom<SettingsScreenResult>((tui, theme, _keybindings, done) => {
35
61
  const localController = new AbortController();
36
62
  const signal = AbortSignal.any([parentSignal, localController.signal]);
37
63
  let changed = false;
38
64
  let closing = false;
39
65
  let saveQueue = Promise.resolve();
40
66
  const state = settingsRuntime.get();
67
+ const fireworksValue = state.settings.fireworksAccountId ?? AUTO;
41
68
  const items: SettingItem[] = [
42
69
  {
43
70
  id: "codexFastMode",
@@ -53,6 +80,15 @@ export async function showUsageSettings(
53
80
  currentValue: state.settings.codexStatusResetCountdown ? ON : OFF,
54
81
  values: [OFF, ON],
55
82
  },
83
+ {
84
+ id: "fireworksAccountId",
85
+ label: "Fireworks account",
86
+ description: "Select Edit to enter a visible account slug, or submit blank to clear it.",
87
+ currentValue: fireworksValue,
88
+ values: state.settings.fireworksAccountId
89
+ ? [state.settings.fireworksAccountId, EDIT]
90
+ : [AUTO, EDIT],
91
+ },
56
92
  ];
57
93
  const container = new Container();
58
94
  container.addChild(new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 1));
@@ -62,7 +98,40 @@ export async function showUsageSettings(
62
98
  if (closing) return;
63
99
  closing = true;
64
100
  localController.abort();
65
- done(changed);
101
+ done({ changed, editFireworksAccount: false });
102
+ };
103
+ const queueUpdate = (
104
+ id: UsageSettingId,
105
+ requested: UsageSettings[UsageSettingId],
106
+ display: string,
107
+ ) => {
108
+ saveQueue = saveQueue.then(async () => {
109
+ const previous = settingsRuntime.get().settings[id];
110
+ if (settingsRuntime.get().kind === "invalid") {
111
+ settingsList.updateValue(id, displaySetting(id, previous));
112
+ if (!signal.aborted && isCurrent()) {
113
+ ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
114
+ tui.requestRender();
115
+ }
116
+ return;
117
+ }
118
+ try {
119
+ await settingsRuntime.update({ [id]: requested }, signal);
120
+ } catch (error) {
121
+ if (signal.aborted || !isCurrent()) return;
122
+ settingsList.updateValue(id, displaySetting(id, previous));
123
+ ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
124
+ tui.requestRender();
125
+ return;
126
+ }
127
+ if (previous !== requested) {
128
+ changed = true;
129
+ onApplied(id);
130
+ }
131
+ if (signal.aborted || !isCurrent()) return;
132
+ settingsList.updateValue(id, display);
133
+ tui.requestRender();
134
+ });
66
135
  };
67
136
  settingsList = new SettingsList(
68
137
  items,
@@ -70,35 +139,18 @@ export async function showUsageSettings(
70
139
  getSettingsListTheme(),
71
140
  (id, value) => {
72
141
  if (closing || signal.aborted || !isCurrent()) return;
73
- const settingId = id as UsageSettingId;
74
- const requested = value !== OFF;
75
- saveQueue = saveQueue.then(async () => {
76
- const previous = settingsRuntime.get().settings[settingId];
77
- if (settingsRuntime.get().kind === "invalid") {
78
- settingsList.updateValue(id, displayValue(previous));
79
- if (!signal.aborted && isCurrent()) {
80
- ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
81
- tui.requestRender();
82
- }
83
- return;
142
+ if (id === "fireworksAccountId") {
143
+ if (value === EDIT) {
144
+ saveQueue = saveQueue.then(() => {
145
+ if (closing || signal.aborted || !isCurrent()) return;
146
+ closing = true;
147
+ done({ changed, editFireworksAccount: true });
148
+ });
84
149
  }
85
- try {
86
- await settingsRuntime.update({ [settingId]: requested }, signal);
87
- } catch (error) {
88
- if (signal.aborted || !isCurrent()) return;
89
- settingsList.updateValue(id, displayValue(previous));
90
- ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
91
- tui.requestRender();
92
- return;
93
- }
94
- if (previous !== requested) {
95
- changed = true;
96
- onApplied(settingId);
97
- }
98
- if (signal.aborted || !isCurrent()) return;
99
- settingsList.updateValue(id, displayValue(requested));
100
- tui.requestRender();
101
- });
150
+ return;
151
+ }
152
+ const settingId = id as "codexFastMode" | "codexStatusResetCountdown";
153
+ queueUpdate(settingId, value !== OFF, value);
102
154
  },
103
155
  cancel,
104
156
  );
@@ -122,6 +174,46 @@ export async function showUsageSettings(
122
174
  });
123
175
  }
124
176
 
125
- function displayValue(enabled: boolean): string {
126
- return enabled ? ON : OFF;
177
+ async function editFireworksAccount(
178
+ ctx: ExtensionCommandContext,
179
+ settingsRuntime: UsageSettingsRuntime,
180
+ signal: AbortSignal,
181
+ isCurrent: () => boolean,
182
+ onApplied: (id: UsageSettingId) => void,
183
+ ): Promise<boolean> {
184
+ while (!signal.aborted && isCurrent()) {
185
+ const state = settingsRuntime.get();
186
+ if (state.kind === "invalid") {
187
+ ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
188
+ return false;
189
+ }
190
+ const entered = await ctx.ui.input(
191
+ "Fireworks account slug · submit blank for Auto",
192
+ state.settings.fireworksAccountId ?? "Example: acme",
193
+ { signal },
194
+ );
195
+ if (signal.aborted || !isCurrent() || entered === undefined) return false;
196
+ const normalized = entered.trim();
197
+ const requested = normalized || undefined;
198
+ if (requested !== undefined && !isFireworksAccountId(requested)) {
199
+ ctx.ui.notify("Enter a URL-safe Fireworks account slug.", "warning");
200
+ continue;
201
+ }
202
+ if (requested === state.settings.fireworksAccountId) return false;
203
+ try {
204
+ await settingsRuntime.update({ fireworksAccountId: requested }, signal);
205
+ } catch (error) {
206
+ if (signal.aborted || !isCurrent()) return false;
207
+ ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
208
+ return false;
209
+ }
210
+ onApplied("fireworksAccountId");
211
+ return true;
212
+ }
213
+ return false;
214
+ }
215
+
216
+ function displaySetting(id: UsageSettingId, value: UsageSettings[UsageSettingId]): string {
217
+ if (id === "fireworksAccountId") return typeof value === "string" ? value : AUTO;
218
+ return value ? ON : OFF;
127
219
  }
package/src/usage.ts CHANGED
@@ -207,13 +207,7 @@ export default function usageExtension(
207
207
  const generation = statusGeneration;
208
208
  statusCountdownTimer = setTimeout(() => {
209
209
  statusCountdownTimer = undefined;
210
- if (
211
- !sessionActive ||
212
- generation !== statusGeneration ||
213
- modelIdentity(ctx.model) !== modelIdentity(model)
214
- ) {
215
- return;
216
- }
210
+ if (!sessionActive || generation !== statusGeneration) return;
217
211
  publishStatus(ctx, outcome, model, false);
218
212
  }, STATUS_COUNTDOWN_REFRESH_MS);
219
213
  statusCountdownTimer.unref?.();
@@ -254,6 +248,10 @@ export default function usageExtension(
254
248
  const expectedSessionGeneration = sessionGeneration;
255
249
  const expectedSessionId = ctx.sessionManager.getSessionId();
256
250
  const expectedModelIdentity = modelIdentity(ctx.model);
251
+ const expectedFireworksAccountId =
252
+ adapter.id === "fireworks" ? settingsRuntime.get().settings.fireworksAccountId : undefined;
253
+ const querySettings =
254
+ adapter.id === "fireworks" ? { fireworksAccountId: expectedFireworksAccountId } : undefined;
257
255
  let auth: ResolvedUsageAuth | undefined;
258
256
  try {
259
257
  auth = await awaitWithDeadline(
@@ -277,11 +275,23 @@ export default function usageExtension(
277
275
  },
278
276
  };
279
277
  }
280
- const requiresRequestBoundaryGuard = adapter.id === "deepseek" || adapter.id === "xai";
278
+ const requiresRequestBoundaryGuard = [
279
+ "baseten",
280
+ "deepseek",
281
+ "fireworks",
282
+ "minimax",
283
+ "minimax-cn",
284
+ "moonshotai",
285
+ "moonshotai-cn",
286
+ "vercel-ai-gateway",
287
+ "xai",
288
+ ].includes(adapter.id);
281
289
  const requestContextChanged = () =>
282
290
  expectedSessionGeneration !== sessionGeneration ||
283
291
  ctx.sessionManager.getSessionId() !== expectedSessionId ||
284
- modelIdentity(ctx.model) !== expectedModelIdentity;
292
+ modelIdentity(ctx.model) !== expectedModelIdentity ||
293
+ (adapter.id === "fireworks" &&
294
+ settingsRuntime.get().settings.fireworksAccountId !== expectedFireworksAccountId);
285
295
  if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
286
296
  if (!auth) {
287
297
  if (displayState === "current") {
@@ -298,11 +308,15 @@ export default function usageExtension(
298
308
  authState: "unavailable",
299
309
  };
300
310
  }
311
+ const queryFingerprint =
312
+ adapter.id === "fireworks"
313
+ ? `${auth.fingerprint}:account:${expectedFireworksAccountId ?? "auto"}`
314
+ : auth.fingerprint;
301
315
  if (displayState === "current") {
302
- transitionCurrentIdentity(`${adapter.id}:${auth.fingerprint}`, adapter.id);
316
+ transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
303
317
  }
304
318
 
305
- const cached = !force ? cache.get(adapter.id, auth.fingerprint) : undefined;
319
+ const cached = !force ? cache.get(adapter.id, queryFingerprint) : undefined;
306
320
  if (cached) {
307
321
  return {
308
322
  state: {
@@ -316,7 +330,7 @@ export default function usageExtension(
316
330
  };
317
331
  }
318
332
 
319
- const failureKey = `${adapter.id}:${auth.fingerprint}`;
333
+ const failureKey = `${adapter.id}:${queryFingerprint}`;
320
334
  const previousFailure = failureBackoff.get(failureKey);
321
335
  if (!force && previousFailure && previousFailure.until > Date.now()) {
322
336
  return {
@@ -335,7 +349,7 @@ export default function usageExtension(
335
349
  const queryId = querySequence;
336
350
  setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
337
351
 
338
- let deepSeekAuthChanged = false;
352
+ let retryableAuthChanged = false;
339
353
  try {
340
354
  const remainingMs = Math.max(1, deadlineAt - Date.now());
341
355
  const guard = requiresRequestBoundaryGuard
@@ -349,18 +363,27 @@ export default function usageExtension(
349
363
  );
350
364
  if (signal.aborted || requestContextChanged()) throw abortError();
351
365
  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.");
366
+ if (["deepseek", "minimax", "minimax-cn"].includes(adapter.id)) {
367
+ retryableAuthChanged = true;
368
+ throw new Error(
369
+ `${adapter.displayName} runtime credential changed during the usage query.`,
370
+ );
355
371
  }
356
372
  throw abortError();
357
373
  }
358
374
  }
359
375
  : undefined;
360
- const report = await queryProviderUsage(adapter, auth, signal, remainingMs, guard);
376
+ const report = await queryProviderUsage(
377
+ adapter,
378
+ auth,
379
+ signal,
380
+ remainingMs,
381
+ guard,
382
+ querySettings,
383
+ );
361
384
  if (guard) await guard();
362
385
  if (latestQueries.get(failureKey) === queryId) {
363
- cache.set(adapter.id, auth.fingerprint, report);
386
+ cache.set(adapter.id, queryFingerprint, report);
364
387
  failureBackoff.delete(failureKey);
365
388
  }
366
389
  return {
@@ -376,7 +399,7 @@ export default function usageExtension(
376
399
  } catch (error) {
377
400
  if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
378
401
  if (
379
- deepSeekAuthChanged &&
402
+ retryableAuthChanged &&
380
403
  authRetry === 0 &&
381
404
  !signal.aborted &&
382
405
  !requestContextChanged() &&
@@ -1103,7 +1126,7 @@ export default function usageExtension(
1103
1126
  }
1104
1127
  },
1105
1128
  });
1106
- pi.on("session_start", (_event, ctx) => {
1129
+ pi.on("session_start", async (_event, ctx) => {
1107
1130
  sessionGeneration += 1;
1108
1131
  statusGeneration += 1;
1109
1132
  clearStatusTimers();
@@ -1111,7 +1134,13 @@ export default function usageExtension(
1111
1134
  activeControllers.clear();
1112
1135
  statusController = undefined;
1113
1136
  sessionActive = true;
1114
- startStatusRefresh(ctx, ctx.model, false);
1137
+ const ownerGeneration = sessionGeneration;
1138
+ try {
1139
+ await fastRuntime.prepareSession(ctx);
1140
+ } catch (error) {
1141
+ if (isStaleExtensionContextError(error) || ownerGeneration !== sessionGeneration) return;
1142
+ throw error;
1143
+ }
1115
1144
  });
1116
1145
  pi.on("session_tree", (_event, ctx) => {
1117
1146
  startStatusRefresh(ctx, ctx.model, false);
@@ -1137,7 +1166,10 @@ export default function usageExtension(
1137
1166
  safeSetStatus(ctx, undefined);
1138
1167
  });
1139
1168
 
1140
- fastRuntime = registerCodexFastMode(pi, settingsRuntime, (ctx) =>
1141
- startStatusRefresh(ctx, ctx.model, false),
1169
+ fastRuntime = registerCodexFastMode(
1170
+ pi,
1171
+ settingsRuntime,
1172
+ (ctx) => startStatusRefresh(ctx, ctx.model, false),
1173
+ { registerSessionStart: false },
1142
1174
  );
1143
1175
  }