@narumitw/pi-usage 0.59.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 +30 -25
- package/dist/index.ts +889 -338
- package/dist/index.ts.map +4 -4
- package/package.json +1 -1
- package/src/core.ts +28 -2
- package/src/format.ts +18 -8
- package/src/index.ts +14 -1
- package/src/providers/fireworks.ts +112 -0
- package/src/providers/zai.ts +109 -5
- package/src/query.ts +171 -175
- package/src/settings.ts +155 -8
- package/src/types.ts +50 -2
- package/src/usage-settings-ui.ts +88 -183
- package/src/usage-targets.ts +143 -0
- package/src/usage.ts +413 -99
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,30 +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
|
-
|
|
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);
|
|
289
310
|
const requestContextChanged = () =>
|
|
290
311
|
expectedSessionGeneration !== sessionGeneration ||
|
|
291
312
|
ctx.sessionManager.getSessionId() !== expectedSessionId ||
|
|
292
313
|
modelIdentity(ctx.model) !== expectedModelIdentity ||
|
|
293
|
-
(adapter.
|
|
294
|
-
settingsRuntime.get().settings.
|
|
314
|
+
(adapter.targets !== undefined &&
|
|
315
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId);
|
|
295
316
|
if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
|
|
296
317
|
if (!auth) {
|
|
297
318
|
if (displayState === "current") {
|
|
@@ -300,101 +321,142 @@ export default function usageExtension(
|
|
|
300
321
|
return {
|
|
301
322
|
state: {
|
|
302
323
|
providerId: adapter.id,
|
|
303
|
-
providerName
|
|
324
|
+
providerName,
|
|
304
325
|
displayState,
|
|
305
326
|
status: "auth-unavailable",
|
|
306
|
-
message: `No runtime credential is configured for ${
|
|
327
|
+
message: `No runtime credential is configured for ${providerName}.`,
|
|
307
328
|
},
|
|
308
329
|
authState: "unavailable",
|
|
309
330
|
};
|
|
310
331
|
}
|
|
311
|
-
const queryFingerprint =
|
|
312
|
-
adapter.id === "fireworks"
|
|
313
|
-
? `${auth.fingerprint}:account:${expectedFireworksAccountId ?? "auto"}`
|
|
314
|
-
: auth.fingerprint;
|
|
315
|
-
if (displayState === "current") {
|
|
316
|
-
transitionCurrentIdentity(`${adapter.id}:${queryFingerprint}`, adapter.id);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const cached = !force ? cache.get(adapter.id, queryFingerprint) : undefined;
|
|
320
|
-
if (cached) {
|
|
321
|
-
return {
|
|
322
|
-
state: {
|
|
323
|
-
providerId: adapter.id,
|
|
324
|
-
providerName: adapter.displayName,
|
|
325
|
-
displayState,
|
|
326
|
-
status: "ready",
|
|
327
|
-
report: cached,
|
|
328
|
-
},
|
|
329
|
-
fingerprint: auth.fingerprint,
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const failureKey = `${adapter.id}:${queryFingerprint}`;
|
|
334
|
-
const previousFailure = failureBackoff.get(failureKey);
|
|
335
|
-
if (!force && previousFailure && previousFailure.until > Date.now()) {
|
|
336
|
-
return {
|
|
337
|
-
state: {
|
|
338
|
-
providerId: adapter.id,
|
|
339
|
-
providerName: adapter.displayName,
|
|
340
|
-
displayState,
|
|
341
|
-
status: "query-failed",
|
|
342
|
-
message: previousFailure.message,
|
|
343
|
-
},
|
|
344
|
-
fingerprint: auth.fingerprint,
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
failureBackoff.delete(failureKey);
|
|
348
|
-
querySequence += 1;
|
|
349
|
-
const queryId = querySequence;
|
|
350
|
-
setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
|
|
351
|
-
|
|
352
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;
|
|
353
356
|
try {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
`${adapter.displayName} runtime credential changed during the usage query.`,
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
throw abortError();
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
: undefined;
|
|
376
|
-
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(
|
|
377
372
|
adapter,
|
|
378
373
|
auth,
|
|
374
|
+
expectedTargetId,
|
|
379
375
|
signal,
|
|
380
|
-
|
|
376
|
+
Math.max(1, deadlineAt - Date.now()),
|
|
381
377
|
guard,
|
|
382
|
-
querySettings,
|
|
383
378
|
);
|
|
384
|
-
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 };
|
|
385
446
|
if (latestQueries.get(failureKey) === queryId) {
|
|
386
|
-
cache.set(adapter.id, queryFingerprint,
|
|
447
|
+
cache.set(adapter.id, queryFingerprint, effectiveReport);
|
|
387
448
|
failureBackoff.delete(failureKey);
|
|
388
449
|
}
|
|
389
450
|
return {
|
|
390
451
|
state: {
|
|
391
452
|
providerId: adapter.id,
|
|
392
|
-
providerName
|
|
453
|
+
providerName,
|
|
393
454
|
displayState,
|
|
394
455
|
status: "ready",
|
|
395
|
-
report,
|
|
456
|
+
report: effectiveReport,
|
|
396
457
|
},
|
|
397
458
|
fingerprint: auth.fingerprint,
|
|
459
|
+
rememberedTargetId: expectedTargetId,
|
|
398
460
|
};
|
|
399
461
|
} catch (error) {
|
|
400
462
|
if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
|
|
@@ -405,7 +467,9 @@ export default function usageExtension(
|
|
|
405
467
|
!requestContextChanged() &&
|
|
406
468
|
Date.now() < deadlineAt
|
|
407
469
|
) {
|
|
408
|
-
if (latestQueries.get(failureKey) === queryId)
|
|
470
|
+
if (queryId !== undefined && latestQueries.get(failureKey) === queryId) {
|
|
471
|
+
latestQueries.delete(failureKey);
|
|
472
|
+
}
|
|
409
473
|
return queryAdapterState(
|
|
410
474
|
ctx,
|
|
411
475
|
adapter,
|
|
@@ -421,7 +485,7 @@ export default function usageExtension(
|
|
|
421
485
|
for (const [key, failure] of failureBackoff) {
|
|
422
486
|
if (failure.until <= now) failureBackoff.delete(key);
|
|
423
487
|
}
|
|
424
|
-
if (latestQueries.get(failureKey) === queryId) {
|
|
488
|
+
if (queryId === undefined || latestQueries.get(failureKey) === queryId) {
|
|
425
489
|
setBoundedMap(
|
|
426
490
|
failureBackoff,
|
|
427
491
|
failureKey,
|
|
@@ -432,16 +496,62 @@ export default function usageExtension(
|
|
|
432
496
|
return {
|
|
433
497
|
state: {
|
|
434
498
|
providerId: adapter.id,
|
|
435
|
-
providerName
|
|
499
|
+
providerName,
|
|
436
500
|
displayState,
|
|
437
501
|
status: "query-failed",
|
|
438
502
|
message,
|
|
439
503
|
},
|
|
440
504
|
fingerprint: auth.fingerprint,
|
|
505
|
+
rememberedTargetId: expectedTargetId,
|
|
441
506
|
};
|
|
442
507
|
}
|
|
443
508
|
};
|
|
444
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
|
+
|
|
445
555
|
const queryCurrentState = async (
|
|
446
556
|
ctx: ExtensionContext,
|
|
447
557
|
model: PiModel | undefined,
|
|
@@ -556,6 +666,10 @@ export default function usageExtension(
|
|
|
556
666
|
return false;
|
|
557
667
|
}
|
|
558
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;
|
|
559
673
|
if (outcome.authState === "unavailable") {
|
|
560
674
|
if (!adapter) return false;
|
|
561
675
|
try {
|
|
@@ -568,6 +682,9 @@ export default function usageExtension(
|
|
|
568
682
|
return (
|
|
569
683
|
generation === statusGeneration &&
|
|
570
684
|
modelIdentity(ctx.model) === modelIdentity(model) &&
|
|
685
|
+
(!adapter.targets ||
|
|
686
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] ===
|
|
687
|
+
outcome.rememberedTargetId) &&
|
|
571
688
|
auth === undefined
|
|
572
689
|
);
|
|
573
690
|
} catch (error) {
|
|
@@ -587,6 +704,9 @@ export default function usageExtension(
|
|
|
587
704
|
return (
|
|
588
705
|
generation === statusGeneration &&
|
|
589
706
|
modelIdentity(ctx.model) === modelIdentity(model) &&
|
|
707
|
+
(!adapter.targets ||
|
|
708
|
+
settingsRuntime.get().settings.selectedTargets[adapter.id] ===
|
|
709
|
+
outcome.rememberedTargetId) &&
|
|
590
710
|
auth?.fingerprint === outcome.fingerprint
|
|
591
711
|
);
|
|
592
712
|
} catch (error) {
|
|
@@ -652,6 +772,121 @@ export default function usageExtension(
|
|
|
652
772
|
let redemptionId: string | undefined;
|
|
653
773
|
let resetOutcome: CodexResetOutcome | undefined;
|
|
654
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
|
+
};
|
|
655
890
|
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
656
891
|
if (controller.signal.aborted || statusGeneration !== menuGeneration) return;
|
|
657
892
|
type Screen =
|
|
@@ -664,6 +899,7 @@ export default function usageExtension(
|
|
|
664
899
|
type Action =
|
|
665
900
|
| "refresh"
|
|
666
901
|
| "settings"
|
|
902
|
+
| "target"
|
|
667
903
|
| "toggle-fast"
|
|
668
904
|
| "another"
|
|
669
905
|
| "all"
|
|
@@ -679,6 +915,8 @@ export default function usageExtension(
|
|
|
679
915
|
screens: {
|
|
680
916
|
main: () => {
|
|
681
917
|
const fastAvailability = fastRuntime.availability(ctx.model);
|
|
918
|
+
const targetState = actionableTargetState();
|
|
919
|
+
const targetAdapter = adapterForProvider(targetState?.providerId);
|
|
682
920
|
const fastLines =
|
|
683
921
|
fastAvailability.kind === "available"
|
|
684
922
|
? [`Fast mode: ${fastAvailability.enabled ? "On" : "Off"}`, FAST_USAGE_WARNING]
|
|
@@ -692,6 +930,15 @@ export default function usageExtension(
|
|
|
692
930
|
items: [
|
|
693
931
|
{ id: "refresh", label: REFRESH_CURRENT, action: "refresh" },
|
|
694
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
|
+
: []),
|
|
695
942
|
...(fastAvailability.kind === "available"
|
|
696
943
|
? [
|
|
697
944
|
{
|
|
@@ -733,7 +980,7 @@ export default function usageExtension(
|
|
|
733
980
|
.filter((adapter) => adapter.id !== ctx.model?.provider)
|
|
734
981
|
.map((adapter) => ({
|
|
735
982
|
id: adapter.id,
|
|
736
|
-
label: adapter.
|
|
983
|
+
label: providerDisplayName(ctx, adapter.id),
|
|
737
984
|
action: "provider" as const,
|
|
738
985
|
})),
|
|
739
986
|
hint: "back",
|
|
@@ -797,6 +1044,61 @@ export default function usageExtension(
|
|
|
797
1044
|
}),
|
|
798
1045
|
},
|
|
799
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
|
+
},
|
|
800
1102
|
settings: async () => {
|
|
801
1103
|
await showUsageSettings(
|
|
802
1104
|
ctx,
|
|
@@ -1021,13 +1323,25 @@ export default function usageExtension(
|
|
|
1021
1323
|
(candidate) => candidate.id === itemId && candidate.id !== ctx.model?.provider,
|
|
1022
1324
|
);
|
|
1023
1325
|
if (!adapter) return { kind: "back" };
|
|
1024
|
-
|
|
1326
|
+
let outcome = await runMenuOperation(
|
|
1025
1327
|
ctx,
|
|
1026
|
-
`Checking ${adapter.
|
|
1328
|
+
`Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
|
|
1027
1329
|
controller.signal,
|
|
1028
1330
|
(signal) => queryAdapterState(ctx, adapter, "configured", false, signal),
|
|
1029
1331
|
);
|
|
1030
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
|
+
}
|
|
1031
1345
|
const revalidated = await queryStableCurrent(
|
|
1032
1346
|
ctx,
|
|
1033
1347
|
false,
|
|
@@ -1074,7 +1388,7 @@ export default function usageExtension(
|
|
|
1074
1388
|
const adapter = adapters[index] as UsageProviderAdapter;
|
|
1075
1389
|
return {
|
|
1076
1390
|
providerId: adapter.id,
|
|
1077
|
-
providerName: adapter.
|
|
1391
|
+
providerName: providerDisplayName(ctx, adapter.id),
|
|
1078
1392
|
displayState: "configured",
|
|
1079
1393
|
status: "query-failed",
|
|
1080
1394
|
message: errorMessage(result.reason),
|