@narumitw/pi-usage 0.60.7 → 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 +91 -339
- 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 +117 -126
- package/src/usage-targets.ts +98 -114
- package/src/usage.ts +1264 -1426
package/src/core.ts
CHANGED
|
@@ -2,246 +2,244 @@ import { createHmac } from "node:crypto";
|
|
|
2
2
|
import type { UsageReport } from "./types.js";
|
|
3
3
|
|
|
4
4
|
export class UsageCache {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
5
|
+
private readonly entries = new Map<string, { createdAt: number; report: UsageReport }>();
|
|
6
|
+
private readonly ttlMs: number;
|
|
7
|
+
private readonly maxEntries: number;
|
|
8
|
+
|
|
9
|
+
constructor(ttlMs: number, maxEntries = 32) {
|
|
10
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) throw new Error("Cache TTL must be positive.");
|
|
11
|
+
if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) {
|
|
12
|
+
throw new Error("Cache entry limit must be a positive integer.");
|
|
13
|
+
}
|
|
14
|
+
this.ttlMs = ttlMs;
|
|
15
|
+
this.maxEntries = maxEntries;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get size(): number {
|
|
19
|
+
return this.entries.size;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get(providerId: string, fingerprint: string, now = Date.now()): UsageReport | undefined {
|
|
23
|
+
this.sweepExpired(now);
|
|
24
|
+
return this.entries.get(cacheKey(providerId, fingerprint))?.report;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
set(providerId: string, fingerprint: string, report: UsageReport, now = Date.now()): void {
|
|
28
|
+
this.sweepExpired(now);
|
|
29
|
+
const key = cacheKey(providerId, fingerprint);
|
|
30
|
+
this.entries.delete(key);
|
|
31
|
+
while (this.entries.size >= this.maxEntries) {
|
|
32
|
+
const oldest = this.entries.keys().next().value;
|
|
33
|
+
if (oldest === undefined) break;
|
|
34
|
+
this.entries.delete(oldest);
|
|
35
|
+
}
|
|
36
|
+
this.entries.set(key, { createdAt: now, report });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
delete(providerId: string, fingerprint: string): void {
|
|
40
|
+
this.entries.delete(cacheKey(providerId, fingerprint));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
clearProvider(providerId: string): void {
|
|
44
|
+
for (const key of this.entries.keys()) {
|
|
45
|
+
if (key.startsWith(`${providerId}:`)) this.entries.delete(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
clear(): void {
|
|
50
|
+
this.entries.clear();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private sweepExpired(now: number): void {
|
|
54
|
+
for (const [key, entry] of this.entries) {
|
|
55
|
+
if (now - entry.createdAt >= this.ttlMs) this.entries.delete(key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export function fingerprintResolvedAuth(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
auth: {
|
|
62
|
+
apiKey?: string;
|
|
63
|
+
headers?: Record<string, string | null>;
|
|
64
|
+
baseUrl?: string;
|
|
65
|
+
env?: Record<string, string>;
|
|
66
|
+
source?: string;
|
|
67
|
+
providerAuth?: {
|
|
68
|
+
apiKey?: string;
|
|
69
|
+
headers?: Record<string, string | null>;
|
|
70
|
+
baseUrl?: string;
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
salt: Uint8Array,
|
|
74
74
|
): string {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
75
|
+
const headers = Object.entries(auth.headers ?? {})
|
|
76
|
+
.map(([name, value]) => [name.toLowerCase(), value] as const)
|
|
77
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
78
|
+
const env = Object.entries(auth.env ?? {}).sort(([left], [right]) => left.localeCompare(right));
|
|
79
|
+
const providerHeaders = Object.entries(auth.providerAuth?.headers ?? {})
|
|
80
|
+
.map(([name, value]) => [name.toLowerCase(), value] as const)
|
|
81
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
82
|
+
const canonical = JSON.stringify({
|
|
83
|
+
apiKey: auth.apiKey ?? "",
|
|
84
|
+
headers,
|
|
85
|
+
baseUrl: auth.baseUrl ?? "",
|
|
86
|
+
env,
|
|
87
|
+
source: auth.source ?? "",
|
|
88
|
+
providerAuth: {
|
|
89
|
+
apiKey: auth.providerAuth?.apiKey ?? "",
|
|
90
|
+
headers: providerHeaders,
|
|
91
|
+
baseUrl: auth.providerAuth?.baseUrl ?? "",
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
return createHmac("sha256", salt).update(canonical).digest("hex");
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
export async function runWithConcurrency<T, R>(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
items: readonly T[],
|
|
99
|
+
limit: number,
|
|
100
|
+
worker: (item: T, index: number, signal: AbortSignal) => Promise<R>,
|
|
101
|
+
signal: AbortSignal,
|
|
102
102
|
): Promise<PromiseSettledResult<R>[]> {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return results;
|
|
103
|
+
if (signal.aborted) throw abortError();
|
|
104
|
+
if (!Number.isSafeInteger(limit) || limit < 1) throw new Error("Concurrency limit must be positive.");
|
|
105
|
+
|
|
106
|
+
const results = new Array<PromiseSettledResult<R>>(items.length);
|
|
107
|
+
let nextIndex = 0;
|
|
108
|
+
const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
109
|
+
while (nextIndex < items.length) {
|
|
110
|
+
if (signal.aborted) throw abortError();
|
|
111
|
+
const index = nextIndex;
|
|
112
|
+
nextIndex += 1;
|
|
113
|
+
try {
|
|
114
|
+
results[index] = {
|
|
115
|
+
status: "fulfilled",
|
|
116
|
+
value: await worker(items[index] as T, index, signal),
|
|
117
|
+
};
|
|
118
|
+
} catch (reason) {
|
|
119
|
+
results[index] = { status: "rejected", reason };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await Promise.all(runners);
|
|
125
|
+
if (signal.aborted) throw abortError();
|
|
126
|
+
return results;
|
|
128
127
|
}
|
|
129
128
|
|
|
130
129
|
export async function awaitWithDeadline<T>(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
operation: Promise<T>,
|
|
131
|
+
signal: AbortSignal,
|
|
132
|
+
timeoutMs: number,
|
|
133
|
+
description: string,
|
|
135
134
|
): Promise<T> {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
135
|
+
if (signal.aborted) throw abortError();
|
|
136
|
+
const controller = new AbortController();
|
|
137
|
+
const abortFromCaller = () => controller.abort();
|
|
138
|
+
signal.addEventListener("abort", abortFromCaller, { once: true });
|
|
139
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
140
|
+
try {
|
|
141
|
+
return await Promise.race([
|
|
142
|
+
operation,
|
|
143
|
+
new Promise<never>((_resolve, reject) => {
|
|
144
|
+
controller.signal.addEventListener(
|
|
145
|
+
"abort",
|
|
146
|
+
() => {
|
|
147
|
+
reject(
|
|
148
|
+
signal.aborted
|
|
149
|
+
? abortError()
|
|
150
|
+
: Object.assign(new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s ${description}.`), {
|
|
151
|
+
name: "TimeoutError",
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
},
|
|
155
|
+
{ once: true },
|
|
156
|
+
);
|
|
157
|
+
}),
|
|
158
|
+
]);
|
|
159
|
+
} finally {
|
|
160
|
+
clearTimeout(timeout);
|
|
161
|
+
signal.removeEventListener("abort", abortFromCaller);
|
|
162
|
+
}
|
|
165
163
|
}
|
|
166
164
|
|
|
167
165
|
export function sanitizeDisplayText(value: string, maxChars = 160): string {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
166
|
+
let result = "";
|
|
167
|
+
for (let index = 0; index < value.length; ) {
|
|
168
|
+
const codePoint = value.codePointAt(index) ?? 0;
|
|
169
|
+
const character = String.fromCodePoint(codePoint);
|
|
170
|
+
if (codePoint === 0x1b || codePoint === 0x9b || codePoint === 0x9d) {
|
|
171
|
+
index = skipTerminalEscape(value, index, codePoint);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) {
|
|
175
|
+
if (codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0d) result += " ";
|
|
176
|
+
index += character.length;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
result += character;
|
|
180
|
+
index += character.length;
|
|
181
|
+
}
|
|
182
|
+
return truncate(result.replace(/\s+/gu, " ").trim(), maxChars);
|
|
185
183
|
}
|
|
186
184
|
|
|
187
185
|
export function redactUsageError(value: string, secrets: readonly string[] = []): string {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
186
|
+
let redacted = value;
|
|
187
|
+
for (const secret of [...new Set(secrets)].filter(Boolean).sort((a, b) => b.length - a.length)) {
|
|
188
|
+
redacted = redacted.replace(new RegExp(escapeRegExp(secret), "g"), "<redacted>");
|
|
189
|
+
}
|
|
190
|
+
redacted = redacted
|
|
191
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer <redacted>")
|
|
192
|
+
.replace(/"(?:access_token|refresh_token|api_key)"\s*:\s*"[^"]+"/gi, (match) => {
|
|
193
|
+
const separator = match.indexOf(":");
|
|
194
|
+
return `${match.slice(0, separator + 1)}"<redacted>"`;
|
|
195
|
+
});
|
|
196
|
+
return sanitizeDisplayText(redacted, 600);
|
|
199
197
|
}
|
|
200
198
|
|
|
201
199
|
export function errorMessage(error: unknown): string {
|
|
202
|
-
|
|
200
|
+
return sanitizeDisplayText(error instanceof Error ? error.message : String(error), 600);
|
|
203
201
|
}
|
|
204
202
|
|
|
205
203
|
export function abortError(): Error {
|
|
206
|
-
|
|
204
|
+
return Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
|
|
207
205
|
}
|
|
208
206
|
|
|
209
207
|
function skipTerminalEscape(value: string, start: number, codePoint: number): number {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
208
|
+
let index = start + 1;
|
|
209
|
+
const next = value.charCodeAt(index);
|
|
210
|
+
const isOsc = codePoint === 0x9d || (codePoint === 0x1b && next === 0x5d);
|
|
211
|
+
if (isOsc) {
|
|
212
|
+
if (codePoint === 0x1b) index += 1;
|
|
213
|
+
while (index < value.length) {
|
|
214
|
+
const current = value.charCodeAt(index);
|
|
215
|
+
if (current === 0x07) return index + 1;
|
|
216
|
+
if (current === 0x1b && value.charCodeAt(index + 1) === 0x5c) return index + 2;
|
|
217
|
+
index += 1;
|
|
218
|
+
}
|
|
219
|
+
return index;
|
|
220
|
+
}
|
|
221
|
+
const isCsi = codePoint === 0x9b || (codePoint === 0x1b && next === 0x5b);
|
|
222
|
+
if (isCsi) {
|
|
223
|
+
if (codePoint === 0x1b) index += 1;
|
|
224
|
+
while (index < value.length) {
|
|
225
|
+
const current = value.charCodeAt(index);
|
|
226
|
+
index += 1;
|
|
227
|
+
if (current >= 0x40 && current <= 0x7e) break;
|
|
228
|
+
}
|
|
229
|
+
return index;
|
|
230
|
+
}
|
|
231
|
+
return Math.min(value.length, start + (codePoint === 0x1b ? 2 : 1));
|
|
234
232
|
}
|
|
235
233
|
|
|
236
234
|
function cacheKey(providerId: string, fingerprint: string): string {
|
|
237
|
-
|
|
235
|
+
return `${providerId}:${fingerprint}`;
|
|
238
236
|
}
|
|
239
237
|
|
|
240
238
|
function escapeRegExp(value: string): string {
|
|
241
|
-
|
|
239
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
242
240
|
}
|
|
243
241
|
|
|
244
242
|
function truncate(value: string, maxChars: number): string {
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
if (value.length <= maxChars) return value;
|
|
244
|
+
return `${value.slice(0, maxChars - 1)}…`;
|
|
247
245
|
}
|