@narumitw/pi-usage 0.51.0 → 0.52.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.
@@ -0,0 +1,83 @@
1
+ import { sanitizeDisplayText } from "../core.js";
2
+ import type { OpenCodeZenPayload, UsageBucket, UsageReport } from "../types.js";
3
+
4
+ const ZEN_WINDOWS = [
5
+ { key: "rolling", label: "Rolling" },
6
+ { key: "weekly", label: "Weekly" },
7
+ { key: "monthly", label: "Monthly" },
8
+ ] as const;
9
+
10
+ export function normalizeOpenCodeZenPayload(
11
+ payload: OpenCodeZenPayload,
12
+ capturedAt: number,
13
+ ): UsageReport {
14
+ const usage = asObject(payload.usage);
15
+ if (!usage) throw new Error("OpenCode Zen usage response was not an object.");
16
+
17
+ const buckets: UsageBucket[] = [];
18
+ const notes: string[] = [];
19
+ for (const window of ZEN_WINDOWS) {
20
+ const raw = asObject(usage[window.key]);
21
+ if (!raw) continue;
22
+ const status = asString(raw.status);
23
+ if (status !== "ok" && status !== "rate-limited") {
24
+ notes.push(`${window.label} window unavailable (${status ?? "unknown status"}).`);
25
+ continue;
26
+ }
27
+ const used = asNonnegativeNumber(raw.percent);
28
+ if (used === undefined) continue;
29
+ const resetsAt = asEpochSeconds(raw.resetsAt);
30
+ buckets.push({
31
+ id: window.key,
32
+ label: `${window.label} window`,
33
+ used,
34
+ remaining: 100 - clampPercent(used),
35
+ limit: 100,
36
+ unit: "percent",
37
+ ...(resetsAt !== undefined ? { resetsAt } : {}),
38
+ });
39
+ }
40
+ if (buckets.length === 0) {
41
+ throw new Error("OpenCode Zen usage endpoint returned no displayable usage data.");
42
+ }
43
+
44
+ return {
45
+ providerId: "opencode-go",
46
+ providerName: "OpenCode Go",
47
+ capturedAt,
48
+ source: "opencode-zen-usage",
49
+ semantics: {
50
+ kind: "consumer-subscription",
51
+ label: "OpenCode Zen plan usage",
52
+ },
53
+ buckets,
54
+ metrics: [],
55
+ ...(notes.length > 0 ? { notes } : {}),
56
+ };
57
+ }
58
+
59
+ function asObject(value: unknown): Record<string, unknown> | undefined {
60
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
61
+ return value as Record<string, unknown>;
62
+ }
63
+
64
+ function asString(value: unknown): string | undefined {
65
+ if (typeof value !== "string") return undefined;
66
+ return sanitizeDisplayText(value, 80) || undefined;
67
+ }
68
+
69
+ function asNonnegativeNumber(value: unknown): number | undefined {
70
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined;
71
+ return value;
72
+ }
73
+
74
+ function asEpochSeconds(value: unknown): number | undefined {
75
+ if (typeof value !== "string" || !value.trim()) return undefined;
76
+ const parsed = Date.parse(value);
77
+ if (Number.isNaN(parsed)) return undefined;
78
+ return Math.floor(parsed / 1000);
79
+ }
80
+
81
+ function clampPercent(value: number): number {
82
+ return Math.min(100, Math.max(0, value));
83
+ }
package/src/query.ts CHANGED
@@ -3,10 +3,12 @@ import { type ExtensionContext, readStoredCredential } from "@earendil-works/pi-
3
3
  import { errorMessage, fingerprintResolvedAuth, redactUsageError } from "./core.js";
4
4
  import { normalizeCodexBackendPayload } from "./providers/codex.js";
5
5
  import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
6
+ import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
6
7
  import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
7
8
  import type {
8
9
  CodexBackendPayload,
9
10
  GitHubCopilotUsagePayload,
11
+ OpenCodeZenPayload,
10
12
  OpenRouterKeyPayload,
11
13
  PiModel,
12
14
  ResolvedUsageAuth,
@@ -74,6 +76,21 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
74
76
  return normalizeOpenRouterKeyPayload(payload as OpenRouterKeyPayload, Date.now());
75
77
  },
76
78
  },
79
+ {
80
+ id: "opencode-go",
81
+ displayName: "OpenCode Go",
82
+ semantics: { kind: "consumer-subscription", label: "OpenCode Zen plan usage" },
83
+ async query(auth, signal, timeoutMs) {
84
+ const payload = await fetchProviderJson(
85
+ opencodeUsageUrl(auth.model.baseUrl),
86
+ auth,
87
+ signal,
88
+ timeoutMs,
89
+ "OpenCode Zen usage endpoint",
90
+ );
91
+ return normalizeOpenCodeZenPayload(payload as OpenCodeZenPayload, Date.now());
92
+ },
93
+ },
77
94
  ];
78
95
 
79
96
  export function adapterForProvider(
@@ -393,6 +410,7 @@ function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
393
410
  const url = new URL(value);
394
411
  if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
395
412
  if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
413
+ if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
396
414
  if (providerId === "github-copilot") {
397
415
  return (
398
416
  url.protocol === "https:" && /^api\.[a-z0-9-]+\.githubcopilot\.com$/u.test(url.hostname)
@@ -418,6 +436,12 @@ function hasHeader(headers: Record<string, string>, name: string): boolean {
418
436
  return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
419
437
  }
420
438
 
439
+ function opencodeUsageUrl(baseUrl: string | undefined): string {
440
+ const base = baseUrl?.trim().replace(/\/+$/u, "");
441
+ if (!base) throw new Error("OpenCode Go model base URL is unavailable.");
442
+ return `${base}/usage`;
443
+ }
444
+
421
445
  function isAbortError(error: unknown): boolean {
422
446
  return error instanceof Error && error.name === "AbortError";
423
447
  }
package/src/types.ts CHANGED
@@ -93,6 +93,10 @@ export type OpenRouterKeyPayload = {
93
93
  data?: unknown;
94
94
  };
95
95
 
96
+ export type OpenCodeZenPayload = {
97
+ usage?: unknown;
98
+ };
99
+
96
100
  export type CodexBackendPayload = {
97
101
  plan_type?: unknown;
98
102
  rate_limit?: unknown;