ai-zero-token 2.0.6 → 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 +19 -1
- package/README.md +5 -5
- package/README.zh-CN.md +5 -5
- package/admin-ui/dist/assets/accounts-p9bqmijS.js +4 -0
- package/admin-ui/dist/assets/{docs--eK_2fzC.js → docs-BQaF_ZMr.js} +1 -1
- package/admin-ui/dist/assets/{image-bed-7wBZ1GhS.js → image-bed-D4w1m7k6.js} +1 -1
- package/admin-ui/dist/assets/index-BRQrU_AA.css +1 -0
- package/admin-ui/dist/assets/{index-CdFYy5j6.js → index-_5Ny0cZf.js} +3 -3
- package/admin-ui/dist/assets/{launch-BiD1Khtg.js → launch-BEDxgkQf.js} +1 -1
- package/admin-ui/dist/assets/{logs-BdoKDqh2.js → logs-BcL0n0Ld.js} +1 -1
- package/admin-ui/dist/assets/{network-detect-BvKns5nQ.js → network-detect-lEfklmIy.js} +1 -1
- package/admin-ui/dist/assets/{overview-wm6M45fu.js → overview-DsUMffIU.js} +1 -1
- package/admin-ui/dist/assets/{profiles-DMOjJORP.js → profiles-C5SmQvju.js} +1 -1
- package/admin-ui/dist/assets/settings-a3HxExcC.js +8 -0
- package/admin-ui/dist/assets/{tester-NrARmlis.js → tester-Ca4JOgAq.js} +1 -1
- package/admin-ui/dist/assets/usage-hMH0gMZ5.js +1 -0
- package/admin-ui/dist/index.html +3 -3
- package/dist/cli/commands/help.js +1 -1
- package/dist/cli/commands/models.js +3 -2
- package/dist/core/context.js +1 -1
- package/dist/core/models/openai-codex-models.js +106 -1
- package/dist/core/providers/http-client.js +142 -12
- package/dist/core/providers/openai-codex/chat.js +139 -8
- package/dist/core/services/auth-service.js +104 -7
- package/dist/core/services/chat-service.js +16 -18
- package/dist/core/services/model-service.js +22 -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 +410 -34
- package/dist/server/index.js +1 -1
- package/docs/API_USAGE.md +1 -1
- package/docs/DESKTOP_RELEASE.md +12 -1
- package/package.json +1 -1
- package/admin-ui/dist/assets/accounts-bCDKXGg9.js +0 -4
- package/admin-ui/dist/assets/index-C22_3Mxq.css +0 -1
- package/admin-ui/dist/assets/settings-DOOu7Kd8.js +0 -5
- package/admin-ui/dist/assets/usage-CdWRVMDV.js +0 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
getCodexModelCatalog,
|
|
4
|
-
hasCodexModel
|
|
4
|
+
hasCodexModel,
|
|
5
|
+
refreshCodexModelCatalogFromNetwork
|
|
5
6
|
} from "../models/openai-codex-models.js";
|
|
6
7
|
class ModelService {
|
|
7
|
-
constructor(configService) {
|
|
8
|
+
constructor(configService, authService) {
|
|
8
9
|
this.configService = configService;
|
|
10
|
+
this.authService = authService;
|
|
9
11
|
}
|
|
10
12
|
async listModels(provider = "openai-codex") {
|
|
11
13
|
if (provider !== "openai-codex") {
|
|
@@ -30,16 +32,28 @@ class ModelService {
|
|
|
30
32
|
if (provider !== "openai-codex") {
|
|
31
33
|
throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
|
|
32
34
|
}
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const profile = await this.authService.requireUsableProfile(provider, {
|
|
36
|
+
skipAutoSwitch: true
|
|
37
|
+
});
|
|
38
|
+
let result;
|
|
39
|
+
try {
|
|
40
|
+
result = await refreshCodexModelCatalogFromNetwork(profile);
|
|
41
|
+
await this.authService.recordProfileRequestSuccess(profile.profileId, void 0, provider, {
|
|
42
|
+
skipAutoSwitch: true
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
await this.authService.recordProfileRequestFailure(profile.profileId, error, void 0, provider, {
|
|
46
|
+
skipAutoSwitch: true
|
|
47
|
+
});
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
const defaultModel = await this.configService.getDefaultModel(provider);
|
|
37
51
|
return {
|
|
38
|
-
models: models.map((model) => ({
|
|
52
|
+
models: result.models.map((model) => ({
|
|
39
53
|
...model,
|
|
40
54
|
isDefault: model.id === defaultModel
|
|
41
55
|
})),
|
|
42
|
-
catalog
|
|
56
|
+
catalog: result.catalog
|
|
43
57
|
};
|
|
44
58
|
}
|
|
45
59
|
async getDefaultModel(provider = "openai-codex") {
|
|
@@ -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;
|