@narumitw/pi-usage 0.60.6 → 0.60.8

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.
@@ -1,150 +1,139 @@
1
1
  import { sanitizeDisplayText } from "../core.js";
2
2
  import type { CodexBackendPayload, UsageBucket, UsageMetric, UsageReport } from "../types.js";
3
3
 
4
- export function normalizeCodexBackendPayload(
5
- payload: CodexBackendPayload,
6
- capturedAt: number,
7
- ): UsageReport {
8
- const buckets: UsageBucket[] = [];
9
- normalizeRateLimitGroup(buckets, "codex", "Codex", payload.rate_limit, false);
4
+ export function normalizeCodexBackendPayload(payload: CodexBackendPayload, capturedAt: number): UsageReport {
5
+ const buckets: UsageBucket[] = [];
6
+ normalizeRateLimitGroup(buckets, "codex", "Codex", payload.rate_limit, false);
10
7
 
11
- const additional = Array.isArray(payload.additional_rate_limits)
12
- ? payload.additional_rate_limits
13
- : [];
14
- for (const item of additional) {
15
- const value = asObject(item);
16
- const id = asString(value?.metered_feature) ?? asString(value?.limit_name);
17
- if (!value || !id) continue;
18
- try {
19
- normalizeRateLimitGroup(
20
- buckets,
21
- id,
22
- asString(value.limit_name) ?? id,
23
- value.rate_limit,
24
- true,
25
- );
26
- } catch {
27
- // Optional provider-specific buckets must not hide otherwise useful primary data.
28
- }
29
- }
8
+ const additional = Array.isArray(payload.additional_rate_limits) ? payload.additional_rate_limits : [];
9
+ for (const item of additional) {
10
+ const value = asObject(item);
11
+ const id = asString(value?.metered_feature) ?? asString(value?.limit_name);
12
+ if (!value || !id) continue;
13
+ try {
14
+ normalizeRateLimitGroup(buckets, id, asString(value.limit_name) ?? id, value.rate_limit, true);
15
+ } catch {
16
+ // Optional provider-specific buckets must not hide otherwise useful primary data.
17
+ }
18
+ }
30
19
 
31
- const metrics: UsageMetric[] = [];
32
- const credits = asObject(payload.credits);
33
- if (credits?.has_credits === true) {
34
- if (credits.unlimited === true) {
35
- metrics.push({ id: "credits", label: "Credits", value: "unlimited" });
36
- } else {
37
- const balance = asNumber(credits.balance);
38
- if (balance !== undefined) {
39
- metrics.push({ id: "credits", label: "Credits", value: balance, unit: "count" });
40
- } else {
41
- metrics.push({ id: "credits", label: "Credits", value: "available" });
42
- }
43
- }
44
- } else if (credits?.has_credits === false) {
45
- metrics.push({ id: "credits", label: "Credits", value: "none" });
46
- }
47
- const resetCredits = asObject(payload.rate_limit_reset_credits);
48
- const resetCount = asNonnegativeInteger(resetCredits?.available_count);
49
- if (resetCount !== undefined) {
50
- metrics.push({
51
- id: "reset-credits",
52
- label: "Usage limit resets",
53
- value: resetCount,
54
- unit: "count",
55
- });
56
- }
57
- if (buckets.length === 0 && metrics.length === 0) {
58
- throw new Error("Codex usage endpoint returned no displayable usage data.");
59
- }
20
+ const metrics: UsageMetric[] = [];
21
+ const credits = asObject(payload.credits);
22
+ if (credits?.has_credits === true) {
23
+ if (credits.unlimited === true) {
24
+ metrics.push({ id: "credits", label: "Credits", value: "unlimited" });
25
+ } else {
26
+ const balance = asNumber(credits.balance);
27
+ if (balance !== undefined) {
28
+ metrics.push({ id: "credits", label: "Credits", value: balance, unit: "count" });
29
+ } else {
30
+ metrics.push({ id: "credits", label: "Credits", value: "available" });
31
+ }
32
+ }
33
+ } else if (credits?.has_credits === false) {
34
+ metrics.push({ id: "credits", label: "Credits", value: "none" });
35
+ }
36
+ const resetCredits = asObject(payload.rate_limit_reset_credits);
37
+ const resetCount = asNonnegativeInteger(resetCredits?.available_count);
38
+ if (resetCount !== undefined) {
39
+ metrics.push({
40
+ id: "reset-credits",
41
+ label: "Usage limit resets",
42
+ value: resetCount,
43
+ unit: "count",
44
+ });
45
+ }
46
+ if (buckets.length === 0 && metrics.length === 0) {
47
+ throw new Error("Codex usage endpoint returned no displayable usage data.");
48
+ }
60
49
 
61
- const planType = asString(payload.plan_type);
62
- return {
63
- providerId: "openai-codex",
64
- providerName: "OpenAI Codex",
65
- capturedAt,
66
- source: "codex-pi-auth",
67
- semantics: {
68
- kind: "consumer-subscription",
69
- label: "ChatGPT subscription limits",
70
- },
71
- buckets,
72
- metrics,
73
- ...(planType ? { notes: [`Plan: ${planType}`] } : {}),
74
- };
50
+ const planType = asString(payload.plan_type);
51
+ return {
52
+ providerId: "openai-codex",
53
+ providerName: "OpenAI Codex",
54
+ capturedAt,
55
+ source: "codex-pi-auth",
56
+ semantics: {
57
+ kind: "consumer-subscription",
58
+ label: "ChatGPT subscription limits",
59
+ },
60
+ buckets,
61
+ metrics,
62
+ ...(planType ? { notes: [`Plan: ${planType}`] } : {}),
63
+ };
75
64
  }
76
65
 
77
66
  function normalizeRateLimitGroup(
78
- buckets: UsageBucket[],
79
- groupId: string,
80
- groupLabel: string,
81
- raw: unknown,
82
- optional: boolean,
67
+ buckets: UsageBucket[],
68
+ groupId: string,
69
+ groupLabel: string,
70
+ raw: unknown,
71
+ optional: boolean,
83
72
  ): void {
84
- if (raw === undefined || raw === null) return;
85
- const details = asObject(raw);
86
- if (!details) {
87
- if (optional) return;
88
- throw new Error("Codex rate limit was not an object.");
89
- }
90
- addWindow(buckets, groupId, groupLabel, "primary", details.primary_window);
91
- addWindow(buckets, groupId, groupLabel, "secondary", details.secondary_window);
73
+ if (raw === undefined || raw === null) return;
74
+ const details = asObject(raw);
75
+ if (!details) {
76
+ if (optional) return;
77
+ throw new Error("Codex rate limit was not an object.");
78
+ }
79
+ addWindow(buckets, groupId, groupLabel, "primary", details.primary_window);
80
+ addWindow(buckets, groupId, groupLabel, "secondary", details.secondary_window);
92
81
  }
93
82
 
94
83
  function addWindow(
95
- buckets: UsageBucket[],
96
- groupId: string,
97
- groupLabel: string,
98
- position: "primary" | "secondary",
99
- raw: unknown,
84
+ buckets: UsageBucket[],
85
+ groupId: string,
86
+ groupLabel: string,
87
+ position: "primary" | "secondary",
88
+ raw: unknown,
100
89
  ): void {
101
- if (raw === undefined || raw === null) return;
102
- const value = asObject(raw);
103
- if (!value) throw new Error("Codex rate-limit window was not an object.");
104
- const used = asNumber(value.used_percent);
105
- if (used === undefined) return;
106
- const seconds = asNumber(value.limit_window_seconds);
107
- const resetsAt = asNumber(value.reset_at);
108
- buckets.push({
109
- id: `${groupId}:${position}`,
110
- label: position === "primary" ? "Primary limit" : "Secondary limit",
111
- groupId,
112
- groupLabel,
113
- modelKeys: [groupId, groupLabel],
114
- used,
115
- remaining: 100 - clampPercent(used),
116
- limit: 100,
117
- unit: "percent",
118
- ...(seconds !== undefined && seconds > 0 ? { windowMinutes: Math.ceil(seconds / 60) } : {}),
119
- ...(resetsAt !== undefined ? { resetsAt } : {}),
120
- });
90
+ if (raw === undefined || raw === null) return;
91
+ const value = asObject(raw);
92
+ if (!value) throw new Error("Codex rate-limit window was not an object.");
93
+ const used = asNumber(value.used_percent);
94
+ if (used === undefined) return;
95
+ const seconds = asNumber(value.limit_window_seconds);
96
+ const resetsAt = asNumber(value.reset_at);
97
+ buckets.push({
98
+ id: `${groupId}:${position}`,
99
+ label: position === "primary" ? "Primary limit" : "Secondary limit",
100
+ groupId,
101
+ groupLabel,
102
+ modelKeys: [groupId, groupLabel],
103
+ used,
104
+ remaining: 100 - clampPercent(used),
105
+ limit: 100,
106
+ unit: "percent",
107
+ ...(seconds !== undefined && seconds > 0 ? { windowMinutes: Math.ceil(seconds / 60) } : {}),
108
+ ...(resetsAt !== undefined ? { resetsAt } : {}),
109
+ });
121
110
  }
122
111
 
123
112
  function asObject(value: unknown): Record<string, unknown> | undefined {
124
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
125
- return value as Record<string, unknown>;
113
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
114
+ return value as Record<string, unknown>;
126
115
  }
127
116
 
128
117
  function asString(value: unknown): string | undefined {
129
- if (typeof value !== "string") return undefined;
130
- return sanitizeDisplayText(value, 160) || undefined;
118
+ if (typeof value !== "string") return undefined;
119
+ return sanitizeDisplayText(value, 160) || undefined;
131
120
  }
132
121
 
133
122
  function asNumber(value: unknown): number | undefined {
134
- if (typeof value === "number" && Number.isFinite(value)) return value;
135
- if (typeof value === "string" && value.trim()) {
136
- const parsed = Number(value);
137
- return Number.isFinite(parsed) ? parsed : undefined;
138
- }
139
- return undefined;
123
+ if (typeof value === "number" && Number.isFinite(value)) return value;
124
+ if (typeof value === "string" && value.trim()) {
125
+ const parsed = Number(value);
126
+ return Number.isFinite(parsed) ? parsed : undefined;
127
+ }
128
+ return undefined;
140
129
  }
141
130
 
142
131
  function asNonnegativeInteger(value: unknown): number | undefined {
143
- const parsed = asNumber(value);
144
- if (parsed === undefined || !Number.isSafeInteger(parsed)) return undefined;
145
- return Math.max(0, parsed);
132
+ const parsed = asNumber(value);
133
+ if (parsed === undefined || !Number.isSafeInteger(parsed)) return undefined;
134
+ return Math.max(0, parsed);
146
135
  }
147
136
 
148
137
  function clampPercent(value: number): number {
149
- return Math.min(100, Math.max(0, value));
138
+ return Math.min(100, Math.max(0, value));
150
139
  }
@@ -4,82 +4,77 @@ const CURRENCIES = ["CNY", "USD"] as const;
4
4
  type DeepSeekCurrency = (typeof CURRENCIES)[number];
5
5
 
6
6
  const BALANCE_FIELDS = [
7
- ["total", "Total balance", "total_balance"],
8
- ["granted", "Granted balance", "granted_balance"],
9
- ["topped-up", "Topped-up balance", "topped_up_balance"],
7
+ ["total", "Total balance", "total_balance"],
8
+ ["granted", "Granted balance", "granted_balance"],
9
+ ["topped-up", "Topped-up balance", "topped_up_balance"],
10
10
  ] as const;
11
11
 
12
- export function normalizeDeepSeekBalancePayload(
13
- payload: DeepSeekBalancePayload,
14
- capturedAt: number,
15
- ): UsageReport {
16
- if (typeof payload.is_available !== "boolean") {
17
- throw new Error("DeepSeek API balance response availability was not a boolean.");
18
- }
19
- if (!Array.isArray(payload.balance_infos) || payload.balance_infos.length === 0) {
20
- throw new Error("DeepSeek API balance response returned no balance information.");
21
- }
12
+ export function normalizeDeepSeekBalancePayload(payload: DeepSeekBalancePayload, capturedAt: number): UsageReport {
13
+ if (typeof payload.is_available !== "boolean") {
14
+ throw new Error("DeepSeek API balance response availability was not a boolean.");
15
+ }
16
+ if (!Array.isArray(payload.balance_infos) || payload.balance_infos.length === 0) {
17
+ throw new Error("DeepSeek API balance response returned no balance information.");
18
+ }
22
19
 
23
- const balances = new Map<DeepSeekCurrency, Record<string, unknown>>();
24
- for (const raw of payload.balance_infos) {
25
- const balance = asObject(raw);
26
- if (!balance) throw new Error("DeepSeek API balance row was not an object.");
27
- const currency = deepSeekCurrency(balance.currency);
28
- if (!currency) throw new Error("DeepSeek API balance row returned an unsupported currency.");
29
- if (balances.has(currency)) {
30
- throw new Error(`DeepSeek API balance response repeated ${currency}.`);
31
- }
32
- for (const [, label, field] of BALANCE_FIELDS) {
33
- if (!decimalAmount(balance[field])) {
34
- throw new Error(`DeepSeek API balance ${label.toLowerCase()} was not a valid amount.`);
35
- }
36
- }
37
- balances.set(currency, balance);
38
- }
20
+ const balances = new Map<DeepSeekCurrency, Record<string, unknown>>();
21
+ for (const raw of payload.balance_infos) {
22
+ const balance = asObject(raw);
23
+ if (!balance) throw new Error("DeepSeek API balance row was not an object.");
24
+ const currency = deepSeekCurrency(balance.currency);
25
+ if (!currency) throw new Error("DeepSeek API balance row returned an unsupported currency.");
26
+ if (balances.has(currency)) {
27
+ throw new Error(`DeepSeek API balance response repeated ${currency}.`);
28
+ }
29
+ for (const [, label, field] of BALANCE_FIELDS) {
30
+ if (!decimalAmount(balance[field])) {
31
+ throw new Error(`DeepSeek API balance ${label.toLowerCase()} was not a valid amount.`);
32
+ }
33
+ }
34
+ balances.set(currency, balance);
35
+ }
39
36
 
40
- const metrics: UsageMetric[] = [
41
- {
42
- id: "api-availability",
43
- label: "API calls",
44
- value: payload.is_available ? "available" : "unavailable",
45
- },
46
- ];
47
- for (const currency of CURRENCIES) {
48
- const balance = balances.get(currency);
49
- if (!balance) continue;
50
- for (const [id, label, field] of BALANCE_FIELDS) {
51
- metrics.push({
52
- id: `${currency.toLowerCase()}-${id}`,
53
- label,
54
- value: balance[field] as string,
55
- unit: "currency",
56
- currency,
57
- });
58
- }
59
- }
37
+ const metrics: UsageMetric[] = [
38
+ {
39
+ id: "api-availability",
40
+ label: "API calls",
41
+ value: payload.is_available ? "available" : "unavailable",
42
+ },
43
+ ];
44
+ for (const currency of CURRENCIES) {
45
+ const balance = balances.get(currency);
46
+ if (!balance) continue;
47
+ for (const [id, label, field] of BALANCE_FIELDS) {
48
+ metrics.push({
49
+ id: `${currency.toLowerCase()}-${id}`,
50
+ label,
51
+ value: balance[field] as string,
52
+ unit: "currency",
53
+ currency,
54
+ });
55
+ }
56
+ }
60
57
 
61
- return {
62
- providerId: "deepseek",
63
- providerName: "DeepSeek",
64
- capturedAt,
65
- source: "deepseek-balance",
66
- semantics: { kind: "api-key", label: "DeepSeek API balance" },
67
- buckets: [],
68
- metrics,
69
- };
58
+ return {
59
+ providerId: "deepseek",
60
+ providerName: "DeepSeek",
61
+ capturedAt,
62
+ source: "deepseek-balance",
63
+ semantics: { kind: "api-key", label: "DeepSeek API balance" },
64
+ buckets: [],
65
+ metrics,
66
+ };
70
67
  }
71
68
 
72
69
  function asObject(value: unknown): Record<string, unknown> | undefined {
73
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
74
- return value as Record<string, unknown>;
70
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
71
+ return value as Record<string, unknown>;
75
72
  }
76
73
 
77
74
  function deepSeekCurrency(value: unknown): DeepSeekCurrency | undefined {
78
- return CURRENCIES.find((currency) => currency === value);
75
+ return CURRENCIES.find((currency) => currency === value);
79
76
  }
80
77
 
81
78
  function decimalAmount(value: unknown): value is string {
82
- return (
83
- typeof value === "string" && value.length <= 64 && /^(?:0|[1-9]\d*)(?:\.\d+)?$/u.test(value)
84
- );
79
+ return typeof value === "string" && value.length <= 64 && /^(?:0|[1-9]\d*)(?:\.\d+)?$/u.test(value);
85
80
  }