@diousk/pi-usage-fast 0.1.0
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/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/index.ts +5478 -0
- package/dist/index.ts.map +7 -0
- package/docs/operations.md +31 -0
- package/docs/providers.md +273 -0
- package/package.json +82 -0
- package/src/codex-fast-runtime.ts +184 -0
- package/src/codex-fast.ts +118 -0
- package/src/codex-resets.ts +347 -0
- package/src/core.ts +245 -0
- package/src/format.ts +678 -0
- package/src/index.ts +116 -0
- package/src/oauth-credential-source.ts +117 -0
- package/src/providers/baseten.ts +51 -0
- package/src/providers/codex.ts +139 -0
- package/src/providers/deepseek.ts +80 -0
- package/src/providers/fireworks.ts +291 -0
- package/src/providers/github-copilot.ts +114 -0
- package/src/providers/kimi-coding.ts +265 -0
- package/src/providers/minimax.ts +280 -0
- package/src/providers/moonshot.ts +59 -0
- package/src/providers/opencode-zen.ts +80 -0
- package/src/providers/openrouter.ts +70 -0
- package/src/providers/vercel-ai-gateway.ts +43 -0
- package/src/providers/xai.ts +179 -0
- package/src/providers/zai-errors.ts +94 -0
- package/src/providers/zai.ts +239 -0
- package/src/query.ts +1073 -0
- package/src/settings.ts +366 -0
- package/src/types.ts +235 -0
- package/src/usage-helpers.ts +40 -0
- package/src/usage-settings-ui.ts +152 -0
- package/src/usage-targets.ts +127 -0
- package/src/usage.ts +1335 -0
package/src/core.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import type { UsageReport } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export class UsageCache {
|
|
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
|
+
}
|
|
59
|
+
|
|
60
|
+
export function fingerprintResolvedAuth(
|
|
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
|
+
): string {
|
|
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
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function runWithConcurrency<T, R>(
|
|
98
|
+
items: readonly T[],
|
|
99
|
+
limit: number,
|
|
100
|
+
worker: (item: T, index: number, signal: AbortSignal) => Promise<R>,
|
|
101
|
+
signal: AbortSignal,
|
|
102
|
+
): Promise<PromiseSettledResult<R>[]> {
|
|
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;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function awaitWithDeadline<T>(
|
|
130
|
+
operation: Promise<T>,
|
|
131
|
+
signal: AbortSignal,
|
|
132
|
+
timeoutMs: number,
|
|
133
|
+
description: string,
|
|
134
|
+
): Promise<T> {
|
|
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
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function sanitizeDisplayText(value: string, maxChars = 160): string {
|
|
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);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function redactUsageError(value: string, secrets: readonly string[] = []): string {
|
|
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);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function errorMessage(error: unknown): string {
|
|
200
|
+
return sanitizeDisplayText(error instanceof Error ? error.message : String(error), 600);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function abortError(): Error {
|
|
204
|
+
return Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function skipTerminalEscape(value: string, start: number, codePoint: number): number {
|
|
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));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function cacheKey(providerId: string, fingerprint: string): string {
|
|
235
|
+
return `${providerId}:${fingerprint}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function escapeRegExp(value: string): string {
|
|
239
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function truncate(value: string, maxChars: number): string {
|
|
243
|
+
if (value.length <= maxChars) return value;
|
|
244
|
+
return `${value.slice(0, maxChars - 1)}…`;
|
|
245
|
+
}
|