@narumitw/pi-usage 0.60.0 → 0.60.2
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/README.md +27 -19
- package/dist/index.ts +804 -322
- package/dist/index.ts.map +4 -4
- package/package.json +1 -1
- package/src/core.ts +28 -2
- package/src/format.ts +37 -23
- package/src/index.ts +13 -0
- package/src/providers/fireworks.ts +112 -0
- package/src/providers/minimax.ts +21 -0
- package/src/query.ts +110 -150
- package/src/settings.ts +155 -8
- package/src/types.ts +39 -2
- package/src/usage-settings-ui.ts +88 -183
- package/src/usage-targets.ts +143 -0
- package/src/usage.ts +419 -107
package/src/usage.ts
CHANGED
|
@@ -37,7 +37,11 @@ import {
|
|
|
37
37
|
queryProviderUsage,
|
|
38
38
|
resolveUsageAuth,
|
|
39
39
|
} from "./query.js";
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
createUsageSettingsRuntime,
|
|
42
|
+
type UsageSettingsRuntime,
|
|
43
|
+
type UsageSettingsState,
|
|
44
|
+
} from "./settings.js";
|
|
41
45
|
import type {
|
|
42
46
|
PiModel,
|
|
43
47
|
ProviderUsageState,
|
|
@@ -54,6 +58,11 @@ import {
|
|
|
54
58
|
setBoundedMap,
|
|
55
59
|
} from "./usage-helpers.js";
|
|
56
60
|
import { showUsageSettings } from "./usage-settings-ui.js";
|
|
61
|
+
import {
|
|
62
|
+
createUsageTargetSelectOptions,
|
|
63
|
+
listUsageTargets,
|
|
64
|
+
resolveUsageTarget,
|
|
65
|
+
} from "./usage-targets.js";
|
|
57
66
|
|
|
58
67
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
59
68
|
const STATUS_COUNTDOWN_REFRESH_MS = 60 * 1000;
|
|
@@ -80,8 +89,13 @@ type QueryOutcome = {
|
|
|
80
89
|
state: ProviderUsageState;
|
|
81
90
|
fingerprint?: string;
|
|
82
91
|
authState?: "unavailable";
|
|
92
|
+
rememberedTargetId?: string;
|
|
83
93
|
};
|
|
84
94
|
|
|
95
|
+
class UsageTargetSelectionChangedError extends Error {
|
|
96
|
+
override readonly name = "UsageTargetSelectionChangedError";
|
|
97
|
+
}
|
|
98
|
+
|
|
85
99
|
type StableCurrent = {
|
|
86
100
|
outcome: QueryOutcome;
|
|
87
101
|
model: PiModel | undefined;
|
|
@@ -171,12 +185,14 @@ export default function usageExtension(
|
|
|
171
185
|
return;
|
|
172
186
|
}
|
|
173
187
|
if (outcome.state.status !== "ready") {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
outcome.state.status === "
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
const chip =
|
|
189
|
+
outcome.state.status === "auth-unavailable"
|
|
190
|
+
? "auth unavailable"
|
|
191
|
+
: outcome.state.status === "selection-required"
|
|
192
|
+
? "selection required"
|
|
193
|
+
: `usage err: ${outcome.state.message.slice(0, 50)}`;
|
|
194
|
+
|
|
195
|
+
if (safeSetStatus(ctx, chip)) {
|
|
180
196
|
if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
|
|
181
197
|
}
|
|
182
198
|
return;
|
|
@@ -248,10 +264,10 @@ export default function usageExtension(
|
|
|
248
264
|
const expectedSessionGeneration = sessionGeneration;
|
|
249
265
|
const expectedSessionId = ctx.sessionManager.getSessionId();
|
|
250
266
|
const expectedModelIdentity = modelIdentity(ctx.model);
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
267
|
+
const expectedTargetId = adapter.targets
|
|
268
|
+
? settingsRuntime.get().settings.selectedTargets[adapter.id]
|
|
269
|
+
: undefined;
|
|
270
|
+
const providerName = providerDisplayName(ctx, adapter.id);
|
|
255
271
|
let auth: ResolvedUsageAuth | undefined;
|
|
256
272
|
try {
|
|
257
273
|
auth = await awaitWithDeadline(
|
|
@@ -268,32 +284,33 @@ export default function usageExtension(
|
|
|
268
284
|
return {
|
|
269
285
|
state: {
|
|
270
286
|
providerId: adapter.id,
|
|
271
|
-
providerName
|
|
287
|
+
providerName,
|
|
272
288
|
displayState,
|
|
273
289
|
status: isTimeoutError(error) ? "query-failed" : "auth-unavailable",
|
|
274
290
|
message: errorMessage(error),
|
|
275
291
|
},
|
|
276
292
|
};
|
|
277
293
|
}
|
|
278
|
-
const requiresRequestBoundaryGuard =
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
294
|
+
const requiresRequestBoundaryGuard =
|
|
295
|
+
adapter.targets !== undefined ||
|
|
296
|
+
[
|
|
297
|
+
"baseten",
|
|
298
|
+
"deepseek",
|
|
299
|
+
"minimax",
|
|
300
|
+
"minimax-cn",
|
|
301
|
+
"moonshotai",
|
|
302
|
+
"moonshotai-cn",
|
|
303
|
+
"vercel-ai-gateway",
|
|
304
|
+
"xai",
|
|
305
|
+
"zai",
|
|
306
|
+
"zai-coding-cn",
|
|
307
|
+
].includes(adapter.id);
|
|
291
308
|
const requestContextChanged = () =>
|
|
292
309
|
expectedSessionGeneration !== sessionGeneration ||
|
|
293
310
|
ctx.sessionManager.getSessionId() !== expectedSessionId ||
|
|
294
311
|
modelIdentity(ctx.model) !== expectedModelIdentity ||
|
|
295
|
-
(adapter.
|
|
296
|
-
settingsRuntime.get().settings.
|
|
312
|
+
(adapter.targets !== undefined &&
|
|
313
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId);
|
|
297
314
|
if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
|
|
298
315
|
if (!auth) {
|
|
299
316
|
if (displayState === "current") {
|
|
@@ -302,101 +319,142 @@ export default function usageExtension(
|
|
|
302
319
|
return {
|
|
303
320
|
state: {
|
|
304
321
|
providerId: adapter.id,
|
|
305
|
-
providerName
|
|
322
|
+
providerName,
|
|
306
323
|
displayState,
|
|
307
324
|
status: "auth-unavailable",
|
|
308
|
-
message: `No runtime credential is configured for ${
|
|
325
|
+
message: `No runtime credential is configured for ${providerName}.`,
|
|
309
326
|
},
|
|
310
327
|
authState: "unavailable",
|
|
311
328
|
};
|
|
312
329
|
}
|
|
313
|
-
const queryFingerprint =
|
|
314
|
-
adapter.id === "fireworks"
|
|
315
|
-
? `${auth.fingerprint}:account:${expectedFireworksAccountId ?? "auto"}`
|
|
316
|
-
: auth.fingerprint;
|
|
317
|
-
if (displayState === "current") {
|
|
318
|
-
transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const cached = !force ? cache.get(adapter.id, queryFingerprint) : undefined;
|
|
322
|
-
if (cached) {
|
|
323
|
-
return {
|
|
324
|
-
state: {
|
|
325
|
-
providerId: adapter.id,
|
|
326
|
-
providerName: adapter.displayName,
|
|
327
|
-
displayState,
|
|
328
|
-
status: "ready",
|
|
329
|
-
report: cached,
|
|
330
|
-
},
|
|
331
|
-
fingerprint: auth.fingerprint,
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const failureKey = `${adapter.id}:${queryFingerprint}`;
|
|
336
|
-
const previousFailure = failureBackoff.get(failureKey);
|
|
337
|
-
if (!force && previousFailure && previousFailure.until > Date.now()) {
|
|
338
|
-
return {
|
|
339
|
-
state: {
|
|
340
|
-
providerId: adapter.id,
|
|
341
|
-
providerName: adapter.displayName,
|
|
342
|
-
displayState,
|
|
343
|
-
status: "query-failed",
|
|
344
|
-
message: previousFailure.message,
|
|
345
|
-
},
|
|
346
|
-
fingerprint: auth.fingerprint,
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
failureBackoff.delete(failureKey);
|
|
350
|
-
querySequence += 1;
|
|
351
|
-
const queryId = querySequence;
|
|
352
|
-
setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
|
|
353
|
-
|
|
354
330
|
let retryableAuthChanged = false;
|
|
331
|
+
const guard = async () => {
|
|
332
|
+
if (signal.aborted || requestContextChanged()) throw abortError();
|
|
333
|
+
if (!requiresRequestBoundaryGuard) return;
|
|
334
|
+
const revalidated = await awaitWithDeadline(
|
|
335
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
336
|
+
signal,
|
|
337
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
338
|
+
`revalidating ${providerName} runtime auth`,
|
|
339
|
+
);
|
|
340
|
+
if (signal.aborted || requestContextChanged()) throw abortError();
|
|
341
|
+
if (revalidated?.fingerprint !== auth.fingerprint) {
|
|
342
|
+
if (["deepseek", "minimax", "minimax-cn"].includes(adapter.id)) {
|
|
343
|
+
retryableAuthChanged = true;
|
|
344
|
+
throw new Error(`${providerName} runtime credential changed during the usage query.`);
|
|
345
|
+
}
|
|
346
|
+
throw abortError();
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
let queryFingerprint = adapter.targets
|
|
350
|
+
? `${auth.fingerprint}:target:${expectedTargetId ?? "unresolved"}`
|
|
351
|
+
: auth.fingerprint;
|
|
352
|
+
let failureKey = `${adapter.id}:${queryFingerprint}`;
|
|
353
|
+
let queryId: number | undefined;
|
|
355
354
|
try {
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
`${adapter.displayName} runtime credential changed during the usage query.`,
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
throw abortError();
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
: undefined;
|
|
378
|
-
const report = await queryProviderUsage(
|
|
355
|
+
const previousDiscoveryFailure = failureBackoff.get(failureKey);
|
|
356
|
+
if (!force && previousDiscoveryFailure && previousDiscoveryFailure.until > Date.now()) {
|
|
357
|
+
return {
|
|
358
|
+
state: {
|
|
359
|
+
providerId: adapter.id,
|
|
360
|
+
providerName,
|
|
361
|
+
displayState,
|
|
362
|
+
status: "query-failed",
|
|
363
|
+
message: previousDiscoveryFailure.message,
|
|
364
|
+
},
|
|
365
|
+
fingerprint: auth.fingerprint,
|
|
366
|
+
rememberedTargetId: expectedTargetId,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const target = await resolveUsageTarget(
|
|
379
370
|
adapter,
|
|
380
371
|
auth,
|
|
372
|
+
expectedTargetId,
|
|
381
373
|
signal,
|
|
382
|
-
|
|
374
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
383
375
|
guard,
|
|
384
|
-
querySettings,
|
|
385
376
|
);
|
|
386
|
-
if (
|
|
377
|
+
if (target.kind === "selection-required") {
|
|
378
|
+
if (displayState === "current") {
|
|
379
|
+
transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
state: {
|
|
383
|
+
providerId: adapter.id,
|
|
384
|
+
providerName,
|
|
385
|
+
displayState,
|
|
386
|
+
status: "selection-required",
|
|
387
|
+
singularLabel: adapter.targets?.singularLabel ?? "target",
|
|
388
|
+
pluralLabel: adapter.targets?.pluralLabel ?? "targets",
|
|
389
|
+
choices: target.choices,
|
|
390
|
+
},
|
|
391
|
+
fingerprint: auth.fingerprint,
|
|
392
|
+
rememberedTargetId: expectedTargetId,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
queryFingerprint = adapter.targets
|
|
396
|
+
? `${auth.fingerprint}:target:${target.targetId ?? "none"}`
|
|
397
|
+
: auth.fingerprint;
|
|
398
|
+
failureKey = `${adapter.id}:${queryFingerprint}`;
|
|
399
|
+
if (displayState === "current") {
|
|
400
|
+
transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
|
|
401
|
+
}
|
|
402
|
+
const cached = !force ? cache.get(adapter.id, queryFingerprint) : undefined;
|
|
403
|
+
if (cached) {
|
|
404
|
+
return {
|
|
405
|
+
state: {
|
|
406
|
+
providerId: adapter.id,
|
|
407
|
+
providerName,
|
|
408
|
+
displayState,
|
|
409
|
+
status: "ready",
|
|
410
|
+
report: cached,
|
|
411
|
+
},
|
|
412
|
+
fingerprint: auth.fingerprint,
|
|
413
|
+
rememberedTargetId: expectedTargetId,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const previousFailure = failureBackoff.get(failureKey);
|
|
417
|
+
if (!force && previousFailure && previousFailure.until > Date.now()) {
|
|
418
|
+
return {
|
|
419
|
+
state: {
|
|
420
|
+
providerId: adapter.id,
|
|
421
|
+
providerName,
|
|
422
|
+
displayState,
|
|
423
|
+
status: "query-failed",
|
|
424
|
+
message: previousFailure.message,
|
|
425
|
+
},
|
|
426
|
+
fingerprint: auth.fingerprint,
|
|
427
|
+
rememberedTargetId: expectedTargetId,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
failureBackoff.delete(failureKey);
|
|
431
|
+
querySequence += 1;
|
|
432
|
+
queryId = querySequence;
|
|
433
|
+
setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
|
|
434
|
+
const report = await queryProviderUsage(
|
|
435
|
+
adapter,
|
|
436
|
+
auth,
|
|
437
|
+
signal,
|
|
438
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
439
|
+
requiresRequestBoundaryGuard ? guard : undefined,
|
|
440
|
+
target.targetId,
|
|
441
|
+
);
|
|
442
|
+
if (requiresRequestBoundaryGuard) await guard();
|
|
443
|
+
const effectiveReport = { ...report, providerName };
|
|
387
444
|
if (latestQueries.get(failureKey) === queryId) {
|
|
388
|
-
cache.set(adapter.id, queryFingerprint,
|
|
445
|
+
cache.set(adapter.id, queryFingerprint, effectiveReport);
|
|
389
446
|
failureBackoff.delete(failureKey);
|
|
390
447
|
}
|
|
391
448
|
return {
|
|
392
449
|
state: {
|
|
393
450
|
providerId: adapter.id,
|
|
394
|
-
providerName
|
|
451
|
+
providerName,
|
|
395
452
|
displayState,
|
|
396
453
|
status: "ready",
|
|
397
|
-
report,
|
|
454
|
+
report: effectiveReport,
|
|
398
455
|
},
|
|
399
456
|
fingerprint: auth.fingerprint,
|
|
457
|
+
rememberedTargetId: expectedTargetId,
|
|
400
458
|
};
|
|
401
459
|
} catch (error) {
|
|
402
460
|
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
@@ -407,7 +465,9 @@ export default function usageExtension(
|
|
|
407
465
|
!requestContextChanged() &&
|
|
408
466
|
Date.now() < deadlineAt
|
|
409
467
|
) {
|
|
410
|
-
if (latestQueries.get(failureKey) === queryId)
|
|
468
|
+
if (queryId !== undefined && latestQueries.get(failureKey) === queryId) {
|
|
469
|
+
latestQueries.delete(failureKey);
|
|
470
|
+
}
|
|
411
471
|
return queryAdapterState(
|
|
412
472
|
ctx,
|
|
413
473
|
adapter,
|
|
@@ -423,7 +483,7 @@ export default function usageExtension(
|
|
|
423
483
|
for (const [key, failure] of failureBackoff) {
|
|
424
484
|
if (failure.until <= now) failureBackoff.delete(key);
|
|
425
485
|
}
|
|
426
|
-
if (latestQueries.get(failureKey) === queryId) {
|
|
486
|
+
if (queryId === undefined || latestQueries.get(failureKey) === queryId) {
|
|
427
487
|
setBoundedMap(
|
|
428
488
|
failureBackoff,
|
|
429
489
|
failureKey,
|
|
@@ -434,16 +494,62 @@ export default function usageExtension(
|
|
|
434
494
|
return {
|
|
435
495
|
state: {
|
|
436
496
|
providerId: adapter.id,
|
|
437
|
-
providerName
|
|
497
|
+
providerName,
|
|
438
498
|
displayState,
|
|
439
499
|
status: "query-failed",
|
|
440
500
|
message,
|
|
441
501
|
},
|
|
442
502
|
fingerprint: auth.fingerprint,
|
|
503
|
+
rememberedTargetId: expectedTargetId,
|
|
443
504
|
};
|
|
444
505
|
}
|
|
445
506
|
};
|
|
446
507
|
|
|
508
|
+
const loadTargetChoices = async (
|
|
509
|
+
ctx: ExtensionContext,
|
|
510
|
+
adapter: UsageProviderAdapter,
|
|
511
|
+
signal: AbortSignal,
|
|
512
|
+
) => {
|
|
513
|
+
if (!adapter.targets) throw new Error("Provider does not support usage targets.");
|
|
514
|
+
const expectedSessionGeneration = sessionGeneration;
|
|
515
|
+
const expectedSessionId = ctx.sessionManager.getSessionId();
|
|
516
|
+
const expectedModel = modelIdentity(ctx.model);
|
|
517
|
+
const expectedTargetId = settingsRuntime.get().settings.selectedTargets[adapter.id];
|
|
518
|
+
const deadlineAt = Date.now() + DEFAULT_TIMEOUT_MS;
|
|
519
|
+
const changed = () =>
|
|
520
|
+
expectedSessionGeneration !== sessionGeneration ||
|
|
521
|
+
ctx.sessionManager.getSessionId() !== expectedSessionId ||
|
|
522
|
+
modelIdentity(ctx.model) !== expectedModel ||
|
|
523
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId;
|
|
524
|
+
const auth = await awaitWithDeadline(
|
|
525
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
526
|
+
signal,
|
|
527
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
528
|
+
`resolving ${providerDisplayName(ctx, adapter.id)} runtime auth`,
|
|
529
|
+
);
|
|
530
|
+
if (!auth || signal.aborted || changed()) throw abortError();
|
|
531
|
+
const guard = async () => {
|
|
532
|
+
if (signal.aborted || changed()) throw abortError();
|
|
533
|
+
const revalidated = await awaitWithDeadline(
|
|
534
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
535
|
+
signal,
|
|
536
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
537
|
+
`revalidating ${providerDisplayName(ctx, adapter.id)} runtime auth`,
|
|
538
|
+
);
|
|
539
|
+
if (signal.aborted || changed() || revalidated?.fingerprint !== auth.fingerprint) {
|
|
540
|
+
throw abortError();
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
const choices = await listUsageTargets(
|
|
544
|
+
adapter,
|
|
545
|
+
auth,
|
|
546
|
+
signal,
|
|
547
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
548
|
+
guard,
|
|
549
|
+
);
|
|
550
|
+
return { choices, fingerprint: auth.fingerprint };
|
|
551
|
+
};
|
|
552
|
+
|
|
447
553
|
const queryCurrentState = async (
|
|
448
554
|
ctx: ExtensionContext,
|
|
449
555
|
model: PiModel | undefined,
|
|
@@ -516,7 +622,9 @@ export default function usageExtension(
|
|
|
516
622
|
) => {
|
|
517
623
|
void refreshCurrentStatus(ctx, model, force).catch((error) => {
|
|
518
624
|
if (isStaleExtensionContextError(error) || isAbortError(error)) return;
|
|
519
|
-
|
|
625
|
+
const message = errorMessage(error);
|
|
626
|
+
console.error("[pi-usage] refresh failed:", message);
|
|
627
|
+
safeSetStatus(ctx, `usage err: ${message.slice(0, 50)}`);
|
|
520
628
|
});
|
|
521
629
|
};
|
|
522
630
|
|
|
@@ -558,6 +666,10 @@ export default function usageExtension(
|
|
|
558
666
|
return false;
|
|
559
667
|
}
|
|
560
668
|
const adapter = adapterForProvider(model?.provider);
|
|
669
|
+
const selectionStillCurrent =
|
|
670
|
+
!adapter?.targets ||
|
|
671
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId;
|
|
672
|
+
if (!selectionStillCurrent) return false;
|
|
561
673
|
if (outcome.authState === "unavailable") {
|
|
562
674
|
if (!adapter) return false;
|
|
563
675
|
try {
|
|
@@ -570,6 +682,9 @@ export default function usageExtension(
|
|
|
570
682
|
return (
|
|
571
683
|
generation === statusGeneration &&
|
|
572
684
|
modelIdentity(ctx.model) === modelIdentity(model) &&
|
|
685
|
+
(!adapter.targets ||
|
|
686
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] ===
|
|
687
|
+
outcome.rememberedTargetId) &&
|
|
573
688
|
auth === undefined
|
|
574
689
|
);
|
|
575
690
|
} catch (error) {
|
|
@@ -589,6 +704,9 @@ export default function usageExtension(
|
|
|
589
704
|
return (
|
|
590
705
|
generation === statusGeneration &&
|
|
591
706
|
modelIdentity(ctx.model) === modelIdentity(model) &&
|
|
707
|
+
(!adapter.targets ||
|
|
708
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] ===
|
|
709
|
+
outcome.rememberedTargetId) &&
|
|
592
710
|
auth?.fingerprint === outcome.fingerprint
|
|
593
711
|
);
|
|
594
712
|
} catch (error) {
|
|
@@ -654,6 +772,121 @@ export default function usageExtension(
|
|
|
654
772
|
let redemptionId: string | undefined;
|
|
655
773
|
let resetOutcome: CodexResetOutcome | undefined;
|
|
656
774
|
let resetFailure: string | undefined;
|
|
775
|
+
const actionableTargetState = (): ProviderUsageState | undefined => {
|
|
776
|
+
if (visibleStates.length !== 1) return undefined;
|
|
777
|
+
const state = visibleStates[0];
|
|
778
|
+
return state &&
|
|
779
|
+
(state.status === "ready" || state.status === "selection-required") &&
|
|
780
|
+
adapterForProvider(state.providerId)?.targets
|
|
781
|
+
? state
|
|
782
|
+
: undefined;
|
|
783
|
+
};
|
|
784
|
+
const promptForTarget = async (
|
|
785
|
+
state: ProviderUsageState,
|
|
786
|
+
stateFingerprint?: string,
|
|
787
|
+
): Promise<boolean> => {
|
|
788
|
+
const adapter = adapterForProvider(state.providerId);
|
|
789
|
+
if (!adapter?.targets) return false;
|
|
790
|
+
const expectedRememberedTargetId =
|
|
791
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id];
|
|
792
|
+
const snapshot =
|
|
793
|
+
state.status === "selection-required" && stateFingerprint
|
|
794
|
+
? { choices: state.choices, fingerprint: stateFingerprint }
|
|
795
|
+
: await runMenuOperation(
|
|
796
|
+
ctx,
|
|
797
|
+
`Loading ${adapter.targets.pluralLabel}…`,
|
|
798
|
+
controller.signal,
|
|
799
|
+
(signal) => loadTargetChoices(ctx, adapter, signal),
|
|
800
|
+
);
|
|
801
|
+
if (!snapshot || controller.signal.aborted || statusGeneration !== menuGeneration) {
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
const selectOptions = createUsageTargetSelectOptions(snapshot.choices);
|
|
805
|
+
const selected = await ctx.ui.select(
|
|
806
|
+
`Select ${adapter.targets.singularLabel} for ${providerDisplayName(ctx, adapter.id)}`,
|
|
807
|
+
[...selectOptions.options],
|
|
808
|
+
{ signal: controller.signal },
|
|
809
|
+
);
|
|
810
|
+
if (
|
|
811
|
+
selected === undefined ||
|
|
812
|
+
controller.signal.aborted ||
|
|
813
|
+
statusGeneration !== menuGeneration ||
|
|
814
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedRememberedTargetId
|
|
815
|
+
) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
const targetId = selectOptions.targetIdFor(selected);
|
|
819
|
+
if (!targetId) return false;
|
|
820
|
+
const revalidated = await runMenuOperation(
|
|
821
|
+
ctx,
|
|
822
|
+
`Revalidating ${adapter.targets.singularLabel}…`,
|
|
823
|
+
controller.signal,
|
|
824
|
+
(signal) => loadTargetChoices(ctx, adapter, signal),
|
|
825
|
+
);
|
|
826
|
+
if (
|
|
827
|
+
!revalidated ||
|
|
828
|
+
controller.signal.aborted ||
|
|
829
|
+
statusGeneration !== menuGeneration ||
|
|
830
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedRememberedTargetId
|
|
831
|
+
) {
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
if (
|
|
835
|
+
revalidated.fingerprint !== snapshot.fingerprint ||
|
|
836
|
+
!revalidated.choices.some((choice) => choice.id === targetId)
|
|
837
|
+
) {
|
|
838
|
+
ctx.ui.notify(
|
|
839
|
+
`${providerDisplayName(ctx, adapter.id)} ${adapter.targets.pluralLabel} changed; choose again.`,
|
|
840
|
+
"warning",
|
|
841
|
+
);
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
let saved: Readonly<UsageSettingsState> | undefined;
|
|
845
|
+
try {
|
|
846
|
+
saved = await runMenuOperation(
|
|
847
|
+
ctx,
|
|
848
|
+
`Saving ${adapter.targets.singularLabel}…`,
|
|
849
|
+
controller.signal,
|
|
850
|
+
(signal) =>
|
|
851
|
+
settingsRuntime.updateSelectedTarget(adapter.id, targetId, signal, async () => {
|
|
852
|
+
let published: Awaited<ReturnType<typeof loadTargetChoices>>;
|
|
853
|
+
try {
|
|
854
|
+
published = await loadTargetChoices(ctx, adapter, signal);
|
|
855
|
+
} catch (error) {
|
|
856
|
+
if (
|
|
857
|
+
signal.aborted ||
|
|
858
|
+
controller.signal.aborted ||
|
|
859
|
+
statusGeneration !== menuGeneration ||
|
|
860
|
+
isStaleExtensionContextError(error)
|
|
861
|
+
) {
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
864
|
+
throw new UsageTargetSelectionChangedError();
|
|
865
|
+
}
|
|
866
|
+
if (
|
|
867
|
+
published.fingerprint !== snapshot.fingerprint ||
|
|
868
|
+
!published.choices.some((choice) => choice.id === targetId)
|
|
869
|
+
) {
|
|
870
|
+
throw new UsageTargetSelectionChangedError();
|
|
871
|
+
}
|
|
872
|
+
}),
|
|
873
|
+
);
|
|
874
|
+
} catch (error) {
|
|
875
|
+
if (error instanceof UsageTargetSelectionChangedError) {
|
|
876
|
+
ctx.ui.notify(
|
|
877
|
+
`${providerDisplayName(ctx, adapter.id)} ${adapter.targets.pluralLabel} changed; choose again.`,
|
|
878
|
+
"warning",
|
|
879
|
+
);
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
throw error;
|
|
883
|
+
}
|
|
884
|
+
if (!saved || controller.signal.aborted || statusGeneration !== menuGeneration) {
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
invalidateProviderState(adapter.id);
|
|
888
|
+
return true;
|
|
889
|
+
};
|
|
657
890
|
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
658
891
|
if (controller.signal.aborted || statusGeneration !== menuGeneration) return;
|
|
659
892
|
type Screen =
|
|
@@ -666,6 +899,7 @@ export default function usageExtension(
|
|
|
666
899
|
type Action =
|
|
667
900
|
| "refresh"
|
|
668
901
|
| "settings"
|
|
902
|
+
| "target"
|
|
669
903
|
| "toggle-fast"
|
|
670
904
|
| "another"
|
|
671
905
|
| "all"
|
|
@@ -681,6 +915,8 @@ export default function usageExtension(
|
|
|
681
915
|
screens: {
|
|
682
916
|
main: () => {
|
|
683
917
|
const fastAvailability = fastRuntime.availability(ctx.model);
|
|
918
|
+
const targetState = actionableTargetState();
|
|
919
|
+
const targetAdapter = adapterForProvider(targetState?.providerId);
|
|
684
920
|
const fastLines =
|
|
685
921
|
fastAvailability.kind === "available"
|
|
686
922
|
? [`Fast mode: ${fastAvailability.enabled ? "On" : "Off"}`, FAST_USAGE_WARNING]
|
|
@@ -694,6 +930,15 @@ export default function usageExtension(
|
|
|
694
930
|
items: [
|
|
695
931
|
{ id: "refresh", label: REFRESH_CURRENT, action: "refresh" },
|
|
696
932
|
{ id: "settings", label: SETTINGS, action: "settings" },
|
|
933
|
+
...(targetState && targetAdapter?.targets
|
|
934
|
+
? [
|
|
935
|
+
{
|
|
936
|
+
id: "target",
|
|
937
|
+
label: `${targetState.status === "selection-required" ? "Select" : "Change"} ${targetAdapter.targets.singularLabel}…`,
|
|
938
|
+
action: "target" as const,
|
|
939
|
+
},
|
|
940
|
+
]
|
|
941
|
+
: []),
|
|
697
942
|
...(fastAvailability.kind === "available"
|
|
698
943
|
? [
|
|
699
944
|
{
|
|
@@ -735,7 +980,7 @@ export default function usageExtension(
|
|
|
735
980
|
.filter((adapter) => adapter.id !== ctx.model?.provider)
|
|
736
981
|
.map((adapter) => ({
|
|
737
982
|
id: adapter.id,
|
|
738
|
-
label: adapter.
|
|
983
|
+
label: providerDisplayName(ctx, adapter.id),
|
|
739
984
|
action: "provider" as const,
|
|
740
985
|
})),
|
|
741
986
|
hint: "back",
|
|
@@ -799,6 +1044,61 @@ export default function usageExtension(
|
|
|
799
1044
|
}),
|
|
800
1045
|
},
|
|
801
1046
|
actions: {
|
|
1047
|
+
target: async () => {
|
|
1048
|
+
const targetState = actionableTargetState();
|
|
1049
|
+
if (!targetState) return { kind: "rejected" };
|
|
1050
|
+
try {
|
|
1051
|
+
const stateFingerprint =
|
|
1052
|
+
targetState === current.state ? current.fingerprint : undefined;
|
|
1053
|
+
if (!(await promptForTarget(targetState, stateFingerprint))) {
|
|
1054
|
+
return { kind: "stay" };
|
|
1055
|
+
}
|
|
1056
|
+
const adapter = adapterForProvider(targetState.providerId);
|
|
1057
|
+
if (!adapter) return { kind: "rejected" };
|
|
1058
|
+
if (targetState.providerId === ctx.model?.provider) {
|
|
1059
|
+
const refreshed = await queryStableCurrent(
|
|
1060
|
+
ctx,
|
|
1061
|
+
true,
|
|
1062
|
+
controller,
|
|
1063
|
+
`Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
|
|
1064
|
+
);
|
|
1065
|
+
if (!refreshed) return { kind: "stay" };
|
|
1066
|
+
stableCurrent = refreshed;
|
|
1067
|
+
current = refreshed.outcome;
|
|
1068
|
+
visibleStates = [current.state];
|
|
1069
|
+
publishStableCurrent(ctx, refreshed);
|
|
1070
|
+
return { kind: "stay" };
|
|
1071
|
+
}
|
|
1072
|
+
const outcome = await runMenuOperation(
|
|
1073
|
+
ctx,
|
|
1074
|
+
`Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
|
|
1075
|
+
controller.signal,
|
|
1076
|
+
(signal) => queryAdapterState(ctx, adapter, "configured", true, signal),
|
|
1077
|
+
);
|
|
1078
|
+
if (!outcome) return { kind: "stay" };
|
|
1079
|
+
const revalidated = await queryStableCurrent(
|
|
1080
|
+
ctx,
|
|
1081
|
+
false,
|
|
1082
|
+
controller,
|
|
1083
|
+
"Revalidating current usage…",
|
|
1084
|
+
);
|
|
1085
|
+
if (!revalidated) return { kind: "stay" };
|
|
1086
|
+
stableCurrent = revalidated;
|
|
1087
|
+
current = revalidated.outcome;
|
|
1088
|
+
visibleStates = [
|
|
1089
|
+
outcome.state.providerId === current.state.providerId
|
|
1090
|
+
? current.state
|
|
1091
|
+
: { ...outcome.state, displayState: "configured" },
|
|
1092
|
+
];
|
|
1093
|
+
return { kind: "stay" };
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
if (isAbortError(error) || isStaleExtensionContextError(error)) {
|
|
1096
|
+
return { kind: "stay" };
|
|
1097
|
+
}
|
|
1098
|
+
ctx.ui.notify(`Could not select target: ${errorMessage(error)}`, "error");
|
|
1099
|
+
return { kind: "stay" };
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
802
1102
|
settings: async () => {
|
|
803
1103
|
await showUsageSettings(
|
|
804
1104
|
ctx,
|
|
@@ -1023,13 +1323,25 @@ export default function usageExtension(
|
|
|
1023
1323
|
(candidate) => candidate.id === itemId && candidate.id !== ctx.model?.provider,
|
|
1024
1324
|
);
|
|
1025
1325
|
if (!adapter) return { kind: "back" };
|
|
1026
|
-
|
|
1326
|
+
let outcome = await runMenuOperation(
|
|
1027
1327
|
ctx,
|
|
1028
|
-
`Checking ${adapter.
|
|
1328
|
+
`Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
|
|
1029
1329
|
controller.signal,
|
|
1030
1330
|
(signal) => queryAdapterState(ctx, adapter, "configured", false, signal),
|
|
1031
1331
|
);
|
|
1032
1332
|
if (!outcome) return { kind: "back" };
|
|
1333
|
+
if (outcome.state.status === "selection-required") {
|
|
1334
|
+
if (!(await promptForTarget(outcome.state, outcome.fingerprint))) {
|
|
1335
|
+
return { kind: "back" };
|
|
1336
|
+
}
|
|
1337
|
+
outcome = await runMenuOperation(
|
|
1338
|
+
ctx,
|
|
1339
|
+
`Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
|
|
1340
|
+
controller.signal,
|
|
1341
|
+
(signal) => queryAdapterState(ctx, adapter, "configured", true, signal),
|
|
1342
|
+
);
|
|
1343
|
+
if (!outcome) return { kind: "back" };
|
|
1344
|
+
}
|
|
1033
1345
|
const revalidated = await queryStableCurrent(
|
|
1034
1346
|
ctx,
|
|
1035
1347
|
false,
|
|
@@ -1076,7 +1388,7 @@ export default function usageExtension(
|
|
|
1076
1388
|
const adapter = adapters[index] as UsageProviderAdapter;
|
|
1077
1389
|
return {
|
|
1078
1390
|
providerId: adapter.id,
|
|
1079
|
-
providerName: adapter.
|
|
1391
|
+
providerName: providerDisplayName(ctx, adapter.id),
|
|
1080
1392
|
displayState: "configured",
|
|
1081
1393
|
status: "query-failed",
|
|
1082
1394
|
message: errorMessage(result.reason),
|