tokenmax-collector 0.2.0 → 0.2.1
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/package.json +2 -2
- package/src/antigravity.ts +34 -8
- package/src/cli.ts +9 -6
- package/src/collect.ts +42 -25
- package/src/pricing.ts +21 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmax-collector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Report local ccusage token usage to a tokenmax instance",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"directory": "packages/collector"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
|
-
"bun": ">=1.
|
|
22
|
+
"bun": ">=1.4.0"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"check-types": "tsc --noEmit",
|
package/src/antigravity.ts
CHANGED
|
@@ -11,6 +11,11 @@ export interface AntigravityStep {
|
|
|
11
11
|
output: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface AntigravityUsage {
|
|
15
|
+
failures: string[];
|
|
16
|
+
steps: AntigravityStep[];
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
interface ProtoField {
|
|
15
20
|
number: number;
|
|
16
21
|
value: bigint | Uint8Array;
|
|
@@ -43,10 +48,16 @@ export function antigravityConversationsDir(home: string): string {
|
|
|
43
48
|
function decodeMessage(bytes: Uint8Array): ProtoField[] {
|
|
44
49
|
const fields: ProtoField[] = [];
|
|
45
50
|
let offset = 0;
|
|
51
|
+
const truncated = (): never => {
|
|
52
|
+
throw new Error("truncated protobuf message");
|
|
53
|
+
};
|
|
46
54
|
const varint = (): bigint => {
|
|
47
55
|
let result = 0n;
|
|
48
56
|
let shift = 0n;
|
|
49
57
|
for (;;) {
|
|
58
|
+
if (offset >= bytes.length) {
|
|
59
|
+
truncated();
|
|
60
|
+
}
|
|
50
61
|
const byte = bytes[offset++];
|
|
51
62
|
result |= BigInt(byte & 0x7f) << shift;
|
|
52
63
|
if (byte < 0x80) {
|
|
@@ -56,6 +67,9 @@ function decodeMessage(bytes: Uint8Array): ProtoField[] {
|
|
|
56
67
|
}
|
|
57
68
|
};
|
|
58
69
|
const bytesOf = (length: number): Uint8Array => {
|
|
70
|
+
if (offset + length > bytes.length) {
|
|
71
|
+
truncated();
|
|
72
|
+
}
|
|
59
73
|
const slice = bytes.subarray(offset, offset + length);
|
|
60
74
|
offset += length;
|
|
61
75
|
return slice;
|
|
@@ -108,7 +122,7 @@ function text(fields: ProtoField[], number: number): string | null {
|
|
|
108
122
|
function openReadOnly(file: string): DatabaseSync {
|
|
109
123
|
const location = existsSync(`${file}-wal`)
|
|
110
124
|
? file
|
|
111
|
-
: `file:${file}?immutable=1`;
|
|
125
|
+
: `file:${file.split("/").map(encodeURIComponent).join("/")}?immutable=1`;
|
|
112
126
|
return new DatabaseSync(location, { readOnly: true });
|
|
113
127
|
}
|
|
114
128
|
|
|
@@ -159,23 +173,35 @@ function readConversation(
|
|
|
159
173
|
|
|
160
174
|
export async function readAntigravitySteps(
|
|
161
175
|
dir: string,
|
|
162
|
-
): Promise<
|
|
176
|
+
): Promise<AntigravityUsage> {
|
|
163
177
|
let files: string[];
|
|
164
178
|
try {
|
|
165
179
|
files = await readdir(dir);
|
|
166
180
|
} catch (error) {
|
|
167
181
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
168
|
-
return [];
|
|
182
|
+
return { failures: [], steps: [] };
|
|
169
183
|
}
|
|
170
184
|
throw error;
|
|
171
185
|
}
|
|
172
186
|
const names = new Map<number, string>();
|
|
173
187
|
const steps: RawStep[] = [];
|
|
188
|
+
const failures: string[] = [];
|
|
174
189
|
for (const file of files.filter((name) => name.endsWith(".db")).sort()) {
|
|
175
|
-
|
|
190
|
+
const conversation = resolve(dir, file);
|
|
191
|
+
const before = steps.length;
|
|
192
|
+
try {
|
|
193
|
+
readConversation(conversation, names, steps);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
steps.length = before;
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
failures.push(`${conversation}: ${message}`);
|
|
198
|
+
}
|
|
176
199
|
}
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
200
|
+
return {
|
|
201
|
+
failures,
|
|
202
|
+
steps: steps.map(({ modelCode, ...step }) => ({
|
|
203
|
+
...step,
|
|
204
|
+
model: names.get(modelCode) ?? unknownModel,
|
|
205
|
+
})),
|
|
206
|
+
};
|
|
181
207
|
}
|
package/src/cli.ts
CHANGED
|
@@ -36,12 +36,15 @@ async function runCollect(io: CliIo): Promise<number> {
|
|
|
36
36
|
runner: io.runner,
|
|
37
37
|
today: io.today,
|
|
38
38
|
});
|
|
39
|
-
if (result.kind === "reported") {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
if (result.kind === "reported" || result.kind === "empty") {
|
|
40
|
+
for (const warning of result.warnings) {
|
|
41
|
+
io.stderr(warning);
|
|
42
|
+
}
|
|
43
|
+
io.stdout(
|
|
44
|
+
result.kind === "reported"
|
|
45
|
+
? `accepted ${result.accepted} days for ${result.machine}`
|
|
46
|
+
: "nothing to report",
|
|
47
|
+
);
|
|
45
48
|
return 0;
|
|
46
49
|
}
|
|
47
50
|
if (result.kind === "missing-config") {
|
package/src/collect.ts
CHANGED
|
@@ -27,8 +27,8 @@ export interface CollectOptions {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export type CollectResult =
|
|
30
|
-
| { kind: "reported"; accepted: number; machine: string }
|
|
31
|
-
| { kind: "empty" }
|
|
30
|
+
| { kind: "reported"; accepted: number; machine: string; warnings: string[] }
|
|
31
|
+
| { kind: "empty"; warnings: string[] }
|
|
32
32
|
| { kind: "missing-config"; configFile: string }
|
|
33
33
|
| { kind: "failed"; message: string };
|
|
34
34
|
|
|
@@ -56,11 +56,17 @@ function acceptedCount(body: string): number | null {
|
|
|
56
56
|
return typeof accepted === "number" ? accepted : null;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
interface AntigravityRows {
|
|
60
|
+
days: UsageDay[];
|
|
61
|
+
warnings: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
59
64
|
async function report(
|
|
60
65
|
fetcher: Fetcher,
|
|
61
66
|
url: string,
|
|
62
67
|
key: string,
|
|
63
68
|
usage: UsageReport,
|
|
69
|
+
warnings: string[],
|
|
64
70
|
): Promise<CollectResult> {
|
|
65
71
|
const response = await fetcher(reportUrl(url), {
|
|
66
72
|
body: JSON.stringify(usage),
|
|
@@ -84,7 +90,7 @@ async function report(
|
|
|
84
90
|
message: `tokenmax responded an unexpected body: ${body}`,
|
|
85
91
|
};
|
|
86
92
|
}
|
|
87
|
-
return { accepted, kind: "reported", machine: usage.machine };
|
|
93
|
+
return { accepted, kind: "reported", machine: usage.machine, warnings };
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
async function antigravityDays(
|
|
@@ -93,16 +99,20 @@ async function antigravityDays(
|
|
|
93
99
|
timezone: string,
|
|
94
100
|
fetcher: Fetcher,
|
|
95
101
|
pricesFile: string,
|
|
96
|
-
): Promise<
|
|
102
|
+
): Promise<AntigravityRows> {
|
|
97
103
|
const since = windowStart(today, timezone);
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
const usage = await readAntigravitySteps(antigravityConversationsDir(home));
|
|
105
|
+
const warnings = usage.failures.map(
|
|
106
|
+
(failure) => `antigravity: skipped ${failure}`,
|
|
107
|
+
);
|
|
108
|
+
const steps = usage.steps.filter(
|
|
109
|
+
(step) => calendarDate(step.at, timezone) >= since,
|
|
110
|
+
);
|
|
101
111
|
if (steps.length === 0) {
|
|
102
|
-
return [];
|
|
112
|
+
return { days: [], warnings };
|
|
103
113
|
}
|
|
104
114
|
const prices = await loadPrices(fetcher, pricesFile, today);
|
|
105
|
-
return mapAntigravitySteps(steps, timezone, prices);
|
|
115
|
+
return { days: mapAntigravitySteps(steps, timezone, prices), warnings };
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
export async function collect(options: CollectOptions): Promise<CollectResult> {
|
|
@@ -133,29 +143,36 @@ export async function collect(options: CollectOptions): Promise<CollectResult> {
|
|
|
133
143
|
sinceArgument(today, timezone),
|
|
134
144
|
timezone,
|
|
135
145
|
);
|
|
136
|
-
days =
|
|
137
|
-
...mapCcusageDays(daily),
|
|
138
|
-
...(await antigravityDays(
|
|
139
|
-
env.home,
|
|
140
|
-
today,
|
|
141
|
-
timezone,
|
|
142
|
-
fetcher,
|
|
143
|
-
paths.pricesFile,
|
|
144
|
-
)),
|
|
145
|
-
];
|
|
146
|
+
days = mapCcusageDays(daily);
|
|
146
147
|
} catch (error) {
|
|
147
148
|
return { kind: "failed", message: messageOf(error) };
|
|
148
149
|
}
|
|
150
|
+
|
|
151
|
+
let antigravity: AntigravityRows;
|
|
152
|
+
try {
|
|
153
|
+
antigravity = await antigravityDays(
|
|
154
|
+
env.home,
|
|
155
|
+
today,
|
|
156
|
+
timezone,
|
|
157
|
+
fetcher,
|
|
158
|
+
paths.pricesFile,
|
|
159
|
+
);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
antigravity = { days: [], warnings: [`antigravity: ${messageOf(error)}`] };
|
|
162
|
+
}
|
|
163
|
+
days.push(...antigravity.days);
|
|
149
164
|
if (days.length === 0) {
|
|
150
|
-
return { kind: "empty" };
|
|
165
|
+
return { kind: "empty", warnings: antigravity.warnings };
|
|
151
166
|
}
|
|
152
167
|
|
|
153
168
|
try {
|
|
154
|
-
return await report(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
169
|
+
return await report(
|
|
170
|
+
fetcher,
|
|
171
|
+
config.config.url,
|
|
172
|
+
config.config.key,
|
|
173
|
+
{ days, machine, timezone },
|
|
174
|
+
antigravity.warnings,
|
|
175
|
+
);
|
|
159
176
|
} catch (error) {
|
|
160
177
|
return { kind: "failed", message: messageOf(error) };
|
|
161
178
|
}
|
package/src/pricing.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import type { Fetcher } from "./http";
|
|
@@ -57,19 +57,32 @@ export function costOf(price: ModelPrice, usage: PricedUsage): number {
|
|
|
57
57
|
|
|
58
58
|
async function readCache(
|
|
59
59
|
pricesFile: string,
|
|
60
|
-
): Promise<{ modifiedAt: number;
|
|
60
|
+
): Promise<{ modifiedAt: number; table: PriceTable } | null> {
|
|
61
|
+
let source: string;
|
|
62
|
+
let modifiedAt: number;
|
|
61
63
|
try {
|
|
62
|
-
|
|
64
|
+
[source, modifiedAt] = await Promise.all([
|
|
63
65
|
readFile(pricesFile, "utf8"),
|
|
64
|
-
stat(pricesFile),
|
|
66
|
+
stat(pricesFile).then((info) => info.mtimeMs),
|
|
65
67
|
]);
|
|
66
|
-
return { modifiedAt: info.mtimeMs, source };
|
|
67
68
|
} catch (error) {
|
|
68
69
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
69
70
|
return null;
|
|
70
71
|
}
|
|
71
72
|
throw error;
|
|
72
73
|
}
|
|
74
|
+
try {
|
|
75
|
+
return { modifiedAt, table: parseLitellmPrices(source) };
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function writeCache(pricesFile: string, source: string): Promise<void> {
|
|
82
|
+
await mkdir(dirname(pricesFile), { recursive: true });
|
|
83
|
+
const partial = `${pricesFile}.${process.pid}.tmp`;
|
|
84
|
+
await writeFile(partial, source);
|
|
85
|
+
await rename(partial, pricesFile);
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
async function fetchPrices(fetcher: Fetcher): Promise<string> {
|
|
@@ -88,7 +101,7 @@ export async function loadPrices(
|
|
|
88
101
|
): Promise<PriceTable> {
|
|
89
102
|
const cached = await readCache(pricesFile);
|
|
90
103
|
if (cached !== null && now.getTime() - cached.modifiedAt < maxAgeMs) {
|
|
91
|
-
return
|
|
104
|
+
return cached.table;
|
|
92
105
|
}
|
|
93
106
|
let source: string;
|
|
94
107
|
let table: PriceTable;
|
|
@@ -97,12 +110,11 @@ export async function loadPrices(
|
|
|
97
110
|
table = parseLitellmPrices(source);
|
|
98
111
|
} catch (error) {
|
|
99
112
|
if (cached !== null) {
|
|
100
|
-
return
|
|
113
|
+
return cached.table;
|
|
101
114
|
}
|
|
102
115
|
const message = error instanceof Error ? error.message : String(error);
|
|
103
116
|
throw new Error(`could not load model prices: ${message}`);
|
|
104
117
|
}
|
|
105
|
-
await
|
|
106
|
-
await writeFile(pricesFile, source);
|
|
118
|
+
await writeCache(pricesFile, source);
|
|
107
119
|
return table;
|
|
108
120
|
}
|