pi-free 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/update-benchmarks.yml +67 -0
- package/.pi/skills/pi-extension-dev/SKILL.md +155 -0
- package/CHANGELOG.md +59 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/config.ts +224 -0
- package/constants.ts +110 -0
- package/docs/free-tier-limits.md +213 -0
- package/docs/model-hopping.md +214 -0
- package/docs/plans/file-reorganization.md +172 -0
- package/docs/plans/package-json-fix.md +143 -0
- package/docs/provider-failover-plan.md +279 -0
- package/lib/json-persistence.ts +102 -0
- package/lib/logger.ts +94 -0
- package/lib/model-enhancer.ts +20 -0
- package/lib/types.ts +108 -0
- package/lib/util.ts +256 -0
- package/package.json +52 -0
- package/provider-factory.ts +221 -0
- package/provider-failover/errors.ts +275 -0
- package/provider-failover/hardcoded-benchmarks.ts +9889 -0
- package/provider-failover/index.ts +194 -0
- package/provider-helper.ts +336 -0
- package/providers/cline-auth.ts +473 -0
- package/providers/cline-models.ts +77 -0
- package/providers/cline.ts +257 -0
- package/providers/factory.ts +125 -0
- package/providers/fireworks.ts +49 -0
- package/providers/kilo-auth.ts +172 -0
- package/providers/kilo-models.ts +26 -0
- package/providers/kilo.ts +144 -0
- package/providers/mistral.ts +144 -0
- package/providers/model-fetcher.ts +138 -0
- package/providers/nvidia.ts +97 -0
- package/providers/ollama.ts +113 -0
- package/providers/openrouter.ts +175 -0
- package/providers/zen.ts +416 -0
- package/scripts/update-benchmarks.ts +255 -0
- package/tests/cline.test.ts +149 -0
- package/tests/errors.test.ts +139 -0
- package/tests/failover.test.ts +94 -0
- package/tests/fireworks.test.ts +148 -0
- package/tests/free-tier-limits.test.ts +191 -0
- package/tests/json-persistence.test.ts +105 -0
- package/tests/kilo.test.ts +186 -0
- package/tests/mistral.test.ts +138 -0
- package/tests/nvidia.test.ts +55 -0
- package/tests/ollama.test.ts +261 -0
- package/tests/openrouter.test.ts +192 -0
- package/tests/usage-tracking.test.ts +150 -0
- package/tests/util.test.ts +413 -0
- package/tests/zen.test.ts +180 -0
- package/todo.md +153 -0
- package/tsconfig.json +26 -0
- package/usage/commands.ts +17 -0
- package/usage/cumulative.ts +193 -0
- package/usage/formatters.ts +131 -0
- package/usage/index.ts +46 -0
- package/usage/limits.ts +166 -0
- package/usage/metrics.ts +222 -0
- package/usage/sessions.ts +355 -0
- package/usage/store.ts +99 -0
- package/usage/tracking.ts +329 -0
- package/usage/widget.ts +90 -0
- package/vitest.config.ts +20 -0
- package/widget/data.ts +113 -0
- package/widget/format.ts +26 -0
- package/widget/render.ts +117 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session file parsing - extracts historical usage data from Pi session files
|
|
3
|
+
*
|
|
4
|
+
* Pi stores sessions in ~/.pi/agent/sessions/{cwd-hash}/*.jsonl
|
|
5
|
+
* Each line is a JSON entry with type "session" or "message"
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { createLogger } from "../lib/logger.ts";
|
|
12
|
+
|
|
13
|
+
const _logger = createLogger("session-parser");
|
|
14
|
+
|
|
15
|
+
export interface SessionMessage {
|
|
16
|
+
provider: string;
|
|
17
|
+
model: string;
|
|
18
|
+
tokensIn: number;
|
|
19
|
+
tokensOut: number;
|
|
20
|
+
cacheRead: number;
|
|
21
|
+
cacheWrite: number;
|
|
22
|
+
cost: number;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ParsedSession {
|
|
27
|
+
sessionId: string;
|
|
28
|
+
messages: SessionMessage[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getSessionsDir(): string {
|
|
32
|
+
const agentDir =
|
|
33
|
+
process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
|
|
34
|
+
return join(agentDir, "sessions");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function getAllSessionFiles(signal?: AbortSignal): Promise<string[]> {
|
|
38
|
+
const sessionsDir = getSessionsDir();
|
|
39
|
+
const files: string[] = [];
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const cwdDirs = await readdir(sessionsDir, { withFileTypes: true });
|
|
43
|
+
for (const dir of cwdDirs) {
|
|
44
|
+
if (signal?.aborted) return files;
|
|
45
|
+
if (!dir.isDirectory()) continue;
|
|
46
|
+
const cwdPath = join(sessionsDir, dir.name);
|
|
47
|
+
try {
|
|
48
|
+
const sessionFiles = await readdir(cwdPath);
|
|
49
|
+
for (const file of sessionFiles) {
|
|
50
|
+
if (file.endsWith(".jsonl")) {
|
|
51
|
+
files.push(join(cwdPath, file));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Skip directories we can't read
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Return empty if we can't read sessions dir
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return files;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function parseSessionFile(
|
|
66
|
+
filePath: string,
|
|
67
|
+
seenHashes: Set<string>,
|
|
68
|
+
signal?: AbortSignal,
|
|
69
|
+
): Promise<ParsedSession | null> {
|
|
70
|
+
try {
|
|
71
|
+
const content = await readFile(filePath, "utf8");
|
|
72
|
+
if (signal?.aborted) return null;
|
|
73
|
+
|
|
74
|
+
const lines = content.trim().split("\n");
|
|
75
|
+
const messages: SessionMessage[] = [];
|
|
76
|
+
let sessionId = "";
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < lines.length; i++) {
|
|
79
|
+
if (signal?.aborted) return null;
|
|
80
|
+
if (i % 500 === 0) {
|
|
81
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const line = lines[i]!;
|
|
85
|
+
if (!line.trim()) continue;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const entry = JSON.parse(line);
|
|
89
|
+
|
|
90
|
+
if (entry.type === "session") {
|
|
91
|
+
sessionId = entry.id;
|
|
92
|
+
} else if (
|
|
93
|
+
entry.type === "message" &&
|
|
94
|
+
entry.message?.role === "assistant"
|
|
95
|
+
) {
|
|
96
|
+
const msg = entry.message;
|
|
97
|
+
if (msg.usage && msg.provider && msg.model) {
|
|
98
|
+
const tokensIn = msg.usage.input || 0;
|
|
99
|
+
const tokensOut = msg.usage.output || 0;
|
|
100
|
+
const cacheRead = msg.usage.cacheRead || 0;
|
|
101
|
+
const cacheWrite = msg.usage.cacheWrite || 0;
|
|
102
|
+
const cost = msg.usage.cost?.total || 0;
|
|
103
|
+
|
|
104
|
+
const fallbackTs = entry.timestamp
|
|
105
|
+
? new Date(entry.timestamp).getTime()
|
|
106
|
+
: 0;
|
|
107
|
+
const timestamp =
|
|
108
|
+
msg.timestamp || (Number.isNaN(fallbackTs) ? 0 : fallbackTs);
|
|
109
|
+
|
|
110
|
+
const totalTokens = tokensIn + tokensOut + cacheRead + cacheWrite;
|
|
111
|
+
const hash = `${timestamp}:${totalTokens}`;
|
|
112
|
+
if (seenHashes.has(hash)) continue;
|
|
113
|
+
seenHashes.add(hash);
|
|
114
|
+
|
|
115
|
+
messages.push({
|
|
116
|
+
provider: msg.provider,
|
|
117
|
+
model: msg.model,
|
|
118
|
+
tokensIn,
|
|
119
|
+
tokensOut,
|
|
120
|
+
cacheRead,
|
|
121
|
+
cacheWrite,
|
|
122
|
+
cost,
|
|
123
|
+
timestamp,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Skip malformed lines
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return sessionId ? { sessionId, messages } : null;
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type TimePeriod = "today" | "thisWeek" | "allTime";
|
|
139
|
+
|
|
140
|
+
export interface TimeRange {
|
|
141
|
+
start: number;
|
|
142
|
+
end?: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function getTimeRange(period: TimePeriod): TimeRange {
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
const startOfToday = new Date();
|
|
148
|
+
startOfToday.setHours(0, 0, 0, 0);
|
|
149
|
+
|
|
150
|
+
switch (period) {
|
|
151
|
+
case "today":
|
|
152
|
+
return { start: startOfToday.getTime(), end: now };
|
|
153
|
+
case "thisWeek": {
|
|
154
|
+
const startOfWeek = new Date();
|
|
155
|
+
const dayOfWeek = startOfWeek.getDay();
|
|
156
|
+
const daysSinceMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
|
157
|
+
startOfWeek.setDate(startOfWeek.getDate() - daysSinceMonday);
|
|
158
|
+
startOfWeek.setHours(0, 0, 0, 0);
|
|
159
|
+
return { start: startOfWeek.getTime(), end: now };
|
|
160
|
+
}
|
|
161
|
+
case "allTime":
|
|
162
|
+
return { start: 0, end: now };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function filterByTimeRange(
|
|
167
|
+
messages: SessionMessage[],
|
|
168
|
+
range: TimeRange,
|
|
169
|
+
): SessionMessage[] {
|
|
170
|
+
return messages.filter((m) => {
|
|
171
|
+
if (m.timestamp < range.start) return false;
|
|
172
|
+
if (range.end && m.timestamp > range.end) return false;
|
|
173
|
+
return true;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface ModelStats {
|
|
178
|
+
count: number;
|
|
179
|
+
tokensIn: number;
|
|
180
|
+
tokensOut: number;
|
|
181
|
+
cacheRead: number;
|
|
182
|
+
cacheWrite: number;
|
|
183
|
+
cost: number;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface ProviderStats {
|
|
187
|
+
messages: number;
|
|
188
|
+
tokensIn: number;
|
|
189
|
+
tokensOut: number;
|
|
190
|
+
cacheRead: number;
|
|
191
|
+
cacheWrite: number;
|
|
192
|
+
cost: number;
|
|
193
|
+
models: Record<string, ModelStats>;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface SessionFileStats {
|
|
197
|
+
totalMessages: number;
|
|
198
|
+
totalTokensIn: number;
|
|
199
|
+
totalTokensOut: number;
|
|
200
|
+
totalCacheRead: number;
|
|
201
|
+
totalCacheWrite: number;
|
|
202
|
+
totalCost: number;
|
|
203
|
+
providers: Record<string, ProviderStats>;
|
|
204
|
+
sessions: Set<string>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function createEmptyStats(): SessionFileStats {
|
|
208
|
+
return {
|
|
209
|
+
totalMessages: 0,
|
|
210
|
+
totalTokensIn: 0,
|
|
211
|
+
totalTokensOut: 0,
|
|
212
|
+
totalCacheRead: 0,
|
|
213
|
+
totalCacheWrite: 0,
|
|
214
|
+
totalCost: 0,
|
|
215
|
+
providers: {},
|
|
216
|
+
sessions: new Set(),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function aggregateMessages(
|
|
221
|
+
messages: SessionMessage[],
|
|
222
|
+
sessionId: string,
|
|
223
|
+
): SessionFileStats {
|
|
224
|
+
const stats = createEmptyStats();
|
|
225
|
+
stats.sessions.add(sessionId);
|
|
226
|
+
|
|
227
|
+
for (const msg of messages) {
|
|
228
|
+
stats.totalMessages++;
|
|
229
|
+
stats.totalTokensIn += msg.tokensIn;
|
|
230
|
+
stats.totalTokensOut += msg.tokensOut;
|
|
231
|
+
stats.totalCacheRead += msg.cacheRead;
|
|
232
|
+
stats.totalCacheWrite += msg.cacheWrite;
|
|
233
|
+
stats.totalCost += msg.cost;
|
|
234
|
+
|
|
235
|
+
if (!stats.providers[msg.provider]) {
|
|
236
|
+
stats.providers[msg.provider] = {
|
|
237
|
+
messages: 0,
|
|
238
|
+
tokensIn: 0,
|
|
239
|
+
tokensOut: 0,
|
|
240
|
+
cacheRead: 0,
|
|
241
|
+
cacheWrite: 0,
|
|
242
|
+
cost: 0,
|
|
243
|
+
models: {},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const provider = stats.providers[msg.provider]!;
|
|
247
|
+
provider.messages++;
|
|
248
|
+
provider.tokensIn += msg.tokensIn;
|
|
249
|
+
provider.tokensOut += msg.tokensOut;
|
|
250
|
+
provider.cacheRead += msg.cacheRead;
|
|
251
|
+
provider.cacheWrite += msg.cacheWrite;
|
|
252
|
+
provider.cost += msg.cost;
|
|
253
|
+
|
|
254
|
+
if (!provider.models[msg.model]) {
|
|
255
|
+
provider.models[msg.model] = {
|
|
256
|
+
count: 0,
|
|
257
|
+
tokensIn: 0,
|
|
258
|
+
tokensOut: 0,
|
|
259
|
+
cacheRead: 0,
|
|
260
|
+
cacheWrite: 0,
|
|
261
|
+
cost: 0,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const model = provider.models[msg.model]!;
|
|
265
|
+
model.count++;
|
|
266
|
+
model.tokensIn += msg.tokensIn;
|
|
267
|
+
model.tokensOut += msg.tokensOut;
|
|
268
|
+
model.cacheRead += msg.cacheRead;
|
|
269
|
+
model.cacheWrite += msg.cacheWrite;
|
|
270
|
+
model.cost += msg.cost;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return stats;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function collectSessionFileStats(
|
|
277
|
+
period: TimePeriod = "allTime",
|
|
278
|
+
signal?: AbortSignal,
|
|
279
|
+
): Promise<SessionFileStats> {
|
|
280
|
+
const range = getTimeRange(period);
|
|
281
|
+
const allStats = createEmptyStats();
|
|
282
|
+
|
|
283
|
+
const sessionFiles = await getAllSessionFiles(signal);
|
|
284
|
+
if (signal?.aborted) return allStats;
|
|
285
|
+
|
|
286
|
+
const seenHashes = new Set<string>();
|
|
287
|
+
|
|
288
|
+
for (const filePath of sessionFiles) {
|
|
289
|
+
if (signal?.aborted) return allStats;
|
|
290
|
+
|
|
291
|
+
const parsed = await parseSessionFile(filePath, seenHashes, signal);
|
|
292
|
+
if (signal?.aborted) return allStats;
|
|
293
|
+
if (!parsed) continue;
|
|
294
|
+
|
|
295
|
+
const filteredMessages = filterByTimeRange(parsed.messages, range);
|
|
296
|
+
if (filteredMessages.length === 0) continue;
|
|
297
|
+
|
|
298
|
+
const fileStats = aggregateMessages(filteredMessages, parsed.sessionId);
|
|
299
|
+
|
|
300
|
+
allStats.sessions.add(parsed.sessionId);
|
|
301
|
+
allStats.totalMessages += fileStats.totalMessages;
|
|
302
|
+
allStats.totalTokensIn += fileStats.totalTokensIn;
|
|
303
|
+
allStats.totalTokensOut += fileStats.totalTokensOut;
|
|
304
|
+
allStats.totalCacheRead += fileStats.totalCacheRead;
|
|
305
|
+
allStats.totalCacheWrite += fileStats.totalCacheWrite;
|
|
306
|
+
allStats.totalCost += fileStats.totalCost;
|
|
307
|
+
|
|
308
|
+
for (const [providerName, providerStats] of Object.entries(
|
|
309
|
+
fileStats.providers,
|
|
310
|
+
)) {
|
|
311
|
+
if (!allStats.providers[providerName]) {
|
|
312
|
+
allStats.providers[providerName] = {
|
|
313
|
+
messages: 0,
|
|
314
|
+
tokensIn: 0,
|
|
315
|
+
tokensOut: 0,
|
|
316
|
+
cacheRead: 0,
|
|
317
|
+
cacheWrite: 0,
|
|
318
|
+
cost: 0,
|
|
319
|
+
models: {},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const allProvider = allStats.providers[providerName]!;
|
|
323
|
+
allProvider.messages += providerStats.messages;
|
|
324
|
+
allProvider.tokensIn += providerStats.tokensIn;
|
|
325
|
+
allProvider.tokensOut += providerStats.tokensOut;
|
|
326
|
+
allProvider.cacheRead += providerStats.cacheRead;
|
|
327
|
+
allProvider.cacheWrite += providerStats.cacheWrite;
|
|
328
|
+
allProvider.cost += providerStats.cost;
|
|
329
|
+
|
|
330
|
+
for (const [modelName, modelStats] of Object.entries(
|
|
331
|
+
providerStats.models,
|
|
332
|
+
)) {
|
|
333
|
+
if (!allProvider.models[modelName]) {
|
|
334
|
+
allProvider.models[modelName] = {
|
|
335
|
+
count: 0,
|
|
336
|
+
tokensIn: 0,
|
|
337
|
+
tokensOut: 0,
|
|
338
|
+
cacheRead: 0,
|
|
339
|
+
cacheWrite: 0,
|
|
340
|
+
cost: 0,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const allModel = allProvider.models[modelName]!;
|
|
344
|
+
allModel.count += modelStats.count;
|
|
345
|
+
allModel.tokensIn += modelStats.tokensIn;
|
|
346
|
+
allModel.tokensOut += modelStats.tokensOut;
|
|
347
|
+
allModel.cacheRead += modelStats.cacheRead;
|
|
348
|
+
allModel.cacheWrite += modelStats.cacheWrite;
|
|
349
|
+
allModel.cost += modelStats.cost;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return allStats;
|
|
355
|
+
}
|
package/usage/store.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent cumulative usage tracking per provider.
|
|
3
|
+
*
|
|
4
|
+
* Stored at ~/./.pi/free-usage.json — survives across sessions.
|
|
5
|
+
* Updated on each turn_end via provider-helper.ts.
|
|
6
|
+
*
|
|
7
|
+
* This answers: "how much free value have I gotten over time?"
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { createJSONStore } from "../lib/json-persistence.js";
|
|
12
|
+
import { createLogger } from "../lib/logger.js";
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
export interface ProviderCumulativeUsage {
|
|
19
|
+
/** Total input tokens across all sessions. */
|
|
20
|
+
tokensIn: number;
|
|
21
|
+
/** Total output tokens across all sessions. */
|
|
22
|
+
tokensOut: number;
|
|
23
|
+
/** Total requests across all sessions. */
|
|
24
|
+
requests: number;
|
|
25
|
+
/** Total cost that would have been charged on a paid tier. */
|
|
26
|
+
costEquivalent: number;
|
|
27
|
+
/** ISO date of first tracked request. */
|
|
28
|
+
firstUsed: string;
|
|
29
|
+
/** ISO date of last tracked request. */
|
|
30
|
+
lastUsed: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CumulativeUsageStore {
|
|
34
|
+
[provider: string]: ProviderCumulativeUsage;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// Storage
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
const PI_DIR = join(process.env.HOME || process.env.USERPROFILE || "", ".pi");
|
|
42
|
+
const USAGE_PATH = join(PI_DIR, "free-usage.json");
|
|
43
|
+
|
|
44
|
+
const logger = createLogger("usage-store");
|
|
45
|
+
|
|
46
|
+
const store = createJSONStore<CumulativeUsageStore>(USAGE_PATH, {});
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// API
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
/** Record a turn's token usage for a provider. */
|
|
53
|
+
export function recordTurn(
|
|
54
|
+
provider: string,
|
|
55
|
+
tokensIn: number,
|
|
56
|
+
tokensOut: number,
|
|
57
|
+
costEquivalent: number,
|
|
58
|
+
): void {
|
|
59
|
+
const data = store.load();
|
|
60
|
+
const now = new Date().toISOString();
|
|
61
|
+
const existing = data[provider];
|
|
62
|
+
|
|
63
|
+
if (existing) {
|
|
64
|
+
existing.tokensIn += tokensIn;
|
|
65
|
+
existing.tokensOut += tokensOut;
|
|
66
|
+
existing.requests += 1;
|
|
67
|
+
existing.costEquivalent += costEquivalent;
|
|
68
|
+
existing.lastUsed = now;
|
|
69
|
+
} else {
|
|
70
|
+
data[provider] = {
|
|
71
|
+
tokensIn,
|
|
72
|
+
tokensOut,
|
|
73
|
+
requests: 1,
|
|
74
|
+
costEquivalent,
|
|
75
|
+
firstUsed: now,
|
|
76
|
+
lastUsed: now,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
store.save(data);
|
|
81
|
+
logger.debug("recorded turn", {
|
|
82
|
+
provider,
|
|
83
|
+
tokensIn,
|
|
84
|
+
tokensOut,
|
|
85
|
+
costEquivalent,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Get cumulative usage for a specific provider. */
|
|
90
|
+
export function getCumulativeUsage(
|
|
91
|
+
provider: string,
|
|
92
|
+
): ProviderCumulativeUsage | null {
|
|
93
|
+
return store.load()[provider] ?? null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Get all cumulative usage data. */
|
|
97
|
+
export function getAllCumulativeUsage(): CumulativeUsageStore {
|
|
98
|
+
return store.load();
|
|
99
|
+
}
|