ai-zero-token 2.0.7 → 2.0.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/CHANGELOG.md +10 -0
- package/admin-ui/dist/assets/{accounts-D3tsDc3k.js → accounts-p9bqmijS.js} +1 -1
- package/admin-ui/dist/assets/{docs-BO-aSEzh.js → docs-BQaF_ZMr.js} +1 -1
- package/admin-ui/dist/assets/{image-bed-Dql7Vqd9.js → image-bed-D4w1m7k6.js} +1 -1
- package/admin-ui/dist/assets/index-BRQrU_AA.css +1 -0
- package/admin-ui/dist/assets/{index-CCiBaGwU.js → index-_5Ny0cZf.js} +2 -2
- package/admin-ui/dist/assets/{launch-DXLo-NIM.js → launch-BEDxgkQf.js} +1 -1
- package/admin-ui/dist/assets/{logs-Cwn8-rDu.js → logs-BcL0n0Ld.js} +1 -1
- package/admin-ui/dist/assets/{network-detect-vzWfL-Tz.js → network-detect-lEfklmIy.js} +1 -1
- package/admin-ui/dist/assets/{overview-B_yad8ge.js → overview-DsUMffIU.js} +1 -1
- package/admin-ui/dist/assets/settings-a3HxExcC.js +8 -0
- package/admin-ui/dist/assets/{tester-BKoMSoCz.js → tester-Ca4JOgAq.js} +1 -1
- package/admin-ui/dist/assets/usage-hMH0gMZ5.js +1 -0
- package/admin-ui/dist/index.html +2 -2
- package/dist/core/providers/openai-codex/chat.js +139 -8
- package/dist/core/services/usage-service.js +402 -31
- package/dist/core/store/codex-auth-store.js +82 -7
- package/dist/server/app.js +234 -14
- package/package.json +1 -1
- package/admin-ui/dist/assets/index-C22_3Mxq.css +0 -1
- package/admin-ui/dist/assets/settings-BdRWcKJb.js +0 -5
- package/admin-ui/dist/assets/usage-B-qQxXzQ.js +0 -1
|
@@ -10,15 +10,62 @@ import {
|
|
|
10
10
|
getUsageLifetimePath
|
|
11
11
|
} from "../store/state-paths.js";
|
|
12
12
|
const durationBucketLimits = [100, 300, 500, 1e3, 2e3, 5e3, 1e4, 3e4, 6e4, 12e4, Number.POSITIVE_INFINITY];
|
|
13
|
+
const openAIGpt54LongContextInputThreshold = 272e3;
|
|
14
|
+
const gpt54Pricing = {
|
|
15
|
+
inputUsdPerToken: 25e-7,
|
|
16
|
+
outputUsdPerToken: 15e-6,
|
|
17
|
+
cacheCreationUsdPerToken: 25e-7,
|
|
18
|
+
cacheReadUsdPerToken: 25e-8,
|
|
19
|
+
longContextInputThreshold: openAIGpt54LongContextInputThreshold,
|
|
20
|
+
longContextInputMultiplier: 2,
|
|
21
|
+
longContextOutputMultiplier: 1.5
|
|
22
|
+
};
|
|
23
|
+
const tokenPricingByModel = {
|
|
24
|
+
"gpt-5.5": gpt54Pricing,
|
|
25
|
+
"gpt-5.4": gpt54Pricing,
|
|
26
|
+
"gpt-5.4-mini": {
|
|
27
|
+
inputUsdPerToken: 75e-8,
|
|
28
|
+
outputUsdPerToken: 45e-7,
|
|
29
|
+
cacheCreationUsdPerToken: 0,
|
|
30
|
+
cacheReadUsdPerToken: 75e-9
|
|
31
|
+
},
|
|
32
|
+
"gpt-5.4-nano": {
|
|
33
|
+
inputUsdPerToken: 2e-7,
|
|
34
|
+
outputUsdPerToken: 125e-8,
|
|
35
|
+
cacheCreationUsdPerToken: 0,
|
|
36
|
+
cacheReadUsdPerToken: 2e-8
|
|
37
|
+
},
|
|
38
|
+
"gpt-5.2": {
|
|
39
|
+
inputUsdPerToken: 175e-8,
|
|
40
|
+
outputUsdPerToken: 14e-6,
|
|
41
|
+
cacheCreationUsdPerToken: 175e-8,
|
|
42
|
+
cacheReadUsdPerToken: 175e-9
|
|
43
|
+
},
|
|
44
|
+
"gpt-5.3-codex": {
|
|
45
|
+
inputUsdPerToken: 15e-7,
|
|
46
|
+
outputUsdPerToken: 12e-6,
|
|
47
|
+
cacheCreationUsdPerToken: 15e-7,
|
|
48
|
+
cacheReadUsdPerToken: 15e-8
|
|
49
|
+
}
|
|
50
|
+
};
|
|
13
51
|
function createAggregate() {
|
|
14
52
|
return {
|
|
15
53
|
requestCount: 0,
|
|
16
54
|
successCount: 0,
|
|
17
55
|
failureCount: 0,
|
|
18
56
|
inputTokens: 0,
|
|
57
|
+
uncachedInputTokens: 0,
|
|
19
58
|
outputTokens: 0,
|
|
20
59
|
totalTokens: 0,
|
|
60
|
+
cacheCreationTokens: 0,
|
|
61
|
+
cacheReadTokens: 0,
|
|
62
|
+
inputCostUsd: 0,
|
|
63
|
+
outputCostUsd: 0,
|
|
64
|
+
cacheCreationCostUsd: 0,
|
|
65
|
+
cacheReadCostUsd: 0,
|
|
66
|
+
estimatedCostUsd: 0,
|
|
21
67
|
unknownTokenCount: 0,
|
|
68
|
+
unknownTokenStatusCounts: {},
|
|
22
69
|
imageCount: 0,
|
|
23
70
|
totalDurationMs: 0,
|
|
24
71
|
averageDurationMs: 0,
|
|
@@ -29,6 +76,7 @@ function createAggregate() {
|
|
|
29
76
|
function cloneAggregate(value) {
|
|
30
77
|
return {
|
|
31
78
|
...value,
|
|
79
|
+
unknownTokenStatusCounts: { ...value.unknownTokenStatusCounts },
|
|
32
80
|
durationBuckets: { ...value.durationBuckets }
|
|
33
81
|
};
|
|
34
82
|
}
|
|
@@ -40,14 +88,25 @@ function normalizeAggregate(value) {
|
|
|
40
88
|
return createAggregate();
|
|
41
89
|
}
|
|
42
90
|
const record = value;
|
|
91
|
+
const inputTokens = Math.max(0, Math.trunc(normalizeNumber(record.inputTokens)));
|
|
92
|
+
const cacheReadTokens = Math.max(0, Math.trunc(normalizeNumber(record.cacheReadTokens)));
|
|
43
93
|
const aggregate = {
|
|
44
94
|
requestCount: Math.max(0, Math.trunc(normalizeNumber(record.requestCount))),
|
|
45
95
|
successCount: Math.max(0, Math.trunc(normalizeNumber(record.successCount))),
|
|
46
96
|
failureCount: Math.max(0, Math.trunc(normalizeNumber(record.failureCount))),
|
|
47
|
-
inputTokens
|
|
97
|
+
inputTokens,
|
|
98
|
+
uncachedInputTokens: Math.max(0, Math.trunc(normalizeNumber(record.uncachedInputTokens, Math.max(0, inputTokens - cacheReadTokens)))),
|
|
48
99
|
outputTokens: Math.max(0, Math.trunc(normalizeNumber(record.outputTokens))),
|
|
49
100
|
totalTokens: Math.max(0, Math.trunc(normalizeNumber(record.totalTokens))),
|
|
101
|
+
cacheCreationTokens: Math.max(0, Math.trunc(normalizeNumber(record.cacheCreationTokens))),
|
|
102
|
+
cacheReadTokens,
|
|
103
|
+
inputCostUsd: Math.max(0, normalizeNumber(record.inputCostUsd)),
|
|
104
|
+
outputCostUsd: Math.max(0, normalizeNumber(record.outputCostUsd)),
|
|
105
|
+
cacheCreationCostUsd: Math.max(0, normalizeNumber(record.cacheCreationCostUsd)),
|
|
106
|
+
cacheReadCostUsd: Math.max(0, normalizeNumber(record.cacheReadCostUsd)),
|
|
107
|
+
estimatedCostUsd: Math.max(0, normalizeNumber(record.estimatedCostUsd)),
|
|
50
108
|
unknownTokenCount: Math.max(0, Math.trunc(normalizeNumber(record.unknownTokenCount))),
|
|
109
|
+
unknownTokenStatusCounts: {},
|
|
51
110
|
imageCount: Math.max(0, Math.trunc(normalizeNumber(record.imageCount))),
|
|
52
111
|
totalDurationMs: Math.max(0, normalizeNumber(record.totalDurationMs)),
|
|
53
112
|
averageDurationMs: 0,
|
|
@@ -59,6 +118,11 @@ function normalizeAggregate(value) {
|
|
|
59
118
|
aggregate.durationBuckets[key] = Math.max(0, Math.trunc(normalizeNumber(item)));
|
|
60
119
|
}
|
|
61
120
|
}
|
|
121
|
+
if (record.unknownTokenStatusCounts && typeof record.unknownTokenStatusCounts === "object") {
|
|
122
|
+
for (const [key, item] of Object.entries(record.unknownTokenStatusCounts)) {
|
|
123
|
+
aggregate.unknownTokenStatusCounts[key] = Math.max(0, Math.trunc(normalizeNumber(item)));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
62
126
|
refreshDerivedMetrics(aggregate);
|
|
63
127
|
return aggregate;
|
|
64
128
|
}
|
|
@@ -96,6 +160,7 @@ function createLifetimeStore() {
|
|
|
96
160
|
byModel: {},
|
|
97
161
|
byEndpoint: {},
|
|
98
162
|
byError: {},
|
|
163
|
+
byTokenUsageStatus: {},
|
|
99
164
|
byImageRoute: {},
|
|
100
165
|
bySource: {}
|
|
101
166
|
};
|
|
@@ -126,6 +191,7 @@ function normalizeLifetimeStore(value) {
|
|
|
126
191
|
byModel: normalizeDimensionStore(record.byModel),
|
|
127
192
|
byEndpoint: normalizeDimensionStore(record.byEndpoint),
|
|
128
193
|
byError: normalizeDimensionStore(record.byError),
|
|
194
|
+
byTokenUsageStatus: normalizeDimensionStore(record.byTokenUsageStatus),
|
|
129
195
|
byImageRoute: normalizeDimensionStore(record.byImageRoute),
|
|
130
196
|
bySource: normalizeDimensionStore(record.bySource)
|
|
131
197
|
};
|
|
@@ -175,18 +241,183 @@ function refreshDerivedMetrics(aggregate) {
|
|
|
175
241
|
function tokenNumber(value) {
|
|
176
242
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.trunc(value) : null;
|
|
177
243
|
}
|
|
244
|
+
function optionalString(value) {
|
|
245
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
246
|
+
}
|
|
247
|
+
function normalizeTokenUsageForEvent(value) {
|
|
248
|
+
if (!value || typeof value !== "object") {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const record = value;
|
|
252
|
+
const usage = {
|
|
253
|
+
inputTokens: tokenNumber(record.inputTokens),
|
|
254
|
+
uncachedInputTokens: tokenNumber(record.uncachedInputTokens),
|
|
255
|
+
outputTokens: tokenNumber(record.outputTokens),
|
|
256
|
+
totalTokens: tokenNumber(record.totalTokens),
|
|
257
|
+
cacheCreationTokens: tokenNumber(record.cacheCreationTokens),
|
|
258
|
+
cacheReadTokens: tokenNumber(record.cacheReadTokens)
|
|
259
|
+
};
|
|
260
|
+
return Object.values(usage).some((item) => item !== null) ? usage : null;
|
|
261
|
+
}
|
|
262
|
+
function normalizeImageRoute(value) {
|
|
263
|
+
return value === "codex-tool" || value === "chatgpt-web" ? value : "none";
|
|
264
|
+
}
|
|
265
|
+
function normalizeTokenUsageStatus(value, tokenUsage, success) {
|
|
266
|
+
if (value === "captured" || value === "missing_terminal" || value === "terminal_without_usage" || value === "parse_failed" || value === "upstream_error" || value === "not_returned") {
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
269
|
+
if (tokenNumber(tokenUsage?.totalTokens) !== null) {
|
|
270
|
+
return "captured";
|
|
271
|
+
}
|
|
272
|
+
return success ? "not_returned" : "upstream_error";
|
|
273
|
+
}
|
|
274
|
+
function normalizeUsageRecordEvent(event) {
|
|
275
|
+
const statusCode = Math.trunc(normalizeNumber(event.statusCode));
|
|
276
|
+
const timestamp = normalizeNumber(event.timestamp, Date.now());
|
|
277
|
+
const success = event.success ?? (statusCode >= 200 && statusCode < 400);
|
|
278
|
+
const tokenUsage = normalizeTokenUsageForEvent(event.tokenUsage);
|
|
279
|
+
return {
|
|
280
|
+
...event,
|
|
281
|
+
id: optionalString(event.id) ?? randomUUID(),
|
|
282
|
+
timestamp,
|
|
283
|
+
statusCode,
|
|
284
|
+
durationMs: Math.max(0, normalizeNumber(event.durationMs)),
|
|
285
|
+
endpoint: optionalString(event.endpoint) ?? "-",
|
|
286
|
+
method: optionalString(event.method) ?? "-",
|
|
287
|
+
model: optionalString(event.model) ?? "-",
|
|
288
|
+
source: optionalString(event.source) ?? "-",
|
|
289
|
+
profileId: optionalString(event.profileId),
|
|
290
|
+
accountId: optionalString(event.accountId),
|
|
291
|
+
accountLabel: optionalString(event.accountLabel),
|
|
292
|
+
planType: optionalString(event.planType),
|
|
293
|
+
errorType: optionalString(event.errorType),
|
|
294
|
+
tokenUsage,
|
|
295
|
+
tokenUsageStatus: normalizeTokenUsageStatus(event.tokenUsageStatus, tokenUsage, success),
|
|
296
|
+
imageRoute: normalizeImageRoute(event.imageRoute),
|
|
297
|
+
imageCount: Math.max(0, Math.trunc(normalizeNumber(event.imageCount))),
|
|
298
|
+
success
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function lastModelSegment(model) {
|
|
302
|
+
const trimmed = model.trim();
|
|
303
|
+
if (!trimmed) {
|
|
304
|
+
return "";
|
|
305
|
+
}
|
|
306
|
+
const parts = trimmed.split("/");
|
|
307
|
+
return parts[parts.length - 1]?.trim() ?? "";
|
|
308
|
+
}
|
|
309
|
+
function canonicalizeModelForPricing(model) {
|
|
310
|
+
let normalized = lastModelSegment(model).toLowerCase();
|
|
311
|
+
if (!normalized) {
|
|
312
|
+
return "";
|
|
313
|
+
}
|
|
314
|
+
normalized = normalized.replaceAll("_", "-").replace(/\s+/g, "-");
|
|
315
|
+
while (normalized.includes("--")) {
|
|
316
|
+
normalized = normalized.replaceAll("--", "-");
|
|
317
|
+
}
|
|
318
|
+
if (normalized.startsWith("gpt5")) {
|
|
319
|
+
normalized = `gpt-5${normalized.slice("gpt5".length)}`;
|
|
320
|
+
}
|
|
321
|
+
normalized = normalized.replaceAll("gpt-5.4mini", "gpt-5.4-mini").replaceAll("gpt-5.4nano", "gpt-5.4-nano").replaceAll("gpt-5.3-codexspark", "gpt-5.3-codex-spark").replaceAll("gpt-5.3codexspark", "gpt-5.3-codex-spark").replaceAll("gpt-5.3codex", "gpt-5.3-codex");
|
|
322
|
+
const compactSuffix = "-openai-compact";
|
|
323
|
+
if (normalized.endsWith(compactSuffix)) {
|
|
324
|
+
normalized = normalized.slice(0, -compactSuffix.length);
|
|
325
|
+
}
|
|
326
|
+
return normalized;
|
|
327
|
+
}
|
|
328
|
+
function pricingKeyForModel(model) {
|
|
329
|
+
const normalized = canonicalizeModelForPricing(model);
|
|
330
|
+
if (!normalized) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
if (normalized.includes("gpt-5.5")) {
|
|
334
|
+
return "gpt-5.5";
|
|
335
|
+
}
|
|
336
|
+
if (normalized.includes("gpt-5.4-mini")) {
|
|
337
|
+
return "gpt-5.4-mini";
|
|
338
|
+
}
|
|
339
|
+
if (normalized.includes("gpt-5.4-nano")) {
|
|
340
|
+
return "gpt-5.4-nano";
|
|
341
|
+
}
|
|
342
|
+
if (normalized.includes("gpt-5.4")) {
|
|
343
|
+
return "gpt-5.4";
|
|
344
|
+
}
|
|
345
|
+
if (normalized.includes("gpt-5.2")) {
|
|
346
|
+
return "gpt-5.2";
|
|
347
|
+
}
|
|
348
|
+
if (normalized.includes("gpt-5.3-codex") || normalized.includes("gpt-5.3") || normalized.includes("codex")) {
|
|
349
|
+
return "gpt-5.3-codex";
|
|
350
|
+
}
|
|
351
|
+
if (normalized.includes("gpt-5")) {
|
|
352
|
+
return "gpt-5.4";
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
function estimateUsageCost(model, tokenUsage) {
|
|
357
|
+
const pricingKey = pricingKeyForModel(model);
|
|
358
|
+
const pricing = pricingKey ? tokenPricingByModel[pricingKey] : void 0;
|
|
359
|
+
if (!pricing) {
|
|
360
|
+
return {
|
|
361
|
+
inputCostUsd: 0,
|
|
362
|
+
outputCostUsd: 0,
|
|
363
|
+
cacheCreationCostUsd: 0,
|
|
364
|
+
cacheReadCostUsd: 0,
|
|
365
|
+
estimatedCostUsd: 0
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const inputTokens = tokenNumber(tokenUsage?.inputTokens) ?? 0;
|
|
369
|
+
const cacheReadTokens = tokenNumber(tokenUsage?.cacheReadTokens) ?? 0;
|
|
370
|
+
const uncachedInputTokens = tokenNumber(tokenUsage?.uncachedInputTokens) ?? Math.max(0, inputTokens - cacheReadTokens);
|
|
371
|
+
const outputTokens = tokenNumber(tokenUsage?.outputTokens) ?? 0;
|
|
372
|
+
const cacheCreationTokens = tokenNumber(tokenUsage?.cacheCreationTokens) ?? 0;
|
|
373
|
+
const totalInputTokens = uncachedInputTokens + cacheReadTokens;
|
|
374
|
+
const longContext = pricing.longContextInputThreshold && totalInputTokens > pricing.longContextInputThreshold;
|
|
375
|
+
const inputMultiplier = longContext ? pricing.longContextInputMultiplier ?? 1 : 1;
|
|
376
|
+
const outputMultiplier = longContext ? pricing.longContextOutputMultiplier ?? 1 : 1;
|
|
377
|
+
const inputCostUsd = uncachedInputTokens * pricing.inputUsdPerToken * inputMultiplier;
|
|
378
|
+
const outputCostUsd = outputTokens * pricing.outputUsdPerToken * outputMultiplier;
|
|
379
|
+
const cacheCreationCostUsd = cacheCreationTokens * pricing.cacheCreationUsdPerToken;
|
|
380
|
+
const cacheReadCostUsd = cacheReadTokens * pricing.cacheReadUsdPerToken;
|
|
381
|
+
return {
|
|
382
|
+
inputCostUsd,
|
|
383
|
+
outputCostUsd,
|
|
384
|
+
cacheCreationCostUsd,
|
|
385
|
+
cacheReadCostUsd,
|
|
386
|
+
estimatedCostUsd: inputCostUsd + outputCostUsd + cacheCreationCostUsd + cacheReadCostUsd
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function tokenUsageStatusForEvent(event) {
|
|
390
|
+
return normalizeTokenUsageStatus(event.tokenUsageStatus, event.tokenUsage ?? null, event.success ?? (event.statusCode >= 200 && event.statusCode < 400));
|
|
391
|
+
}
|
|
178
392
|
function addToAggregate(aggregate, event) {
|
|
179
393
|
const success = event.success ?? (event.statusCode >= 200 && event.statusCode < 400);
|
|
180
394
|
const inputTokens = tokenNumber(event.tokenUsage?.inputTokens);
|
|
395
|
+
const cacheReadTokens = tokenNumber(event.tokenUsage?.cacheReadTokens);
|
|
396
|
+
const cacheCreationTokens = tokenNumber(event.tokenUsage?.cacheCreationTokens);
|
|
397
|
+
const inferredUncachedInputTokens = inputTokens !== null ? Math.max(0, inputTokens - (cacheReadTokens ?? 0)) : null;
|
|
398
|
+
const uncachedInputTokens = tokenNumber(event.tokenUsage?.uncachedInputTokens) ?? inferredUncachedInputTokens;
|
|
181
399
|
const outputTokens = tokenNumber(event.tokenUsage?.outputTokens);
|
|
182
400
|
const totalTokens = tokenNumber(event.tokenUsage?.totalTokens) ?? (inputTokens !== null || outputTokens !== null ? (inputTokens ?? 0) + (outputTokens ?? 0) : null);
|
|
401
|
+
const cost = estimateUsageCost(event.model, event.tokenUsage);
|
|
183
402
|
aggregate.requestCount += 1;
|
|
184
403
|
aggregate.successCount += success ? 1 : 0;
|
|
185
404
|
aggregate.failureCount += success ? 0 : 1;
|
|
186
405
|
aggregate.inputTokens += inputTokens ?? 0;
|
|
406
|
+
aggregate.uncachedInputTokens += uncachedInputTokens ?? 0;
|
|
187
407
|
aggregate.outputTokens += outputTokens ?? 0;
|
|
188
408
|
aggregate.totalTokens += totalTokens ?? 0;
|
|
189
|
-
aggregate.
|
|
409
|
+
aggregate.cacheCreationTokens += cacheCreationTokens ?? 0;
|
|
410
|
+
aggregate.cacheReadTokens += cacheReadTokens ?? 0;
|
|
411
|
+
aggregate.inputCostUsd += cost.inputCostUsd;
|
|
412
|
+
aggregate.outputCostUsd += cost.outputCostUsd;
|
|
413
|
+
aggregate.cacheCreationCostUsd += cost.cacheCreationCostUsd;
|
|
414
|
+
aggregate.cacheReadCostUsd += cost.cacheReadCostUsd;
|
|
415
|
+
aggregate.estimatedCostUsd += cost.estimatedCostUsd;
|
|
416
|
+
if (totalTokens === null) {
|
|
417
|
+
const tokenUsageStatus = tokenUsageStatusForEvent(event);
|
|
418
|
+
aggregate.unknownTokenCount += 1;
|
|
419
|
+
aggregate.unknownTokenStatusCounts[tokenUsageStatus] = (aggregate.unknownTokenStatusCounts[tokenUsageStatus] ?? 0) + 1;
|
|
420
|
+
}
|
|
190
421
|
aggregate.imageCount += Math.max(0, Math.trunc(event.imageCount ?? 0));
|
|
191
422
|
aggregate.totalDurationMs += Math.max(0, event.durationMs);
|
|
192
423
|
const bucket = durationBucketKey(event.durationMs);
|
|
@@ -202,6 +433,42 @@ function imageRouteLabel(route) {
|
|
|
202
433
|
}
|
|
203
434
|
return "\u975E\u751F\u56FE";
|
|
204
435
|
}
|
|
436
|
+
function tokenUsageStatusLabel(status) {
|
|
437
|
+
if (status === "captured") {
|
|
438
|
+
return "\u5DF2\u6355\u83B7\u7528\u91CF";
|
|
439
|
+
}
|
|
440
|
+
if (status === "missing_terminal") {
|
|
441
|
+
return "\u7F3A\u5C11\u7EC8\u6001\u4E8B\u4EF6";
|
|
442
|
+
}
|
|
443
|
+
if (status === "terminal_without_usage") {
|
|
444
|
+
return "\u7EC8\u6001\u65E0 usage";
|
|
445
|
+
}
|
|
446
|
+
if (status === "parse_failed") {
|
|
447
|
+
return "SSE \u89E3\u6790\u5931\u8D25";
|
|
448
|
+
}
|
|
449
|
+
if (status === "upstream_error") {
|
|
450
|
+
return "\u4E0A\u6E38\u9519\u8BEF";
|
|
451
|
+
}
|
|
452
|
+
return "\u672A\u8FD4\u56DE usage";
|
|
453
|
+
}
|
|
454
|
+
function addEventToStores(daily, lifetime, normalized) {
|
|
455
|
+
const date = formatLocalDate(normalized.timestamp);
|
|
456
|
+
daily.days[date] = daily.days[date] ? normalizeAggregate(daily.days[date]) : createAggregate();
|
|
457
|
+
addToAggregate(daily.days[date], normalized);
|
|
458
|
+
addToAggregate(lifetime.aggregate, normalized);
|
|
459
|
+
bumpDimension(lifetime.byAccount, normalized.profileId || normalized.accountId || normalized.accountLabel || "-", normalized.accountLabel || normalized.accountId || normalized.profileId || "-", normalized);
|
|
460
|
+
bumpDimension(lifetime.byModel, normalized.model || "-", normalized.model || "-", normalized);
|
|
461
|
+
bumpDimension(lifetime.byEndpoint, `${normalized.method} ${normalized.endpoint}`, `${normalized.method} ${normalized.endpoint}`, normalized);
|
|
462
|
+
bumpDimension(lifetime.bySource, normalized.source || "-", normalized.source || "-", normalized);
|
|
463
|
+
bumpDimension(lifetime.byTokenUsageStatus, normalized.tokenUsageStatus ?? "not_returned", tokenUsageStatusLabel(normalized.tokenUsageStatus ?? "not_returned"), normalized);
|
|
464
|
+
bumpDimension(lifetime.byImageRoute, normalized.imageRoute, imageRouteLabel(normalized.imageRoute), normalized);
|
|
465
|
+
if (!normalized.success) {
|
|
466
|
+
const errorType = normalized.errorType?.trim() || `HTTP ${normalized.statusCode}`;
|
|
467
|
+
bumpDimension(lifetime.byError, errorType, errorType, normalized);
|
|
468
|
+
}
|
|
469
|
+
daily.updatedAt = Math.max(daily.updatedAt, normalized.timestamp);
|
|
470
|
+
lifetime.updatedAt = Math.max(lifetime.updatedAt, normalized.timestamp);
|
|
471
|
+
}
|
|
205
472
|
function bumpDimension(store, key, label, event) {
|
|
206
473
|
const normalizedKey = key.trim() || "-";
|
|
207
474
|
const existing = store[normalizedKey] ?? {
|
|
@@ -227,6 +494,14 @@ async function readJsonFile(filePath) {
|
|
|
227
494
|
return null;
|
|
228
495
|
}
|
|
229
496
|
}
|
|
497
|
+
async function fileExists(filePath) {
|
|
498
|
+
try {
|
|
499
|
+
await fs.access(filePath);
|
|
500
|
+
return true;
|
|
501
|
+
} catch {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
230
505
|
async function writeJsonAtomic(filePath, value) {
|
|
231
506
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
232
507
|
const tempPath = `${filePath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
@@ -239,6 +514,86 @@ async function writeJsonAtomic(filePath, value) {
|
|
|
239
514
|
throw error;
|
|
240
515
|
}
|
|
241
516
|
}
|
|
517
|
+
function formatBackupTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
518
|
+
const year = date.getFullYear();
|
|
519
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
520
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
521
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
522
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
523
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
524
|
+
return `${year}${month}${day}-${hours}${minutes}${seconds}`;
|
|
525
|
+
}
|
|
526
|
+
async function uniqueBackupDir(usageDir) {
|
|
527
|
+
const parentDir = path.dirname(usageDir);
|
|
528
|
+
const baseName = `${path.basename(usageDir)}.backup.${formatBackupTimestamp()}`;
|
|
529
|
+
let candidate = path.join(parentDir, baseName);
|
|
530
|
+
for (let index = 2; await fileExists(candidate); index += 1) {
|
|
531
|
+
candidate = path.join(parentDir, `${baseName}-${index}`);
|
|
532
|
+
}
|
|
533
|
+
return candidate;
|
|
534
|
+
}
|
|
535
|
+
function aggregateNeedsCostBackfill(aggregate) {
|
|
536
|
+
return aggregate.totalTokens > 0 && aggregate.estimatedCostUsd <= 0;
|
|
537
|
+
}
|
|
538
|
+
function shouldBackfillUsageCosts(daily, lifetime) {
|
|
539
|
+
const knownPricedModelMissingCost = Object.values(lifetime.byModel).some(
|
|
540
|
+
(row) => aggregateNeedsCostBackfill(row.aggregate) && pricingKeyForModel(row.key) !== null
|
|
541
|
+
);
|
|
542
|
+
if (!knownPricedModelMissingCost) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
return aggregateNeedsCostBackfill(lifetime.aggregate) || Object.values(daily.days).some(aggregateNeedsCostBackfill);
|
|
546
|
+
}
|
|
547
|
+
function shouldBackfillUsageDiagnostics(daily, lifetime) {
|
|
548
|
+
if (lifetime.aggregate.requestCount <= 0) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
if (Object.keys(lifetime.byTokenUsageStatus).length === 0) {
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
if (lifetime.aggregate.unknownTokenCount > 0 && Object.keys(lifetime.aggregate.unknownTokenStatusCounts).length === 0) {
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
return Object.values(daily.days).some(
|
|
558
|
+
(aggregate) => aggregate.unknownTokenCount > 0 && Object.keys(aggregate.unknownTokenStatusCounts).length === 0
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
async function rebuildStoresFromEventLogs() {
|
|
562
|
+
let entries;
|
|
563
|
+
try {
|
|
564
|
+
entries = await fs.readdir(getUsageEventsDir());
|
|
565
|
+
} catch {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
const eventFiles = entries.filter((entry) => entry.endsWith(".jsonl")).sort();
|
|
569
|
+
if (eventFiles.length === 0) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
const daily = createDailyStore();
|
|
573
|
+
const lifetime = createLifetimeStore();
|
|
574
|
+
let seen = false;
|
|
575
|
+
for (const fileName of eventFiles) {
|
|
576
|
+
let content;
|
|
577
|
+
try {
|
|
578
|
+
content = await fs.readFile(path.join(getUsageEventsDir(), fileName), "utf8");
|
|
579
|
+
} catch {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
for (const line of content.split(/\r?\n/)) {
|
|
583
|
+
const trimmed = line.trim();
|
|
584
|
+
if (!trimmed) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
const parsed = JSON.parse(trimmed);
|
|
589
|
+
addEventToStores(daily, lifetime, normalizeUsageRecordEvent(parsed));
|
|
590
|
+
seen = true;
|
|
591
|
+
} catch {
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return seen ? { daily, lifetime } : null;
|
|
596
|
+
}
|
|
242
597
|
class UsageService {
|
|
243
598
|
startedAt = Date.now();
|
|
244
599
|
startupAggregate = createAggregate();
|
|
@@ -248,39 +603,13 @@ class UsageService {
|
|
|
248
603
|
saveQueue = Promise.resolve();
|
|
249
604
|
async record(event) {
|
|
250
605
|
await this.ensureLoaded();
|
|
251
|
-
const
|
|
606
|
+
const normalized = normalizeUsageRecordEvent(event);
|
|
607
|
+
const timestamp = normalized.timestamp;
|
|
252
608
|
const date = formatLocalDate(timestamp);
|
|
253
|
-
const normalized = {
|
|
254
|
-
...event,
|
|
255
|
-
id: event.id ?? randomUUID(),
|
|
256
|
-
timestamp,
|
|
257
|
-
statusCode: event.statusCode,
|
|
258
|
-
durationMs: Math.max(0, event.durationMs),
|
|
259
|
-
endpoint: event.endpoint || "-",
|
|
260
|
-
method: event.method || "-",
|
|
261
|
-
model: event.model || "-",
|
|
262
|
-
source: event.source || "-",
|
|
263
|
-
imageRoute: event.imageRoute ?? "none",
|
|
264
|
-
imageCount: Math.max(0, Math.trunc(event.imageCount ?? 0)),
|
|
265
|
-
success: event.success ?? (event.statusCode >= 200 && event.statusCode < 400)
|
|
266
|
-
};
|
|
267
609
|
const daily = this.dailyStore ?? createDailyStore();
|
|
268
610
|
const lifetime = this.lifetimeStore ?? createLifetimeStore();
|
|
269
|
-
daily.days[date] = daily.days[date] ? normalizeAggregate(daily.days[date]) : createAggregate();
|
|
270
611
|
addToAggregate(this.startupAggregate, normalized);
|
|
271
|
-
|
|
272
|
-
addToAggregate(lifetime.aggregate, normalized);
|
|
273
|
-
bumpDimension(lifetime.byAccount, normalized.profileId || normalized.accountId || normalized.accountLabel || "-", normalized.accountLabel || normalized.accountId || normalized.profileId || "-", normalized);
|
|
274
|
-
bumpDimension(lifetime.byModel, normalized.model || "-", normalized.model || "-", normalized);
|
|
275
|
-
bumpDimension(lifetime.byEndpoint, `${normalized.method} ${normalized.endpoint}`, `${normalized.method} ${normalized.endpoint}`, normalized);
|
|
276
|
-
bumpDimension(lifetime.bySource, normalized.source || "-", normalized.source || "-", normalized);
|
|
277
|
-
bumpDimension(lifetime.byImageRoute, normalized.imageRoute ?? "none", imageRouteLabel(normalized.imageRoute ?? "none"), normalized);
|
|
278
|
-
if (!normalized.success) {
|
|
279
|
-
const errorType = normalized.errorType?.trim() || `HTTP ${normalized.statusCode}`;
|
|
280
|
-
bumpDimension(lifetime.byError, errorType, errorType, normalized);
|
|
281
|
-
}
|
|
282
|
-
daily.updatedAt = timestamp;
|
|
283
|
-
lifetime.updatedAt = timestamp;
|
|
612
|
+
addEventToStores(daily, lifetime, normalized);
|
|
284
613
|
this.dailyStore = daily;
|
|
285
614
|
this.lifetimeStore = lifetime;
|
|
286
615
|
const eventPath = path.join(getUsageEventsDir(), `${date}.jsonl`);
|
|
@@ -323,10 +652,41 @@ class UsageService {
|
|
|
323
652
|
byModel: topRows(lifetime.byModel, 16),
|
|
324
653
|
byEndpoint: topRows(lifetime.byEndpoint, 16),
|
|
325
654
|
byError: topRows(lifetime.byError, 16),
|
|
655
|
+
byTokenUsageStatus: topRows(lifetime.byTokenUsageStatus, 8),
|
|
326
656
|
byImageRoute: topRows(lifetime.byImageRoute, 8),
|
|
327
657
|
bySource: topRows(lifetime.bySource, 8)
|
|
328
658
|
};
|
|
329
659
|
}
|
|
660
|
+
async backupAndReset() {
|
|
661
|
+
await this.ensureLoaded();
|
|
662
|
+
let backupDir = "";
|
|
663
|
+
const reset = async () => {
|
|
664
|
+
const usageDir = getUsageDir();
|
|
665
|
+
await fs.mkdir(path.dirname(usageDir), { recursive: true });
|
|
666
|
+
backupDir = await uniqueBackupDir(usageDir);
|
|
667
|
+
if (await fileExists(usageDir)) {
|
|
668
|
+
await fs.rename(usageDir, backupDir);
|
|
669
|
+
} else {
|
|
670
|
+
await fs.mkdir(backupDir, { recursive: true });
|
|
671
|
+
}
|
|
672
|
+
const daily = createDailyStore();
|
|
673
|
+
const lifetime = createLifetimeStore();
|
|
674
|
+
Object.assign(this.startupAggregate, createAggregate());
|
|
675
|
+
this.dailyStore = daily;
|
|
676
|
+
this.lifetimeStore = lifetime;
|
|
677
|
+
await fs.mkdir(getUsageEventsDir(), { recursive: true });
|
|
678
|
+
await Promise.all([
|
|
679
|
+
writeJsonAtomic(getUsageDailyPath(), daily),
|
|
680
|
+
writeJsonAtomic(getUsageLifetimePath(), lifetime)
|
|
681
|
+
]);
|
|
682
|
+
};
|
|
683
|
+
this.saveQueue = this.saveQueue.then(reset, reset);
|
|
684
|
+
await this.saveQueue;
|
|
685
|
+
return {
|
|
686
|
+
backupDir,
|
|
687
|
+
usage: await this.getSummary()
|
|
688
|
+
};
|
|
689
|
+
}
|
|
330
690
|
async ensureLoaded() {
|
|
331
691
|
if (!this.loadPromise) {
|
|
332
692
|
this.loadPromise = (async () => {
|
|
@@ -339,6 +699,17 @@ class UsageService {
|
|
|
339
699
|
]);
|
|
340
700
|
this.dailyStore = normalizeDailyStore(dailyRaw);
|
|
341
701
|
this.lifetimeStore = normalizeLifetimeStore(lifetimeRaw);
|
|
702
|
+
if (shouldBackfillUsageCosts(this.dailyStore, this.lifetimeStore) || shouldBackfillUsageDiagnostics(this.dailyStore, this.lifetimeStore)) {
|
|
703
|
+
const rebuilt = await rebuildStoresFromEventLogs();
|
|
704
|
+
if (rebuilt) {
|
|
705
|
+
this.dailyStore = rebuilt.daily;
|
|
706
|
+
this.lifetimeStore = rebuilt.lifetime;
|
|
707
|
+
await Promise.all([
|
|
708
|
+
writeJsonAtomic(getUsageDailyPath(), rebuilt.daily),
|
|
709
|
+
writeJsonAtomic(getUsageLifetimePath(), rebuilt.lifetime)
|
|
710
|
+
]);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
342
713
|
})();
|
|
343
714
|
}
|
|
344
715
|
await this.loadPromise;
|
|
@@ -48,10 +48,71 @@ function sqliteQuote(value) {
|
|
|
48
48
|
async function runSqlite(dbPath, sql) {
|
|
49
49
|
const { stdout } = await execFileAsync("sqlite3", [dbPath, sql], {
|
|
50
50
|
timeout: 15e3,
|
|
51
|
-
maxBuffer: 1024 * 1024
|
|
51
|
+
maxBuffer: 8 * 1024 * 1024
|
|
52
52
|
});
|
|
53
53
|
return stdout.trim();
|
|
54
54
|
}
|
|
55
|
+
function resolveCodexSessionPath(value) {
|
|
56
|
+
return path.isAbsolute(value) ? value : path.join(getCodexHomeDir(), value);
|
|
57
|
+
}
|
|
58
|
+
function parseLegacyHistoryThreadRows(raw) {
|
|
59
|
+
if (!raw.trim()) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
return raw.split(/\r?\n/).map((line) => {
|
|
63
|
+
const separator = line.indexOf(" ");
|
|
64
|
+
if (separator === -1) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const id = line.slice(0, separator).trim();
|
|
68
|
+
const rolloutPath = line.slice(separator + 1).trim();
|
|
69
|
+
return id && rolloutPath ? { id, rolloutPath } : null;
|
|
70
|
+
}).filter((item) => Boolean(item));
|
|
71
|
+
}
|
|
72
|
+
async function patchSessionRolloutProvider(rolloutPath, backupSuffix, fromProvider, toProvider) {
|
|
73
|
+
const targetPath = resolveCodexSessionPath(rolloutPath);
|
|
74
|
+
let raw = "";
|
|
75
|
+
try {
|
|
76
|
+
raw = await fs.readFile(targetPath, "utf8");
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
const newline = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
84
|
+
const trailingNewline = raw.endsWith("\n");
|
|
85
|
+
const lines = raw.replace(/\r?\n$/u, "").split(/\r?\n/u);
|
|
86
|
+
let changed = false;
|
|
87
|
+
for (let index = 0; index < Math.min(lines.length, 20); index += 1) {
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(lines[index] ?? "");
|
|
90
|
+
if (!isRecord(parsed) || parsed.type !== "session_meta" || !isRecord(parsed.payload)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (parsed.payload.model_provider !== fromProvider) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
parsed.payload.model_provider = toProvider;
|
|
97
|
+
lines[index] = JSON.stringify(parsed);
|
|
98
|
+
changed = true;
|
|
99
|
+
break;
|
|
100
|
+
} catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!changed) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
await fs.copyFile(targetPath, `${targetPath}.azt-backup-${backupSuffix}`);
|
|
108
|
+
const tmpPath = `${targetPath}.tmp-${process.pid}`;
|
|
109
|
+
await fs.writeFile(tmpPath, `${lines.join(newline)}${trailingNewline ? newline : ""}`, {
|
|
110
|
+
encoding: "utf8",
|
|
111
|
+
mode: 384
|
|
112
|
+
});
|
|
113
|
+
await fs.rename(tmpPath, targetPath);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
55
116
|
async function migrateLegacyCodexHistoryProvider() {
|
|
56
117
|
const dbPath = getCodexStateDbPath();
|
|
57
118
|
if (!await fileExists(dbPath)) {
|
|
@@ -62,20 +123,32 @@ async function migrateLegacyCodexHistoryProvider() {
|
|
|
62
123
|
};
|
|
63
124
|
}
|
|
64
125
|
try {
|
|
65
|
-
const
|
|
126
|
+
const rowsRaw = await runSqlite(
|
|
66
127
|
dbPath,
|
|
67
|
-
`select
|
|
128
|
+
`select id || char(9) || rollout_path from threads where model_provider=${sqliteQuote(LEGACY_CODEX_PROVIDER_ID)};`
|
|
68
129
|
);
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
130
|
+
const legacyThreads = parseLegacyHistoryThreadRows(rowsRaw);
|
|
131
|
+
if (legacyThreads.length <= 0) {
|
|
71
132
|
return {
|
|
72
133
|
path: dbPath,
|
|
73
134
|
migratedCount: 0,
|
|
74
135
|
skipped: true
|
|
75
136
|
};
|
|
76
137
|
}
|
|
77
|
-
const
|
|
138
|
+
const backupSuffix = createBackupSuffix();
|
|
139
|
+
const backupPath = `${dbPath}.azt-backup-${backupSuffix}`;
|
|
78
140
|
await runSqlite(dbPath, `.backup ${sqliteQuote(backupPath)}`);
|
|
141
|
+
let rolloutPatchedCount = 0;
|
|
142
|
+
const rolloutPatchErrors = [];
|
|
143
|
+
for (const thread of legacyThreads) {
|
|
144
|
+
try {
|
|
145
|
+
if (await patchSessionRolloutProvider(thread.rolloutPath, backupSuffix, LEGACY_CODEX_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID)) {
|
|
146
|
+
rolloutPatchedCount += 1;
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
rolloutPatchErrors.push(`${thread.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
79
152
|
await runSqlite(
|
|
80
153
|
dbPath,
|
|
81
154
|
`update threads set model_provider=${sqliteQuote(OPENAI_CODEX_PROVIDER_ID)} where model_provider=${sqliteQuote(LEGACY_CODEX_PROVIDER_ID)};`
|
|
@@ -83,7 +156,9 @@ async function migrateLegacyCodexHistoryProvider() {
|
|
|
83
156
|
return {
|
|
84
157
|
path: dbPath,
|
|
85
158
|
backupPath,
|
|
86
|
-
migratedCount
|
|
159
|
+
migratedCount: legacyThreads.length,
|
|
160
|
+
rolloutPatchedCount,
|
|
161
|
+
rolloutPatchErrors: rolloutPatchErrors.length ? rolloutPatchErrors.slice(0, 20) : void 0
|
|
87
162
|
};
|
|
88
163
|
} catch (error) {
|
|
89
164
|
return {
|