@narumitw/pi-usage 0.61.0 → 0.61.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/docs/providers.md CHANGED
@@ -5,6 +5,7 @@
5
5
  This reference preserves each provider's endpoint, authentication boundary, billing semantics, and contract evidence.
6
6
  The README contains the capability overview and shared security requirements.
7
7
 
8
+ - [OpenAI native ChatGPT OAuth](#openai-native-chatgpt-oauth)
8
9
  - [OpenAI Codex](#openai-codex)
9
10
  - [Kimi For Coding](#kimi-for-coding)
10
11
  - [Moonshot AI](#moonshot-ai-api-balance)
@@ -21,9 +22,35 @@ The README contains the capability overview and shared security requirements.
21
22
 
22
23
  ## 📋 Provider semantics
23
24
 
25
+ ### OpenAI native ChatGPT OAuth
26
+
27
+ - Provider ID: `openai`
28
+ - Semantics: verified ChatGPT plan authentication, not measured subscription usage
29
+ - Source: Pi's fresh native OAuth resolution and an exact complete credential match; no usage HTTP endpoint
30
+ - Displayed data: **Connected (native OAuth)**, an explicit numerical-usage/reset-time limitation, and [ChatGPT usage settings](https://chatgpt.com/settings/usage)
31
+ - Statusline: `chatgpt usage: web only`
32
+
33
+ Use `/login openai` in a Pi runtime that supports native Sign in with ChatGPT, then select an official `https://api.openai.com` model.
34
+ Authentication requires Pi's freshly resolved provider source to be `OAuth`, matching effective model and provider authorization, and a complete matching credential with `clientId` and the `chatgpt.tokens.use.direct` scope.
35
+ Equivalent duplicate candidates are accepted; conflicting, incomplete, or mismatched native grants fail closed.
36
+ Stored OAuth alone and Pi's cached OAuth flag do not establish the active runtime account.
37
+ Runtime/API-key overrides, model-level Authorization replacements or removals, and custom/proxy origins cannot inherit native subscription status.
38
+ OpenAI API-key usage reporting remains unsupported.
39
+
40
+ The report verifies authentication, not the plan tier, allowance, remaining quota, or reset time.
41
+ Automatic refresh only revalidates auth; it does not poll a usage endpoint.
42
+ The settings link is guidance, not an automatically opened browser or a promise that the browser is signed into Pi's account.
43
+ Native tokens are never sent to `/backend-api/wham`, and opaque native metadata is not decoded as a legacy Codex account ID.
44
+ Legacy Fast payload rewriting, pricing correction, reset countdowns, and earned reset redemption do not apply to native `openai`.
45
+ Existing `openai-codex` behavior below is unchanged.
46
+
47
+ The boundary is verified against Pi's [`openai-chatgpt.ts`](https://github.com/earendil-works/pi/blob/d86654abb8862e201933517d6f1fce9f88dd117f/packages/ai/src/auth/oauth/openai-chatgpt.ts) at `d86654abb8862e201933517d6f1fce9f88dd117f` and OpenAI's [models and inference](https://developers.openai.com/siwc/token-sharing-open-source/models-and-inference), [token reference](https://developers.openai.com/siwc/token-sharing-open-source/token-reference), and [UI/UX guidelines](https://developers.openai.com/siwc/ui-ux-guidelines).
48
+ These contracts establish the public inference and authentication boundary, not a numerical usage API.
49
+ Verification uses synthetic credentials with Pi's real auth resolver; live browser login and account-specific quota retrieval have not been verified.
50
+
24
51
  ### OpenAI Codex
25
52
 
26
- - Provider ID: `openai-codex`
53
+ - Provider ID: `openai-codex` (legacy; not an alias for native `openai`)
27
54
  - Semantics: ChatGPT consumer subscription limits
28
55
  - Source: the Codex usage and earned-reset endpoints using Pi's resolved runtime authorization
29
56
  - Displayed data: returned duration-based windows, resets, credits, earned usage-limit resets, and additional model buckets
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-usage",
3
- "version": "0.61.0",
3
+ "version": "0.61.2",
4
4
  "description": "Pi extension that shows current-account usage and DeepSeek API balance for supported providers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -57,10 +57,10 @@
57
57
  "@earendil-works/pi-tui": "*"
58
58
  },
59
59
  "devDependencies": {
60
- "@earendil-works/pi-ai": "0.86.0",
61
- "@earendil-works/pi-coding-agent": "0.86.0",
62
- "@earendil-works/pi-tui": "0.86.0",
63
- "@types/node": "26.6.2",
60
+ "@earendil-works/pi-ai": "1.0.0",
61
+ "@earendil-works/pi-coding-agent": "1.0.0",
62
+ "@earendil-works/pi-tui": "1.0.0",
63
+ "@types/node": "26.6.4",
64
64
  "esbuild": "0.28.2",
65
65
  "typescript": "7.0.2"
66
66
  },
package/src/format.ts CHANGED
@@ -21,7 +21,9 @@ export function formatUsageReport(report: UsageReport, displayState: UsageDispla
21
21
  ? report.source === "minimax-account-balance"
22
22
  ? `${report.providerName} API Balance`
23
23
  : `${report.providerName} Token Plan`
24
- : `${report.providerName} Usage`;
24
+ : report.source === "openai-chatgpt-auth"
25
+ ? `${report.providerName} ChatGPT Plan Status`
26
+ : `${report.providerName} Usage`;
25
27
  const lines = [`${title} · ${stateLabel}`];
26
28
  if (report.accountLabel) lines.push(`Account: ${report.accountLabel}`);
27
29
  lines.push(`Semantics: ${report.semantics.label}`, "");
@@ -57,6 +59,7 @@ export function formatUsageStatusline(
57
59
  showCodexResetCountdown = true,
58
60
  codexStatusPercentage: CodexStatusPercentage = "remaining",
59
61
  ): string | undefined {
62
+ if (report.providerId === "openai" && report.source === "openai-chatgpt-auth") return "chatgpt usage: web only";
60
63
  if (report.providerId === "baseten") return formatBasetenStatusline(report);
61
64
  if (report.providerId === "openai-codex") {
62
65
  return formatCodexStatusline(report, model, now, showCodexResetCountdown, codexStatusPercentage);
@@ -76,5 +76,5 @@ function deepSeekCurrency(value: unknown): DeepSeekCurrency | undefined {
76
76
  }
77
77
 
78
78
  function decimalAmount(value: unknown): value is string {
79
- return typeof value === "string" && value.length <= 64 && /^(?:0|[1-9]\d*)(?:\.\d+)?$/u.test(value);
79
+ return typeof value === "string" && value.length <= 64 && /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/u.test(value);
80
80
  }
@@ -0,0 +1,107 @@
1
+ import type { PiModel, ResolvedUsageAuth, UsageProviderAdapter } from "../types.js";
2
+
3
+ export const CHATGPT_USAGE_SETTINGS_URL = "https://chatgpt.com/settings/usage";
4
+ const DIRECT_TOKEN_SCOPE = "chatgpt.tokens.use.direct";
5
+
6
+ type RequestAuth = {
7
+ apiKey?: string;
8
+ headers?: Record<string, string | null>;
9
+ };
10
+
11
+ export class UnsupportedOpenAIUsageAuthError extends Error {
12
+ override readonly name = "UnsupportedOpenAIUsageAuthError";
13
+
14
+ constructor() {
15
+ super("OpenAI API-key usage reporting is not supported; ChatGPT plan status requires native /login openai OAuth.");
16
+ }
17
+ }
18
+
19
+ // Native Sign in with ChatGPT grants access to api.openai.com, not the legacy wham contract.
20
+ // Report only verified authentication, never invented quotas or backend endpoint compatibility.
21
+ // https://developers.openai.com/siwc/token-sharing-open-source/token-reference
22
+ // https://developers.openai.com/siwc/ui-ux-guidelines
23
+ export const OPENAI_CHATGPT_ADAPTER: UsageProviderAdapter = {
24
+ id: "openai",
25
+ displayName: "OpenAI",
26
+ semantics: { kind: "consumer-subscription", label: "ChatGPT plan authentication status" },
27
+ async query(_auth, signal, _timeoutMs, guard) {
28
+ if (!guard) throw new Error("ChatGPT plan status requires runtime-auth revalidation.");
29
+ signal.throwIfAborted();
30
+ await guard();
31
+ signal.throwIfAborted();
32
+ return {
33
+ providerId: "openai",
34
+ providerName: "OpenAI",
35
+ capturedAt: Date.now(),
36
+ source: "openai-chatgpt-auth",
37
+ semantics: { kind: "consumer-subscription", label: "ChatGPT plan authentication status" },
38
+ buckets: [],
39
+ metrics: [{ id: "plan-auth", label: "ChatGPT plan authentication", value: "Connected (native OAuth)" }],
40
+ notes: [
41
+ "Numerical usage and reset times are unavailable in pi-usage for native OpenAI OAuth.",
42
+ `Manage usage: ${CHATGPT_USAGE_SETTINGS_URL}`,
43
+ "Fast mode and earned reset redemption remain legacy openai-codex features.",
44
+ ],
45
+ };
46
+ },
47
+ };
48
+
49
+ export function resolveOpenAIChatGPTAuth(
50
+ auth: RequestAuth,
51
+ providerAuth: RequestAuth | undefined,
52
+ source: string | undefined,
53
+ model: PiModel,
54
+ candidates: readonly unknown[],
55
+ ): ResolvedUsageAuth {
56
+ // isUsingOAuth() is a cached snapshot; source alone is also insufficient when a model header
57
+ // overrides Authorization. Match both freshly resolved credentials and the exact native grant.
58
+ if (source !== "OAuth") throw new UnsupportedOpenAIUsageAuthError();
59
+ const access = accessToken(auth);
60
+ if (!access || access !== accessToken(providerAuth)) {
61
+ throw new Error("The active OpenAI model authorization does not match its resolved OAuth credential.");
62
+ }
63
+ const matches = new Map<string, string>();
64
+ for (const candidate of candidates) {
65
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
66
+ const credential = candidate as Record<string, unknown>;
67
+ if (credential.type !== "oauth" || credential.access !== access) continue;
68
+ if (
69
+ typeof credential.refresh !== "string" ||
70
+ !credential.refresh ||
71
+ typeof credential.expires !== "number" ||
72
+ !Number.isFinite(credential.expires) ||
73
+ typeof credential.clientId !== "string" ||
74
+ !credential.clientId.trim() ||
75
+ !Array.isArray(credential.scopes) ||
76
+ !credential.scopes.every((scope) => typeof scope === "string") ||
77
+ !credential.scopes.includes(DIRECT_TOKEN_SCOPE)
78
+ ) {
79
+ throw new Error(
80
+ "The matching OpenAI OAuth credential is not a complete native ChatGPT grant; reconnect with /login openai.",
81
+ );
82
+ }
83
+ matches.set(JSON.stringify([credential.clientId, credential.refresh]), credential.refresh);
84
+ }
85
+ if (matches.size > 1)
86
+ throw new Error("Conflicting native ChatGPT credentials match the active OpenAI runtime account.");
87
+ const refresh = matches.values().next().value;
88
+ if (!refresh)
89
+ throw new Error("The active OpenAI runtime account does not match an available native ChatGPT OAuth credential.");
90
+ const authorization = `Bearer ${access}`;
91
+ return {
92
+ apiKey: access,
93
+ headers: { Authorization: authorization },
94
+ fingerprint: "",
95
+ secrets: [access, refresh, authorization],
96
+ model,
97
+ };
98
+ }
99
+
100
+ function accessToken(auth: RequestAuth | undefined): string | undefined {
101
+ if (!auth) return undefined;
102
+ const authorization = Object.entries(auth.headers ?? {}).filter(([name]) => name.toLowerCase() === "authorization");
103
+ if (authorization.length > 1) throw new Error("OpenAI runtime authorization headers are ambiguous.");
104
+ // Explicit null or non-Bearer overrides must not fall back to the underlying OAuth apiKey.
105
+ if (authorization.length === 1) return /^Bearer\s+(.+)$/iu.exec(authorization[0]?.[1] ?? "")?.[1];
106
+ return auth.apiKey;
107
+ }
package/src/query.ts CHANGED
@@ -12,6 +12,11 @@ import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.j
12
12
  import { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
13
13
  import { type MiniMaxProviderId, miniMaxUsageKind, normalizeMiniMaxUsagePayload } from "./providers/minimax.js";
14
14
  import { normalizeMoonshotBalancePayload } from "./providers/moonshot.js";
15
+ import {
16
+ OPENAI_CHATGPT_ADAPTER,
17
+ resolveOpenAIChatGPTAuth,
18
+ UnsupportedOpenAIUsageAuthError,
19
+ } from "./providers/openai-chatgpt.js";
15
20
  import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
16
21
  import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
17
22
  import { normalizeVercelAIGatewayCreditsPayload } from "./providers/vercel-ai-gateway.js";
@@ -95,6 +100,7 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
95
100
  return normalizeBasetenBillingUsagePayload(payload, Date.now());
96
101
  },
97
102
  },
103
+ OPENAI_CHATGPT_ADAPTER,
98
104
  {
99
105
  id: "openai-codex",
100
106
  displayName: "OpenAI Codex",
@@ -344,7 +350,7 @@ export async function resolveUsageAuth(
344
350
  const model = candidateModels(ctx, adapter.id).find((candidate) => hasOfficialOrigin(candidate, adapter.id));
345
351
  if (!model) return undefined;
346
352
  if (
347
- ["github-copilot", "xai"].includes(adapter.id) &&
353
+ ["github-copilot", "openai", "xai"].includes(adapter.id) &&
348
354
  candidateReader?.waitUntilReady &&
349
355
  !(await candidateReader.waitUntilReady(ctx, adapter.id))
350
356
  ) {
@@ -364,9 +370,11 @@ export async function resolveUsageAuth(
364
370
  if (!currentModel || typeof registry.getApiKeyAndHeaders !== "function") return undefined;
365
371
  const result = await registry.getApiKeyAndHeaders(currentModel);
366
372
  if (!result.ok) throw new Error(redactUsageError(result.error));
367
- return authorizationFrom(result) ? result : undefined;
373
+ // Native plan status must inspect explicit empty/null Authorization overrides, not fall
374
+ // back to provider OAuth when the selected model removes or replaces its authorization.
375
+ return adapter.id === "openai" || authorizationFrom(result) ? result : undefined;
368
376
  };
369
- const resolveSelectedAuthLast = ["deepseek", "minimax", "minimax-cn"].includes(adapter.id);
377
+ const resolveSelectedAuthLast = ["deepseek", "minimax", "minimax-cn", "openai"].includes(adapter.id);
370
378
  if (!resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
371
379
  if (typeof registry.getProviderAuth !== "function") {
372
380
  throw new Error("pi-usage requires Pi 0.81.0 or newer to validate resolved provider auth.");
@@ -421,6 +429,18 @@ export async function resolveUsageAuth(
421
429
  ),
422
430
  };
423
431
  };
432
+ if (adapter.id === "openai") {
433
+ // Pi can return empty compatibility model auth without any provider credential.
434
+ if (!providerResult) return undefined;
435
+ if (providerResult.source !== "OAuth") throw new UnsupportedOpenAIUsageAuthError();
436
+ const offered = candidateReader
437
+ ? await candidateReader(ctx, adapter.id)
438
+ : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
439
+ if (!offered.ok) throw new Error("OpenAI OAuth credential discovery failed closed.");
440
+ return finalize(
441
+ resolveOpenAIChatGPTAuth(auth, providerResult.auth, providerResult.source, model, offered.candidates),
442
+ );
443
+ }
424
444
  if (adapter.id === "github-copilot") {
425
445
  const offered = candidateReader
426
446
  ? await candidateReader(ctx, adapter.id)
@@ -897,6 +917,7 @@ function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
897
917
  if (providerId === "baseten") {
898
918
  return ["https://inference.baseten.co", "https://api.baseten.co"].includes(url.origin);
899
919
  }
920
+ if (providerId === "openai") return url.origin === "https://api.openai.com";
900
921
  if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
901
922
  if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
902
923
  if (providerId === "fireworks") return url.origin === "https://api.fireworks.ai";
package/src/usage.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  import { abortError, awaitWithDeadline, errorMessage, runWithConcurrency, UsageCache } from "./core.js";
22
22
  import { formatProviderStates, formatUsageStatusline } from "./format.js";
23
23
  import { createOAuthCredentialCandidateReader } from "./oauth-credential-source.js";
24
+ import { UnsupportedOpenAIUsageAuthError } from "./providers/openai-chatgpt.js";
24
25
  import { adapterForProvider, isStaleExtensionContextError, queryProviderUsage, resolveUsageAuth } from "./query.js";
25
26
  import { createUsageSettingsRuntime, type UsageSettingsRuntime, type UsageSettingsState } from "./settings.js";
26
27
  import type {
@@ -65,7 +66,7 @@ type UsageExtensionDependencies = {
65
66
  type QueryOutcome = {
66
67
  state: ProviderUsageState;
67
68
  fingerprint?: string;
68
- authState?: "unavailable";
69
+ authState?: "unavailable" | "unsupported" | "failed";
69
70
  rememberedTargetId?: string;
70
71
  };
71
72
 
@@ -243,6 +244,12 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
243
244
  );
244
245
  } catch (error) {
245
246
  if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
247
+ const authState =
248
+ error instanceof UnsupportedOpenAIUsageAuthError
249
+ ? "unsupported"
250
+ : adapter.id === "openai" && !isTimeoutError(error)
251
+ ? "failed"
252
+ : undefined;
246
253
  if (displayState === "current") {
247
254
  transitionCurrentIdentity(`${adapter.id}:auth-error`, adapter.id);
248
255
  }
@@ -251,9 +258,11 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
251
258
  providerId: adapter.id,
252
259
  providerName,
253
260
  displayState,
254
- status: isTimeoutError(error) ? "query-failed" : "auth-unavailable",
261
+ status:
262
+ authState === "unsupported" ? "unsupported" : isTimeoutError(error) ? "query-failed" : "auth-unavailable",
255
263
  message: errorMessage(error),
256
264
  },
265
+ ...(authState ? { authState } : {}),
257
266
  };
258
267
  }
259
268
  const requiresRequestBoundaryGuard =
@@ -265,6 +274,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
265
274
  "minimax-cn",
266
275
  "moonshotai",
267
276
  "moonshotai-cn",
277
+ "openai",
268
278
  "vercel-ai-gateway",
269
279
  "xai",
270
280
  "zai",
@@ -598,11 +608,11 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
598
608
  if (generation !== statusGeneration || modelIdentity(ctx.model) !== modelIdentity(model)) {
599
609
  return false;
600
610
  }
601
- const adapter = adapterForProvider(model?.provider);
611
+ const adapter = adapterForProvider(outcome.state.providerId);
602
612
  const selectionStillCurrent =
603
613
  !adapter?.targets || settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId;
604
614
  if (!selectionStillCurrent) return false;
605
- if (outcome.authState === "unavailable") {
615
+ if (outcome.authState) {
606
616
  if (!adapter) return false;
607
617
  try {
608
618
  const auth = await awaitWithDeadline(
@@ -616,11 +626,27 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
616
626
  modelIdentity(ctx.model) === modelIdentity(model) &&
617
627
  (!adapter.targets ||
618
628
  settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId) &&
629
+ outcome.authState === "unavailable" &&
619
630
  auth === undefined
620
631
  );
621
632
  } catch (error) {
622
633
  if (isAbortError(error) || isStaleExtensionContextError(error)) throw error;
623
- return false;
634
+ // Stable failures are still useful. A recovered credential or changed reason must
635
+ // retry, but confirming the same native failure must not create a refresh loop.
636
+ const unchangedFailure =
637
+ (outcome.authState === "unsupported" && error instanceof UnsupportedOpenAIUsageAuthError) ||
638
+ (outcome.authState === "failed" &&
639
+ outcome.state.status === "auth-unavailable" &&
640
+ !(error instanceof UnsupportedOpenAIUsageAuthError) &&
641
+ !isTimeoutError(error) &&
642
+ errorMessage(error) === outcome.state.message);
643
+ return (
644
+ unchangedFailure &&
645
+ generation === statusGeneration &&
646
+ modelIdentity(ctx.model) === modelIdentity(model) &&
647
+ (!adapter.targets ||
648
+ settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId)
649
+ );
624
650
  }
625
651
  }
626
652
  if (!outcome.fingerprint) return true;
@@ -645,6 +671,27 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
645
671
  }
646
672
  };
647
673
 
674
+ const queryStableAdapterState = async (
675
+ ctx: ExtensionContext,
676
+ adapter: UsageProviderAdapter,
677
+ displayState: UsageDisplayState,
678
+ force: boolean,
679
+ signal: AbortSignal,
680
+ ): Promise<QueryOutcome> => {
681
+ if (adapter.id !== "openai") return queryAdapterState(ctx, adapter, displayState, force, signal);
682
+ // Configured/all-provider views must confirm native auth failures too, independently
683
+ // of the selected provider's subsequent revalidation.
684
+ for (let attempt = 0; attempt < 3; attempt += 1) {
685
+ const model = ctx.model;
686
+ const generation = statusGeneration;
687
+ const outcome = await queryAdapterState(ctx, adapter, displayState, force, signal);
688
+ if (signal.aborted) throw abortError();
689
+ if (!outcome.authState || (await outcomeStillCurrent(ctx, model, generation, outcome, signal))) return outcome;
690
+ force = false;
691
+ }
692
+ throw new Error("OpenAI runtime authentication kept changing; reopen /usage to retry.");
693
+ };
694
+
648
695
  const queryStableCurrent = async (
649
696
  ctx: ExtensionCommandContext,
650
697
  force: boolean,
@@ -976,7 +1023,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
976
1023
  ctx,
977
1024
  `Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
978
1025
  controller.signal,
979
- (signal) => queryAdapterState(ctx, adapter, "configured", true, signal),
1026
+ (signal) => queryStableAdapterState(ctx, adapter, "configured", true, signal),
980
1027
  );
981
1028
  if (!outcome) return { kind: "stay" };
982
1029
  const revalidated = await queryStableCurrent(ctx, false, controller, "Revalidating current usage…");
@@ -1189,7 +1236,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
1189
1236
  ctx,
1190
1237
  `Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
1191
1238
  controller.signal,
1192
- (signal) => queryAdapterState(ctx, adapter, "configured", false, signal),
1239
+ (signal) => queryStableAdapterState(ctx, adapter, "configured", false, signal),
1193
1240
  );
1194
1241
  if (!outcome) return { kind: "back" };
1195
1242
  if (outcome.state.status === "selection-required") {
@@ -1200,7 +1247,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
1200
1247
  ctx,
1201
1248
  `Checking ${providerDisplayName(ctx, adapter.id)} usage…`,
1202
1249
  controller.signal,
1203
- (signal) => queryAdapterState(ctx, adapter, "configured", true, signal),
1250
+ (signal) => queryStableAdapterState(ctx, adapter, "configured", true, signal),
1204
1251
  );
1205
1252
  if (!outcome) return { kind: "back" };
1206
1253
  }
@@ -1227,7 +1274,7 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
1227
1274
  adapters,
1228
1275
  ALL_PROVIDER_CONCURRENCY,
1229
1276
  (adapter, _index, workerSignal) =>
1230
- queryAdapterState(
1277
+ queryStableAdapterState(
1231
1278
  ctx,
1232
1279
  adapter,
1233
1280
  adapter.id === currentProviderId ? "current" : "configured",