@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 CHANGED
@@ -4,11 +4,13 @@
4
4
 
5
5
  Inspect usage and DeepSeek API balance for Pi's active provider account, query other configured providers, and toggle Fast mode for supported OpenAI Codex models.
6
6
  The extension keeps each provider's native quota, allowance, and spending semantics instead of treating unlike values as equivalent.
7
+ Native `openai` ChatGPT OAuth shows verified authentication status and a usage-settings link, not numerical quotas or reset times.
7
8
  xAI OAuth subscription reporting follows the reviewed Grok Build contract and runs only after an explicit `/usage` action.
8
9
 
9
10
  ## ✨ Features
10
11
 
11
12
  - Shows active-account usage and next actions through `/usage`.
13
+ - Recognizes native `/login openai` ChatGPT OAuth separately from unsupported OpenAI API-key usage reporting.
12
14
  - Reports subscription allowances, API balances, and spending for the supported providers listed below without mixing their billing semantics.
13
15
  - Toggles persistent Codex Fast routing through `/fast` or the usage menu.
14
16
  - Redeems eligible Codex resets only after fresh account matching and explicit confirmation.
@@ -48,6 +50,8 @@ The package declares `dist/index.ts`, so an unbuilt local checkout must run the
48
50
  Run `/usage` in TUI or RPC mode to inspect the active provider, refresh its usage, or choose another configured provider.
49
51
  When a provider exposes several billing targets, `/usage` asks for one target before querying usage.
50
52
  Run `/fast` to toggle Fast mode for a supported active Codex model.
