@narumitw/pi-usage 0.61.1 → 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/README.md +13 -2
- package/dist/index.ts +116 -14
- package/dist/index.ts.map +3 -3
- package/docs/providers.md +28 -1
- package/package.json +5 -5
- package/src/format.ts +4 -1
- package/src/providers/openai-chatgpt.ts +107 -0
- package/src/query.ts +24 -3
- package/src/usage.ts +56 -9
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.
|
|
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.
|
|
61
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
62
|
-
"@earendil-works/pi-tui": "0.
|
|
63
|
-
"@types/node": "26.6.
|
|
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
|
-
:
|
|
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);
|
|
@@ -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
|
-
|
|
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:
|
|
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(
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
1277
|
+
queryStableAdapterState(
|
|
1231
1278
|
ctx,
|
|
1232
1279
|
adapter,
|
|
1233
1280
|
adapter.id === currentProviderId ? "current" : "configured",
|