@narumitw/pi-usage 0.57.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.
@@ -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
@@ -254,6 +254,10 @@ export default function usageExtension(
254
254
  const expectedSessionGeneration = sessionGeneration;
255
255
  const expectedSessionId = ctx.sessionManager.getSessionId();
256
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;
257
261
  let auth: ResolvedUsageAuth | undefined;
258
262
  try {
259
263
  auth = await awaitWithDeadline(
@@ -277,11 +281,13 @@ export default function usageExtension(
277
281
  },
278
282
  };
279
283
  }
280
- const requiresRequestBoundaryGuard = adapter.id === "deepseek" || adapter.id === "xai";
284
+ const requiresRequestBoundaryGuard = ["deepseek", "fireworks", "xai"].includes(adapter.id);
281
285
  const requestContextChanged = () =>
282
286
  expectedSessionGeneration !== sessionGeneration ||
283
287
  ctx.sessionManager.getSessionId() !== expectedSessionId ||
284
- modelIdentity(ctx.model) !== expectedModelIdentity;
288
+ modelIdentity(ctx.model) !== expectedModelIdentity ||
289
+ (adapter.id === "fireworks" &&
290
+ settingsRuntime.get().settings.fireworksAccountId !== expectedFireworksAccountId);
285
291
  if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
286
292
  if (!auth) {
287
293
  if (displayState === "current") {
@@ -298,11 +304,15 @@ export default function usageExtension(
298
304
  authState: "unavailable",
299
305
  };
300
306
  }
307
+ const queryFingerprint =
308
+ adapter.id === "fireworks"
309
+ ? `${auth.fingerprint}:account:${expectedFireworksAccountId ?? "auto"}`
310
+ : auth.fingerprint;
301
311
  if (displayState === "current") {
302
- transitionCurrentIdentity(`${adapter.id}:${auth.fingerprint}`, adapter.id);
312
+ transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
303
313
  }
304
314
 
305
- const cached = !force ? cache.get(adapter.id, auth.fingerprint) : undefined;
315
+ const cached = !force ? cache.get(adapter.id, queryFingerprint) : undefined;
306
316
  if (cached) {
307
317
  return {
308
318
  state: {
@@ -316,7 +326,7 @@ export default function usageExtension(
316
326
  };
317
327
  }
318
328
 
319
- const failureKey = `${adapter.id}:${auth.fingerprint}`;
329
+ const failureKey = `${adapter.id}:${queryFingerprint}`;
320
330
  const previousFailure = failureBackoff.get(failureKey);
321
331
  if (!force && previousFailure && previousFailure.until > Date.now()) {
322
332
  return {
@@ -357,10 +367,17 @@ export default function usageExtension(
357
367
  }
358
368
  }
359
369
  : undefined;
360
- const report = await queryProviderUsage(adapter, auth, signal, remainingMs, guard);
370
+ const report = await queryProviderUsage(
371
+ adapter,
372
+ auth,
373
+ signal,
374
+ remainingMs,
375
+ guard,
376
+ querySettings,
377
+ );
361
378
  if (guard) await guard();
362
379
  if (latestQueries.get(failureKey) === queryId) {
363
- cache.set(adapter.id, auth.fingerprint, report);
380
+ cache.set(adapter.id, queryFingerprint, report);
364
381
  failureBackoff.delete(failureKey);
365
382
  }
366
383
  return {
@@ -1103,7 +1120,7 @@ export default function usageExtension(
1103
1120
  }
1104
1121
  },
1105
1122
  });
1106
- pi.on("session_start", (_event, ctx) => {
1123
+ pi.on("session_start", async (_event, ctx) => {
1107
1124
  sessionGeneration += 1;
1108
1125
  statusGeneration += 1;
1109
1126
  clearStatusTimers();
@@ -1111,7 +1128,13 @@ export default function usageExtension(
1111
1128
  activeControllers.clear();
1112
1129
  statusController = undefined;
1113
1130
  sessionActive = true;
1114
- 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
+ }
1115
1138
  });
1116
1139
  pi.on("session_tree", (_event, ctx) => {
1117
1140
  startStatusRefresh(ctx, ctx.model, false);
@@ -1137,7 +1160,10 @@ export default function usageExtension(
1137
1160
  safeSetStatus(ctx, undefined);
1138
1161
  });
1139
1162
 
1140
- fastRuntime = registerCodexFastMode(pi, settingsRuntime, (ctx) =>
1141
- startStatusRefresh(ctx, ctx.model, false),
1163
+ fastRuntime = registerCodexFastMode(
1164
+ pi,
1165
+ settingsRuntime,
1166
+ (ctx) => startStatusRefresh(ctx, ctx.model, false),
1167
+ { registerSessionStart: false },
1142
1168
  );
1143
1169
  }