@diousk/pi-usage-fast 0.1.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/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/index.ts +5478 -0
- package/dist/index.ts.map +7 -0
- package/docs/operations.md +31 -0
- package/docs/providers.md +273 -0
- package/package.json +82 -0
- package/src/codex-fast-runtime.ts +184 -0
- package/src/codex-fast.ts +118 -0
- package/src/codex-resets.ts +347 -0
- package/src/core.ts +245 -0
- package/src/format.ts +678 -0
- package/src/index.ts +116 -0
- package/src/oauth-credential-source.ts +117 -0
- package/src/providers/baseten.ts +51 -0
- package/src/providers/codex.ts +139 -0
- package/src/providers/deepseek.ts +80 -0
- package/src/providers/fireworks.ts +291 -0
- package/src/providers/github-copilot.ts +114 -0
- package/src/providers/kimi-coding.ts +265 -0
- package/src/providers/minimax.ts +280 -0
- package/src/providers/moonshot.ts +59 -0
- package/src/providers/opencode-zen.ts +80 -0
- package/src/providers/openrouter.ts +70 -0
- package/src/providers/vercel-ai-gateway.ts +43 -0
- package/src/providers/xai.ts +179 -0
- package/src/providers/zai-errors.ts +94 -0
- package/src/providers/zai.ts +239 -0
- package/src/query.ts +1073 -0
- package/src/settings.ts +366 -0
- package/src/types.ts +235 -0
- package/src/usage-helpers.ts +40 -0
- package/src/usage-settings-ui.ts +152 -0
- package/src/usage-targets.ts +127 -0
- package/src/usage.ts +1335 -0
package/src/usage.ts
ADDED
|
@@ -0,0 +1,1335 @@
|
|
|
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.
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { FAST_USAGE_WARNING, registerCodexFastMode } from "./codex-fast-runtime.js";
|
|
6
|
+
import {
|
|
7
|
+
type CodexResetAvailability,
|
|
8
|
+
type CodexResetOption,
|
|
9
|
+
type CodexResetOutcome,
|
|
10
|
+
codexResetActionDescription,
|
|
11
|
+
codexResetCount,
|
|
12
|
+
consumeCodexResetCredit,
|
|
13
|
+
formatCodexResetOutcome,
|
|
14
|
+
genericCodexResetOption,
|
|
15
|
+
listCodexResetCredits,
|
|
16
|
+
resetConfirmationLines,
|
|
17
|
+
resetLabel,
|
|
18
|
+
resetOptionExpiration,
|
|
19
|
+
resolveCodexResetAuth,
|
|
20
|
+
} from "./codex-resets.js";
|
|
21
|
+
import { abortError, awaitWithDeadline, errorMessage, runWithConcurrency, UsageCache } from "./core.js";
|
|
22
|
+
import { formatProviderStates, formatUsageStatusline } from "./format.js";
|
|
23
|
+
import { createOAuthCredentialCandidateReader } from "./oauth-credential-source.js";
|
|
24
|
+
import { adapterForProvider, isStaleExtensionContextError, queryProviderUsage, resolveUsageAuth } from "./query.js";
|
|
25
|
+
import { createUsageSettingsRuntime, type UsageSettingsRuntime, type UsageSettingsState } from "./settings.js";
|
|
26
|
+
import type {
|
|
27
|
+
PiModel,
|
|
28
|
+
ProviderUsageState,
|
|
29
|
+
ResolvedUsageAuth,
|
|
30
|
+
UsageDisplayState,
|
|
31
|
+
UsageProviderAdapter,
|
|
32
|
+
} from "./types.js";
|
|
33
|
+
import {
|
|
34
|
+
configuredAdapters,
|
|
35
|
+
isAbortError,
|
|
36
|
+
isTimeoutError,
|
|
37
|
+
modelIdentity,
|
|
38
|
+
providerDisplayName,
|
|
39
|
+
setBoundedMap,
|
|
40
|
+
} from "./usage-helpers.js";
|
|
41
|
+
import { showUsageSettings } from "./usage-settings-ui.js";
|
|
42
|
+
import { createUsageTargetSelectOptions, listUsageTargets, resolveUsageTarget } from "./usage-targets.js";
|
|
43
|
+
|
|
44
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
45
|
+
const STATUS_COUNTDOWN_REFRESH_MS = 60 * 1000;
|
|
46
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
47
|
+
const ALL_PROVIDER_CONCURRENCY = 2;
|
|
48
|
+
const FAILURE_BACKOFF_MS = 30_000;
|
|
49
|
+
const MAX_ACCOUNT_STATES = 32;
|
|
50
|
+
const STATUS_KEY = "usage";
|
|
51
|
+
|
|
52
|
+
const REFRESH_CURRENT = "Refresh current usage";
|
|
53
|
+
const VIEW_ANOTHER = "View another configured provider…";
|
|
54
|
+
const VIEW_ALL = "View all configured providers…";
|
|
55
|
+
const CLOSE = "Close";
|
|
56
|
+
const SETTINGS = "Settings";
|
|
57
|
+
const REDEEM_CODEX_RESET = "Redeem usage limit reset…";
|
|
58
|
+
|
|
59
|
+
type UsageExtensionDependencies = {
|
|
60
|
+
credentialReader?: (providerId: string) => unknown;
|
|
61
|
+
createRedemptionId?: () => string;
|
|
62
|
+
settingsRuntime?: UsageSettingsRuntime;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type QueryOutcome = {
|
|
66
|
+
state: ProviderUsageState;
|
|
67
|
+
fingerprint?: string;
|
|
68
|
+
authState?: "unavailable";
|
|
69
|
+
rememberedTargetId?: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
class UsageTargetSelectionChangedError extends Error {
|
|
73
|
+
override readonly name = "UsageTargetSelectionChangedError";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type StableCurrent = {
|
|
77
|
+
outcome: QueryOutcome;
|
|
78
|
+
model: PiModel | undefined;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExtensionDependencies = {}) {
|
|
82
|
+
const credentialReader = dependencies.credentialReader;
|
|
83
|
+
const credentialCandidates = createOAuthCredentialCandidateReader(pi, credentialReader);
|
|
84
|
+
const createRedemptionId = dependencies.createRedemptionId ?? randomUUID;
|
|
85
|
+
const settingsRuntime = dependencies.settingsRuntime ?? createUsageSettingsRuntime();
|
|
86
|
+
const cache = new UsageCache(CACHE_TTL_MS);
|
|
87
|
+
const failureBackoff = new Map<string, { until: number; message: string }>();
|
|
88
|
+
const latestQueries = new Map<string, number>();
|
|
89
|
+
const activeControllers = new Set<AbortController>();
|
|
90
|
+
let querySequence = 0;
|
|
91
|
+
let activeCurrentIdentity: string | undefined;
|
|
92
|
+
let sessionActive = false;
|
|
93
|
+
let statusGeneration = 0;
|
|
94
|
+
let sessionGeneration = 0;
|
|
95
|
+
let statusRefreshTimer: ReturnType<typeof setTimeout> | undefined;
|
|
96
|
+
let statusCountdownTimer: ReturnType<typeof setTimeout> | undefined;
|
|
97
|
+
let statusController: AbortController | undefined;
|
|
98
|
+
let fastRuntime: ReturnType<typeof registerCodexFastMode>;
|
|
99
|
+
|
|
100
|
+
const clearStatusRefreshTimer = () => {
|
|
101
|
+
if (statusRefreshTimer) clearTimeout(statusRefreshTimer);
|
|
102
|
+
statusRefreshTimer = undefined;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const clearStatusCountdownTimer = () => {
|
|
106
|
+
if (statusCountdownTimer) clearTimeout(statusCountdownTimer);
|
|
107
|
+
statusCountdownTimer = undefined;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const clearStatusTimers = () => {
|
|
111
|
+
clearStatusRefreshTimer();
|
|
112
|
+
clearStatusCountdownTimer();
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const safeSetStatus = (ctx: ExtensionContext, value: string | undefined): boolean => {
|
|
116
|
+
try {
|
|
117
|
+
ctx.ui.setStatus(STATUS_KEY, value);
|
|
118
|
+
return true;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (isStaleExtensionContextError(error)) return false;
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const clearStatus = (ctx: ExtensionContext) => {
|
|
126
|
+
statusGeneration += 1;
|
|
127
|
+
statusController?.abort();
|
|
128
|
+
statusController = undefined;
|
|
129
|
+
clearStatusTimers();
|
|
130
|
+
safeSetStatus(ctx, undefined);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const scheduleStatusRefresh = (ctx: ExtensionContext, model: PiModel) => {
|
|
134
|
+
clearStatusRefreshTimer();
|
|
135
|
+
const generation = statusGeneration;
|
|
136
|
+
statusRefreshTimer = setTimeout(() => {
|
|
137
|
+
statusRefreshTimer = undefined;
|
|
138
|
+
if (!sessionActive || generation !== statusGeneration) return;
|
|
139
|
+
startStatusRefresh(ctx, model, true);
|
|
140
|
+
}, CACHE_TTL_MS);
|
|
141
|
+
statusRefreshTimer.unref?.();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const publishStatus = (ctx: ExtensionContext, outcome: QueryOutcome, model: PiModel, shouldSchedule: boolean) => {
|
|
145
|
+
clearStatusCountdownTimer();
|
|
146
|
+
if (adapterForProvider(model.provider)?.publishesStatusline === false) {
|
|
147
|
+
clearStatusRefreshTimer();
|
|
148
|
+
safeSetStatus(ctx, undefined);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (outcome.state.status === "unsupported") {
|
|
152
|
+
clearStatusRefreshTimer();
|
|
153
|
+
safeSetStatus(ctx, undefined);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (outcome.state.status !== "ready") {
|
|
157
|
+
const chip =
|
|
158
|
+
outcome.state.status === "auth-unavailable"
|
|
159
|
+
? "auth unavailable"
|
|
160
|
+
: outcome.state.status === "selection-required"
|
|
161
|
+
? "selection required"
|
|
162
|
+
: `usage err: ${outcome.state.message.slice(0, 50)}`;
|
|
163
|
+
|
|
164
|
+
if (safeSetStatus(ctx, chip)) {
|
|
165
|
+
if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const settings = settingsRuntime.get().settings;
|
|
170
|
+
const showCodexResetCountdown =
|
|
171
|
+
outcome.state.report.providerId === "openai-codex" && settings.codexStatusResetCountdown;
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const rawValue = formatUsageStatusline(
|
|
174
|
+
outcome.state.report,
|
|
175
|
+
model,
|
|
176
|
+
now,
|
|
177
|
+
showCodexResetCountdown,
|
|
178
|
+
settings.codexStatusPercentage,
|
|
179
|
+
);
|
|
180
|
+
const value = rawValue ? fastRuntime.decorateStatus(model, rawValue) : undefined;
|
|
181
|
+
if (!safeSetStatus(ctx, value)) return;
|
|
182
|
+
if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
|
|
183
|
+
if (
|
|
184
|
+
sessionActive &&
|
|
185
|
+
showCodexResetCountdown &&
|
|
186
|
+
outcome.state.report.buckets.some(
|
|
187
|
+
(bucket) => bucket.resetsAt !== undefined && Number.isFinite(bucket.resetsAt) && bucket.resetsAt * 1_000 > now,
|
|
188
|
+
)
|
|
189
|
+
) {
|
|
190
|
+
const generation = statusGeneration;
|
|
191
|
+
statusCountdownTimer = setTimeout(() => {
|
|
192
|
+
statusCountdownTimer = undefined;
|
|
193
|
+
if (!sessionActive || generation !== statusGeneration) return;
|
|
194
|
+
publishStatus(ctx, outcome, model, false);
|
|
195
|
+
}, STATUS_COUNTDOWN_REFRESH_MS);
|
|
196
|
+
statusCountdownTimer.unref?.();
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const invalidateProviderState = (providerId: string) => {
|
|
201
|
+
cache.clearProvider(providerId);
|
|
202
|
+
for (const key of failureBackoff.keys()) {
|
|
203
|
+
if (key.startsWith(`${providerId}:`)) failureBackoff.delete(key);
|
|
204
|
+
}
|
|
205
|
+
for (const key of latestQueries.keys()) {
|
|
206
|
+
if (key.startsWith(`${providerId}:`)) latestQueries.delete(key);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const transitionCurrentIdentity = (nextIdentity: string, providerId: string) => {
|
|
211
|
+
if (!activeCurrentIdentity || activeCurrentIdentity === nextIdentity) {
|
|
212
|
+
activeCurrentIdentity = nextIdentity;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const previousProviderId = activeCurrentIdentity.split(":", 1)[0] ?? "";
|
|
216
|
+
for (const id of new Set([previousProviderId, providerId])) {
|
|
217
|
+
if (id) invalidateProviderState(id);
|
|
218
|
+
}
|
|
219
|
+
activeCurrentIdentity = nextIdentity;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const queryAdapterState = async (
|
|
223
|
+
ctx: ExtensionContext,
|
|
224
|
+
adapter: UsageProviderAdapter,
|
|
225
|
+
displayState: UsageDisplayState,
|
|
226
|
+
force: boolean,
|
|
227
|
+
signal: AbortSignal,
|
|
228
|
+
authRetry = 0,
|
|
229
|
+
deadlineAt = Date.now() + DEFAULT_TIMEOUT_MS,
|
|
230
|
+
): Promise<QueryOutcome> => {
|
|
231
|
+
const expectedSessionGeneration = sessionGeneration;
|
|
232
|
+
const expectedSessionId = ctx.sessionManager.getSessionId();
|
|
233
|
+
const expectedModelIdentity = modelIdentity(ctx.model);
|
|
234
|
+
const expectedTargetId = adapter.targets ? settingsRuntime.get().settings.selectedTargets[adapter.id] : undefined;
|
|
235
|
+
const providerName = providerDisplayName(ctx, adapter.id);
|
|
236
|
+
let auth: ResolvedUsageAuth | undefined;
|
|
237
|
+
try {
|
|
238
|
+
auth = await awaitWithDeadline(
|
|
239
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
240
|
+
signal,
|
|
241
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
242
|
+
`resolving ${adapter.displayName} runtime auth`,
|
|
243
|
+
);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
246
|
+
if (displayState === "current") {
|
|
247
|
+
transitionCurrentIdentity(`${adapter.id}:auth-error`, adapter.id);
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
state: {
|
|
251
|
+
providerId: adapter.id,
|
|
252
|
+
providerName,
|
|
253
|
+
displayState,
|
|
254
|
+
status: isTimeoutError(error) ? "query-failed" : "auth-unavailable",
|
|
255
|
+
message: errorMessage(error),
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const requiresRequestBoundaryGuard =
|
|
260
|
+
adapter.targets !== undefined ||
|
|
261
|
+
[
|
|
262
|
+
"baseten",
|
|
263
|
+
"deepseek",
|
|
264
|
+
"minimax",
|
|
265
|
+
"minimax-cn",
|
|
266
|
+
"moonshotai",
|
|
267
|
+
"moonshotai-cn",
|
|
268
|
+
"vercel-ai-gateway",
|
|
269
|
+
"xai",
|
|
270
|
+
"zai",
|
|
271
|
+
"zai-coding-cn",
|
|
272
|
+
].includes(adapter.id);
|
|
273
|
+
const requestContextChanged = () =>
|
|
274
|
+
expectedSessionGeneration !== sessionGeneration ||
|
|
275
|
+
ctx.sessionManager.getSessionId() !== expectedSessionId ||
|
|
276
|
+
modelIdentity(ctx.model) !== expectedModelIdentity ||
|
|
277
|
+
(adapter.targets !== undefined &&
|
|
278
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId);
|
|
279
|
+
if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
|
|
280
|
+
if (!auth) {
|
|
281
|
+
if (displayState === "current") {
|
|
282
|
+
transitionCurrentIdentity(`${adapter.id}:unavailable`, adapter.id);
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
state: {
|
|
286
|
+
providerId: adapter.id,
|
|
287
|
+
providerName,
|
|
288
|
+
displayState,
|
|
289
|
+
status: "auth-unavailable",
|
|
290
|
+
message: `No runtime credential is configured for ${providerName}.`,
|
|
291
|
+
},
|
|
292
|
+
authState: "unavailable",
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
let retryableAuthChanged = false;
|
|
296
|
+
const guard = async () => {
|
|
297
|
+
if (signal.aborted || requestContextChanged()) throw abortError();
|
|
298
|
+
if (!requiresRequestBoundaryGuard) return;
|
|
299
|
+
const revalidated = await awaitWithDeadline(
|
|
300
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
301
|
+
signal,
|
|
302
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
303
|
+
`revalidating ${providerName} runtime auth`,
|
|
304
|
+
);
|
|
305
|
+
if (signal.aborted || requestContextChanged()) throw abortError();
|
|
306
|
+
if (revalidated?.fingerprint !== auth.fingerprint) {
|
|
307
|
+
if (["deepseek", "minimax", "minimax-cn"].includes(adapter.id)) {
|
|
308
|
+
retryableAuthChanged = true;
|
|
309
|
+
throw new Error(`${providerName} runtime credential changed during the usage query.`);
|
|
310
|
+
}
|
|
311
|
+
throw abortError();
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
let queryFingerprint = adapter.targets
|
|
315
|
+
? `${auth.fingerprint}:target:${expectedTargetId ?? "unresolved"}`
|
|
316
|
+
: auth.fingerprint;
|
|
317
|
+
let failureKey = `${adapter.id}:${queryFingerprint}`;
|
|
318
|
+
let queryId: number | undefined;
|
|
319
|
+
try {
|
|
320
|
+
const previousDiscoveryFailure = failureBackoff.get(failureKey);
|
|
321
|
+
if (!force && previousDiscoveryFailure && previousDiscoveryFailure.until > Date.now()) {
|
|
322
|
+
return {
|
|
323
|
+
state: {
|
|
324
|
+
providerId: adapter.id,
|
|
325
|
+
providerName,
|
|
326
|
+
displayState,
|
|
327
|
+
status: "query-failed",
|
|
328
|
+
message: previousDiscoveryFailure.message,
|
|
329
|
+
},
|
|
330
|
+
fingerprint: auth.fingerprint,
|
|
331
|
+
rememberedTargetId: expectedTargetId,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const target = await resolveUsageTarget(
|
|
335
|
+
adapter,
|
|
336
|
+
auth,
|
|
337
|
+
expectedTargetId,
|
|
338
|
+
signal,
|
|
339
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
340
|
+
guard,
|
|
341
|
+
);
|
|
342
|
+
if (target.kind === "selection-required") {
|
|
343
|
+
if (displayState === "current") {
|
|
344
|
+
transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
state: {
|
|
348
|
+
providerId: adapter.id,
|
|
349
|
+
providerName,
|
|
350
|
+
displayState,
|
|
351
|
+
status: "selection-required",
|
|
352
|
+
singularLabel: adapter.targets?.singularLabel ?? "target",
|
|
353
|
+
pluralLabel: adapter.targets?.pluralLabel ?? "targets",
|
|
354
|
+
choices: target.choices,
|
|
355
|
+
},
|
|
356
|
+
fingerprint: auth.fingerprint,
|
|
357
|
+
rememberedTargetId: expectedTargetId,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
queryFingerprint = adapter.targets ? `${auth.fingerprint}:target:${target.targetId ?? "none"}` : auth.fingerprint;
|
|
361
|
+
failureKey = `${adapter.id}:${queryFingerprint}`;
|
|
362
|
+
if (displayState === "current") {
|
|
363
|
+
transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
|
|
364
|
+
}
|
|
365
|
+
const cached = !force ? cache.get(adapter.id, queryFingerprint) : undefined;
|
|
366
|
+
if (cached) {
|
|
367
|
+
return {
|
|
368
|
+
state: {
|
|
369
|
+
providerId: adapter.id,
|
|
370
|
+
providerName,
|
|
371
|
+
displayState,
|
|
372
|
+
status: "ready",
|
|
373
|
+
report: cached,
|
|
374
|
+
},
|
|
375
|
+
fingerprint: auth.fingerprint,
|
|
376
|
+
rememberedTargetId: expectedTargetId,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const previousFailure = failureBackoff.get(failureKey);
|
|
380
|
+
if (!force && previousFailure && previousFailure.until > Date.now()) {
|
|
381
|
+
return {
|
|
382
|
+
state: {
|
|
383
|
+
providerId: adapter.id,
|
|
384
|
+
providerName,
|
|
385
|
+
displayState,
|
|
386
|
+
status: "query-failed",
|
|
387
|
+
message: previousFailure.message,
|
|
388
|
+
},
|
|
389
|
+
fingerprint: auth.fingerprint,
|
|
390
|
+
rememberedTargetId: expectedTargetId,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
failureBackoff.delete(failureKey);
|
|
394
|
+
querySequence += 1;
|
|
395
|
+
queryId = querySequence;
|
|
396
|
+
setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
|
|
397
|
+
const report = await queryProviderUsage(
|
|
398
|
+
adapter,
|
|
399
|
+
auth,
|
|
400
|
+
signal,
|
|
401
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
402
|
+
requiresRequestBoundaryGuard ? guard : undefined,
|
|
403
|
+
target.targetId,
|
|
404
|
+
);
|
|
405
|
+
if (requiresRequestBoundaryGuard) await guard();
|
|
406
|
+
const effectiveReport = { ...report, providerName };
|
|
407
|
+
if (latestQueries.get(failureKey) === queryId) {
|
|
408
|
+
cache.set(adapter.id, queryFingerprint, effectiveReport);
|
|
409
|
+
failureBackoff.delete(failureKey);
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
state: {
|
|
413
|
+
providerId: adapter.id,
|
|
414
|
+
providerName,
|
|
415
|
+
displayState,
|
|
416
|
+
status: "ready",
|
|
417
|
+
report: effectiveReport,
|
|
418
|
+
},
|
|
419
|
+
fingerprint: auth.fingerprint,
|
|
420
|
+
rememberedTargetId: expectedTargetId,
|
|
421
|
+
};
|
|
422
|
+
} catch (error) {
|
|
423
|
+
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
424
|
+
if (
|
|
425
|
+
retryableAuthChanged &&
|
|
426
|
+
authRetry === 0 &&
|
|
427
|
+
!signal.aborted &&
|
|
428
|
+
!requestContextChanged() &&
|
|
429
|
+
Date.now() < deadlineAt
|
|
430
|
+
) {
|
|
431
|
+
if (queryId !== undefined && latestQueries.get(failureKey) === queryId) {
|
|
432
|
+
latestQueries.delete(failureKey);
|
|
433
|
+
}
|
|
434
|
+
return queryAdapterState(ctx, adapter, displayState, true, signal, authRetry + 1, deadlineAt);
|
|
435
|
+
}
|
|
436
|
+
const message = errorMessage(error);
|
|
437
|
+
const now = Date.now();
|
|
438
|
+
for (const [key, failure] of failureBackoff) {
|
|
439
|
+
if (failure.until <= now) failureBackoff.delete(key);
|
|
440
|
+
}
|
|
441
|
+
if (queryId === undefined || latestQueries.get(failureKey) === queryId) {
|
|
442
|
+
if (adapter.invalidateCacheOnFailure) cache.delete(adapter.id, queryFingerprint);
|
|
443
|
+
setBoundedMap(failureBackoff, failureKey, { until: now + FAILURE_BACKOFF_MS, message }, MAX_ACCOUNT_STATES);
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
state: {
|
|
447
|
+
providerId: adapter.id,
|
|
448
|
+
providerName,
|
|
449
|
+
displayState,
|
|
450
|
+
status: "query-failed",
|
|
451
|
+
message,
|
|
452
|
+
},
|
|
453
|
+
fingerprint: auth.fingerprint,
|
|
454
|
+
rememberedTargetId: expectedTargetId,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const loadTargetChoices = async (ctx: ExtensionContext, adapter: UsageProviderAdapter, signal: AbortSignal) => {
|
|
460
|
+
if (!adapter.targets) throw new Error("Provider does not support usage targets.");
|
|
461
|
+
const expectedSessionGeneration = sessionGeneration;
|
|
462
|
+
const expectedSessionId = ctx.sessionManager.getSessionId();
|
|
463
|
+
const expectedModel = modelIdentity(ctx.model);
|
|
464
|
+
const expectedTargetId = settingsRuntime.get().settings.selectedTargets[adapter.id];
|
|
465
|
+
const deadlineAt = Date.now() + DEFAULT_TIMEOUT_MS;
|
|
466
|
+
const changed = () =>
|
|
467
|
+
expectedSessionGeneration !== sessionGeneration ||
|
|
468
|
+
ctx.sessionManager.getSessionId() !== expectedSessionId ||
|
|
469
|
+
modelIdentity(ctx.model) !== expectedModel ||
|
|
470
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId;
|
|
471
|
+
const auth = await awaitWithDeadline(
|
|
472
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
473
|
+
signal,
|
|
474
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
475
|
+
`resolving ${providerDisplayName(ctx, adapter.id)} runtime auth`,
|
|
476
|
+
);
|
|
477
|
+
if (!auth || signal.aborted || changed()) throw abortError();
|
|
478
|
+
const guard = async () => {
|
|
479
|
+
if (signal.aborted || changed()) throw abortError();
|
|
480
|
+
const revalidated = await awaitWithDeadline(
|
|
481
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
482
|
+
signal,
|
|
483
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
484
|
+
`revalidating ${providerDisplayName(ctx, adapter.id)} runtime auth`,
|
|
485
|
+
);
|
|
486
|
+
if (signal.aborted || changed() || revalidated?.fingerprint !== auth.fingerprint) {
|
|
487
|
+
throw abortError();
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
const choices = await listUsageTargets(adapter, auth, signal, Math.max(1, deadlineAt - Date.now()), guard);
|
|
491
|
+
return { choices, fingerprint: auth.fingerprint };
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const queryCurrentState = async (
|
|
495
|
+
ctx: ExtensionContext,
|
|
496
|
+
model: PiModel | undefined,
|
|
497
|
+
force: boolean,
|
|
498
|
+
signal: AbortSignal,
|
|
499
|
+
): Promise<QueryOutcome> => {
|
|
500
|
+
const adapter = adapterForProvider(model?.provider);
|
|
501
|
+
if (!adapter) {
|
|
502
|
+
const providerId = model?.provider ?? "none";
|
|
503
|
+
transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
|
|
504
|
+
return {
|
|
505
|
+
state: {
|
|
506
|
+
providerId,
|
|
507
|
+
providerName: providerDisplayName(ctx, providerId),
|
|
508
|
+
displayState: "current",
|
|
509
|
+
status: "unsupported",
|
|
510
|
+
message: model
|
|
511
|
+
? `Usage reporting is not supported for ${providerDisplayName(ctx, providerId)}.`
|
|
512
|
+
: "No model is selected.",
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return queryAdapterState(ctx, adapter, "current", force, signal);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const refreshCurrentStatus = async (ctx: ExtensionContext, model: PiModel | undefined, force: boolean) => {
|
|
520
|
+
const adapter = adapterForProvider(model?.provider);
|
|
521
|
+
if (!adapter || !model) {
|
|
522
|
+
const providerId = model?.provider ?? "none";
|
|
523
|
+
transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
|
|
524
|
+
clearStatus(ctx);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (adapter.publishesStatusline === false) {
|
|
528
|
+
clearStatus(ctx);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
statusGeneration += 1;
|
|
532
|
+
const generation = statusGeneration;
|
|
533
|
+
clearStatusCountdownTimer();
|
|
534
|
+
statusController?.abort();
|
|
535
|
+
const controller = new AbortController();
|
|
536
|
+
statusController = controller;
|
|
537
|
+
activeControllers.add(controller);
|
|
538
|
+
try {
|
|
539
|
+
if (!safeSetStatus(ctx, "checking")) return;
|
|
540
|
+
const outcome = await queryCurrentState(ctx, model, force, controller.signal);
|
|
541
|
+
if (!sessionActive || generation !== statusGeneration || controller.signal.aborted) return;
|
|
542
|
+
if (!(await outcomeStillCurrent(ctx, model, generation, outcome, controller.signal))) {
|
|
543
|
+
if (sessionActive && generation === statusGeneration) {
|
|
544
|
+
queueMicrotask(() => startStatusRefresh(ctx, ctx.model, false));
|
|
545
|
+
}
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
publishStatus(ctx, outcome, model, true);
|
|
549
|
+
} finally {
|
|
550
|
+
activeControllers.delete(controller);
|
|
551
|
+
if (statusController === controller) statusController = undefined;
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const startStatusRefresh = (ctx: ExtensionContext, model: PiModel | undefined, force: boolean) => {
|
|
556
|
+
void refreshCurrentStatus(ctx, model, force).catch((error) => {
|
|
557
|
+
if (isStaleExtensionContextError(error) || isAbortError(error)) return;
|
|
558
|
+
const message = errorMessage(error);
|
|
559
|
+
console.error("[pi-usage] refresh failed:", message);
|
|
560
|
+
safeSetStatus(ctx, `usage err: ${message.slice(0, 50)}`);
|
|
561
|
+
});
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const runMenuOperation = async <T>(
|
|
565
|
+
ctx: ExtensionCommandContext,
|
|
566
|
+
label: string,
|
|
567
|
+
parentSignal: AbortSignal,
|
|
568
|
+
operation: (signal: AbortSignal) => Promise<T>,
|
|
569
|
+
cancellable = true,
|
|
570
|
+
): Promise<T | undefined> => {
|
|
571
|
+
const { runTask } = await import("@narumitw/pi-tui-kit");
|
|
572
|
+
if (parentSignal.aborted) return undefined;
|
|
573
|
+
const result = await runTask(ctx, {
|
|
574
|
+
label,
|
|
575
|
+
signal: parentSignal,
|
|
576
|
+
cancellable,
|
|
577
|
+
onError: () => undefined,
|
|
578
|
+
task: ({ signal }) => operation(signal),
|
|
579
|
+
});
|
|
580
|
+
switch (result.kind) {
|
|
581
|
+
case "completed":
|
|
582
|
+
return result.value;
|
|
583
|
+
case "cancelled":
|
|
584
|
+
case "stale":
|
|
585
|
+
return undefined;
|
|
586
|
+
case "error":
|
|
587
|
+
throw result.error;
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const outcomeStillCurrent = async (
|
|
592
|
+
ctx: ExtensionContext,
|
|
593
|
+
model: PiModel | undefined,
|
|
594
|
+
generation: number,
|
|
595
|
+
outcome: QueryOutcome,
|
|
596
|
+
signal: AbortSignal,
|
|
597
|
+
): Promise<boolean> => {
|
|
598
|
+
if (generation !== statusGeneration || modelIdentity(ctx.model) !== modelIdentity(model)) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
const adapter = adapterForProvider(model?.provider);
|
|
602
|
+
const selectionStillCurrent =
|
|
603
|
+
!adapter?.targets || settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId;
|
|
604
|
+
if (!selectionStillCurrent) return false;
|
|
605
|
+
if (outcome.authState === "unavailable") {
|
|
606
|
+
if (!adapter) return false;
|
|
607
|
+
try {
|
|
608
|
+
const auth = await awaitWithDeadline(
|
|
609
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
610
|
+
signal,
|
|
611
|
+
DEFAULT_TIMEOUT_MS,
|
|
612
|
+
`revalidating ${adapter.displayName} runtime auth`,
|
|
613
|
+
);
|
|
614
|
+
return (
|
|
615
|
+
generation === statusGeneration &&
|
|
616
|
+
modelIdentity(ctx.model) === modelIdentity(model) &&
|
|
617
|
+
(!adapter.targets ||
|
|
618
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId) &&
|
|
619
|
+
auth === undefined
|
|
620
|
+
);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (isAbortError(error) || isStaleExtensionContextError(error)) throw error;
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (!outcome.fingerprint) return true;
|
|
627
|
+
if (!adapter) return false;
|
|
628
|
+
try {
|
|
629
|
+
const auth = await awaitWithDeadline(
|
|
630
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
631
|
+
signal,
|
|
632
|
+
DEFAULT_TIMEOUT_MS,
|
|
633
|
+
`revalidating ${adapter.displayName} runtime auth`,
|
|
634
|
+
);
|
|
635
|
+
return (
|
|
636
|
+
generation === statusGeneration &&
|
|
637
|
+
modelIdentity(ctx.model) === modelIdentity(model) &&
|
|
638
|
+
(!adapter.targets ||
|
|
639
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId) &&
|
|
640
|
+
auth?.fingerprint === outcome.fingerprint
|
|
641
|
+
);
|
|
642
|
+
} catch (error) {
|
|
643
|
+
if (isAbortError(error) || isStaleExtensionContextError(error)) throw error;
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const queryStableCurrent = async (
|
|
649
|
+
ctx: ExtensionCommandContext,
|
|
650
|
+
force: boolean,
|
|
651
|
+
controller: AbortController,
|
|
652
|
+
label: string,
|
|
653
|
+
): Promise<StableCurrent | undefined> => {
|
|
654
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
655
|
+
const model = ctx.model;
|
|
656
|
+
const generation = statusGeneration;
|
|
657
|
+
const result = await runMenuOperation(ctx, label, controller.signal, async (signal) => {
|
|
658
|
+
const outcome = await queryCurrentState(ctx, model, force, signal);
|
|
659
|
+
return {
|
|
660
|
+
outcome,
|
|
661
|
+
stable: await outcomeStillCurrent(ctx, model, generation, outcome, signal),
|
|
662
|
+
};
|
|
663
|
+
});
|
|
664
|
+
if (!result) return undefined;
|
|
665
|
+
if (result.stable) return { outcome: result.outcome, model };
|
|
666
|
+
force = false;
|
|
667
|
+
}
|
|
668
|
+
ctx.ui.notify("The active model or account kept changing; reopen /usage to retry.", "warning");
|
|
669
|
+
return undefined;
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
const publishStableCurrent = (ctx: ExtensionCommandContext, current: StableCurrent) => {
|
|
673
|
+
if (current.model) publishStatus(ctx, current.outcome, current.model, sessionActive);
|
|
674
|
+
else safeSetStatus(ctx, undefined);
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
const showMenu = async (ctx: ExtensionCommandContext): Promise<void> => {
|
|
678
|
+
if (!ctx.hasUI) throw new Error("/usage requires TUI or RPC mode.");
|
|
679
|
+
statusGeneration += 1;
|
|
680
|
+
const menuGeneration = statusGeneration;
|
|
681
|
+
statusController?.abort();
|
|
682
|
+
statusController = undefined;
|
|
683
|
+
clearStatusTimers();
|
|
684
|
+
const controller = new AbortController();
|
|
685
|
+
activeControllers.add(controller);
|
|
686
|
+
try {
|
|
687
|
+
let stableCurrent = await queryStableCurrent(ctx, false, controller, "Checking current usage…");
|
|
688
|
+
if (!stableCurrent) return;
|
|
689
|
+
publishStableCurrent(ctx, stableCurrent);
|
|
690
|
+
let current = stableCurrent.outcome;
|
|
691
|
+
let visibleStates: ProviderUsageState[] = [current.state];
|
|
692
|
+
let fastState = settingsRuntime.get();
|
|
693
|
+
let resetAvailability: CodexResetAvailability | undefined;
|
|
694
|
+
let selectedReset: CodexResetOption | undefined;
|
|
695
|
+
let resetAuthFingerprint: string | undefined;
|
|
696
|
+
let resetModelIdentity: string | undefined;
|
|
697
|
+
let redemptionId: string | undefined;
|
|
698
|
+
let resetOutcome: CodexResetOutcome | undefined;
|
|
699
|
+
let resetFailure: string | undefined;
|
|
700
|
+
const actionableTargetState = (): ProviderUsageState | undefined => {
|
|
701
|
+
if (visibleStates.length !== 1) return undefined;
|
|
702
|
+
const state = visibleStates[0];
|
|
703
|
+
return state &&
|
|
704
|
+
(state.status === "ready" || state.status === "selection-required") &&
|
|
705
|
+
adapterForProvider(state.providerId)?.targets
|
|
706
|
+
? state
|
|
707
|
+
: undefined;
|
|
708
|
+
};
|
|
709
|
+
const promptForTarget = async (state: ProviderUsageState, stateFingerprint?: string): Promise<boolean> => {
|
|
710
|
+
const adapter = adapterForProvider(state.providerId);
|
|
711
|
+
if (!adapter?.targets) return false;
|
|
712
|
+
const expectedRememberedTargetId = settingsRuntime.get().settings.selectedTargets[adapter.id];
|
|
713
|
+
const snapshot =
|
|
714
|
+
state.status === "selection-required" && stateFingerprint
|
|
715
|
+
? { choices: state.choices, fingerprint: stateFingerprint }
|
|
716
|
+
: await runMenuOperation(ctx, `Loading ${adapter.targets.pluralLabel}…`, controller.signal, (signal) =>
|
|
717
|
+
loadTargetChoices(ctx, adapter, signal),
|
|
718
|
+
);
|
|
719
|
+
if (!snapshot || controller.signal.aborted || statusGeneration !== menuGeneration) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
const selectOptions = createUsageTargetSelectOptions(snapshot.choices);
|
|
723
|
+
const selected = await ctx.ui.select(
|
|
724
|
+
`Select ${adapter.targets.singularLabel} for ${providerDisplayName(ctx, adapter.id)}`,
|
|
725
|
+
[...selectOptions.options],
|
|
726
|
+
{ signal: controller.signal },
|
|
727
|
+
);
|
|
728
|
+
if (
|
|
729
|
+
selected === undefined ||
|
|
730
|
+
controller.signal.aborted ||
|
|
731
|
+
statusGeneration !== menuGeneration ||
|
|
732
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedRememberedTargetId
|
|
733
|
+
) {
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
const targetId = selectOptions.targetIdFor(selected);
|
|
737
|
+
if (!targetId) return false;
|
|
738
|
+
const revalidated = await runMenuOperation(
|
|
739
|
+
ctx,
|
|
740
|
+
`Revalidating ${adapter.targets.singularLabel}…`,
|
|
741
|
+
controller.signal,
|
|
742
|
+
(signal) => loadTargetChoices(ctx, adapter, signal),
|
|
743
|
+
);
|
|
744
|
+
if (
|
|
745
|
+
!revalidated ||
|
|
746
|
+
controller.signal.aborted ||
|
|
747
|
+
statusGeneration !== menuGeneration ||
|
|
748
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedRememberedTargetId
|
|
749
|
+
) {
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
if (
|
|
753
|
+
revalidated.fingerprint !== snapshot.fingerprint ||
|
|
754
|
+
!revalidated.choices.some((choice) => choice.id === targetId)
|
|
755
|
+
) {
|
|
756
|
+
ctx.ui.notify(
|
|
757
|
+
`${providerDisplayName(ctx, adapter.id)} ${adapter.targets.pluralLabel} changed; choose again.`,
|
|
758
|
+
"warning",
|
|
759
|
+
);
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
let saved: Readonly<UsageSettingsState> | undefined;
|
|
763
|
+
try {
|
|
764
|
+
saved = await runMenuOperation(ctx, `Saving ${adapter.targets.singularLabel}…`, controller.signal, (signal) =>
|
|
765
|
+
settingsRuntime.updateSelectedTarget(adapter.id, targetId, signal, async () => {
|
|
766
|
+
let published: Awaited<ReturnType<typeof loadTargetChoices>>;
|
|
767
|
+
try {
|
|
768
|
+
published = await loadTargetChoices(ctx, adapter, signal);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
if (
|
|
771
|
+
signal.aborted ||
|
|
772
|
+
controller.signal.aborted ||
|
|
773
|
+
statusGeneration !== menuGeneration ||
|
|
774
|
+
isStaleExtensionContextError(error)
|
|
775
|
+
) {
|
|
776
|
+
throw error;
|
|
777
|
+
}
|
|
778
|
+
throw new UsageTargetSelectionChangedError();
|
|
779
|
+
}
|
|
780
|
+
if (
|
|
781
|
+
published.fingerprint !== snapshot.fingerprint ||
|
|
782
|
+
!published.choices.some((choice) => choice.id === targetId)
|
|
783
|
+
) {
|
|
784
|
+
throw new UsageTargetSelectionChangedError();
|
|
785
|
+
}
|
|
786
|
+
}),
|
|
787
|
+
);
|
|
788
|
+
} catch (error) {
|
|
789
|
+
if (error instanceof UsageTargetSelectionChangedError) {
|
|
790
|
+
ctx.ui.notify(
|
|
791
|
+
`${providerDisplayName(ctx, adapter.id)} ${adapter.targets.pluralLabel} changed; choose again.`,
|
|
792
|
+
"warning",
|
|
793
|
+
);
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
throw error;
|
|
797
|
+
}
|
|
798
|
+
if (!saved || controller.signal.aborted || statusGeneration !== menuGeneration) {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
invalidateProviderState(adapter.id);
|
|
802
|
+
return true;
|
|
803
|
+
};
|
|
804
|
+
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
805
|
+
if (controller.signal.aborted || statusGeneration !== menuGeneration) return;
|
|
806
|
+
type Screen = "main" | "providers" | "reset-picker" | "reset-confirm" | "reset-result" | "reset-error";
|
|
807
|
+
type Action =
|
|
808
|
+
| "refresh"
|
|
809
|
+
| "settings"
|
|
810
|
+
| "target"
|
|
811
|
+
| "toggle-fast"
|
|
812
|
+
| "another"
|
|
813
|
+
| "all"
|
|
814
|
+
| "provider"
|
|
815
|
+
| "open-resets"
|
|
816
|
+
| "select-reset"
|
|
817
|
+
| "cancel-reset"
|
|
818
|
+
| "consume-reset"
|
|
819
|
+
| "back-to-usage"
|
|
820
|
+
| "back-to-resets";
|
|
821
|
+
const menu = defineMenu<undefined, Screen, Action, ExtensionCommandContext>({
|
|
822
|
+
start: "main",
|
|
823
|
+
screens: {
|
|
824
|
+
main: () => {
|
|
825
|
+
const fastAvailability = fastRuntime.availability(ctx.model);
|
|
826
|
+
const targetState = actionableTargetState();
|
|
827
|
+
const targetAdapter = adapterForProvider(targetState?.providerId);
|
|
828
|
+
const fastLines =
|
|
829
|
+
fastAvailability.kind === "available"
|
|
830
|
+
? [`Fast mode: ${fastAvailability.enabled ? "On" : "Off"}`, FAST_USAGE_WARNING]
|
|
831
|
+
: fastAvailability.kind === "unavailable"
|
|
832
|
+
? [`Fast mode: Unavailable · ${fastAvailability.reason}`]
|
|
833
|
+
: [];
|
|
834
|
+
return {
|
|
835
|
+
kind: "actions",
|
|
836
|
+
title: "Provider usage",
|
|
837
|
+
lines: [...formatProviderStates(visibleStates).split("\n"), ...fastLines],
|
|
838
|
+
items: [
|
|
839
|
+
{ id: "refresh", label: REFRESH_CURRENT, action: "refresh" },
|
|
840
|
+
{ id: "settings", label: SETTINGS, action: "settings" },
|
|
841
|
+
...(targetState && targetAdapter?.targets
|
|
842
|
+
? [
|
|
843
|
+
{
|
|
844
|
+
id: "target",
|
|
845
|
+
label: `${targetState.status === "selection-required" ? "Select" : "Change"} ${targetAdapter.targets.singularLabel}…`,
|
|
846
|
+
action: "target" as const,
|
|
847
|
+
},
|
|
848
|
+
]
|
|
849
|
+
: []),
|
|
850
|
+
...(fastAvailability.kind === "available"
|
|
851
|
+
? [
|
|
852
|
+
{
|
|
853
|
+
id: "toggle-fast",
|
|
854
|
+
label: fastAvailability.enabled ? "Turn Fast mode off" : "Turn Fast mode on",
|
|
855
|
+
description:
|
|
856
|
+
fastState.kind === "invalid"
|
|
857
|
+
? "Repair pi-usage.json and reload before changing Fast mode."
|
|
858
|
+
: FAST_USAGE_WARNING,
|
|
859
|
+
disabled: fastState.kind === "invalid",
|
|
860
|
+
action: "toggle-fast" as const,
|
|
861
|
+
},
|
|
862
|
+
]
|
|
863
|
+
: []),
|
|
864
|
+
...(current.state.status === "ready" && current.state.providerId === "openai-codex"
|
|
865
|
+
? [
|
|
866
|
+
{
|
|
867
|
+
id: "open-resets",
|
|
868
|
+
label: REDEEM_CODEX_RESET,
|
|
869
|
+
description: codexResetActionDescription(current.state.report),
|
|
870
|
+
disabled: codexResetCount(current.state.report) === 0,
|
|
871
|
+
action: "open-resets" as const,
|
|
872
|
+
},
|
|
873
|
+
]
|
|
874
|
+
: []),
|
|
875
|
+
{ id: "another", label: VIEW_ANOTHER, action: "another" },
|
|
876
|
+
{ id: "all", label: VIEW_ALL, action: "all" },
|
|
877
|
+
{ id: "close", label: CLOSE, close: true },
|
|
878
|
+
],
|
|
879
|
+
hint: "close",
|
|
880
|
+
};
|
|
881
|
+
},
|
|
882
|
+
providers: () => ({
|
|
883
|
+
kind: "actions",
|
|
884
|
+
title: "Select a configured provider",
|
|
885
|
+
items: configuredAdapters(ctx)
|
|
886
|
+
.filter((adapter) => adapter.id !== ctx.model?.provider)
|
|
887
|
+
.map((adapter) => ({
|
|
888
|
+
id: adapter.id,
|
|
889
|
+
label: providerDisplayName(ctx, adapter.id),
|
|
890
|
+
action: "provider" as const,
|
|
891
|
+
})),
|
|
892
|
+
hint: "back",
|
|
893
|
+
}),
|
|
894
|
+
"reset-picker": () => ({
|
|
895
|
+
kind: "choice",
|
|
896
|
+
title: "Usage limit resets",
|
|
897
|
+
lines: [
|
|
898
|
+
`${resetAvailability?.availableCount ?? 0} ${resetLabel(resetAvailability?.availableCount ?? 0)} available.`,
|
|
899
|
+
],
|
|
900
|
+
items: (resetAvailability?.options ?? []).map((option, index) => ({
|
|
901
|
+
id: `reset-${index}`,
|
|
902
|
+
label: option.title,
|
|
903
|
+
description: resetOptionExpiration(option),
|
|
904
|
+
details: [option.description],
|
|
905
|
+
})),
|
|
906
|
+
action: "select-reset",
|
|
907
|
+
initialItemId: "reset-0",
|
|
908
|
+
hint: "back",
|
|
909
|
+
}),
|
|
910
|
+
"reset-confirm": () => ({
|
|
911
|
+
kind: "actions",
|
|
912
|
+
title: "Use this reset?",
|
|
913
|
+
lines: resetConfirmationLines(selectedReset),
|
|
914
|
+
items: [
|
|
915
|
+
{ id: "cancel-reset", label: "No, go back", action: "cancel-reset" },
|
|
916
|
+
{ id: "consume-reset", label: "Yes, use reset", action: "consume-reset" },
|
|
917
|
+
],
|
|
918
|
+
hint: "back",
|
|
919
|
+
}),
|
|
920
|
+
"reset-result": () => ({
|
|
921
|
+
kind: "actions",
|
|
922
|
+
title: "Usage limit resets",
|
|
923
|
+
lines: [
|
|
924
|
+
formatCodexResetOutcome(
|
|
925
|
+
resetOutcome,
|
|
926
|
+
current.state.status === "ready" ? codexResetCount(current.state.report) : undefined,
|
|
927
|
+
),
|
|
928
|
+
],
|
|
929
|
+
items: [
|
|
930
|
+
{
|
|
931
|
+
id: "back-to-usage",
|
|
932
|
+
label: "Back to usage",
|
|
933
|
+
action: "back-to-usage",
|
|
934
|
+
},
|
|
935
|
+
{ id: "close", label: CLOSE, close: true },
|
|
936
|
+
],
|
|
937
|
+
hint: "back",
|
|
938
|
+
}),
|
|
939
|
+
"reset-error": () => ({
|
|
940
|
+
kind: "actions",
|
|
941
|
+
title: "Usage limit resets",
|
|
942
|
+
lines: [resetFailure ?? "Couldn't reset usage. Please try again."],
|
|
943
|
+
items: [
|
|
944
|
+
{ id: "consume-reset", label: "Try again", action: "consume-reset" },
|
|
945
|
+
{ id: "back-to-resets", label: "Back", action: "back-to-resets" },
|
|
946
|
+
],
|
|
947
|
+
hint: "back",
|
|
948
|
+
}),
|
|
949
|
+
},
|
|
950
|
+
actions: {
|
|
951
|
+
target: async () => {
|
|
952
|
+
const targetState = actionableTargetState();
|
|
953
|
+
if (!targetState) return { kind: "rejected" };
|
|
954
|
+
try {
|
|
955
|
+
const stateFingerprint = targetState === current.state ? current.fingerprint : undefined;
|
|
956
|
+
if (!(await promptForTarget(targetState, stateFingerprint))) {
|
|
957
|
+
return { kind: "stay" };
|
|
958
|
+
}
|
|
959
|
+
const adapter = adapterForProvider(targetState.providerId);
|
|
960
|
+
if (!adapter) return { kind: "rejected" };
|
|
961
|
+
if (targetState.providerId === ctx.model?.provider) {
|
|
962
|
+
const refreshed = await queryStableCurrent(
|
|
963
|
+
ctx,
|
|
964
|
+
true,
|
|
965
|
+
controller,
|
|
966
|
+
`Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
|
|
967
|
+
);
|
|
968
|
+
if (!refreshed) return { kind: "stay" };
|
|
969
|
+
stableCurrent = refreshed;
|
|
970
|
+
current = refreshed.outcome;
|
|
971
|
+
visibleStates = [current.state];
|
|
972
|
+
publishStableCurrent(ctx, refreshed);
|
|
973
|
+
return { kind: "stay" };
|
|
974
|
+
}
|
|
975
|
+
const outcome = await runMenuOperation(
|
|
976
|
+
ctx,
|
|
977
|
+
`Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
|
|
978
|
+
controller.signal,
|
|
979
|
+
(signal) => queryAdapterState(ctx, adapter, "configured", true, signal),
|
|
980
|
+
);
|
|
981
|
+
if (!outcome) return { kind: "stay" };
|
|
982
|
+
const revalidated = await queryStableCurrent(ctx, false, controller, "Revalidating current usage…");
|
|
983
|
+
if (!revalidated) return { kind: "stay" };
|
|
984
|
+
stableCurrent = revalidated;
|
|
985
|
+
current = revalidated.outcome;
|
|
986
|
+
visibleStates = [
|
|
987
|
+
outcome.state.providerId === current.state.providerId
|
|
988
|
+
? current.state
|
|
989
|
+
: { ...outcome.state, displayState: "configured" },
|
|
990
|
+
];
|
|
991
|
+
return { kind: "stay" };
|
|
992
|
+
} catch (error) {
|
|
993
|
+
if (isAbortError(error) || isStaleExtensionContextError(error)) {
|
|
994
|
+
return { kind: "stay" };
|
|
995
|
+
}
|
|
996
|
+
ctx.ui.notify(`Could not select target: ${errorMessage(error)}`, "error");
|
|
997
|
+
return { kind: "stay" };
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
settings: async () => {
|
|
1001
|
+
await showUsageSettings(
|
|
1002
|
+
ctx,
|
|
1003
|
+
settingsRuntime,
|
|
1004
|
+
controller.signal,
|
|
1005
|
+
() => statusGeneration === menuGeneration && !controller.signal.aborted,
|
|
1006
|
+
(id) => {
|
|
1007
|
+
if (
|
|
1008
|
+
(id === "codexStatusResetCountdown" || id === "codexStatusPercentage") &&
|
|
1009
|
+
stableCurrent &&
|
|
1010
|
+
statusGeneration === menuGeneration &&
|
|
1011
|
+
!controller.signal.aborted
|
|
1012
|
+
) {
|
|
1013
|
+
publishStableCurrent(ctx, stableCurrent);
|
|
1014
|
+
}
|
|
1015
|
+
},
|
|
1016
|
+
);
|
|
1017
|
+
fastState = settingsRuntime.get();
|
|
1018
|
+
const revalidated = await queryStableCurrent(ctx, false, controller, "Applying usage settings…");
|
|
1019
|
+
if (!revalidated) return { kind: "stay" };
|
|
1020
|
+
stableCurrent = revalidated;
|
|
1021
|
+
current = revalidated.outcome;
|
|
1022
|
+
visibleStates = [current.state];
|
|
1023
|
+
publishStableCurrent(ctx, revalidated);
|
|
1024
|
+
return { kind: "stay" };
|
|
1025
|
+
},
|
|
1026
|
+
"toggle-fast": async () => {
|
|
1027
|
+
const availability = fastRuntime.availability(ctx.model);
|
|
1028
|
+
if (availability.kind !== "available" || fastState.kind === "invalid") {
|
|
1029
|
+
return { kind: "rejected" };
|
|
1030
|
+
}
|
|
1031
|
+
const changed = await fastRuntime.toggle(ctx, !availability.enabled, controller.signal);
|
|
1032
|
+
if (!changed) return { kind: "rejected" };
|
|
1033
|
+
fastState = settingsRuntime.get();
|
|
1034
|
+
return { kind: "stay" };
|
|
1035
|
+
},
|
|
1036
|
+
"open-resets": async () => {
|
|
1037
|
+
const summaryCount =
|
|
1038
|
+
current.state.status === "ready" && current.state.providerId === "openai-codex"
|
|
1039
|
+
? codexResetCount(current.state.report)
|
|
1040
|
+
: undefined;
|
|
1041
|
+
try {
|
|
1042
|
+
const loaded = await runMenuOperation(
|
|
1043
|
+
ctx,
|
|
1044
|
+
"Checking usage limit resets…",
|
|
1045
|
+
controller.signal,
|
|
1046
|
+
async (signal) => {
|
|
1047
|
+
const expectedModel = modelIdentity(ctx.model);
|
|
1048
|
+
const auth = await awaitWithDeadline(
|
|
1049
|
+
resolveCodexResetAuth(ctx, undefined, credentialReader, credentialCandidates),
|
|
1050
|
+
signal,
|
|
1051
|
+
DEFAULT_TIMEOUT_MS,
|
|
1052
|
+
"resolving current Codex reset authentication",
|
|
1053
|
+
);
|
|
1054
|
+
let availability: CodexResetAvailability;
|
|
1055
|
+
try {
|
|
1056
|
+
availability = await listCodexResetCredits(auth, signal, DEFAULT_TIMEOUT_MS);
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
if (isAbortError(error) || summaryCount === undefined || summaryCount <= 0) {
|
|
1059
|
+
throw error;
|
|
1060
|
+
}
|
|
1061
|
+
availability = {
|
|
1062
|
+
availableCount: summaryCount,
|
|
1063
|
+
options: [genericCodexResetOption()],
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
const revalidated = await awaitWithDeadline(
|
|
1067
|
+
resolveCodexResetAuth(ctx, undefined, credentialReader, credentialCandidates),
|
|
1068
|
+
signal,
|
|
1069
|
+
DEFAULT_TIMEOUT_MS,
|
|
1070
|
+
"revalidating current Codex reset authentication",
|
|
1071
|
+
);
|
|
1072
|
+
if (modelIdentity(ctx.model) !== expectedModel || revalidated.fingerprint !== auth.fingerprint) {
|
|
1073
|
+
throw new Error("The active Codex model or account changed while loading usage limit resets.");
|
|
1074
|
+
}
|
|
1075
|
+
return { availability, auth, expectedModel };
|
|
1076
|
+
},
|
|
1077
|
+
);
|
|
1078
|
+
if (!loaded) return { kind: "stay" };
|
|
1079
|
+
resetAvailability = loaded.availability;
|
|
1080
|
+
resetAuthFingerprint = loaded.auth.fingerprint;
|
|
1081
|
+
resetModelIdentity = loaded.expectedModel;
|
|
1082
|
+
selectedReset = undefined;
|
|
1083
|
+
redemptionId = undefined;
|
|
1084
|
+
resetOutcome = undefined;
|
|
1085
|
+
resetFailure = undefined;
|
|
1086
|
+
return {
|
|
1087
|
+
kind: "to",
|
|
1088
|
+
screen: loaded.availability.availableCount > 0 ? "reset-picker" : "reset-result",
|
|
1089
|
+
};
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
if (isAbortError(error) || isStaleExtensionContextError(error)) {
|
|
1092
|
+
return { kind: "stay" };
|
|
1093
|
+
}
|
|
1094
|
+
ctx.ui.notify(`Couldn't load usage limit resets: ${errorMessage(error)}`, "error");
|
|
1095
|
+
return { kind: "stay" };
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
"select-reset": ({ itemId }) => {
|
|
1099
|
+
const index = Number(itemId.replace(/^reset-/u, ""));
|
|
1100
|
+
const option = Number.isSafeInteger(index) ? resetAvailability?.options[index] : undefined;
|
|
1101
|
+
if (!option) return { kind: "rejected" };
|
|
1102
|
+
selectedReset = option;
|
|
1103
|
+
redemptionId = undefined;
|
|
1104
|
+
resetFailure = undefined;
|
|
1105
|
+
return { kind: "to", screen: "reset-confirm" };
|
|
1106
|
+
},
|
|
1107
|
+
"cancel-reset": () => ({ kind: "back" }),
|
|
1108
|
+
"consume-reset": async () => {
|
|
1109
|
+
if (!selectedReset || !resetAuthFingerprint || !resetModelIdentity) {
|
|
1110
|
+
return { kind: "rejected" };
|
|
1111
|
+
}
|
|
1112
|
+
try {
|
|
1113
|
+
redemptionId ??= createRedemptionId();
|
|
1114
|
+
const attemptId = redemptionId;
|
|
1115
|
+
const option = selectedReset;
|
|
1116
|
+
const expectedFingerprint = resetAuthFingerprint;
|
|
1117
|
+
const expectedModel = resetModelIdentity;
|
|
1118
|
+
const result = await runMenuOperation(
|
|
1119
|
+
ctx,
|
|
1120
|
+
"Resetting your usage…",
|
|
1121
|
+
controller.signal,
|
|
1122
|
+
async (signal) => {
|
|
1123
|
+
const auth = await awaitWithDeadline(
|
|
1124
|
+
resolveCodexResetAuth(ctx, undefined, credentialReader, credentialCandidates),
|
|
1125
|
+
signal,
|
|
1126
|
+
DEFAULT_TIMEOUT_MS,
|
|
1127
|
+
"revalidating current Codex reset authentication",
|
|
1128
|
+
);
|
|
1129
|
+
if (modelIdentity(ctx.model) !== expectedModel || auth.fingerprint !== expectedFingerprint) {
|
|
1130
|
+
throw new Error("The active Codex model or account changed; the reset was not used.");
|
|
1131
|
+
}
|
|
1132
|
+
const outcome = await consumeCodexResetCredit(auth, option, attemptId, signal, DEFAULT_TIMEOUT_MS);
|
|
1133
|
+
invalidateProviderState("openai-codex");
|
|
1134
|
+
const model = ctx.model;
|
|
1135
|
+
if (modelIdentity(model) !== expectedModel) return { outcome };
|
|
1136
|
+
const refreshed = await queryCurrentState(ctx, model, true, signal);
|
|
1137
|
+
const stable = await outcomeStillCurrent(ctx, model, menuGeneration, refreshed, signal);
|
|
1138
|
+
return stable ? { outcome, refreshed, model } : { outcome };
|
|
1139
|
+
},
|
|
1140
|
+
false,
|
|
1141
|
+
);
|
|
1142
|
+
if (!result) return { kind: "close" };
|
|
1143
|
+
resetOutcome = result.outcome;
|
|
1144
|
+
resetFailure = undefined;
|
|
1145
|
+
if (result.refreshed && result.model) {
|
|
1146
|
+
stableCurrent = { outcome: result.refreshed, model: result.model };
|
|
1147
|
+
current = result.refreshed;
|
|
1148
|
+
visibleStates = [current.state];
|
|
1149
|
+
publishStableCurrent(ctx, stableCurrent);
|
|
1150
|
+
}
|
|
1151
|
+
return { kind: "to", screen: "reset-result" };
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
if (isAbortError(error) || isStaleExtensionContextError(error)) {
|
|
1154
|
+
return { kind: "close" };
|
|
1155
|
+
}
|
|
1156
|
+
resetFailure = `Couldn't reset usage: ${errorMessage(error)}. Try again with the same request.`;
|
|
1157
|
+
return { kind: "to", screen: "reset-error" };
|
|
1158
|
+
}
|
|
1159
|
+
},
|
|
1160
|
+
"back-to-usage": () => ({ kind: "to", screen: "main" }),
|
|
1161
|
+
"back-to-resets": () => {
|
|
1162
|
+
redemptionId = undefined;
|
|
1163
|
+
resetFailure = undefined;
|
|
1164
|
+
return { kind: "to", screen: "reset-picker" };
|
|
1165
|
+
},
|
|
1166
|
+
refresh: async () => {
|
|
1167
|
+
const refreshed = await queryStableCurrent(ctx, true, controller, "Refreshing current usage…");
|
|
1168
|
+
if (!refreshed) return { kind: "stay" };
|
|
1169
|
+
stableCurrent = refreshed;
|
|
1170
|
+
publishStableCurrent(ctx, refreshed);
|
|
1171
|
+
current = refreshed.outcome;
|
|
1172
|
+
visibleStates = [current.state];
|
|
1173
|
+
return { kind: "stay" };
|
|
1174
|
+
},
|
|
1175
|
+
another: async () => {
|
|
1176
|
+
const others = configuredAdapters(ctx).filter((adapter) => adapter.id !== ctx.model?.provider);
|
|
1177
|
+
if (others.length === 0) {
|
|
1178
|
+
ctx.ui.notify("No other supported provider has configured runtime auth.", "info");
|
|
1179
|
+
return { kind: "stay" };
|
|
1180
|
+
}
|
|
1181
|
+
return { kind: "to", screen: "providers" };
|
|
1182
|
+
},
|
|
1183
|
+
provider: async ({ itemId }) => {
|
|
1184
|
+
const adapter = configuredAdapters(ctx).find(
|
|
1185
|
+
(candidate) => candidate.id === itemId && candidate.id !== ctx.model?.provider,
|
|
1186
|
+
);
|
|
1187
|
+
if (!adapter) return { kind: "back" };
|
|
1188
|
+
let outcome = await runMenuOperation(
|
|
1189
|
+
ctx,
|
|
1190
|
+
`Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
|
|
1191
|
+
controller.signal,
|
|
1192
|
+
(signal) => queryAdapterState(ctx, adapter, "configured", false, signal),
|
|
1193
|
+
);
|
|
1194
|
+
if (!outcome) return { kind: "back" };
|
|
1195
|
+
if (outcome.state.status === "selection-required") {
|
|
1196
|
+
if (!(await promptForTarget(outcome.state, outcome.fingerprint))) {
|
|
1197
|
+
return { kind: "back" };
|
|
1198
|
+
}
|
|
1199
|
+
outcome = await runMenuOperation(
|
|
1200
|
+
ctx,
|
|
1201
|
+
`Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
|
|
1202
|
+
controller.signal,
|
|
1203
|
+
(signal) => queryAdapterState(ctx, adapter, "configured", true, signal),
|
|
1204
|
+
);
|
|
1205
|
+
if (!outcome) return { kind: "back" };
|
|
1206
|
+
}
|
|
1207
|
+
const revalidated = await queryStableCurrent(ctx, false, controller, "Revalidating current usage…");
|
|
1208
|
+
if (!revalidated) return { kind: "back" };
|
|
1209
|
+
stableCurrent = revalidated;
|
|
1210
|
+
current = revalidated.outcome;
|
|
1211
|
+
visibleStates = [
|
|
1212
|
+
outcome.state.providerId === current.state.providerId
|
|
1213
|
+
? current.state
|
|
1214
|
+
: { ...outcome.state, displayState: "configured" },
|
|
1215
|
+
];
|
|
1216
|
+
return { kind: "back" };
|
|
1217
|
+
},
|
|
1218
|
+
all: async () => {
|
|
1219
|
+
const adapters = configuredAdapters(ctx);
|
|
1220
|
+
const currentProviderId = ctx.model?.provider;
|
|
1221
|
+
const settled = await runMenuOperation(
|
|
1222
|
+
ctx,
|
|
1223
|
+
"Checking configured provider usage…",
|
|
1224
|
+
controller.signal,
|
|
1225
|
+
(signal) =>
|
|
1226
|
+
runWithConcurrency(
|
|
1227
|
+
adapters,
|
|
1228
|
+
ALL_PROVIDER_CONCURRENCY,
|
|
1229
|
+
(adapter, _index, workerSignal) =>
|
|
1230
|
+
queryAdapterState(
|
|
1231
|
+
ctx,
|
|
1232
|
+
adapter,
|
|
1233
|
+
adapter.id === currentProviderId ? "current" : "configured",
|
|
1234
|
+
true,
|
|
1235
|
+
workerSignal,
|
|
1236
|
+
),
|
|
1237
|
+
signal,
|
|
1238
|
+
),
|
|
1239
|
+
);
|
|
1240
|
+
if (!settled) return { kind: "stay" };
|
|
1241
|
+
const queriedStates: ProviderUsageState[] = settled.map((result, index) => {
|
|
1242
|
+
if (result.status === "fulfilled") {
|
|
1243
|
+
return { ...result.value.state, displayState: "configured" };
|
|
1244
|
+
}
|
|
1245
|
+
const adapter = adapters[index] as UsageProviderAdapter;
|
|
1246
|
+
return {
|
|
1247
|
+
providerId: adapter.id,
|
|
1248
|
+
providerName: providerDisplayName(ctx, adapter.id),
|
|
1249
|
+
displayState: "configured",
|
|
1250
|
+
status: "query-failed",
|
|
1251
|
+
message: errorMessage(result.reason),
|
|
1252
|
+
};
|
|
1253
|
+
});
|
|
1254
|
+
const revalidated = await queryStableCurrent(ctx, false, controller, "Revalidating current usage…");
|
|
1255
|
+
if (!revalidated) return { kind: "stay" };
|
|
1256
|
+
stableCurrent = revalidated;
|
|
1257
|
+
current = revalidated.outcome;
|
|
1258
|
+
visibleStates = [
|
|
1259
|
+
current.state,
|
|
1260
|
+
...queriedStates.filter((state) => state.providerId !== current.state.providerId),
|
|
1261
|
+
];
|
|
1262
|
+
return { kind: "stay" };
|
|
1263
|
+
},
|
|
1264
|
+
},
|
|
1265
|
+
});
|
|
1266
|
+
await runMenu(ctx, menu, {
|
|
1267
|
+
getState: () => undefined,
|
|
1268
|
+
signal: controller.signal,
|
|
1269
|
+
isCurrent: () => statusGeneration === menuGeneration && !controller.signal.aborted,
|
|
1270
|
+
});
|
|
1271
|
+
} finally {
|
|
1272
|
+
controller.abort(new DOMException("Usage menu closed", "AbortError"));
|
|
1273
|
+
activeControllers.delete(controller);
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
pi.registerCommand("usage", {
|
|
1278
|
+
description: "Show usage for the current runtime account",
|
|
1279
|
+
handler: async (args, ctx) => {
|
|
1280
|
+
if (args.trim()) {
|
|
1281
|
+
ctx.ui.notify("/usage does not accept arguments; choose an action from its menu.", "warning");
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
try {
|
|
1285
|
+
await showMenu(ctx);
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
if (isStaleExtensionContextError(error) || isAbortError(error)) return;
|
|
1288
|
+
throw error;
|
|
1289
|
+
}
|
|
1290
|
+
},
|
|
1291
|
+
});
|
|
1292
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1293
|
+
sessionGeneration += 1;
|
|
1294
|
+
statusGeneration += 1;
|
|
1295
|
+
clearStatusTimers();
|
|
1296
|
+
for (const controller of activeControllers) controller.abort();
|
|
1297
|
+
activeControllers.clear();
|
|
1298
|
+
statusController = undefined;
|
|
1299
|
+
sessionActive = true;
|
|
1300
|
+
const ownerGeneration = sessionGeneration;
|
|
1301
|
+
try {
|
|
1302
|
+
await fastRuntime.prepareSession(ctx);
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
if (isStaleExtensionContextError(error) || ownerGeneration !== sessionGeneration) return;
|
|
1305
|
+
throw error;
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
1309
|
+
startStatusRefresh(ctx, ctx.model, false);
|
|
1310
|
+
});
|
|
1311
|
+
pi.on("model_select", (event, ctx) => {
|
|
1312
|
+
startStatusRefresh(ctx, event.model, false);
|
|
1313
|
+
});
|
|
1314
|
+
pi.on("turn_start", (_event, ctx) => {
|
|
1315
|
+
startStatusRefresh(ctx, ctx.model, false);
|
|
1316
|
+
});
|
|
1317
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
1318
|
+
sessionActive = false;
|
|
1319
|
+
sessionGeneration += 1;
|
|
1320
|
+
statusGeneration += 1;
|
|
1321
|
+
clearStatusTimers();
|
|
1322
|
+
for (const controller of activeControllers) controller.abort();
|
|
1323
|
+
activeControllers.clear();
|
|
1324
|
+
statusController = undefined;
|
|
1325
|
+
cache.clear();
|
|
1326
|
+
failureBackoff.clear();
|
|
1327
|
+
latestQueries.clear();
|
|
1328
|
+
activeCurrentIdentity = undefined;
|
|
1329
|
+
safeSetStatus(ctx, undefined);
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
fastRuntime = registerCodexFastMode(pi, settingsRuntime, (ctx) => startStatusRefresh(ctx, ctx.model, false), {
|
|
1333
|
+
registerSessionStart: false,
|
|
1334
|
+
});
|
|
1335
|
+
}
|