@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.
- package/dist/index.ts +104 -363
- package/dist/index.ts.map +2 -2
- package/docs/providers.md +2 -1
- package/package.json +73 -74
- package/src/codex-fast-runtime.ts +148 -184
- package/src/codex-fast.ts +74 -83
- package/src/codex-resets.ts +249 -278
- package/src/core.ts +204 -206
- package/src/format.ts +519 -574
- package/src/index.ts +77 -77
- package/src/oauth-credential-source.ts +57 -65
- package/src/providers/baseten.ts +32 -36
- package/src/providers/codex.ts +109 -120
- package/src/providers/deepseek.ts +59 -64
- package/src/providers/fireworks.ts +219 -238
- package/src/providers/github-copilot.ts +89 -95
- package/src/providers/kimi-coding.ts +202 -213
- package/src/providers/minimax.ts +214 -219
- package/src/providers/moonshot.ts +41 -46
- package/src/providers/opencode-zen.ts +56 -59
- package/src/providers/openrouter.ts +50 -53
- package/src/providers/vercel-ai-gateway.ts +33 -33
- package/src/providers/xai.ts +141 -148
- package/src/providers/zai-errors.ts +75 -75
- package/src/providers/zai.ts +173 -189
- package/src/query.ts +887 -967
- package/src/settings.ts +275 -304
- package/src/types.ts +145 -145
- package/src/usage-helpers.ts +18 -18
- package/src/usage-settings-ui.ts +118 -145
- package/src/usage-targets.ts +98 -114
- package/src/usage.ts +1264 -1426
|
@@ -15,262 +15,251 @@ const FIXED_POINT_UNITS_PER_CENT = 1_000_000;
|
|
|
15
15
|
* Kimi Code 676e4d82240855044fe809fea89ce1dbe8e512cf defines `GET /coding/v1/usages`,
|
|
16
16
|
* numeric-string plan rows, proto-style windows, and 1,000,000 fixed-point units per cent:
|
|
17
17
|
* https://github.com/MoonshotAI/kimi-code/blob/676e4d82240855044fe809fea89ce1dbe8e512cf/packages/oauth/src/managed-usage.ts
|
|
18
|
+
* Live responses observed on 2026-09-12 omit both `used` and `remaining` on untouched rows and may report only
|
|
19
|
+
* `remaining` once usage starts, so a row with a positive `limit` and no counters means zero usage, and when only
|
|
20
|
+
* one counter is present the other is derived from `limit`. Counters that are present but malformed stay rejected.
|
|
18
21
|
*/
|
|
19
|
-
export function normalizeKimiCodingUsagePayload(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
): UsageReport {
|
|
23
|
-
const root = asObject(payload);
|
|
24
|
-
if (!root) throw new Error("Kimi Coding usage response was not an object.");
|
|
22
|
+
export function normalizeKimiCodingUsagePayload(payload: KimiCodingUsagePayload, capturedAt: number): UsageReport {
|
|
23
|
+
const root = asObject(payload);
|
|
24
|
+
if (!root) throw new Error("Kimi Coding usage response was not an object.");
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
const candidates: UsageBucket[] = [];
|
|
27
|
+
let omittedWindow = false;
|
|
28
|
+
const summary = parseUsageRow(root.usage, WEEKLY_WINDOW_MINUTES, "Weekly window");
|
|
29
|
+
if (summary) candidates.push(summary);
|
|
30
|
+
else if (root.usage !== undefined) omittedWindow = true;
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
if (Array.isArray(root.limits)) {
|
|
33
|
+
for (const raw of root.limits) {
|
|
34
|
+
const item = asObject(raw);
|
|
35
|
+
const windowMinutes = parseWindowMinutes(item?.window);
|
|
36
|
+
const label = sanitizedLabel(item?.name);
|
|
37
|
+
const bucket =
|
|
38
|
+
windowMinutes === undefined
|
|
39
|
+
? undefined
|
|
40
|
+
: parseUsageRow(item?.detail, windowMinutes, label ?? defaultWindowLabel(windowMinutes));
|
|
41
|
+
if (bucket) candidates.push(bucket);
|
|
42
|
+
else omittedWindow = true;
|
|
43
|
+
}
|
|
44
|
+
} else if (root.limits !== undefined) {
|
|
45
|
+
omittedWindow = true;
|
|
46
|
+
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
const buckets: UsageBucket[] = [];
|
|
49
|
+
const byWindow = new Map<number, UsageBucket[]>();
|
|
50
|
+
for (const bucket of candidates) {
|
|
51
|
+
const windowMinutes = bucket.windowMinutes as number;
|
|
52
|
+
byWindow.set(windowMinutes, [...(byWindow.get(windowMinutes) ?? []), bucket]);
|
|
53
|
+
}
|
|
54
|
+
for (const rows of byWindow.values()) {
|
|
55
|
+
if (rows.length === 1) buckets.push(rows[0] as UsageBucket);
|
|
56
|
+
else omittedWindow = true;
|
|
57
|
+
}
|
|
58
|
+
buckets.sort((left, right) => (left.windowMinutes ?? 0) - (right.windowMinutes ?? 0));
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
const metrics = parseBoosterWallet(root.boosterWallet);
|
|
61
|
+
if (buckets.length === 0 && metrics.length === 0) {
|
|
62
|
+
throw new Error("Kimi Coding usage endpoint returned no displayable usage data.");
|
|
63
|
+
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
: {}),
|
|
76
|
-
};
|
|
65
|
+
return {
|
|
66
|
+
providerId: "kimi-coding",
|
|
67
|
+
providerName: "Kimi For Coding",
|
|
68
|
+
capturedAt,
|
|
69
|
+
source: "kimi-managed-usage",
|
|
70
|
+
semantics: { kind: "consumer-subscription", label: "Kimi Coding Plan usage" },
|
|
71
|
+
buckets,
|
|
72
|
+
metrics,
|
|
73
|
+
...(omittedWindow ? { notes: ["Unsupported, malformed, or duplicate plan windows were unavailable."] } : {}),
|
|
74
|
+
};
|
|
77
75
|
}
|
|
78
76
|
|
|
79
|
-
function parseUsageRow(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
77
|
+
function parseUsageRow(value: unknown, windowMinutes: number, label: string): UsageBucket | undefined {
|
|
78
|
+
const row = asObject(value);
|
|
79
|
+
if (!row) return undefined;
|
|
80
|
+
const limit = asNonnegativeInteger(row.limit);
|
|
81
|
+
if (limit === undefined || limit === 0) return undefined;
|
|
82
|
+
const usedRaw = asNonnegativeInteger(row.used);
|
|
83
|
+
const remainingRaw = asNonnegativeInteger(row.remaining);
|
|
84
|
+
// Counters that are present but malformed reject the row instead of silently inventing a count.
|
|
85
|
+
if (
|
|
86
|
+
(row.used !== undefined && usedRaw === undefined) ||
|
|
87
|
+
(row.remaining !== undefined && remainingRaw === undefined)
|
|
88
|
+
) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
// Live responses omit both counters on untouched rows, so a bare `limit` means zero usage.
|
|
92
|
+
const used = usedRaw ?? (remainingRaw === undefined ? 0 : Math.max(0, limit - remainingRaw));
|
|
93
|
+
const remaining = remainingRaw ?? Math.max(0, limit - used);
|
|
94
|
+
const resetsAt = asIsoEpochSeconds(row.resetTime);
|
|
95
|
+
return {
|
|
96
|
+
id: windowId(windowMinutes),
|
|
97
|
+
label,
|
|
98
|
+
used,
|
|
99
|
+
remaining,
|
|
100
|
+
limit,
|
|
101
|
+
unit: "count",
|
|
102
|
+
windowMinutes,
|
|
103
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
104
|
+
};
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
function parseWindowMinutes(value: unknown): number | undefined {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
108
|
+
const window = asObject(value);
|
|
109
|
+
if (!window) return undefined;
|
|
110
|
+
const duration = asPositiveInteger(window.duration);
|
|
111
|
+
if (duration === undefined) return undefined;
|
|
112
|
+
const multiplier =
|
|
113
|
+
window.timeUnit === "TIME_UNIT_MINUTE"
|
|
114
|
+
? 1
|
|
115
|
+
: window.timeUnit === "TIME_UNIT_HOUR"
|
|
116
|
+
? 60
|
|
117
|
+
: window.timeUnit === "TIME_UNIT_DAY"
|
|
118
|
+
? 1_440
|
|
119
|
+
: window.timeUnit === "TIME_UNIT_WEEK"
|
|
120
|
+
? 10_080
|
|
121
|
+
: undefined;
|
|
122
|
+
if (multiplier === undefined) return undefined;
|
|
123
|
+
const minutes = duration * multiplier;
|
|
124
|
+
return Number.isSafeInteger(minutes) ? minutes : undefined;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
function parseBoosterWallet(value: unknown): UsageMetric[] {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const left = fixedPointToMajor(leftRaw);
|
|
141
|
-
if (total === undefined || left === undefined) return [];
|
|
128
|
+
const wallet = asObject(value);
|
|
129
|
+
const balance = asObject(wallet?.balance);
|
|
130
|
+
if (!wallet || !balance || balance.type !== "BOOSTER") return [];
|
|
131
|
+
const totalRaw = asPositiveInteger(balance.amount);
|
|
132
|
+
if (totalRaw === undefined) return [];
|
|
133
|
+
const leftRaw = asNonnegativeInteger(balance.amountLeft) ?? 0;
|
|
134
|
+
const monthlyLimit = parseMoney(wallet.monthlyChargeLimit);
|
|
135
|
+
const monthlyUsed = parseMoney(wallet.monthlyUsed);
|
|
136
|
+
const currencies = new Set(
|
|
137
|
+
[monthlyLimit?.currency, monthlyUsed?.currency].filter((currency): currency is string => currency !== undefined),
|
|
138
|
+
);
|
|
139
|
+
if (currencies.size !== 1) return [];
|
|
140
|
+
const currency = currencies.values().next().value;
|
|
141
|
+
if (!currency) return [];
|
|
142
|
+
const total = fixedPointToMajor(totalRaw);
|
|
143
|
+
const left = fixedPointToMajor(leftRaw);
|
|
144
|
+
if (total === undefined || left === undefined) return [];
|
|
142
145
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
146
|
+
const metrics: UsageMetric[] = [
|
|
147
|
+
{ id: "booster-balance", label: "Balance", value: left, unit: "currency", currency },
|
|
148
|
+
{ id: "booster-total", label: "Total balance", value: total, unit: "currency", currency },
|
|
149
|
+
];
|
|
150
|
+
if (monthlyUsed) {
|
|
151
|
+
metrics.push({
|
|
152
|
+
id: "booster-monthly-used",
|
|
153
|
+
label: "Used this month",
|
|
154
|
+
value: monthlyUsed.cents / 100,
|
|
155
|
+
unit: "currency",
|
|
156
|
+
currency,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (wallet.monthlyChargeLimitEnabled === false) {
|
|
160
|
+
metrics.push({
|
|
161
|
+
id: "booster-monthly-limit",
|
|
162
|
+
label: "Monthly limit",
|
|
163
|
+
value: "unlimited",
|
|
164
|
+
unit: "currency",
|
|
165
|
+
currency,
|
|
166
|
+
});
|
|
167
|
+
} else if (wallet.monthlyChargeLimitEnabled === true && monthlyLimit) {
|
|
168
|
+
metrics.push({
|
|
169
|
+
id: "booster-monthly-limit",
|
|
170
|
+
label: "Monthly limit",
|
|
171
|
+
value: monthlyLimit.cents / 100,
|
|
172
|
+
unit: "currency",
|
|
173
|
+
currency,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return metrics;
|
|
174
177
|
}
|
|
175
178
|
|
|
176
179
|
function parseMoney(value: unknown): { cents: number; currency: string } | undefined {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
const money = asObject(value);
|
|
181
|
+
if (!money) return undefined;
|
|
182
|
+
const cents = asNonnegativeInteger(money.priceInCents);
|
|
183
|
+
if (cents === undefined) return undefined;
|
|
184
|
+
const currency = asCurrency(money.currency);
|
|
185
|
+
if (!currency) return undefined;
|
|
186
|
+
return { cents, currency };
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
function fixedPointToMajor(value: number): number | undefined {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
const cents = value / FIXED_POINT_UNITS_PER_CENT;
|
|
191
|
+
const roundedCents = cents > 0 && cents < 1 ? 1 : Math.round(cents);
|
|
192
|
+
const major = roundedCents / 100;
|
|
193
|
+
return Number.isSafeInteger(roundedCents) && Number.isFinite(major) ? major : undefined;
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
198
|
+
return value as Record<string, unknown>;
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
function asNonnegativeInteger(value: unknown): number | undefined {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
202
|
+
if (typeof value === "string" && !/^\d+$/u.test(value)) return undefined;
|
|
203
|
+
const number = typeof value === "string" ? Number(value) : value;
|
|
204
|
+
if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 0) return undefined;
|
|
205
|
+
return number;
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
function asPositiveInteger(value: unknown): number | undefined {
|
|
206
|
-
|
|
207
|
-
|
|
209
|
+
const number = asNonnegativeInteger(value);
|
|
210
|
+
return number !== undefined && number > 0 ? number : undefined;
|
|
208
211
|
}
|
|
209
212
|
|
|
210
213
|
function asIsoEpochSeconds(value: unknown): number | undefined {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
month < 1 ||
|
|
237
|
-
month > 12 ||
|
|
238
|
-
day < 1 ||
|
|
239
|
-
day > new Date(Date.UTC(year, month, 0)).getUTCDate() ||
|
|
240
|
-
hour > 23 ||
|
|
241
|
-
minute > 59 ||
|
|
242
|
-
second > 59 ||
|
|
243
|
-
(offsetHour !== undefined && Number(offsetHour) > 23) ||
|
|
244
|
-
(offsetMinute !== undefined && Number(offsetMinute) > 59)
|
|
245
|
-
) {
|
|
246
|
-
return undefined;
|
|
247
|
-
}
|
|
248
|
-
const millis = Date.parse(value);
|
|
249
|
-
return Number.isFinite(millis) && millis >= 0 ? Math.floor(millis / 1000) : undefined;
|
|
214
|
+
if (typeof value !== "string") return undefined;
|
|
215
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|([+-])(\d{2}):(\d{2}))$/u.exec(value);
|
|
216
|
+
if (!match) return undefined;
|
|
217
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText, , offsetHour, offsetMinute] = match;
|
|
218
|
+
const year = Number(yearText);
|
|
219
|
+
const month = Number(monthText);
|
|
220
|
+
const day = Number(dayText);
|
|
221
|
+
const hour = Number(hourText);
|
|
222
|
+
const minute = Number(minuteText);
|
|
223
|
+
const second = Number(secondText);
|
|
224
|
+
if (
|
|
225
|
+
month < 1 ||
|
|
226
|
+
month > 12 ||
|
|
227
|
+
day < 1 ||
|
|
228
|
+
day > new Date(Date.UTC(year, month, 0)).getUTCDate() ||
|
|
229
|
+
hour > 23 ||
|
|
230
|
+
minute > 59 ||
|
|
231
|
+
second > 59 ||
|
|
232
|
+
(offsetHour !== undefined && Number(offsetHour) > 23) ||
|
|
233
|
+
(offsetMinute !== undefined && Number(offsetMinute) > 59)
|
|
234
|
+
) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
const millis = Date.parse(value);
|
|
238
|
+
return Number.isFinite(millis) && millis >= 0 ? Math.floor(millis / 1000) : undefined;
|
|
250
239
|
}
|
|
251
240
|
|
|
252
241
|
function sanitizedLabel(value: unknown): string | undefined {
|
|
253
|
-
|
|
254
|
-
|
|
242
|
+
if (typeof value !== "string") return undefined;
|
|
243
|
+
return sanitizeDisplayText(value, 80) || undefined;
|
|
255
244
|
}
|
|
256
245
|
|
|
257
246
|
function asCurrency(value: unknown): string | undefined {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
247
|
+
if (typeof value !== "string") return undefined;
|
|
248
|
+
const currency = sanitizeDisplayText(value, 3).toUpperCase();
|
|
249
|
+
return /^[A-Z]{3}$/u.test(currency) ? currency : undefined;
|
|
261
250
|
}
|
|
262
251
|
|
|
263
252
|
function windowId(minutes: number): string {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
253
|
+
if (minutes === FIVE_HOUR_WINDOW_MINUTES) return "five-hour";
|
|
254
|
+
if (minutes === DAILY_WINDOW_MINUTES) return "daily";
|
|
255
|
+
if (minutes === WEEKLY_WINDOW_MINUTES) return "weekly";
|
|
256
|
+
return `window-${minutes}-minutes`;
|
|
268
257
|
}
|
|
269
258
|
|
|
270
259
|
function defaultWindowLabel(minutes: number): string {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
260
|
+
if (minutes === WEEKLY_WINDOW_MINUTES) return "Weekly window";
|
|
261
|
+
if (minutes % 10_080 === 0) return `${minutes / 10_080}w window`;
|
|
262
|
+
if (minutes % 1_440 === 0) return `${minutes / 1_440}d window`;
|
|
263
|
+
if (minutes % 60 === 0) return `${minutes / 60}h window`;
|
|
264
|
+
return `${minutes}m window`;
|
|
276
265
|
}
|