@narumitw/pi-usage 0.60.0 → 0.60.1
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 +754 -300
- package/dist/index.ts.map +4 -4
- package/package.json +1 -1
- package/src/core.ts +28 -2
- package/src/format.ts +3 -0
- package/src/index.ts +13 -0
- package/src/providers/fireworks.ts +112 -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 +413 -101
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;
|
|
@@ -174,7 +188,11 @@ export default function usageExtension(
|
|
|
174
188
|
if (
|
|
175
189
|
safeSetStatus(
|
|
176
190
|
ctx,
|
|
177
|
-
outcome.state.status === "auth-unavailable"
|
|
191
|
+
outcome.state.status === "auth-unavailable"
|
|
192
|
+
? "auth unavailable"
|
|
193
|
+
: outcome.state.status === "selection-required"
|
|
194
|
+
? "selection required"
|
|
195
|
+
: "usage error",
|
|
178
196
|
)
|
|
179
197
|
) {
|
|
180
198
|
if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
|
|
@@ -248,10 +266,10 @@ export default function usageExtension(
|
|
|
248
266
|
const expectedSessionGeneration = sessionGeneration;
|
|
249
267
|
const expectedSessionId = ctx.sessionManager.getSessionId();
|
|
250
268
|
const expectedModelIdentity = modelIdentity(ctx.model);
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
269
|
+
const expectedTargetId = adapter.targets
|
|
270
|
+
? settingsRuntime.get().settings.selectedTargets[adapter.id]
|
|
271
|
+
: undefined;
|
|
272
|
+
const providerName = providerDisplayName(ctx, adapter.id);
|
|
255
273
|
let auth: ResolvedUsageAuth | undefined;
|
|
256
274
|
try {
|
|
257
275
|
auth = await awaitWithDeadline(
|
|
@@ -268,32 +286,33 @@ export default function usageExtension(
|
|
|
268
286
|
return {
|
|
269
287
|
state: {
|
|
270
288
|
providerId: adapter.id,
|
|
271
|
-
providerName
|
|
289
|
+
providerName,
|
|
272
290
|
displayState,
|
|
273
291
|
status: isTimeoutError(error) ? "query-failed" : "auth-unavailable",
|
|
274
292
|
message: errorMessage(error),
|
|
275
293
|
},
|
|
276
294
|
};
|
|
277
295
|
}
|
|
278
|
-
const requiresRequestBoundaryGuard =
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
296
|
+
const requiresRequestBoundaryGuard =
|
|
297
|
+
adapter.targets !== undefined ||
|
|
298
|
+
[
|
|
299
|
+
"baseten",
|
|
300
|
+
"deepseek",
|
|
301
|
+
"minimax",
|
|
302
|
+
"minimax-cn",
|
|
303
|
+
"moonshotai",
|
|
304
|
+
"moonshotai-cn",
|
|
305
|
+
"vercel-ai-gateway",
|
|
306
|
+
"xai",
|
|
307
|
+
"zai",
|
|
308
|
+
"zai-coding-cn",
|
|
309
|
+
].includes(adapter.id);
|
|
291
310
|
const requestContextChanged = () =>
|
|
292
311
|
expectedSessionGeneration !== sessionGeneration ||
|
|
293
312
|
ctx.sessionManager.getSessionId() !== expectedSessionId ||
|
|
294
313
|
modelIdentity(ctx.model) !== expectedModelIdentity ||
|
|
295
|
-
(adapter.
|
|
296
|
-
settingsRuntime.get().settings.
|
|
314
|
+
(adapter.targets !== undefined &&
|
|
315
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId);
|
|
297
316
|
if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
|
|
298
317
|
if (!auth) {
|
|
299
318
|
if (displayState === "current") {
|
|
@@ -302,101 +321,142 @@ export default function usageExtension(
|
|
|
302
321
|
return {
|
|
303
322
|
state: {
|
|
304
323
|
providerId: adapter.id,
|
|
305
|
-
providerName
|
|
324
|
+
providerName,
|
|
306
325
|
displayState,
|
|
307
326
|
status: "auth-unavailable",
|
|
308
|
-
message: `No runtime credential is configured for ${
|
|
327
|
+
message: `No runtime credential is configured for ${providerName}.`,
|
|
309
328
|
},
|
|
310
329
|
authState: "unavailable",
|
|
311
330
|
};
|
|
312
331
|
}
|
|
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
332
|
let retryableAuthChanged = false;
|
|
333
|
+
const guard = async () => {
|
|
334
|
+
if (signal.aborted || requestContextChanged()) throw abortError();
|
|
335
|
+
if (!requiresRequestBoundaryGuard) return;
|
|
336
|
+
const revalidated = await awaitWithDeadline(
|
|
337
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
338
|
+
signal,
|
|
339
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
340
|
+
`revalidating ${providerName} runtime auth`,
|
|
341
|
+
);
|
|
342
|
+
if (signal.aborted || requestContextChanged()) throw abortError();
|
|
343
|
+
if (revalidated?.fingerprint !== auth.fingerprint) {
|
|
344
|
+
if (["deepseek", "minimax", "minimax-cn"].includes(adapter.id)) {
|
|
345
|
+
retryableAuthChanged = true;
|
|
346
|
+
throw new Error(`${providerName} runtime credential changed during the usage query.`);
|
|
347
|
+
}
|
|
348
|
+
throw abortError();
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
let queryFingerprint = adapter.targets
|
|
352
|
+
? `${auth.fingerprint}:target:${expectedTargetId ?? "unresolved"}`
|
|
353
|
+
: auth.fingerprint;
|
|
354
|
+
let failureKey = `${adapter.id}:${queryFingerprint}`;
|
|
355
|
+
let queryId: number | undefined;
|
|
355
356
|
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(
|
|
357
|
+
const previousDiscoveryFailure = failureBackoff.get(failureKey);
|
|
358
|
+
if (!force && previousDiscoveryFailure && previousDiscoveryFailure.until > Date.now()) {
|
|
359
|
+
return {
|
|
360
|
+
state: {
|
|
361
|
+
providerId: adapter.id,
|
|
362
|
+
providerName,
|
|
363
|
+
displayState,
|
|
364
|
+
status: "query-failed",
|
|
365
|
+
message: previousDiscoveryFailure.message,
|
|
366
|
+
},
|
|
367
|
+
fingerprint: auth.fingerprint,
|
|
368
|
+
rememberedTargetId: expectedTargetId,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const target = await resolveUsageTarget(
|
|
379
372
|
adapter,
|
|
380
373
|
auth,
|
|
374
|
+
expectedTargetId,
|
|
381
375
|
signal,
|
|
382
|
-
|
|
376
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
383
377
|
guard,
|
|
384
|
-
querySettings,
|
|
385
378
|
);
|
|
386
|
-
if (
|
|
379
|
+
if (target.kind === "selection-required") {
|
|
380
|
+
if (displayState === "current") {
|
|
381
|
+
transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
state: {
|
|
385
|
+
providerId: adapter.id,
|
|
386
|
+
providerName,
|
|
387
|
+
displayState,
|
|
388
|
+
status: "selection-required",
|
|
389
|
+
singularLabel: adapter.targets?.singularLabel ?? "target",
|
|
390
|
+
pluralLabel: adapter.targets?.pluralLabel ?? "targets",
|
|
391
|
+
choices: target.choices,
|
|
392
|
+
},
|
|
393
|
+
fingerprint: auth.fingerprint,
|
|
394
|
+
rememberedTargetId: expectedTargetId,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
queryFingerprint = adapter.targets
|
|
398
|
+
? `${auth.fingerprint}:target:${target.targetId ?? "none"}`
|
|
399
|
+
: auth.fingerprint;
|
|
400
|
+
failureKey = `${adapter.id}:${queryFingerprint}`;
|
|
401
|
+
if (displayState === "current") {
|
|
402
|
+
transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
|
|
403
|
+
}
|
|
404
|
+
const cached = !force ? cache.get(adapter.id, queryFingerprint) : undefined;
|
|
405
|
+
if (cached) {
|
|
406
|
+
return {
|
|
407
|
+
state: {
|
|
408
|
+
providerId: adapter.id,
|
|
409
|
+
providerName,
|
|
410
|
+
displayState,
|
|
411
|
+
status: "ready",
|
|
412
|
+
report: cached,
|
|
413
|
+
},
|
|
414
|
+
fingerprint: auth.fingerprint,
|
|
415
|
+
rememberedTargetId: expectedTargetId,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const previousFailure = failureBackoff.get(failureKey);
|
|
419
|
+
if (!force && previousFailure && previousFailure.until > Date.now()) {
|
|
420
|
+
return {
|
|
421
|
+
state: {
|
|
422
|
+
providerId: adapter.id,
|
|
423
|
+
providerName,
|
|
424
|
+
displayState,
|
|
425
|
+
status: "query-failed",
|
|
426
|
+
message: previousFailure.message,
|
|
427
|
+
},
|
|
428
|
+
fingerprint: auth.fingerprint,
|
|
429
|
+
rememberedTargetId: expectedTargetId,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
failureBackoff.delete(failureKey);
|
|
433
|
+
querySequence += 1;
|
|
434
|
+
queryId = querySequence;
|
|
435
|
+
setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
|
|
436
|
+
const report = await queryProviderUsage(
|
|
437
|
+
adapter,
|
|
438
|
+
auth,
|
|
439
|
+
signal,
|
|
440
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
441
|
+
requiresRequestBoundaryGuard ? guard : undefined,
|
|
442
|
+
target.targetId,
|
|
443
|
+
);
|
|
444
|
+
if (requiresRequestBoundaryGuard) await guard();
|
|
445
|
+
const effectiveReport = { ...report, providerName };
|
|
387
446
|
if (latestQueries.get(failureKey) === queryId) {
|
|
388
|
-
cache.set(adapter.id, queryFingerprint,
|
|
447
|
+
cache.set(adapter.id, queryFingerprint, effectiveReport);
|
|
389
448
|
failureBackoff.delete(failureKey);
|
|
390
449
|
}
|
|
391
450
|
return {
|
|
392
451
|
state: {
|
|
393
452
|
providerId: adapter.id,
|
|
394
|
-
providerName
|
|
453
|
+
providerName,
|
|
395
454
|
displayState,
|
|
396
455
|
status: "ready",
|
|
397
|
-
report,
|
|
456
|
+
report: effectiveReport,
|
|
398
457
|
},
|
|
399
458
|
fingerprint: auth.fingerprint,
|
|
459
|
+
rememberedTargetId: expectedTargetId,
|
|
400
460
|
};
|
|
401
461
|
} catch (error) {
|
|
402
462
|
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
@@ -407,7 +467,9 @@ export default function usageExtension(
|
|
|
407
467
|
!requestContextChanged() &&
|
|
408
468
|
Date.now() < deadlineAt
|
|
409
469
|
) {
|
|
410
|
-
if (latestQueries.get(failureKey) === queryId)
|
|
470
|
+
if (queryId !== undefined && latestQueries.get(failureKey) === queryId) {
|
|
471
|
+
latestQueries.delete(failureKey);
|
|
472
|
+
}
|
|
411
473
|
return queryAdapterState(
|
|
412
474
|
ctx,
|
|
413
475
|
adapter,
|
|
@@ -423,7 +485,7 @@ export default function usageExtension(
|
|
|
423
485
|
for (const [key, failure] of failureBackoff) {
|
|
424
486
|
if (failure.until <= now) failureBackoff.delete(key);
|
|
425
487
|
}
|
|
426
|
-
if (latestQueries.get(failureKey) === queryId) {
|
|
488
|
+
if (queryId === undefined || latestQueries.get(failureKey) === queryId) {
|
|
427
489
|
setBoundedMap(
|
|
428
490
|
failureBackoff,
|
|
429
491
|
failureKey,
|
|
@@ -434,16 +496,62 @@ export default function usageExtension(
|
|
|
434
496
|
return {
|
|
435
497
|
state: {
|
|
436
498
|
providerId: adapter.id,
|
|
437
|
-
providerName
|
|
499
|
+
providerName,
|
|
438
500
|
displayState,
|
|
439
501
|
status: "query-failed",
|
|
440
502
|
message,
|
|
441
503
|
},
|
|
442
504
|
fingerprint: auth.fingerprint,
|
|
505
|
+
rememberedTargetId: expectedTargetId,
|
|
443
506
|
};
|
|
444
507
|
}
|
|
445
508
|
};
|
|
446
509
|
|
|
510
|
+
const loadTargetChoices = async (
|
|
511
|
+
ctx: ExtensionContext,
|
|
512
|
+
adapter: UsageProviderAdapter,
|
|
513
|
+
signal: AbortSignal,
|
|
514
|
+
) => {
|
|
515
|
+
if (!adapter.targets) throw new Error("Provider does not support usage targets.");
|
|
516
|
+
const expectedSessionGeneration = sessionGeneration;
|
|
517
|
+
const expectedSessionId = ctx.sessionManager.getSessionId();
|
|
518
|
+
const expectedModel = modelIdentity(ctx.model);
|
|
519
|
+
const expectedTargetId = settingsRuntime.get().settings.selectedTargets[adapter.id];
|
|
520
|
+
const deadlineAt = Date.now() + DEFAULT_TIMEOUT_MS;
|
|
521
|
+
const changed = () =>
|
|
522
|
+
expectedSessionGeneration !== sessionGeneration ||
|
|
523
|
+
ctx.sessionManager.getSessionId() !== expectedSessionId ||
|
|
524
|
+
modelIdentity(ctx.model) !== expectedModel ||
|
|
525
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId;
|
|
526
|
+
const auth = await awaitWithDeadline(
|
|
527
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
528
|
+
signal,
|
|
529
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
530
|
+
`resolving ${providerDisplayName(ctx, adapter.id)} runtime auth`,
|
|
531
|
+
);
|
|
532
|
+
if (!auth || signal.aborted || changed()) throw abortError();
|
|
533
|
+
const guard = async () => {
|
|
534
|
+
if (signal.aborted || changed()) throw abortError();
|
|
535
|
+
const revalidated = await awaitWithDeadline(
|
|
536
|
+
resolveUsageAuth(ctx, adapter, undefined, credentialReader, credentialCandidates),
|
|
537
|
+
signal,
|
|
538
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
539
|
+
`revalidating ${providerDisplayName(ctx, adapter.id)} runtime auth`,
|
|
540
|
+
);
|
|
541
|
+
if (signal.aborted || changed() || revalidated?.fingerprint !== auth.fingerprint) {
|
|
542
|
+
throw abortError();
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
const choices = await listUsageTargets(
|
|
546
|
+
adapter,
|
|
547
|
+
auth,
|
|
548
|
+
signal,
|
|
549
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
550
|
+
guard,
|
|
551
|
+
);
|
|
552
|
+
return { choices, fingerprint: auth.fingerprint };
|
|
553
|
+
};
|
|
554
|
+
|
|
447
555
|
const queryCurrentState = async (
|
|
448
556
|
ctx: ExtensionContext,
|
|
449
557
|
model: PiModel | undefined,
|
|
@@ -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),
|