@bacnh85/pi-sub 0.1.12 → 0.1.13

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.
Files changed (2) hide show
  1. package/extensions/index.ts +31 -35
  2. package/package.json +1 -1
@@ -66,7 +66,6 @@ interface SubscriptionUsageSnapshot {
66
66
  activeAccount?: SubscriptionAccountSnapshot;
67
67
  fetchedAt: number;
68
68
  error?: string;
69
- cost?: number;
70
69
  }
71
70
 
72
71
  type SubscriptionProviderAdapter = {
@@ -89,6 +88,7 @@ interface State {
89
88
  lastTokPerSec?: number;
90
89
  cumulativeOutput: number;
91
90
  cumulativeDurationMs: number;
91
+ cumulativeCost: number;
92
92
  }
93
93
 
94
94
  function isCodexModel(model: ModelLike): boolean {
@@ -213,9 +213,9 @@ function formatRemainingTime(resetAtSec: number | undefined): string | undefined
213
213
  if (remainingSec <= 0) return "0M";
214
214
  const remainingMin = Math.ceil(remainingSec / 60);
215
215
  if (remainingMin < 60) return `${remainingMin}M`;
216
- const remainingH = Math.ceil(remainingMin / 60);
216
+ const remainingH = Math.ceil(remainingSec / 3600);
217
217
  if (remainingH < 24) return `${remainingH}H`;
218
- const remainingD = Math.ceil(remainingH / 24);
218
+ const remainingD = Math.ceil(remainingSec / 86400);
219
219
  return `${remainingD}D`;
220
220
  }
221
221
 
@@ -276,7 +276,7 @@ function parseUsageResponse(body: unknown): UsageApiSnapshot | undefined {
276
276
  async function readPiCodexAuth(): Promise<PiAuthEntry & { accountId: string }> {
277
277
  const auth = await readJsonFile<PiAuthFile>(piAuthPath());
278
278
  const entry = auth[CODEX_PROVIDER];
279
- const accountId = getCodexAccountId(entry!);
279
+ const accountId = getCodexAccountId(entry);
280
280
  if (!entry?.access || !accountId) throw new Error("Missing openai-codex OAuth entry in Pi auth");
281
281
  return { ...entry, accountId };
282
282
  }
@@ -388,7 +388,7 @@ interface ZaiUsageApiResponse {
388
388
  interface ZaiUsageApiError {
389
389
  code: number;
390
390
  msg: string;
391
- success: boolean;
391
+ success?: boolean;
392
392
  }
393
393
 
394
394
  function zaiLimitToUsageWindow(limit: ZaiLimitEntry): UsageWindow | undefined {
@@ -430,9 +430,11 @@ async function fetchZaiUsage(signal?: AbortSignal): Promise<SubscriptionUsageSna
430
430
  const body = await response.json();
431
431
 
432
432
  // Z.ai returns HTTP 200 even on auth errors: {"code":401,"msg":"...","success":false}
433
+ // Also handle missing success field, empty msg, or presence of code.
433
434
  const apiError = body as ZaiUsageApiError;
434
- if (typeof apiError.success === "boolean" && !apiError.success && apiError.msg) {
435
- throw new Error(`Z.ai API error: ${apiError.msg}`);
435
+ if (apiError.code >= 400 || (typeof apiError.success === "boolean" && !apiError.success) || (apiError.msg && apiError.msg.length > 0 && apiError.success === undefined)) {
436
+ const message = apiError.msg || `HTTP status ${apiError.code}`;
437
+ throw new Error(`Z.ai API error: ${message}`);
436
438
  }
437
439
 
438
440
  const parsed = body as ZaiUsageApiResponse;
@@ -481,9 +483,8 @@ function supportedAdapter(model: ModelLike): SubscriptionProviderAdapter | undef
481
483
 
482
484
  function formatRemaining(window: UsageWindow | undefined): string {
483
485
  if (!window) return "?";
484
- if (window.remaining !== undefined && window.remainingLabel) return `${window.remaining}%/${window.remainingLabel}`;
486
+ if (window.remainingLabel) return `${window.remaining}%/${window.remainingLabel}`;
485
487
  if (window.remaining !== undefined) return `${window.remaining}%`;
486
- if (window.percent !== undefined && window.remainingLabel) return `${Math.max(0, 100 - window.percent)}%/${window.remainingLabel}`;
487
488
  return "?";
488
489
  }
489
490
 
@@ -503,16 +504,6 @@ function windowSegments(account: SubscriptionAccountSnapshot | undefined): strin
503
504
  return segments;
504
505
  }
505
506
 
506
- function aggregateSessionCost(ctx: ExtensionContext): number {
507
- let total = 0;
508
- for (const entry of ctx.sessionManager.getBranch()) {
509
- if (entry.type === "message" && entry.message.role === "assistant") {
510
- total += (entry.message.usage as any)?.cost?.total ?? 0;
511
- }
512
- }
513
- return total;
514
- }
515
-
516
507
  function renderSubscriptionLine(ctx: ExtensionContext, state: State): void {
517
508
  if (!state.adapter) {
518
509
  ctx.ui.setStatus(STATUS_KEY, undefined);
@@ -532,9 +523,9 @@ function renderSubscriptionLine(ctx: ExtensionContext, state: State): void {
532
523
  const windowParts = windowSegments(account);
533
524
  const accountPart = formatFooterAccount(account);
534
525
  const segments = accountPart ? [accountPart, ...windowParts] : [...windowParts];
535
- const cost = snapshot.cost;
526
+ const cost = state.cumulativeCost;
536
527
  const hasWindows = windowParts.length > 0;
537
- if (cost !== undefined && cost > 0) segments.push(`$${cost.toFixed(2)}`);
528
+ if (cost > 0) segments.push(`$${cost.toFixed(2)}`);
538
529
  if (state.lastTokPerSec !== undefined) segments.push(`${state.lastTokPerSec} tok/s`);
539
530
  if (segments.length === 0) {
540
531
  line = `Sub ${state.adapter.displayName}`;
@@ -597,7 +588,6 @@ async function refreshUsage(ctx: ExtensionContext, state: State, force: boolean)
597
588
  renderSubscriptionLine(ctx, state);
598
589
  state.inFlight = adapter.fetchUsage(ctx.signal).then((snapshot) => {
599
590
  if (state.refreshGeneration !== generation) return snapshot;
600
- snapshot.cost = aggregateSessionCost(ctx);
601
591
  state.snapshot = snapshot;
602
592
  state.lastRefreshAt = Date.now();
603
593
  renderSubscriptionLine(ctx, state);
@@ -628,7 +618,7 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
628
618
  if (!snapshot) return "Subscription usage has not been loaded yet.";
629
619
  if (snapshot.error) return `${snapshot.providerDisplayName}: ${snapshot.error}`;
630
620
  if (snapshot.accounts.length === 0) {
631
- const costLine = snapshot.cost !== undefined && snapshot.cost > 0 ? `\nSession cost: $${snapshot.cost.toFixed(2)}` : "";
621
+ const costLine = state.cumulativeCost > 0 ? `\nSession cost: $${state.cumulativeCost.toFixed(2)}` : "";
632
622
  const modelInfo = state.model?.id ? ` · Model: ${state.model.id}` : "";
633
623
  return `Provider: ${snapshot.providerDisplayName}${modelInfo} · Fetched: ${new Date(snapshot.fetchedAt).toLocaleTimeString()}\n${snapshot.providerDisplayName} does not expose usage windows.${costLine}`;
634
624
  }
@@ -660,7 +650,7 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
660
650
  return `${row.active} ${cols.join(" ")} ${row.snapshot.lastActivity ?? ""}`;
661
651
  });
662
652
 
663
- const costLine = snapshot.cost !== undefined ? `\nSession cost: $${snapshot.cost.toFixed(2)}` : "";
653
+ const costLine = state.cumulativeCost > 0 ? `\nSession cost: $${state.cumulativeCost.toFixed(2)}` : "";
664
654
  const tokPerSecLine = state.lastTokPerSec !== undefined
665
655
  ? `\nLast response: ${state.lastTokPerSec} tok/s` +
666
656
  (state.cumulativeDurationMs > 0
@@ -675,7 +665,7 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
675
665
  }
676
666
 
677
667
  export default function (pi: ExtensionAPI) {
678
- const state: State = { lastRefreshAt: 0, refreshGeneration: 0, cumulativeOutput: 0, cumulativeDurationMs: 0 };
668
+ const state: State = { lastRefreshAt: 0, refreshGeneration: 0, cumulativeOutput: 0, cumulativeDurationMs: 0, cumulativeCost: 0 };
679
669
 
680
670
  pi.on("session_start", async (_event, ctx) => {
681
671
  updateActiveAdapter(ctx, state, ctx.model);
@@ -692,20 +682,26 @@ export default function (pi: ExtensionAPI) {
692
682
  });
693
683
 
694
684
  pi.on("message_end", async (event, ctx) => {
695
- if (event.message.role === "assistant" && state.responseStartTime) {
696
- const output = (event.message.usage as any)?.output ?? 0;
697
- const elapsed = Date.now() - state.responseStartTime;
698
- state.responseStartTime = undefined;
699
- if (elapsed > 0 && output > 0) {
700
- state.lastTokPerSec = Math.round(output / (elapsed / 1000));
701
- state.cumulativeOutput += output;
702
- state.cumulativeDurationMs += elapsed;
703
- if (state.adapter) renderSubscriptionLine(ctx, state);
685
+ if (event.message.role === "assistant") {
686
+ state.cumulativeCost += (event.message.usage as any)?.cost?.total ?? 0;
687
+ if (state.responseStartTime) {
688
+ const output = (event.message.usage as any)?.output ?? 0;
689
+ const elapsed = Date.now() - state.responseStartTime;
690
+ state.responseStartTime = undefined;
691
+ if (elapsed > 0 && output > 0) {
692
+ state.lastTokPerSec = Math.round(output / (elapsed / 1000));
693
+ state.cumulativeOutput += output;
694
+ state.cumulativeDurationMs += elapsed;
695
+ }
704
696
  }
697
+ if (state.adapter) renderSubscriptionLine(ctx, state);
705
698
  }
706
699
  });
707
700
 
708
- pi.on("after_provider_response", async (_event, ctx) => {
701
+ pi.on("after_provider_response", async (event, ctx) => {
702
+ if (event.status >= 400) {
703
+ state.responseStartTime = undefined;
704
+ }
709
705
  if (state.adapter) scheduleRefresh(ctx, state);
710
706
  });
711
707
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-sub",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Pi extension showing subscription usage for supported model providers.",
5
5
  "type": "module",
6
6
  "license": "MIT",