tokenmax-collector 0.2.1 → 0.3.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 +1 -1
- package/src/cli.ts +16 -6
- package/src/collect.ts +124 -75
- package/src/config.ts +16 -3
- package/src/devin.ts +94 -0
- package/src/install.ts +16 -2
- package/src/machine.ts +3 -2
- package/src/mapping.ts +52 -7
- package/src/pricing.ts +6 -1
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -40,12 +40,22 @@ async function runCollect(io: CliIo): Promise<number> {
|
|
|
40
40
|
for (const warning of result.warnings) {
|
|
41
41
|
io.stderr(warning);
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
if (result.kind === "empty") {
|
|
44
|
+
io.stdout("nothing to report");
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
let exitCode = 0;
|
|
48
|
+
for (const target of result.targets) {
|
|
49
|
+
if ("accepted" in target) {
|
|
50
|
+
io.stdout(
|
|
51
|
+
`accepted ${target.accepted} days for ${result.machine} at ${target.url}`,
|
|
52
|
+
);
|
|
53
|
+
} else {
|
|
54
|
+
io.stderr(`${target.url}: ${target.message}`);
|
|
55
|
+
exitCode = 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return exitCode;
|
|
49
59
|
}
|
|
50
60
|
if (result.kind === "missing-config") {
|
|
51
61
|
io.stderr(
|
package/src/collect.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type AntigravityStep,
|
|
2
3
|
antigravityConversationsDir,
|
|
3
4
|
readAntigravitySteps,
|
|
4
5
|
} from "./antigravity";
|
|
@@ -9,12 +10,19 @@ import {
|
|
|
9
10
|
windowStart,
|
|
10
11
|
} from "./ccusage";
|
|
11
12
|
import { type CommandRunner, runCommand } from "./command";
|
|
12
|
-
import { readConfig, runtimeTimezone } from "./config";
|
|
13
|
+
import { type CollectorTarget, readConfig, runtimeTimezone } from "./config";
|
|
14
|
+
import { type DevinStep, devinTranscriptsDir, readDevinSteps } from "./devin";
|
|
13
15
|
import type { Fetcher } from "./http";
|
|
14
16
|
import { type MachineIdentity, machineId } from "./machine";
|
|
15
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
antigravityProvider,
|
|
19
|
+
devinProvider,
|
|
20
|
+
mapAntigravitySteps,
|
|
21
|
+
mapCcusageDays,
|
|
22
|
+
mapDevinSteps,
|
|
23
|
+
} from "./mapping";
|
|
16
24
|
import { type CollectorEnv, collectorPaths, processEnv } from "./paths";
|
|
17
|
-
import { loadPrices } from "./pricing";
|
|
25
|
+
import { loadPrices, type PriceTable } from "./pricing";
|
|
18
26
|
import type { UsageDay, UsageReport } from "./usage";
|
|
19
27
|
|
|
20
28
|
export interface CollectOptions {
|
|
@@ -26,8 +34,17 @@ export interface CollectOptions {
|
|
|
26
34
|
timezone?: string;
|
|
27
35
|
}
|
|
28
36
|
|
|
37
|
+
export type TargetResult =
|
|
38
|
+
| { accepted: number; url: string }
|
|
39
|
+
| { message: string; url: string };
|
|
40
|
+
|
|
29
41
|
export type CollectResult =
|
|
30
|
-
| {
|
|
42
|
+
| {
|
|
43
|
+
kind: "reported";
|
|
44
|
+
machine: string;
|
|
45
|
+
targets: TargetResult[];
|
|
46
|
+
warnings: string[];
|
|
47
|
+
}
|
|
31
48
|
| { kind: "empty"; warnings: string[] }
|
|
32
49
|
| { kind: "missing-config"; configFile: string }
|
|
33
50
|
| { kind: "failed"; message: string };
|
|
@@ -56,63 +73,100 @@ function acceptedCount(body: string): number | null {
|
|
|
56
73
|
return typeof accepted === "number" ? accepted : null;
|
|
57
74
|
}
|
|
58
75
|
|
|
59
|
-
interface
|
|
76
|
+
interface LocalRows {
|
|
60
77
|
days: UsageDay[];
|
|
61
78
|
warnings: string[];
|
|
62
79
|
}
|
|
63
80
|
|
|
81
|
+
interface LocalSource<Step extends { at: Date }> {
|
|
82
|
+
map: (steps: Step[], timezone: string, prices: PriceTable) => UsageDay[];
|
|
83
|
+
provider: string;
|
|
84
|
+
read: (home: string) => Promise<{ failures: string[]; steps: Step[] }>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface LocalWindow {
|
|
88
|
+
fetcher: Fetcher;
|
|
89
|
+
home: string;
|
|
90
|
+
pricesFile: string;
|
|
91
|
+
since: string;
|
|
92
|
+
timezone: string;
|
|
93
|
+
today: Date;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const antigravitySource: LocalSource<AntigravityStep> = {
|
|
97
|
+
map: mapAntigravitySteps,
|
|
98
|
+
provider: antigravityProvider,
|
|
99
|
+
read: (home) => readAntigravitySteps(antigravityConversationsDir(home)),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const devinSource: LocalSource<DevinStep> = {
|
|
103
|
+
map: mapDevinSteps,
|
|
104
|
+
provider: devinProvider,
|
|
105
|
+
read: (home) => readDevinSteps(devinTranscriptsDir(home)),
|
|
106
|
+
};
|
|
107
|
+
|
|
64
108
|
async function report(
|
|
65
109
|
fetcher: Fetcher,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
110
|
+
target: CollectorTarget,
|
|
111
|
+
body: string,
|
|
112
|
+
): Promise<TargetResult> {
|
|
113
|
+
const { url } = target;
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetcher(reportUrl(url), {
|
|
116
|
+
body,
|
|
117
|
+
headers: {
|
|
118
|
+
Authorization: `Bearer ${target.key}`,
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
},
|
|
121
|
+
method: "POST",
|
|
122
|
+
});
|
|
123
|
+
const answer = await response.text();
|
|
124
|
+
if (response.status !== 200) {
|
|
125
|
+
return {
|
|
126
|
+
message: `tokenmax responded ${response.status}: ${answer}`,
|
|
127
|
+
url,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const accepted = acceptedCount(answer);
|
|
131
|
+
if (accepted === null) {
|
|
132
|
+
return {
|
|
133
|
+
message: `tokenmax responded an unexpected body: ${answer}`,
|
|
134
|
+
url,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return { accepted, url };
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return { message: messageOf(error), url };
|
|
92
140
|
}
|
|
93
|
-
return { accepted, kind: "reported", machine: usage.machine, warnings };
|
|
94
141
|
}
|
|
95
142
|
|
|
96
|
-
async function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
143
|
+
async function localDays<Step extends { at: Date }>(
|
|
144
|
+
source: LocalSource<Step>,
|
|
145
|
+
window: LocalWindow,
|
|
146
|
+
): Promise<LocalRows> {
|
|
147
|
+
try {
|
|
148
|
+
const usage = await source.read(window.home);
|
|
149
|
+
const warnings = usage.failures.map(
|
|
150
|
+
(failure) => `${source.provider}: skipped ${failure}`,
|
|
151
|
+
);
|
|
152
|
+
const steps = usage.steps.filter(
|
|
153
|
+
(step) => calendarDate(step.at, window.timezone) >= window.since,
|
|
154
|
+
);
|
|
155
|
+
if (steps.length === 0) {
|
|
156
|
+
return { days: [], warnings };
|
|
157
|
+
}
|
|
158
|
+
const prices = await loadPrices(
|
|
159
|
+
window.fetcher,
|
|
160
|
+
window.pricesFile,
|
|
161
|
+
window.today,
|
|
162
|
+
);
|
|
163
|
+
return { days: source.map(steps, window.timezone, prices), warnings };
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return {
|
|
166
|
+
days: [],
|
|
167
|
+
warnings: [`${source.provider}: ${messageOf(error)}`],
|
|
168
|
+
};
|
|
113
169
|
}
|
|
114
|
-
const prices = await loadPrices(fetcher, pricesFile, today);
|
|
115
|
-
return { days: mapAntigravitySteps(steps, timezone, prices), warnings };
|
|
116
170
|
}
|
|
117
171
|
|
|
118
172
|
export async function collect(options: CollectOptions): Promise<CollectResult> {
|
|
@@ -148,32 +202,27 @@ export async function collect(options: CollectOptions): Promise<CollectResult> {
|
|
|
148
202
|
return { kind: "failed", message: messageOf(error) };
|
|
149
203
|
}
|
|
150
204
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
days.push(...antigravity.days);
|
|
205
|
+
const window: LocalWindow = {
|
|
206
|
+
fetcher,
|
|
207
|
+
home: env.home,
|
|
208
|
+
pricesFile: paths.pricesFile,
|
|
209
|
+
since: windowStart(today, timezone),
|
|
210
|
+
timezone,
|
|
211
|
+
today,
|
|
212
|
+
};
|
|
213
|
+
const antigravity = await localDays(antigravitySource, window);
|
|
214
|
+
const devin = await localDays(devinSource, window);
|
|
215
|
+
days.push(...antigravity.days, ...devin.days);
|
|
216
|
+
const warnings = [...antigravity.warnings, ...devin.warnings];
|
|
164
217
|
if (days.length === 0) {
|
|
165
|
-
return { kind: "empty", warnings
|
|
218
|
+
return { kind: "empty", warnings };
|
|
166
219
|
}
|
|
167
220
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
{ days, machine, timezone },
|
|
174
|
-
antigravity.warnings,
|
|
175
|
-
);
|
|
176
|
-
} catch (error) {
|
|
177
|
-
return { kind: "failed", message: messageOf(error) };
|
|
221
|
+
const usage: UsageReport = { days, machine, timezone };
|
|
222
|
+
const body = JSON.stringify(usage);
|
|
223
|
+
const targets: TargetResult[] = [];
|
|
224
|
+
for (const target of config.config.targets) {
|
|
225
|
+
targets.push(await report(fetcher, target, body));
|
|
178
226
|
}
|
|
227
|
+
return { kind: "reported", machine, targets, warnings };
|
|
179
228
|
}
|
package/src/config.ts
CHANGED
|
@@ -2,12 +2,25 @@ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
|
-
export const
|
|
5
|
+
export const collectorTarget = z.object({
|
|
6
6
|
key: z.string().min(1),
|
|
7
|
-
timezone: z.string().min(1).optional(),
|
|
8
7
|
url: z.url({ protocol: /^https?$/ }),
|
|
9
8
|
});
|
|
10
9
|
|
|
10
|
+
const timezone = z.string().min(1).optional();
|
|
11
|
+
|
|
12
|
+
export const collectorConfig = z.object({
|
|
13
|
+
targets: z.array(collectorTarget).min(1),
|
|
14
|
+
timezone,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const singleTargetConfig = collectorTarget
|
|
18
|
+
.extend({ timezone })
|
|
19
|
+
.transform(({ key, url, ...rest }) => ({ targets: [{ key, url }], ...rest }));
|
|
20
|
+
|
|
21
|
+
const storedConfig = z.union([collectorConfig, singleTargetConfig]);
|
|
22
|
+
|
|
23
|
+
export type CollectorTarget = z.infer<typeof collectorTarget>;
|
|
11
24
|
export type CollectorConfig = z.infer<typeof collectorConfig>;
|
|
12
25
|
|
|
13
26
|
export function canonicalTimezone(value: string): string | null {
|
|
@@ -62,7 +75,7 @@ export async function readConfig(
|
|
|
62
75
|
};
|
|
63
76
|
}
|
|
64
77
|
|
|
65
|
-
const parsed =
|
|
78
|
+
const parsed = storedConfig.safeParse(payload);
|
|
66
79
|
if (!parsed.success) {
|
|
67
80
|
return {
|
|
68
81
|
kind: "invalid",
|
package/src/devin.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
export interface DevinStep {
|
|
6
|
+
at: Date;
|
|
7
|
+
cacheCreate: number;
|
|
8
|
+
cacheRead: number;
|
|
9
|
+
input: number;
|
|
10
|
+
model: string;
|
|
11
|
+
output: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DevinUsage {
|
|
15
|
+
failures: string[];
|
|
16
|
+
steps: DevinStep[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const unknownModel = "devin-unknown";
|
|
20
|
+
|
|
21
|
+
const metrics = z.object({
|
|
22
|
+
cached_tokens: z.int().optional(),
|
|
23
|
+
completion_tokens: z.int(),
|
|
24
|
+
extra: z
|
|
25
|
+
.object({ cache_creation_input_tokens: z.int().optional() })
|
|
26
|
+
.optional(),
|
|
27
|
+
prompt_tokens: z.int(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const step = z.object({
|
|
31
|
+
metrics: metrics.optional(),
|
|
32
|
+
model_name: z.string().optional(),
|
|
33
|
+
source: z.string(),
|
|
34
|
+
timestamp: z.iso.datetime({ offset: true }),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const transcript = z.object({ steps: z.array(step) });
|
|
38
|
+
|
|
39
|
+
const issueText = (error: z.ZodError): string =>
|
|
40
|
+
error.issues
|
|
41
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
42
|
+
.join("; ");
|
|
43
|
+
|
|
44
|
+
export function devinTranscriptsDir(home: string): string {
|
|
45
|
+
return resolve(home, ".local", "share", "devin", "cli", "transcripts");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseTranscript(source: string): DevinStep[] {
|
|
49
|
+
const parsed = transcript.safeParse(JSON.parse(source));
|
|
50
|
+
if (!parsed.success) {
|
|
51
|
+
throw new Error(issueText(parsed.error));
|
|
52
|
+
}
|
|
53
|
+
const steps: DevinStep[] = [];
|
|
54
|
+
for (const entry of parsed.data.steps) {
|
|
55
|
+
if (entry.source !== "agent" || entry.metrics === undefined) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const cacheRead = entry.metrics.cached_tokens ?? 0;
|
|
59
|
+
const cacheCreate = entry.metrics.extra?.cache_creation_input_tokens ?? 0;
|
|
60
|
+
steps.push({
|
|
61
|
+
at: new Date(entry.timestamp),
|
|
62
|
+
cacheCreate,
|
|
63
|
+
cacheRead,
|
|
64
|
+
input: entry.metrics.prompt_tokens - cacheRead - cacheCreate,
|
|
65
|
+
model: entry.model_name ?? unknownModel,
|
|
66
|
+
output: entry.metrics.completion_tokens,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return steps;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function readDevinSteps(dir: string): Promise<DevinUsage> {
|
|
73
|
+
let files: string[];
|
|
74
|
+
try {
|
|
75
|
+
files = await readdir(dir);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
78
|
+
return { failures: [], steps: [] };
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
const steps: DevinStep[] = [];
|
|
83
|
+
const failures: string[] = [];
|
|
84
|
+
for (const file of files.filter((name) => name.endsWith(".json")).sort()) {
|
|
85
|
+
const path = resolve(dir, file);
|
|
86
|
+
try {
|
|
87
|
+
steps.push(...parseTranscript(await readFile(path, "utf8")));
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
90
|
+
failures.push(`${path}: ${message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { failures, steps };
|
|
94
|
+
}
|
package/src/install.ts
CHANGED
|
@@ -3,8 +3,10 @@ import { dirname } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import {
|
|
5
5
|
type CollectorConfig,
|
|
6
|
+
type CollectorTarget,
|
|
6
7
|
canonicalTimezone,
|
|
7
8
|
collectorConfig,
|
|
9
|
+
readConfig,
|
|
8
10
|
runtimeTimezone,
|
|
9
11
|
writeConfig,
|
|
10
12
|
} from "./config";
|
|
@@ -67,6 +69,13 @@ function scheduleFiles(
|
|
|
67
69
|
];
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
async function configuredTargets(
|
|
73
|
+
configFile: string,
|
|
74
|
+
): Promise<CollectorTarget[]> {
|
|
75
|
+
const existing = await readConfig(configFile);
|
|
76
|
+
return existing.kind === "ok" ? existing.config.targets : [];
|
|
77
|
+
}
|
|
78
|
+
|
|
70
79
|
export async function install(options: InstallOptions): Promise<InstallPlan> {
|
|
71
80
|
const env = options.env ?? processEnv();
|
|
72
81
|
const platform = resolvePlatform(options.platform ?? process.platform);
|
|
@@ -76,10 +85,12 @@ export async function install(options: InstallOptions): Promise<InstallPlan> {
|
|
|
76
85
|
if (timezone === null) {
|
|
77
86
|
throw new Error(`invalid timezone: ${requestedTimezone}`);
|
|
78
87
|
}
|
|
88
|
+
const others = (await configuredTargets(paths.configFile)).filter(
|
|
89
|
+
(target) => target.url !== options.url,
|
|
90
|
+
);
|
|
79
91
|
const config = collectorConfig.parse({
|
|
80
|
-
key: options.key,
|
|
92
|
+
targets: [...others, { key: options.key, url: options.url }],
|
|
81
93
|
timezone,
|
|
82
|
-
url: options.url,
|
|
83
94
|
});
|
|
84
95
|
const cliPath = options.cliPath ?? resolveCliPath();
|
|
85
96
|
if (
|
|
@@ -106,6 +117,9 @@ export async function install(options: InstallOptions): Promise<InstallPlan> {
|
|
|
106
117
|
const log = options.log ?? console.log;
|
|
107
118
|
|
|
108
119
|
log(`config: ${plan.configFile} (600, key redacted)`);
|
|
120
|
+
for (const target of plan.config.targets) {
|
|
121
|
+
log(`target: ${target.url}`);
|
|
122
|
+
}
|
|
109
123
|
for (const file of plan.files) {
|
|
110
124
|
log(`schedule: ${file.path}`);
|
|
111
125
|
}
|
package/src/machine.ts
CHANGED
|
@@ -11,12 +11,13 @@ const disallowed = /[^A-Za-z0-9._-]/g;
|
|
|
11
11
|
|
|
12
12
|
const platformUuidPattern = /"IOPlatformUUID"\s*=\s*"([^"]+)"/;
|
|
13
13
|
|
|
14
|
+
/** The hostname changes with the network, and every spelling of it used to open
|
|
15
|
+
* a new machine on the server, so the platform identifier travels alone. */
|
|
14
16
|
export function machineId(
|
|
15
17
|
hostnameValue: string,
|
|
16
18
|
platformUuid: string | null,
|
|
17
19
|
): string {
|
|
18
|
-
const raw =
|
|
19
|
-
platformUuid === null ? hostnameValue : `${hostnameValue}-${platformUuid}`;
|
|
20
|
+
const raw = platformUuid ?? hostnameValue;
|
|
20
21
|
return raw.replace(disallowed, "-").slice(0, 64);
|
|
21
22
|
}
|
|
22
23
|
|
package/src/mapping.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import type { AntigravityStep } from "./antigravity";
|
|
2
2
|
import { type CcusageDaily, calendarDate } from "./ccusage";
|
|
3
|
-
import {
|
|
3
|
+
import type { DevinStep } from "./devin";
|
|
4
|
+
import { costOf, type ModelPrice, type PriceTable } from "./pricing";
|
|
4
5
|
import type { UsageDay } from "./usage";
|
|
5
6
|
|
|
6
7
|
export const antigravityProvider = "antigravity";
|
|
8
|
+
export const devinProvider = "devin";
|
|
7
9
|
|
|
8
10
|
const agentModelPrefix = /^\[[^\]]*\]\s*/;
|
|
11
|
+
const effortSuffix = /-(low|medium|high|xhigh)$/;
|
|
12
|
+
const gptMinorVersion = /^gpt-(\d+)-(\d+)-/;
|
|
13
|
+
|
|
14
|
+
const litellmModel = (devinModel: string): string =>
|
|
15
|
+
devinModel.replace(effortSuffix, "").replace(gptMinorVersion, "gpt-$1.$2-");
|
|
9
16
|
|
|
10
17
|
const rowKey = (day: UsageDay): string =>
|
|
11
18
|
`${day.date}\u0000${day.provider}\u0000${day.model}`;
|
|
@@ -53,23 +60,33 @@ export function mapCcusageDays(output: CcusageDaily): UsageDay[] {
|
|
|
53
60
|
return sortedRows(rows);
|
|
54
61
|
}
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
interface LocalStep {
|
|
64
|
+
at: Date;
|
|
65
|
+
cacheCreate?: number;
|
|
66
|
+
cacheRead: number;
|
|
67
|
+
input: number;
|
|
68
|
+
model: string;
|
|
69
|
+
output: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function mapLocalSteps(
|
|
73
|
+
steps: LocalStep[],
|
|
74
|
+
provider: string,
|
|
58
75
|
timezone: string,
|
|
59
|
-
|
|
76
|
+
priceOf: (model: string) => ModelPrice | undefined,
|
|
60
77
|
): UsageDay[] {
|
|
61
78
|
const rows = new Map<string, UsageDay>();
|
|
62
79
|
|
|
63
80
|
for (const step of steps) {
|
|
64
81
|
const row: UsageDay = {
|
|
65
|
-
cache_create: 0,
|
|
82
|
+
cache_create: step.cacheCreate ?? 0,
|
|
66
83
|
cache_read: step.cacheRead,
|
|
67
84
|
cost_usd: 0,
|
|
68
85
|
date: calendarDate(step.at, timezone),
|
|
69
86
|
input: step.input,
|
|
70
87
|
model: step.model,
|
|
71
88
|
output: step.output,
|
|
72
|
-
provider
|
|
89
|
+
provider,
|
|
73
90
|
};
|
|
74
91
|
if (isEmptyRow(row)) {
|
|
75
92
|
continue;
|
|
@@ -80,17 +97,19 @@ export function mapAntigravitySteps(
|
|
|
80
97
|
rows.set(key, row);
|
|
81
98
|
continue;
|
|
82
99
|
}
|
|
100
|
+
existing.cache_create += row.cache_create;
|
|
83
101
|
existing.cache_read += row.cache_read;
|
|
84
102
|
existing.input += row.input;
|
|
85
103
|
existing.output += row.output;
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
for (const row of rows.values()) {
|
|
89
|
-
const price =
|
|
107
|
+
const price = priceOf(row.model);
|
|
90
108
|
row.cost_usd =
|
|
91
109
|
price === undefined
|
|
92
110
|
? 0
|
|
93
111
|
: costOf(price, {
|
|
112
|
+
cacheCreate: row.cache_create,
|
|
94
113
|
cacheRead: row.cache_read,
|
|
95
114
|
input: row.input,
|
|
96
115
|
output: row.output,
|
|
@@ -100,6 +119,32 @@ export function mapAntigravitySteps(
|
|
|
100
119
|
return sortedRows(rows);
|
|
101
120
|
}
|
|
102
121
|
|
|
122
|
+
export function mapAntigravitySteps(
|
|
123
|
+
steps: AntigravityStep[],
|
|
124
|
+
timezone: string,
|
|
125
|
+
prices: PriceTable,
|
|
126
|
+
): UsageDay[] {
|
|
127
|
+
return mapLocalSteps(
|
|
128
|
+
steps,
|
|
129
|
+
antigravityProvider,
|
|
130
|
+
timezone,
|
|
131
|
+
(model) => prices.get(model) ?? prices.get(`gemini/${model}`),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function mapDevinSteps(
|
|
136
|
+
steps: DevinStep[],
|
|
137
|
+
timezone: string,
|
|
138
|
+
prices: PriceTable,
|
|
139
|
+
): UsageDay[] {
|
|
140
|
+
return mapLocalSteps(
|
|
141
|
+
steps.map((step) => ({ ...step, model: litellmModel(step.model) })),
|
|
142
|
+
devinProvider,
|
|
143
|
+
timezone,
|
|
144
|
+
(model) => prices.get(model),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
103
148
|
function sortedRows(rows: Map<string, UsageDay>): UsageDay[] {
|
|
104
149
|
return [...rows.values()].sort(
|
|
105
150
|
(a, b) =>
|
package/src/pricing.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import type { Fetcher } from "./http";
|
|
5
5
|
|
|
6
6
|
export interface ModelPrice {
|
|
7
|
+
cacheCreate: number;
|
|
7
8
|
cacheRead: number;
|
|
8
9
|
input: number;
|
|
9
10
|
output: number;
|
|
@@ -12,6 +13,7 @@ export interface ModelPrice {
|
|
|
12
13
|
export type PriceTable = Map<string, ModelPrice>;
|
|
13
14
|
|
|
14
15
|
export interface PricedUsage {
|
|
16
|
+
cacheCreate?: number;
|
|
15
17
|
cacheRead: number;
|
|
16
18
|
input: number;
|
|
17
19
|
output: number;
|
|
@@ -23,6 +25,7 @@ export const litellmPricesUrl =
|
|
|
23
25
|
const maxAgeMs = 24 * 60 * 60 * 1000;
|
|
24
26
|
|
|
25
27
|
const pricedEntry = z.object({
|
|
28
|
+
cache_creation_input_token_cost: z.number().optional(),
|
|
26
29
|
cache_read_input_token_cost: z.number().optional(),
|
|
27
30
|
input_cost_per_token: z.number(),
|
|
28
31
|
output_cost_per_token: z.number(),
|
|
@@ -38,6 +41,7 @@ export function parseLitellmPrices(source: string): PriceTable {
|
|
|
38
41
|
const priced = pricedEntry.safeParse(entry);
|
|
39
42
|
if (priced.success) {
|
|
40
43
|
table.set(model, {
|
|
44
|
+
cacheCreate: priced.data.cache_creation_input_token_cost ?? 0,
|
|
41
45
|
cacheRead: priced.data.cache_read_input_token_cost ?? 0,
|
|
42
46
|
input: priced.data.input_cost_per_token,
|
|
43
47
|
output: priced.data.output_cost_per_token,
|
|
@@ -51,7 +55,8 @@ export function costOf(price: ModelPrice, usage: PricedUsage): number {
|
|
|
51
55
|
return (
|
|
52
56
|
usage.input * price.input +
|
|
53
57
|
usage.output * price.output +
|
|
54
|
-
usage.cacheRead * price.cacheRead
|
|
58
|
+
usage.cacheRead * price.cacheRead +
|
|
59
|
+
(usage.cacheCreate ?? 0) * price.cacheCreate
|
|
55
60
|
);
|
|
56
61
|
}
|
|
57
62
|
|