@praeviso/code-env-switch 0.1.4 → 0.1.5
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/README.md +17 -0
- package/README_zh.md +17 -0
- package/bin/commands/list.js +44 -2
- package/bin/statusline/format.js +6 -9
- package/bin/statusline/index.js +46 -8
- package/bin/statusline/input.js +9 -91
- package/bin/statusline/usage/claude.js +181 -0
- package/bin/statusline/usage/codex.js +168 -0
- package/bin/statusline/usage.js +20 -76
- package/bin/usage/index.js +396 -41
- package/bin/usage/pricing.js +303 -0
- package/code-env.example.json +55 -0
- package/package.json +1 -1
- package/src/commands/list.ts +74 -4
- package/src/statusline/format.ts +9 -10
- package/src/statusline/index.ts +74 -24
- package/src/statusline/input.ts +13 -154
- package/src/statusline/types.ts +6 -0
- package/src/statusline/usage/claude.ts +299 -0
- package/src/statusline/usage/codex.ts +263 -0
- package/src/statusline/usage.ts +24 -119
- package/src/types.ts +23 -0
- package/src/usage/index.ts +519 -35
- package/src/usage/pricing.ts +323 -0
- package/PLAN.md +0 -33
package/src/usage/index.ts
CHANGED
|
@@ -4,23 +4,36 @@
|
|
|
4
4
|
import * as fs from "fs";
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import * as os from "os";
|
|
7
|
-
import type { Config, ProfileType } from "../types";
|
|
7
|
+
import type { Config, Profile, ProfileType } from "../types";
|
|
8
8
|
import { resolvePath } from "../shell/utils";
|
|
9
9
|
import { normalizeType, inferProfileType, getProfileDisplayName } from "../profile/type";
|
|
10
|
+
import { calculateUsageCost, resolvePricingForProfile } from "./pricing";
|
|
10
11
|
|
|
11
12
|
interface UsageRecord {
|
|
12
13
|
ts: string;
|
|
13
14
|
type: string;
|
|
14
15
|
profileKey: string | null;
|
|
15
16
|
profileName: string | null;
|
|
17
|
+
model: string | null;
|
|
18
|
+
sessionId?: string | null;
|
|
16
19
|
inputTokens: number;
|
|
17
20
|
outputTokens: number;
|
|
21
|
+
cacheReadTokens: number;
|
|
22
|
+
cacheWriteTokens: number;
|
|
18
23
|
totalTokens: number;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
interface UsageTotals {
|
|
22
27
|
today: number;
|
|
23
28
|
total: number;
|
|
29
|
+
todayInput: number;
|
|
30
|
+
totalInput: number;
|
|
31
|
+
todayOutput: number;
|
|
32
|
+
totalOutput: number;
|
|
33
|
+
todayCacheRead: number;
|
|
34
|
+
totalCacheRead: number;
|
|
35
|
+
todayCacheWrite: number;
|
|
36
|
+
totalCacheWrite: number;
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
interface UsageTotalsIndex {
|
|
@@ -28,26 +41,44 @@ interface UsageTotalsIndex {
|
|
|
28
41
|
byName: Map<string, UsageTotals>;
|
|
29
42
|
}
|
|
30
43
|
|
|
44
|
+
interface UsageCostTotals {
|
|
45
|
+
today: number;
|
|
46
|
+
total: number;
|
|
47
|
+
todayTokens: number;
|
|
48
|
+
totalTokens: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface UsageCostIndex {
|
|
52
|
+
byKey: Map<string, UsageCostTotals>;
|
|
53
|
+
byName: Map<string, UsageCostTotals>;
|
|
54
|
+
}
|
|
55
|
+
|
|
31
56
|
interface UsageStateEntry {
|
|
32
57
|
mtimeMs: number;
|
|
33
58
|
size: number;
|
|
34
59
|
type: ProfileType;
|
|
35
60
|
inputTokens: number;
|
|
36
61
|
outputTokens: number;
|
|
62
|
+
cacheReadTokens: number;
|
|
63
|
+
cacheWriteTokens: number;
|
|
37
64
|
totalTokens: number;
|
|
38
65
|
startTs: string | null;
|
|
39
66
|
endTs: string | null;
|
|
40
67
|
cwd: string | null;
|
|
68
|
+
model?: string | null;
|
|
41
69
|
}
|
|
42
70
|
|
|
43
71
|
interface UsageSessionEntry {
|
|
44
72
|
type: ProfileType;
|
|
45
73
|
inputTokens: number;
|
|
46
74
|
outputTokens: number;
|
|
75
|
+
cacheReadTokens: number;
|
|
76
|
+
cacheWriteTokens: number;
|
|
47
77
|
totalTokens: number;
|
|
48
78
|
startTs: string | null;
|
|
49
79
|
endTs: string | null;
|
|
50
80
|
cwd: string | null;
|
|
81
|
+
model?: string | null;
|
|
51
82
|
}
|
|
52
83
|
|
|
53
84
|
interface UsageStateFile {
|
|
@@ -82,19 +113,46 @@ interface ProfileResolveResult {
|
|
|
82
113
|
interface SessionStats {
|
|
83
114
|
inputTokens: number;
|
|
84
115
|
outputTokens: number;
|
|
116
|
+
cacheReadTokens: number;
|
|
117
|
+
cacheWriteTokens: number;
|
|
85
118
|
totalTokens: number;
|
|
86
119
|
startTs: string | null;
|
|
87
120
|
endTs: string | null;
|
|
88
121
|
cwd: string | null;
|
|
89
122
|
sessionId: string | null;
|
|
123
|
+
model: string | null;
|
|
90
124
|
}
|
|
91
125
|
|
|
92
126
|
interface UsageTotalsInput {
|
|
93
127
|
inputTokens: number | null;
|
|
94
128
|
outputTokens: number | null;
|
|
129
|
+
cacheReadTokens: number | null;
|
|
130
|
+
cacheWriteTokens: number | null;
|
|
95
131
|
totalTokens: number | null;
|
|
96
132
|
}
|
|
97
133
|
|
|
134
|
+
function resolveProfileForRecord(
|
|
135
|
+
config: Config,
|
|
136
|
+
type: string | null,
|
|
137
|
+
record: UsageRecord
|
|
138
|
+
): Profile | null {
|
|
139
|
+
if (record.profileKey && config.profiles && config.profiles[record.profileKey]) {
|
|
140
|
+
return config.profiles[record.profileKey];
|
|
141
|
+
}
|
|
142
|
+
if (record.profileName && config.profiles) {
|
|
143
|
+
const matches = Object.entries(config.profiles).find(([key, entry]) => {
|
|
144
|
+
const displayName = getProfileDisplayName(key, entry, type || undefined);
|
|
145
|
+
return (
|
|
146
|
+
displayName === record.profileName ||
|
|
147
|
+
entry.name === record.profileName ||
|
|
148
|
+
key === record.profileName
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
if (matches) return matches[1];
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
98
156
|
function resolveDefaultConfigDir(configPath: string | null): string {
|
|
99
157
|
if (configPath) return path.dirname(configPath);
|
|
100
158
|
return path.join(os.homedir(), ".config", "code-env");
|
|
@@ -141,40 +199,175 @@ export function formatTokenCount(value: number | null | undefined): string {
|
|
|
141
199
|
return `${(value / 1_000_000_000).toFixed(2)}B`;
|
|
142
200
|
}
|
|
143
201
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
202
|
+
function createUsageTotals(): UsageTotals {
|
|
203
|
+
return {
|
|
204
|
+
today: 0,
|
|
205
|
+
total: 0,
|
|
206
|
+
todayInput: 0,
|
|
207
|
+
totalInput: 0,
|
|
208
|
+
todayOutput: 0,
|
|
209
|
+
totalOutput: 0,
|
|
210
|
+
todayCacheRead: 0,
|
|
211
|
+
totalCacheRead: 0,
|
|
212
|
+
todayCacheWrite: 0,
|
|
213
|
+
totalCacheWrite: 0,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function createUsageCostTotals(): UsageCostTotals {
|
|
218
|
+
return { today: 0, total: 0, todayTokens: 0, totalTokens: 0 };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function toUsageNumber(value: number | null | undefined): number {
|
|
222
|
+
const num = Number(value ?? 0);
|
|
223
|
+
return Number.isFinite(num) ? num : 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getTodayWindow(): { startMs: number; endMs: number } {
|
|
147
227
|
const todayStart = new Date();
|
|
148
228
|
todayStart.setHours(0, 0, 0, 0);
|
|
149
|
-
const
|
|
229
|
+
const startMs = todayStart.getTime();
|
|
150
230
|
const tomorrowStart = new Date(todayStart);
|
|
151
231
|
tomorrowStart.setDate(todayStart.getDate() + 1);
|
|
152
|
-
|
|
232
|
+
return { startMs, endMs: tomorrowStart.getTime() };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isTimestampInWindow(
|
|
236
|
+
ts: string,
|
|
237
|
+
startMs: number,
|
|
238
|
+
endMs: number
|
|
239
|
+
): boolean {
|
|
240
|
+
if (!ts) return false;
|
|
241
|
+
const time = new Date(ts).getTime();
|
|
242
|
+
if (Number.isNaN(time)) return false;
|
|
243
|
+
return time >= startMs && time < endMs;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function buildUsageTotals(records: UsageRecord[]): UsageTotalsIndex {
|
|
247
|
+
const byKey = new Map<string, UsageTotals>();
|
|
248
|
+
const byName = new Map<string, UsageTotals>();
|
|
249
|
+
const { startMs, endMs } = getTodayWindow();
|
|
153
250
|
|
|
154
251
|
const isToday = (ts: string) => {
|
|
155
|
-
|
|
156
|
-
const time = new Date(ts).getTime();
|
|
157
|
-
if (Number.isNaN(time)) return false;
|
|
158
|
-
return time >= todayStartMs && time < tomorrowStartMs;
|
|
252
|
+
return isTimestampInWindow(ts, startMs, endMs);
|
|
159
253
|
};
|
|
160
254
|
|
|
161
|
-
const addTotals = (
|
|
255
|
+
const addTotals = (
|
|
256
|
+
map: Map<string, UsageTotals>,
|
|
257
|
+
key: string,
|
|
258
|
+
amounts: {
|
|
259
|
+
total: number;
|
|
260
|
+
input: number;
|
|
261
|
+
output: number;
|
|
262
|
+
cacheRead: number;
|
|
263
|
+
cacheWrite: number;
|
|
264
|
+
},
|
|
265
|
+
ts: string
|
|
266
|
+
) => {
|
|
162
267
|
if (!key) return;
|
|
163
|
-
const current = map.get(key) ||
|
|
164
|
-
current.total +=
|
|
165
|
-
|
|
268
|
+
const current = map.get(key) || createUsageTotals();
|
|
269
|
+
current.total += amounts.total;
|
|
270
|
+
current.totalInput += amounts.input;
|
|
271
|
+
current.totalOutput += amounts.output;
|
|
272
|
+
current.totalCacheRead += amounts.cacheRead;
|
|
273
|
+
current.totalCacheWrite += amounts.cacheWrite;
|
|
274
|
+
if (isToday(ts)) {
|
|
275
|
+
current.today += amounts.total;
|
|
276
|
+
current.todayInput += amounts.input;
|
|
277
|
+
current.todayOutput += amounts.output;
|
|
278
|
+
current.todayCacheRead += amounts.cacheRead;
|
|
279
|
+
current.todayCacheWrite += amounts.cacheWrite;
|
|
280
|
+
}
|
|
166
281
|
map.set(key, current);
|
|
167
282
|
};
|
|
168
283
|
|
|
169
284
|
for (const record of records) {
|
|
170
285
|
const type = normalizeUsageType(record.type) || "";
|
|
171
|
-
const
|
|
286
|
+
const input = toUsageNumber(record.inputTokens);
|
|
287
|
+
const output = toUsageNumber(record.outputTokens);
|
|
288
|
+
const cacheRead = toUsageNumber(record.cacheReadTokens);
|
|
289
|
+
const cacheWrite = toUsageNumber(record.cacheWriteTokens);
|
|
290
|
+
const computedTotal = input + output + cacheRead + cacheWrite;
|
|
291
|
+
const rawTotal = Number(record.totalTokens ?? computedTotal);
|
|
292
|
+
const total = Number.isFinite(rawTotal)
|
|
293
|
+
? Math.max(rawTotal, computedTotal)
|
|
294
|
+
: computedTotal;
|
|
172
295
|
if (!Number.isFinite(total)) continue;
|
|
173
296
|
if (record.profileKey) {
|
|
174
|
-
addTotals(
|
|
297
|
+
addTotals(
|
|
298
|
+
byKey,
|
|
299
|
+
`${type}||${record.profileKey}`,
|
|
300
|
+
{ total, input, output, cacheRead, cacheWrite },
|
|
301
|
+
record.ts
|
|
302
|
+
);
|
|
175
303
|
}
|
|
176
304
|
if (record.profileName) {
|
|
177
|
-
addTotals(
|
|
305
|
+
addTotals(
|
|
306
|
+
byName,
|
|
307
|
+
`${type}||${record.profileName}`,
|
|
308
|
+
{ total, input, output, cacheRead, cacheWrite },
|
|
309
|
+
record.ts
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return { byKey, byName };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function buildUsageCostIndex(records: UsageRecord[], config: Config): UsageCostIndex {
|
|
318
|
+
const byKey = new Map<string, UsageCostTotals>();
|
|
319
|
+
const byName = new Map<string, UsageCostTotals>();
|
|
320
|
+
const { startMs, endMs } = getTodayWindow();
|
|
321
|
+
|
|
322
|
+
const addCost = (
|
|
323
|
+
map: Map<string, UsageCostTotals>,
|
|
324
|
+
key: string,
|
|
325
|
+
cost: number,
|
|
326
|
+
tokens: number,
|
|
327
|
+
ts: string
|
|
328
|
+
) => {
|
|
329
|
+
if (!key) return;
|
|
330
|
+
const current = map.get(key) || createUsageCostTotals();
|
|
331
|
+
current.total += cost;
|
|
332
|
+
current.totalTokens += tokens;
|
|
333
|
+
if (isTimestampInWindow(ts, startMs, endMs)) {
|
|
334
|
+
current.today += cost;
|
|
335
|
+
current.todayTokens += tokens;
|
|
336
|
+
}
|
|
337
|
+
map.set(key, current);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
for (const record of records) {
|
|
341
|
+
const model = normalizeModelValue(record.model);
|
|
342
|
+
if (!model) continue;
|
|
343
|
+
const type = normalizeUsageType(record.type) || "";
|
|
344
|
+
const profile =
|
|
345
|
+
record.profileKey && config.profiles ? config.profiles[record.profileKey] : null;
|
|
346
|
+
const pricing = resolvePricingForProfile(config, profile || null, model);
|
|
347
|
+
if (!pricing) continue;
|
|
348
|
+
const cost = calculateUsageCost(
|
|
349
|
+
{
|
|
350
|
+
totalTokens: record.totalTokens,
|
|
351
|
+
inputTokens: record.inputTokens,
|
|
352
|
+
outputTokens: record.outputTokens,
|
|
353
|
+
cacheReadTokens: record.cacheReadTokens,
|
|
354
|
+
cacheWriteTokens: record.cacheWriteTokens,
|
|
355
|
+
},
|
|
356
|
+
pricing
|
|
357
|
+
);
|
|
358
|
+
if (cost === null || !Number.isFinite(cost)) continue;
|
|
359
|
+
const billedTokens =
|
|
360
|
+
toUsageNumber(record.inputTokens) +
|
|
361
|
+
toUsageNumber(record.outputTokens) +
|
|
362
|
+
toUsageNumber(record.cacheReadTokens) +
|
|
363
|
+
toUsageNumber(record.cacheWriteTokens);
|
|
364
|
+
const billedTotal =
|
|
365
|
+
billedTokens > 0 ? billedTokens : toUsageNumber(record.totalTokens);
|
|
366
|
+
if (record.profileKey) {
|
|
367
|
+
addCost(byKey, `${type}||${record.profileKey}`, cost, billedTotal, record.ts);
|
|
368
|
+
}
|
|
369
|
+
if (record.profileName) {
|
|
370
|
+
addCost(byName, `${type}||${record.profileName}`, cost, billedTotal, record.ts);
|
|
178
371
|
}
|
|
179
372
|
}
|
|
180
373
|
|
|
@@ -189,6 +382,12 @@ function normalizeUsageType(type: string | null | undefined): string | null {
|
|
|
189
382
|
return trimmed ? trimmed : null;
|
|
190
383
|
}
|
|
191
384
|
|
|
385
|
+
function normalizeModelValue(value: unknown): string | null {
|
|
386
|
+
if (typeof value !== "string") return null;
|
|
387
|
+
const trimmed = value.trim();
|
|
388
|
+
return trimmed ? trimmed : null;
|
|
389
|
+
}
|
|
390
|
+
|
|
192
391
|
function buildSessionKey(type: ProfileType | null, sessionId: string): string {
|
|
193
392
|
const normalized = normalizeUsageType(type || "");
|
|
194
393
|
return normalized ? `${normalized}::${sessionId}` : sessionId;
|
|
@@ -226,6 +425,72 @@ export function readUsageTotalsIndex(
|
|
|
226
425
|
return buildUsageTotals(records);
|
|
227
426
|
}
|
|
228
427
|
|
|
428
|
+
export function readUsageCostIndex(
|
|
429
|
+
config: Config,
|
|
430
|
+
configPath: string | null,
|
|
431
|
+
syncUsage: boolean
|
|
432
|
+
): UsageCostIndex | null {
|
|
433
|
+
const usagePath = getUsagePath(config, configPath);
|
|
434
|
+
if (!usagePath) return null;
|
|
435
|
+
if (syncUsage) {
|
|
436
|
+
syncUsageFromSessions(config, configPath, usagePath);
|
|
437
|
+
}
|
|
438
|
+
const records = readUsageRecords(usagePath);
|
|
439
|
+
if (records.length === 0) return null;
|
|
440
|
+
const costs = buildUsageCostIndex(records, config);
|
|
441
|
+
if (costs.byKey.size === 0 && costs.byName.size === 0) return null;
|
|
442
|
+
return costs;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function readUsageSessionCost(
|
|
446
|
+
config: Config,
|
|
447
|
+
configPath: string | null,
|
|
448
|
+
type: string | null,
|
|
449
|
+
sessionId: string | null,
|
|
450
|
+
syncUsage: boolean
|
|
451
|
+
): number | null {
|
|
452
|
+
if (!sessionId) return null;
|
|
453
|
+
const usagePath = getUsagePath(config, configPath);
|
|
454
|
+
if (!usagePath) return null;
|
|
455
|
+
if (syncUsage) {
|
|
456
|
+
syncUsageFromSessions(config, configPath, usagePath);
|
|
457
|
+
}
|
|
458
|
+
const records = readUsageRecords(usagePath);
|
|
459
|
+
if (records.length === 0) return null;
|
|
460
|
+
const normalizedType = normalizeUsageType(type);
|
|
461
|
+
let total = 0;
|
|
462
|
+
let hasCost = false;
|
|
463
|
+
for (const record of records) {
|
|
464
|
+
if (!record.sessionId) continue;
|
|
465
|
+
if (record.sessionId !== sessionId) continue;
|
|
466
|
+
if (
|
|
467
|
+
normalizedType &&
|
|
468
|
+
normalizeUsageType(record.type) !== normalizedType
|
|
469
|
+
) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const model = normalizeModelValue(record.model);
|
|
473
|
+
if (!model) continue;
|
|
474
|
+
const profile = resolveProfileForRecord(config, normalizedType, record);
|
|
475
|
+
const pricing = resolvePricingForProfile(config, profile, model);
|
|
476
|
+
if (!pricing) continue;
|
|
477
|
+
const cost = calculateUsageCost(
|
|
478
|
+
{
|
|
479
|
+
totalTokens: record.totalTokens,
|
|
480
|
+
inputTokens: record.inputTokens,
|
|
481
|
+
outputTokens: record.outputTokens,
|
|
482
|
+
cacheReadTokens: record.cacheReadTokens,
|
|
483
|
+
cacheWriteTokens: record.cacheWriteTokens,
|
|
484
|
+
},
|
|
485
|
+
pricing
|
|
486
|
+
);
|
|
487
|
+
if (cost === null || !Number.isFinite(cost)) continue;
|
|
488
|
+
total += cost;
|
|
489
|
+
hasCost = true;
|
|
490
|
+
}
|
|
491
|
+
return hasCost ? total : null;
|
|
492
|
+
}
|
|
493
|
+
|
|
229
494
|
export function resolveUsageTotalsForProfile(
|
|
230
495
|
totals: UsageTotalsIndex,
|
|
231
496
|
type: string | null,
|
|
@@ -241,6 +506,21 @@ export function resolveUsageTotalsForProfile(
|
|
|
241
506
|
);
|
|
242
507
|
}
|
|
243
508
|
|
|
509
|
+
export function resolveUsageCostForProfile(
|
|
510
|
+
costs: UsageCostIndex,
|
|
511
|
+
type: string | null,
|
|
512
|
+
profileKey: string | null,
|
|
513
|
+
profileName: string | null
|
|
514
|
+
): UsageCostTotals | null {
|
|
515
|
+
const keyLookup = buildUsageLookupKey(type, profileKey);
|
|
516
|
+
const nameLookup = buildUsageLookupKey(type, profileName);
|
|
517
|
+
return (
|
|
518
|
+
(keyLookup && costs.byKey.get(keyLookup)) ||
|
|
519
|
+
(nameLookup && costs.byName.get(nameLookup)) ||
|
|
520
|
+
null
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
244
524
|
export function syncUsageFromStatuslineInput(
|
|
245
525
|
config: Config,
|
|
246
526
|
configPath: string | null,
|
|
@@ -249,19 +529,28 @@ export function syncUsageFromStatuslineInput(
|
|
|
249
529
|
profileName: string | null,
|
|
250
530
|
sessionId: string | null,
|
|
251
531
|
totals: UsageTotalsInput | null,
|
|
252
|
-
cwd: string | null
|
|
532
|
+
cwd: string | null,
|
|
533
|
+
model: string | null
|
|
253
534
|
): void {
|
|
254
535
|
if (!sessionId) return;
|
|
255
536
|
if (!totals) return;
|
|
256
537
|
if (!profileKey && !profileName) return;
|
|
538
|
+
const resolvedModel = normalizeModelValue(model);
|
|
539
|
+
if (!resolvedModel) return;
|
|
257
540
|
const normalizedType = normalizeType(type || "");
|
|
258
541
|
if (!normalizedType) return;
|
|
259
542
|
const usagePath = getUsagePath(config, configPath);
|
|
260
543
|
if (!usagePath) return;
|
|
261
544
|
const inputTokens = toFiniteNumber(totals.inputTokens) ?? 0;
|
|
262
545
|
const outputTokens = toFiniteNumber(totals.outputTokens) ?? 0;
|
|
546
|
+
const cacheReadTokens = toFiniteNumber(totals.cacheReadTokens) ?? 0;
|
|
547
|
+
const cacheWriteTokens = toFiniteNumber(totals.cacheWriteTokens) ?? 0;
|
|
263
548
|
const totalTokens =
|
|
264
|
-
toFiniteNumber(totals.totalTokens) ??
|
|
549
|
+
toFiniteNumber(totals.totalTokens) ??
|
|
550
|
+
inputTokens +
|
|
551
|
+
outputTokens +
|
|
552
|
+
cacheReadTokens +
|
|
553
|
+
cacheWriteTokens;
|
|
265
554
|
if (!Number.isFinite(totalTokens)) return;
|
|
266
555
|
|
|
267
556
|
const statePath = getUsageStatePath(usagePath, config);
|
|
@@ -273,16 +562,28 @@ export function syncUsageFromStatuslineInput(
|
|
|
273
562
|
const sessions = state.sessions || {};
|
|
274
563
|
const key = buildSessionKey(normalizedType, sessionId);
|
|
275
564
|
const prev = sessions[key];
|
|
276
|
-
const prevInput = prev ? prev.inputTokens : 0;
|
|
277
|
-
const prevOutput = prev ? prev.outputTokens : 0;
|
|
278
|
-
const
|
|
565
|
+
const prevInput = prev ? toUsageNumber(prev.inputTokens) : 0;
|
|
566
|
+
const prevOutput = prev ? toUsageNumber(prev.outputTokens) : 0;
|
|
567
|
+
const prevCacheRead = prev ? toUsageNumber(prev.cacheReadTokens) : 0;
|
|
568
|
+
const prevCacheWrite = prev ? toUsageNumber(prev.cacheWriteTokens) : 0;
|
|
569
|
+
const prevTotal = prev ? toUsageNumber(prev.totalTokens) : 0;
|
|
279
570
|
|
|
280
571
|
let deltaInput = inputTokens - prevInput;
|
|
281
572
|
let deltaOutput = outputTokens - prevOutput;
|
|
573
|
+
let deltaCacheRead = cacheReadTokens - prevCacheRead;
|
|
574
|
+
let deltaCacheWrite = cacheWriteTokens - prevCacheWrite;
|
|
282
575
|
let deltaTotal = totalTokens - prevTotal;
|
|
283
|
-
if (
|
|
576
|
+
if (
|
|
577
|
+
deltaTotal < 0 ||
|
|
578
|
+
deltaInput < 0 ||
|
|
579
|
+
deltaOutput < 0 ||
|
|
580
|
+
deltaCacheRead < 0 ||
|
|
581
|
+
deltaCacheWrite < 0
|
|
582
|
+
) {
|
|
284
583
|
deltaInput = inputTokens;
|
|
285
584
|
deltaOutput = outputTokens;
|
|
585
|
+
deltaCacheRead = cacheReadTokens;
|
|
586
|
+
deltaCacheWrite = cacheWriteTokens;
|
|
286
587
|
deltaTotal = totalTokens;
|
|
287
588
|
}
|
|
288
589
|
|
|
@@ -292,8 +593,12 @@ export function syncUsageFromStatuslineInput(
|
|
|
292
593
|
type: normalizedType,
|
|
293
594
|
profileKey: profileKey || null,
|
|
294
595
|
profileName: profileName || null,
|
|
596
|
+
model: resolvedModel,
|
|
597
|
+
sessionId,
|
|
295
598
|
inputTokens: deltaInput,
|
|
296
599
|
outputTokens: deltaOutput,
|
|
600
|
+
cacheReadTokens: deltaCacheRead,
|
|
601
|
+
cacheWriteTokens: deltaCacheWrite,
|
|
297
602
|
totalTokens: deltaTotal,
|
|
298
603
|
};
|
|
299
604
|
appendUsageRecord(usagePath, record);
|
|
@@ -304,10 +609,13 @@ export function syncUsageFromStatuslineInput(
|
|
|
304
609
|
type: normalizedType,
|
|
305
610
|
inputTokens,
|
|
306
611
|
outputTokens,
|
|
612
|
+
cacheReadTokens,
|
|
613
|
+
cacheWriteTokens,
|
|
307
614
|
totalTokens,
|
|
308
615
|
startTs: prev ? prev.startTs : now,
|
|
309
616
|
endTs: now,
|
|
310
617
|
cwd: cwd || (prev ? prev.cwd : null),
|
|
618
|
+
model: resolvedModel,
|
|
311
619
|
};
|
|
312
620
|
state.sessions = sessions;
|
|
313
621
|
writeUsageState(statePath, state);
|
|
@@ -595,6 +903,66 @@ function updateMinMaxTs(
|
|
|
595
903
|
}
|
|
596
904
|
}
|
|
597
905
|
|
|
906
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
907
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function coerceModelFromValue(value: unknown): string | null {
|
|
911
|
+
if (typeof value === "string") return normalizeModelValue(value);
|
|
912
|
+
if (!isPlainObject(value)) return null;
|
|
913
|
+
return (
|
|
914
|
+
normalizeModelValue(value.displayName) ||
|
|
915
|
+
normalizeModelValue(value.display_name) ||
|
|
916
|
+
normalizeModelValue(value.name) ||
|
|
917
|
+
normalizeModelValue(value.id) ||
|
|
918
|
+
normalizeModelValue(value.model) ||
|
|
919
|
+
normalizeModelValue(value.model_name) ||
|
|
920
|
+
normalizeModelValue(value.model_id) ||
|
|
921
|
+
normalizeModelValue(value.modelId) ||
|
|
922
|
+
null
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function pickModelFromObject(record: Record<string, unknown>): string | null {
|
|
927
|
+
return (
|
|
928
|
+
coerceModelFromValue(record.model) ||
|
|
929
|
+
normalizeModelValue(record.model_name) ||
|
|
930
|
+
normalizeModelValue(record.modelName) ||
|
|
931
|
+
normalizeModelValue(record.model_id) ||
|
|
932
|
+
normalizeModelValue(record.modelId) ||
|
|
933
|
+
normalizeModelValue(record.model_display_name) ||
|
|
934
|
+
normalizeModelValue(record.modelDisplayName) ||
|
|
935
|
+
null
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function extractModelFromRecord(record: Record<string, unknown>): string | null {
|
|
940
|
+
const direct = pickModelFromObject(record);
|
|
941
|
+
if (direct) return direct;
|
|
942
|
+
const message = isPlainObject(record.message)
|
|
943
|
+
? (record.message as Record<string, unknown>)
|
|
944
|
+
: null;
|
|
945
|
+
if (message) {
|
|
946
|
+
const fromMessage = pickModelFromObject(message);
|
|
947
|
+
if (fromMessage) return fromMessage;
|
|
948
|
+
}
|
|
949
|
+
const payload = isPlainObject(record.payload)
|
|
950
|
+
? (record.payload as Record<string, unknown>)
|
|
951
|
+
: null;
|
|
952
|
+
if (payload) {
|
|
953
|
+
const fromPayload = pickModelFromObject(payload);
|
|
954
|
+
if (fromPayload) return fromPayload;
|
|
955
|
+
const info = isPlainObject(payload.info)
|
|
956
|
+
? (payload.info as Record<string, unknown>)
|
|
957
|
+
: null;
|
|
958
|
+
if (info) {
|
|
959
|
+
const fromInfo = pickModelFromObject(info);
|
|
960
|
+
if (fromInfo) return fromInfo;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
|
|
598
966
|
function parseCodexSessionFile(filePath: string): SessionStats {
|
|
599
967
|
const raw = fs.readFileSync(filePath, "utf8");
|
|
600
968
|
const lines = raw.split(/\r?\n/);
|
|
@@ -608,6 +976,7 @@ function parseCodexSessionFile(filePath: string): SessionStats {
|
|
|
608
976
|
const tsRange = { start: null as string | null, end: null as string | null };
|
|
609
977
|
let cwd: string | null = null;
|
|
610
978
|
let sessionId: string | null = null;
|
|
979
|
+
let model: string | null = null;
|
|
611
980
|
|
|
612
981
|
for (const line of lines) {
|
|
613
982
|
const trimmed = line.trim();
|
|
@@ -615,6 +984,10 @@ function parseCodexSessionFile(filePath: string): SessionStats {
|
|
|
615
984
|
try {
|
|
616
985
|
const parsed = JSON.parse(trimmed);
|
|
617
986
|
if (!parsed || typeof parsed !== "object") continue;
|
|
987
|
+
if (!model) {
|
|
988
|
+
const candidate = extractModelFromRecord(parsed as Record<string, unknown>);
|
|
989
|
+
if (candidate) model = candidate;
|
|
990
|
+
}
|
|
618
991
|
if (parsed.timestamp) updateMinMaxTs(tsRange, String(parsed.timestamp));
|
|
619
992
|
if (!cwd && parsed.type === "session_meta") {
|
|
620
993
|
const payload = parsed.payload || {};
|
|
@@ -667,11 +1040,14 @@ function parseCodexSessionFile(filePath: string): SessionStats {
|
|
|
667
1040
|
return {
|
|
668
1041
|
inputTokens: maxInput,
|
|
669
1042
|
outputTokens: maxOutput,
|
|
1043
|
+
cacheReadTokens: 0,
|
|
1044
|
+
cacheWriteTokens: 0,
|
|
670
1045
|
totalTokens: maxTotal,
|
|
671
1046
|
startTs: tsRange.start,
|
|
672
1047
|
endTs: tsRange.end,
|
|
673
1048
|
cwd,
|
|
674
1049
|
sessionId,
|
|
1050
|
+
model,
|
|
675
1051
|
};
|
|
676
1052
|
}
|
|
677
1053
|
|
|
@@ -681,9 +1057,12 @@ function parseClaudeSessionFile(filePath: string): SessionStats {
|
|
|
681
1057
|
let totalTokens = 0;
|
|
682
1058
|
let inputTokens = 0;
|
|
683
1059
|
let outputTokens = 0;
|
|
1060
|
+
let cacheReadTokens = 0;
|
|
1061
|
+
let cacheWriteTokens = 0;
|
|
684
1062
|
const tsRange = { start: null as string | null, end: null as string | null };
|
|
685
1063
|
let cwd: string | null = null;
|
|
686
1064
|
let sessionId: string | null = null;
|
|
1065
|
+
let model: string | null = null;
|
|
687
1066
|
|
|
688
1067
|
for (const line of lines) {
|
|
689
1068
|
const trimmed = line.trim();
|
|
@@ -691,6 +1070,10 @@ function parseClaudeSessionFile(filePath: string): SessionStats {
|
|
|
691
1070
|
try {
|
|
692
1071
|
const parsed = JSON.parse(trimmed);
|
|
693
1072
|
if (!parsed || typeof parsed !== "object") continue;
|
|
1073
|
+
if (!model) {
|
|
1074
|
+
const candidate = extractModelFromRecord(parsed as Record<string, unknown>);
|
|
1075
|
+
if (candidate) model = candidate;
|
|
1076
|
+
}
|
|
694
1077
|
if (parsed.timestamp) updateMinMaxTs(tsRange, String(parsed.timestamp));
|
|
695
1078
|
if (!cwd && parsed.cwd) cwd = String(parsed.cwd);
|
|
696
1079
|
if (!sessionId && parsed.sessionId) {
|
|
@@ -705,6 +1088,8 @@ function parseClaudeSessionFile(filePath: string): SessionStats {
|
|
|
705
1088
|
const cacheRead = Number(usage.cache_read_input_tokens ?? 0);
|
|
706
1089
|
if (Number.isFinite(input)) inputTokens += input;
|
|
707
1090
|
if (Number.isFinite(output)) outputTokens += output;
|
|
1091
|
+
if (Number.isFinite(cacheCreate)) cacheWriteTokens += cacheCreate;
|
|
1092
|
+
if (Number.isFinite(cacheRead)) cacheReadTokens += cacheRead;
|
|
708
1093
|
totalTokens +=
|
|
709
1094
|
(Number.isFinite(input) ? input : 0) +
|
|
710
1095
|
(Number.isFinite(output) ? output : 0) +
|
|
@@ -718,11 +1103,14 @@ function parseClaudeSessionFile(filePath: string): SessionStats {
|
|
|
718
1103
|
return {
|
|
719
1104
|
inputTokens,
|
|
720
1105
|
outputTokens,
|
|
1106
|
+
cacheReadTokens,
|
|
1107
|
+
cacheWriteTokens,
|
|
721
1108
|
totalTokens,
|
|
722
1109
|
startTs: tsRange.start,
|
|
723
1110
|
endTs: tsRange.end,
|
|
724
1111
|
cwd,
|
|
725
1112
|
sessionId,
|
|
1113
|
+
model,
|
|
726
1114
|
};
|
|
727
1115
|
}
|
|
728
1116
|
|
|
@@ -827,16 +1215,56 @@ export function readUsageRecords(usagePath: string): UsageRecord[] {
|
|
|
827
1215
|
if (!parsed || typeof parsed !== "object") continue;
|
|
828
1216
|
const input = Number(parsed.inputTokens ?? 0);
|
|
829
1217
|
const output = Number(parsed.outputTokens ?? 0);
|
|
830
|
-
const
|
|
1218
|
+
const cacheRead = Number(
|
|
1219
|
+
parsed.cacheReadTokens ??
|
|
1220
|
+
parsed.cache_read_input_tokens ??
|
|
1221
|
+
parsed.cacheReadInputTokens ??
|
|
1222
|
+
0
|
|
1223
|
+
);
|
|
1224
|
+
const cacheWrite = Number(
|
|
1225
|
+
parsed.cacheWriteTokens ??
|
|
1226
|
+
parsed.cache_creation_input_tokens ??
|
|
1227
|
+
parsed.cache_write_input_tokens ??
|
|
1228
|
+
parsed.cacheWriteInputTokens ??
|
|
1229
|
+
0
|
|
1230
|
+
);
|
|
1231
|
+
const computedTotal =
|
|
1232
|
+
(Number.isFinite(input) ? input : 0) +
|
|
1233
|
+
(Number.isFinite(output) ? output : 0) +
|
|
1234
|
+
(Number.isFinite(cacheRead) ? cacheRead : 0) +
|
|
1235
|
+
(Number.isFinite(cacheWrite) ? cacheWrite : 0);
|
|
1236
|
+
const total = Number(
|
|
1237
|
+
parsed.totalTokens ?? computedTotal
|
|
1238
|
+
);
|
|
1239
|
+
const finalTotal = Number.isFinite(total)
|
|
1240
|
+
? Math.max(total, computedTotal)
|
|
1241
|
+
: computedTotal;
|
|
831
1242
|
const type = normalizeType(parsed.type) || String(parsed.type ?? "unknown");
|
|
1243
|
+
const model =
|
|
1244
|
+
normalizeModelValue(parsed.model) ||
|
|
1245
|
+
normalizeModelValue(parsed.model_name) ||
|
|
1246
|
+
normalizeModelValue(parsed.modelName) ||
|
|
1247
|
+
normalizeModelValue(parsed.model_id) ||
|
|
1248
|
+
normalizeModelValue(parsed.modelId) ||
|
|
1249
|
+
null;
|
|
1250
|
+
const sessionId =
|
|
1251
|
+
parsed.sessionId ||
|
|
1252
|
+
parsed.session_id ||
|
|
1253
|
+
parsed.sessionID ||
|
|
1254
|
+
parsed.session ||
|
|
1255
|
+
null;
|
|
832
1256
|
records.push({
|
|
833
1257
|
ts: String(parsed.ts ?? ""),
|
|
834
1258
|
type,
|
|
835
1259
|
profileKey: parsed.profileKey ? String(parsed.profileKey) : null,
|
|
836
1260
|
profileName: parsed.profileName ? String(parsed.profileName) : null,
|
|
1261
|
+
model,
|
|
1262
|
+
sessionId: sessionId ? String(sessionId) : null,
|
|
837
1263
|
inputTokens: Number.isFinite(input) ? input : 0,
|
|
838
1264
|
outputTokens: Number.isFinite(output) ? output : 0,
|
|
839
|
-
|
|
1265
|
+
cacheReadTokens: Number.isFinite(cacheRead) ? cacheRead : 0,
|
|
1266
|
+
cacheWriteTokens: Number.isFinite(cacheWrite) ? cacheWrite : 0,
|
|
1267
|
+
totalTokens: Number.isFinite(finalTotal) ? finalTotal : 0,
|
|
840
1268
|
});
|
|
841
1269
|
} catch {
|
|
842
1270
|
// ignore invalid lines
|
|
@@ -896,29 +1324,54 @@ export function syncUsageFromSessions(
|
|
|
896
1324
|
const sessionKey =
|
|
897
1325
|
stats.sessionId ? buildSessionKey(type, stats.sessionId) : null;
|
|
898
1326
|
const sessionPrev = sessionKey ? sessions[sessionKey] : null;
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
1327
|
+
const resolvedModel =
|
|
1328
|
+
(sessionPrev && sessionPrev.model) ||
|
|
1329
|
+
stats.model ||
|
|
1330
|
+
(prev && prev.model) ||
|
|
1331
|
+
null;
|
|
1332
|
+
const prevInput = prev ? toUsageNumber(prev.inputTokens) : 0;
|
|
1333
|
+
const prevOutput = prev ? toUsageNumber(prev.outputTokens) : 0;
|
|
1334
|
+
const prevCacheRead = prev ? toUsageNumber(prev.cacheReadTokens) : 0;
|
|
1335
|
+
const prevCacheWrite = prev ? toUsageNumber(prev.cacheWriteTokens) : 0;
|
|
1336
|
+
const prevTotal = prev ? toUsageNumber(prev.totalTokens) : 0;
|
|
902
1337
|
const prevInputMax = sessionPrev
|
|
903
|
-
? Math.max(prevInput, sessionPrev.inputTokens)
|
|
1338
|
+
? Math.max(prevInput, toUsageNumber(sessionPrev.inputTokens))
|
|
904
1339
|
: prevInput;
|
|
905
1340
|
const prevOutputMax = sessionPrev
|
|
906
|
-
? Math.max(prevOutput, sessionPrev.outputTokens)
|
|
1341
|
+
? Math.max(prevOutput, toUsageNumber(sessionPrev.outputTokens))
|
|
907
1342
|
: prevOutput;
|
|
1343
|
+
const prevCacheReadMax = sessionPrev
|
|
1344
|
+
? Math.max(prevCacheRead, toUsageNumber(sessionPrev.cacheReadTokens))
|
|
1345
|
+
: prevCacheRead;
|
|
1346
|
+
const prevCacheWriteMax = sessionPrev
|
|
1347
|
+
? Math.max(prevCacheWrite, toUsageNumber(sessionPrev.cacheWriteTokens))
|
|
1348
|
+
: prevCacheWrite;
|
|
908
1349
|
const prevTotalMax = sessionPrev
|
|
909
|
-
? Math.max(prevTotal, sessionPrev.totalTokens)
|
|
1350
|
+
? Math.max(prevTotal, toUsageNumber(sessionPrev.totalTokens))
|
|
910
1351
|
: prevTotal;
|
|
911
1352
|
let deltaInput = stats.inputTokens - prevInputMax;
|
|
912
1353
|
let deltaOutput = stats.outputTokens - prevOutputMax;
|
|
1354
|
+
let deltaCacheRead = stats.cacheReadTokens - prevCacheReadMax;
|
|
1355
|
+
let deltaCacheWrite = stats.cacheWriteTokens - prevCacheWriteMax;
|
|
913
1356
|
let deltaTotal = stats.totalTokens - prevTotalMax;
|
|
914
|
-
if (
|
|
1357
|
+
if (
|
|
1358
|
+
deltaTotal < 0 ||
|
|
1359
|
+
deltaInput < 0 ||
|
|
1360
|
+
deltaOutput < 0 ||
|
|
1361
|
+
deltaCacheRead < 0 ||
|
|
1362
|
+
deltaCacheWrite < 0
|
|
1363
|
+
) {
|
|
915
1364
|
if (sessionPrev) {
|
|
916
1365
|
deltaInput = 0;
|
|
917
1366
|
deltaOutput = 0;
|
|
1367
|
+
deltaCacheRead = 0;
|
|
1368
|
+
deltaCacheWrite = 0;
|
|
918
1369
|
deltaTotal = 0;
|
|
919
1370
|
} else {
|
|
920
1371
|
deltaInput = stats.inputTokens;
|
|
921
1372
|
deltaOutput = stats.outputTokens;
|
|
1373
|
+
deltaCacheRead = stats.cacheReadTokens;
|
|
1374
|
+
deltaCacheWrite = stats.cacheWriteTokens;
|
|
922
1375
|
deltaTotal = stats.totalTokens;
|
|
923
1376
|
}
|
|
924
1377
|
}
|
|
@@ -928,30 +1381,58 @@ export function syncUsageFromSessions(
|
|
|
928
1381
|
type,
|
|
929
1382
|
profileKey: resolved.match.profileKey,
|
|
930
1383
|
profileName: resolved.match.profileName,
|
|
1384
|
+
model: resolvedModel,
|
|
1385
|
+
sessionId: stats.sessionId,
|
|
931
1386
|
inputTokens: deltaInput,
|
|
932
1387
|
outputTokens: deltaOutput,
|
|
1388
|
+
cacheReadTokens: deltaCacheRead,
|
|
1389
|
+
cacheWriteTokens: deltaCacheWrite,
|
|
933
1390
|
totalTokens: deltaTotal,
|
|
934
1391
|
};
|
|
935
1392
|
appendUsageRecord(usagePath, record);
|
|
936
1393
|
}
|
|
937
1394
|
if (sessionKey) {
|
|
938
1395
|
const nextInput = sessionPrev
|
|
939
|
-
? Math.max(
|
|
1396
|
+
? Math.max(
|
|
1397
|
+
toUsageNumber(sessionPrev.inputTokens),
|
|
1398
|
+
stats.inputTokens
|
|
1399
|
+
)
|
|
940
1400
|
: stats.inputTokens;
|
|
941
1401
|
const nextOutput = sessionPrev
|
|
942
|
-
? Math.max(
|
|
1402
|
+
? Math.max(
|
|
1403
|
+
toUsageNumber(sessionPrev.outputTokens),
|
|
1404
|
+
stats.outputTokens
|
|
1405
|
+
)
|
|
943
1406
|
: stats.outputTokens;
|
|
1407
|
+
const nextCacheRead = sessionPrev
|
|
1408
|
+
? Math.max(
|
|
1409
|
+
toUsageNumber(sessionPrev.cacheReadTokens),
|
|
1410
|
+
stats.cacheReadTokens
|
|
1411
|
+
)
|
|
1412
|
+
: stats.cacheReadTokens;
|
|
1413
|
+
const nextCacheWrite = sessionPrev
|
|
1414
|
+
? Math.max(
|
|
1415
|
+
toUsageNumber(sessionPrev.cacheWriteTokens),
|
|
1416
|
+
stats.cacheWriteTokens
|
|
1417
|
+
)
|
|
1418
|
+
: stats.cacheWriteTokens;
|
|
944
1419
|
const nextTotal = sessionPrev
|
|
945
|
-
? Math.max(
|
|
1420
|
+
? Math.max(
|
|
1421
|
+
toUsageNumber(sessionPrev.totalTokens),
|
|
1422
|
+
stats.totalTokens
|
|
1423
|
+
)
|
|
946
1424
|
: stats.totalTokens;
|
|
947
1425
|
sessions[sessionKey] = {
|
|
948
1426
|
type,
|
|
949
1427
|
inputTokens: nextInput,
|
|
950
1428
|
outputTokens: nextOutput,
|
|
1429
|
+
cacheReadTokens: nextCacheRead,
|
|
1430
|
+
cacheWriteTokens: nextCacheWrite,
|
|
951
1431
|
totalTokens: nextTotal,
|
|
952
1432
|
startTs: sessionPrev ? sessionPrev.startTs : stats.startTs,
|
|
953
1433
|
endTs: stats.endTs || (sessionPrev ? sessionPrev.endTs : null),
|
|
954
1434
|
cwd: stats.cwd || (sessionPrev ? sessionPrev.cwd : null),
|
|
1435
|
+
model: resolvedModel,
|
|
955
1436
|
};
|
|
956
1437
|
}
|
|
957
1438
|
files[filePath] = {
|
|
@@ -960,10 +1441,13 @@ export function syncUsageFromSessions(
|
|
|
960
1441
|
type,
|
|
961
1442
|
inputTokens: stats.inputTokens,
|
|
962
1443
|
outputTokens: stats.outputTokens,
|
|
1444
|
+
cacheReadTokens: stats.cacheReadTokens,
|
|
1445
|
+
cacheWriteTokens: stats.cacheWriteTokens,
|
|
963
1446
|
totalTokens: stats.totalTokens,
|
|
964
1447
|
startTs: stats.startTs,
|
|
965
1448
|
endTs: stats.endTs,
|
|
966
1449
|
cwd: stats.cwd,
|
|
1450
|
+
model: resolvedModel,
|
|
967
1451
|
};
|
|
968
1452
|
};
|
|
969
1453
|
|