53
+ For native `/login openai`, select an `openai` model and run `/usage` to verify ChatGPT authentication and find [ChatGPT usage settings](https://chatgpt.com/settings/usage).
54
+ Native OpenAI Fast mode and earned reset redemption are not implemented; those actions remain specific to legacy `openai-codex`.
51
55
 
52
56
  ## 💬 Commands
53
57
 
@@ -127,7 +131,8 @@ Currencies and billing targets remain separate.
127
131
 
128
132
  | Provider | Reported data |
129
133
  | --- | --- |
130
- | OpenAI Codex | Subscription windows, credits, resets, and model buckets |
134
+ | OpenAI (native ChatGPT OAuth) | Verified authentication status and usage-settings link; no numerical quota or reset data |
135
+ | OpenAI Codex (legacy) | Subscription windows, credits, resets, and model buckets |
131
136
  | Kimi For Coding | Plan request windows and a separate booster wallet |
132
137
  | Moonshot AI Global/China | Current API balance in USD/CNY |
133
138
  | MiniMax Global/China | Token Plan windows or pay-as-you-go API balance |
@@ -169,6 +174,8 @@ MiniMax publishes Token Plan window percentages or the regional pay-as-you-go av
169
174
  Baseten publishes the exact trailing 30-day Model APIs net subtotal after credits.
170
175
  xAI is always menu-only and never starts a scheduled status refresh.
171
176
  Z.AI statusline usage refreshes every five minutes while the selected model remains on Z.AI.
177
+ Verified native OpenAI OAuth publishes `chatgpt usage: web only`; its five-minute refresh revalidates authentication locally without calling a usage endpoint.
178
+ OpenAI API-key auth clears this status.
172
179
 
173
180
  Queries for another provider or all providers never publish their results to the statusline.
174
181
  `@narumitw/pi-statusline` supplies the default `📊` icon; `pi-usage` publishes text-only values.
@@ -197,6 +204,9 @@ Behavior changes:
197
204
  Credential candidates are collected synchronously in memory after any compatible readiness promises settle, and are not cached, persisted, logged, formatted, or appended to the Pi session.
198
205
  The protocols carry no account name or extension identity.
199
206
  Only the selected provider's exact runtime match is used, and secrets are sent only to its validated official origin.
207
+ Native OpenAI status requires fresh Pi OAuth resolution and a matching complete ChatGPT grant, including the direct-token scope and client ID.
208
+ Model-level Authorization overrides cannot inherit subscription status from a stored login.
209
+ The native status adapter sends no usage request and never forwards native tokens to legacy ChatGPT backend endpoints.
200
210
  DeepSeek balance requests require Bearer authentication, send only that resolved credential from Pi's runtime auth to `https://api.deepseek.com/user/balance`, and refuse redirects.
201
211
  Fireworks spend requests send only that resolved credential to the official `https://api.fireworks.ai` account-listing and billing-summary endpoints and refuse redirects.
202
212
  Moonshot balance requests send only the resolved Bearer credential to the matching official Global or China balance origin and refuse redirects.
@@ -210,7 +220,8 @@ An absent or incompatible peer preserves standalone fallback and fail-closed mis
210
220
 
211
221
  ## 🚧 Limitations
212
222
 
213
- - Only providers with a meaningful usage source and verifiable Pi runtime auth are supported.
223
+ - Numerical usage requires a meaningful provider usage source and verifiable Pi runtime auth.
224
+ - Native `openai` reports authentication only: numerical usage, reset times, Fast mode, and earned reset redemption are not supported, and OpenAI API-key usage reporting remains unsupported.
214
225
  - GitHub Copilot quota, Kimi managed usage, Z.AI quota, and OpenAI Codex reset redemption rely on provider-owned endpoints that may change without notice.
215
226
  - Codex reset redemption requires a current ChatGPT OAuth credential from Pi's login or a compatible credential source; Codex API keys cannot redeem earned subscription resets.
216
227
  - xAI usage supports only a uniquely matched Pi OAuth subscription credential; xAI API keys and Management API credentials are unsupported.
package/dist/index.ts CHANGED
@@ -1429,6 +1429,80 @@ function amount(value, label, allowNegative) {
1429
1429
  return String(value);
1430
1430
  }
1431
1431
 
1432
+ // src/providers/openai-chatgpt.ts
1433
+ var CHATGPT_USAGE_SETTINGS_URL = "https://chatgpt.com/settings/usage";
1434
+ var DIRECT_TOKEN_SCOPE = "chatgpt.tokens.use.direct";
1435
+ var UnsupportedOpenAIUsageAuthError = class extends Error {
1436
+ name = "UnsupportedOpenAIUsageAuthError";
1437
+ constructor() {
1438
+ super("OpenAI API-key usage reporting is not supported; ChatGPT plan status requires native /login openai OAuth.");
1439
+ }
1440
+ };
1441
+ var OPENAI_CHATGPT_ADAPTER = {
1442
+ id: "openai",
1443
+ displayName: "OpenAI",
1444
+ semantics: { kind: "consumer-subscription", label: "ChatGPT plan authentication status" },
1445
+ async query(_auth, signal, _timeoutMs, guard) {
1446
+ if (!guard) throw new Error("ChatGPT plan status requires runtime-auth revalidation.");
1447
+ signal.throwIfAborted();
1448
+ await guard();
1449
+ signal.throwIfAborted();
1450
+ return {
1451
+ providerId: "openai",
1452
+ providerName: "OpenAI",
1453
+ capturedAt: Date.now(),
1454
+ source: "openai-chatgpt-auth",
1455
+ semantics: { kind: "consumer-subscription", label: "ChatGPT plan authentication status" },
1456
+ buckets: [],
1457
+ metrics: [{ id: "plan-auth", label: "ChatGPT plan authentication", value: "Connected (native OAuth)" }],
1458
+ notes: [
1459
+ "Numerical usage and reset times are unavailable in pi-usage for native OpenAI OAuth.",
1460
+ `Manage usage: ${CHATGPT_USAGE_SETTINGS_URL}`,
1461
+ "Fast mode and earned reset redemption remain legacy openai-codex features."
1462
+ ]
1463
+ };
1464
+ }
1465
+ };
1466
+ function resolveOpenAIChatGPTAuth(auth, providerAuth, source, model, candidates) {
1467
+ if (source !== "OAuth") throw new UnsupportedOpenAIUsageAuthError();
1468
+ const access = accessToken(auth);
1469
+ if (!access || access !== accessToken(providerAuth)) {
1470
+ throw new Error("The active OpenAI model authorization does not match its resolved OAuth credential.");
1471
+ }
1472
+ const matches = /* @__PURE__ */ new Map();
1473
+ for (const candidate of candidates) {
1474
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
1475
+ const credential = candidate;
1476
+ if (credential.type !== "oauth" || credential.access !== access) continue;
1477
+ if (typeof credential.refresh !== "string" || !credential.refresh || typeof credential.expires !== "number" || !Number.isFinite(credential.expires) || typeof credential.clientId !== "string" || !credential.clientId.trim() || !Array.isArray(credential.scopes) || !credential.scopes.every((scope) => typeof scope === "string") || !credential.scopes.includes(DIRECT_TOKEN_SCOPE)) {
1478
+ throw new Error(
1479
+ "The matching OpenAI OAuth credential is not a complete native ChatGPT grant; reconnect with /login openai."
1480
+ );
1481
+ }
1482
+ matches.set(JSON.stringify([credential.clientId, credential.refresh]), credential.refresh);
1483
+ }
1484
+ if (matches.size > 1)
1485
+ throw new Error("Conflicting native ChatGPT credentials match the active OpenAI runtime account.");
1486
+ const refresh = matches.values().next().value;
1487
+ if (!refresh)
1488
+ throw new Error("The active OpenAI runtime account does not match an available native ChatGPT OAuth credential.");
1489
+ const authorization = `Bearer ${access}`;
1490
+ return {
1491
+ apiKey: access,
1492
+ headers: { Authorization: authorization },
1493
+ fingerprint: "",
1494
+ secrets: [access, refresh, authorization],
1495
+ model
1496
+ };
1497
+ }
1498
+ function accessToken(auth) {
1499
+ if (!auth) return void 0;
1500
+ const authorization = Object.entries(auth.headers ?? {}).filter(([name]) => name.toLowerCase() === "authorization");
1501
+ if (authorization.length > 1) throw new Error("OpenAI runtime authorization headers are ambiguous.");
1502
+ if (authorization.length === 1) return /^Bearer\s+(.+)$/iu.exec(authorization[0]?.[1] ?? "")?.[1];
1503
+ return auth.apiKey;
1504
+ }
1505
+
1432
1506
  // src/providers/opencode-zen.ts
1433
1507
  var ZEN_WINDOWS = [
1434
1508
  { key: "rolling", label: "Rolling" },
@@ -2144,6 +2218,7 @@ var SUPPORTED_ADAPTERS = [
2144
2218
  return normalizeBasetenBillingUsagePayload(payload, Date.now());
2145
2219
  }
2146
2220
  },
2221
+ OPENAI_CHATGPT_ADAPTER,
2147
2222
  {
2148
2223
  id: "openai-codex",
2149
2224
  displayName: "OpenAI Codex",
@@ -2368,7 +2443,7 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
2368
2443
  }
2369
2444
  const model = candidateModels(ctx, adapter.id).find((candidate) => hasOfficialOrigin(candidate, adapter.id));
2370
2445
  if (!model) return void 0;
2371
- if (["github-copilot", "xai"].includes(adapter.id) && candidateReader?.waitUntilReady && !await candidateReader.waitUntilReady(ctx, adapter.id)) {
2446
+ if (["github-copilot", "openai", "xai"].includes(adapter.id) && candidateReader?.waitUntilReady && !await candidateReader.waitUntilReady(ctx, adapter.id)) {
2372
2447
  throw new Error(`${adapter.displayName} OAuth credential readiness failed closed.`);
2373
2448
  }
2374
2449
  const registry = ctx.modelRegistry;
@@ -2384,9 +2459,9 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
2384
2459
  if (!currentModel || typeof registry.getApiKeyAndHeaders !== "function") return void 0;
2385
2460
  const result = await registry.getApiKeyAndHeaders(currentModel);
2386
2461
  if (!result.ok) throw new Error(redactUsageError(result.error));
2387
- return authorizationFrom(result) ? result : void 0;
2462
+ return adapter.id === "openai" || authorizationFrom(result) ? result : void 0;
2388
2463
  };
2389
- const resolveSelectedAuthLast = ["deepseek", "minimax", "minimax-cn"].includes(adapter.id);
2464
+ const resolveSelectedAuthLast = ["deepseek", "minimax", "minimax-cn", "openai"].includes(adapter.id);
2390
2465
  if (!resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
2391
2466
  if (typeof registry.getProviderAuth !== "function") {
2392
2467
  throw new Error("pi-usage requires Pi 0.81.0 or newer to validate resolved provider auth.");
@@ -2439,6 +2514,15 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
2439
2514
  )
2440
2515
  };
2441
2516
  };
2517
+ if (adapter.id === "openai") {
2518
+ if (!providerResult) return void 0;
2519
+ if (providerResult.source !== "OAuth") throw new UnsupportedOpenAIUsageAuthError();
2520
+ const offered = candidateReader ? await candidateReader(ctx, adapter.id) : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
2521
+ if (!offered.ok) throw new Error("OpenAI OAuth credential discovery failed closed.");
2522
+ return finalize(
2523
+ resolveOpenAIChatGPTAuth(auth, providerResult.auth, providerResult.source, model, offered.candidates)
2524
+ );
2525
+ }
2442
2526
  if (adapter.id === "github-copilot") {
2443
2527
  const offered = candidateReader ? await candidateReader(ctx, adapter.id) : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
2444
2528
  if (!offered.ok) {
@@ -2805,6 +2889,7 @@ function hasOfficialUrlOrigin(value, providerId) {
2805
2889
  if (providerId === "baseten") {
2806
2890
  return ["https://inference.baseten.co", "https://api.baseten.co"].includes(url.origin);
2807
2891
  }
2892
+ if (providerId === "openai") return url.origin === "https://api.openai.com";
2808
2893
  if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
2809
2894
  if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
2810
2895
  if (providerId === "fireworks") return url.origin === "https://api.fireworks.ai";
@@ -3202,7 +3287,7 @@ var BAR_SEGMENTS = 20;
3202
3287
  var VALUE_COLUMN = 29;
3203
3288
  function formatUsageReport(report2, displayState) {
3204
3289
  const stateLabel = displayState === "current" ? "Current" : "Configured";
3205
- const title = report2.providerId === "baseten" ? "Baseten Model APIs Spend" : report2.providerId === "deepseek" ? "DeepSeek API Balance" : report2.providerId === "fireworks" ? "Fireworks API Spend" : report2.providerId === "vercel-ai-gateway" ? "Vercel AI Gateway Credits" : report2.providerId === "moonshotai" || report2.providerId === "moonshotai-cn" ? `${report2.providerName} Balance` : report2.providerId === "minimax" || report2.providerId === "minimax-cn" ? report2.source === "minimax-account-balance" ? `${report2.providerName} API Balance` : `${report2.providerName} Token Plan` : `${report2.providerName} Usage`;
3290
+ const title = report2.providerId === "baseten" ? "Baseten Model APIs Spend" : report2.providerId === "deepseek" ? "DeepSeek API Balance" : report2.providerId === "fireworks" ? "Fireworks API Spend" : report2.providerId === "vercel-ai-gateway" ? "Vercel AI Gateway Credits" : report2.providerId === "moonshotai" || report2.providerId === "moonshotai-cn" ? `${report2.providerName} Balance` : report2.providerId === "minimax" || report2.providerId === "minimax-cn" ? report2.source === "minimax-account-balance" ? `${report2.providerName} API Balance` : `${report2.providerName} Token Plan` : report2.source === "openai-chatgpt-auth" ? `${report2.providerName} ChatGPT Plan Status` : `${report2.providerName} Usage`;
3206
3291
  const lines = [`${title} \xB7 ${stateLabel}`];
3207
3292
  if (report2.accountLabel) lines.push(`Account: ${report2.accountLabel}`);
3208
3293
  lines.push(`Semantics: ${report2.semantics.label}`, "");
@@ -3229,6 +3314,7 @@ function formatUsageReport(report2, displayState) {
3229
3314
  return lines.join("\n").trimEnd();
3230
3315
  }
3231
3316
  function formatUsageStatusline(report2, model, now = Date.now(), showCodexResetCountdown = true, codexStatusPercentage = "remaining") {
3317
+ if (report2.providerId === "openai" && report2.source === "openai-chatgpt-auth") return "chatgpt usage: web only";
3232
3318
  if (report2.providerId === "baseten") return formatBasetenStatusline(report2);
3233
3319
  if (report2.providerId === "openai-codex") {
3234
3320
  return formatCodexStatusline(report2, model, now, showCodexResetCountdown, codexStatusPercentage);
@@ -4502,6 +4588,7 @@ function usageExtension(pi, dependencies = {}) {
4502
4588
  );
4503
4589
  } catch (error) {
4504
4590
  if (isStaleExtensionContextError(error) || isAbortError3(error)) throw error;
4591
+ const authState = error instanceof UnsupportedOpenAIUsageAuthError ? "unsupported" : adapter.id === "openai" && !isTimeoutError(error) ? "failed" : void 0;
4505
4592
  if (displayState === "current") {
4506
4593
  transitionCurrentIdentity(`${adapter.id}:auth-error`, adapter.id);
4507
4594
  }
@@ -4510,9 +4597,10 @@ function usageExtension(pi, dependencies = {}) {
4510
4597
  providerId: adapter.id,
4511
4598
  providerName,
4512
4599
  displayState,
4513
- status: isTimeoutError(error) ? "query-failed" : "auth-unavailable",
4600
+ status: authState === "unsupported" ? "unsupported" : isTimeoutError(error) ? "query-failed" : "auth-unavailable",
4514
4601
  message: errorMessage(error)
4515
- }
4602
+ },
4603
+ ...authState ? { authState } : {}
4516
4604
  };
4517
4605
  }
4518
4606
  const requiresRequestBoundaryGuard = adapter.targets !== void 0 || [
@@ -4522,6 +4610,7 @@ function usageExtension(pi, dependencies = {}) {
4522
4610
  "minimax-cn",
4523
4611
  "moonshotai",
4524
4612
  "moonshotai-cn",
4613
+ "openai",
4525
4614
  "vercel-ai-gateway",
4526
4615
  "xai",
4527
4616
  "zai",
@@ -4813,10 +4902,10 @@ function usageExtension(pi, dependencies = {}) {
4813
4902
  if (generation !== statusGeneration || modelIdentity(ctx.model) !== modelIdentity(model)) {
4814
4903
  return false;
4815
4904
  }
4816
- const adapter = adapterForProvider(model?.provider);
4905
+ const adapter = adapterForProvider(outcome.state.providerId);
4817
4906
  const selectionStillCurrent = !adapter?.targets || settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId;
4818
4907
  if (!selectionStillCurrent) return false;
4819
- if (outcome.authState === "unavailable") {
4908
+ if (outcome.authState) {
4820
4909
  if (!adapter) return false;
4821
4910
  try {
4822
4911
  const auth = await awaitWithDeadline(
@@ -4825,10 +4914,11 @@ function usageExtension(pi, dependencies = {}) {
4825
4914
  DEFAULT_TIMEOUT_MS,
4826
4915
  `revalidating ${adapter.displayName} runtime auth`
4827
4916
  );
4828
- return generation === statusGeneration && modelIdentity(ctx.model) === modelIdentity(model) && (!adapter.targets || settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId) && auth === void 0;
4917
+ return generation === statusGeneration && modelIdentity(ctx.model) === modelIdentity(model) && (!adapter.targets || settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId) && outcome.authState === "unavailable" && auth === void 0;
4829
4918
  } catch (error) {
4830
4919
  if (isAbortError3(error) || isStaleExtensionContextError(error)) throw error;
4831
- return false;
4920
+ const unchangedFailure = outcome.authState === "unsupported" && error instanceof UnsupportedOpenAIUsageAuthError || outcome.authState === "failed" && outcome.state.status === "auth-unavailable" && !(error instanceof UnsupportedOpenAIUsageAuthError) && !isTimeoutError(error) && errorMessage(error) === outcome.state.message;
4921
+ return unchangedFailure && generation === statusGeneration && modelIdentity(ctx.model) === modelIdentity(model) && (!adapter.targets || settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId);
4832
4922
  }
4833
4923
  }
4834
4924
  if (!outcome.fingerprint) return true;
@@ -4846,6 +4936,18 @@ function usageExtension(pi, dependencies = {}) {
4846
4936
  return false;
4847
4937
  }
4848
4938
  };
4939
+ const queryStableAdapterState = async (ctx, adapter, displayState, force, signal) => {
4940
+ if (adapter.id !== "openai") return queryAdapterState(ctx, adapter, displayState, force, signal);
4941
+ for (let attempt = 0; attempt < 3; attempt += 1) {
4942
+ const model = ctx.model;
4943
+ const generation = statusGeneration;
4944
+ const outcome = await queryAdapterState(ctx, adapter, displayState, force, signal);
4945
+ if (signal.aborted) throw abortError();
4946
+ if (!outcome.authState || await outcomeStillCurrent(ctx, model, generation, outcome, signal)) return outcome;
4947
+ force = false;
4948
+ }
4949
+ throw new Error("OpenAI runtime authentication kept changing; reopen /usage to retry.");
4950
+ };
4849
4951
  const queryStableCurrent = async (ctx, force, controller, label) => {
4850
4952
  for (let attempt = 0; attempt < 3; attempt += 1) {
4851
4953
  const model = ctx.model;
@@ -5117,7 +5219,7 @@ function usageExtension(pi, dependencies = {}) {
5117
5219
  ctx,
5118
5220
  `Checking ${providerDisplayName(ctx, adapter.id)} usage\u2026`,
5119
5221
  controller.signal,
5120
- (signal) => queryAdapterState(ctx, adapter, "configured", true, signal)
5222
+ (signal) => queryStableAdapterState(ctx, adapter, "configured", true, signal)
5121
5223
  );
5122
5224
  if (!outcome) return { kind: "stay" };
5123
5225
  const revalidated = await queryStableCurrent(ctx, false, controller, "Revalidating current usage\u2026");
@@ -5320,7 +5422,7 @@ function usageExtension(pi, dependencies = {}) {
5320
5422
  ctx,
5321
5423
  `Checking ${providerDisplayName(ctx, adapter.id)} usage\u2026`,
5322
5424
  controller.signal,
5323
- (signal) => queryAdapterState(ctx, adapter, "configured", false, signal)
5425
+ (signal) => queryStableAdapterState(ctx, adapter, "configured", false, signal)
5324
5426
  );
5325
5427
  if (!outcome) return { kind: "back" };
5326
5428
  if (outcome.state.status === "selection-required") {
@@ -5331,7 +5433,7 @@ function usageExtension(pi, dependencies = {}) {
5331
5433
  ctx,
5332
5434
  `Checking ${providerDisplayName(ctx, adapter.id)} usage\u2026`,
5333
5435
  controller.signal,
5334
- (signal) => queryAdapterState(ctx, adapter, "configured", true, signal)
5436
+ (signal) => queryStableAdapterState(ctx, adapter, "configured", true, signal)
5335
5437
  );
5336
5438
  if (!outcome) return { kind: "back" };
5337
5439
  }
@@ -5354,7 +5456,7 @@ function usageExtension(pi, dependencies = {}) {
5354
5456
  (signal) => runWithConcurrency(
5355
5457
  adapters,
5356
5458
  ALL_PROVIDER_CONCURRENCY,
5357
- (adapter, _index, workerSignal) => queryAdapterState(
5459
+ (adapter, _index, workerSignal) => queryStableAdapterState(
5358
5460
  ctx,
5359
5461
  adapter,
5360
5462
  adapter.id === currentProviderId ? "current" : "configured",