Open History to see the last commit
1import { describe, expect, it } from "@effect/vitest";
2
3import { GROK_COST_USD_TICKS_PER_DOLLAR, parseGrokLine } from "./usage.ts";
4
5describe("parseGrokLine", () => {
6 function turnCompleted(overrides?: {
7 sessionId?: string;
8 promptId?: string;
9 timestamp?: number;
10 agentTimestampMs?: number;
11 usage?: Record<string, unknown>;
12 modelUsage?: Record<string, Record<string, unknown>> | null;
13 }): string {
14 const modelUsage =
15 overrides && "modelUsage" in overrides
16 ? overrides.modelUsage
17 : {
18 "grok-4.5-build": {
19 inputTokens: 20_272,
20 outputTokens: 272,
21 totalTokens: 20_544,
22 cachedReadTokens: 11_264,
23 cacheCreationTokens: 0,
24 reasoningTokens: 180,
25 costUsdTicks: 230_272_000,
26 },
27 };
28
29 return JSON.stringify({
30 timestamp: overrides?.timestamp ?? 1_786_372_566,
31 method: "_x.ai/session/update",
32 params: {
33 sessionId: overrides?.sessionId ?? "019fec1a-12f7-72f2-9b1f-7778a00aea3c",
34 update: {
35 sessionUpdate: "turn_completed",
36 prompt_id: overrides?.promptId ?? "prompt-1",
37 stop_reason: "end_turn",
38 usage: {
39 inputTokens: 20_272,
40 outputTokens: 272,
41 totalTokens: 20_544,
42 cachedReadTokens: 11_264,
43 cacheCreationTokens: 0,
44 reasoningTokens: 180,
45 costUsdTicks: 230_272_000,
46 ...(modelUsage === null ? {} : { modelUsage }),
47 ...overrides?.usage,
48 },
49 },
50 _meta: {
51 eventId: "event-1",
52 agentTimestampMs: overrides?.agentTimestampMs ?? 1_786_372_566_485,
53 },
54 },
55 });
56 }
57
58 it("extracts per-model totals and provider-reported cost ticks", () => {
59 const records = parseGrokLine(turnCompleted());
60
61 expect(records).toHaveLength(1);
62 const [record] = records;
63 expect(record?.provider).toBe("grok");
64 expect(record?.model).toBe("grok-4.5-build");
65 expect(record?.sessionId).toBe("019fec1a-12f7-72f2-9b1f-7778a00aea3c");
66 expect(record?.timestampMs).toBe(1_786_372_566_485);
67 expect(record?.totals).toEqual({
68 uncachedInputTokens: 20_272 - 11_264,
69 cachedInputTokens: 11_264,
70 cacheCreationTokens: 0,
71 outputTokens: 272,
72 reasoningTokens: 180,
73 });
74 expect(record?.reportedCostUsd).toBeCloseTo(230_272_000 / GROK_COST_USD_TICKS_PER_DOLLAR, 12);
75 expect(record?.dedupeKey).toBe(
76 "019fec1a-12f7-72f2-9b1f-7778a00aea3c:prompt-1:1786372566485:grok-4.5-build",
77 );
78 });
79
80 it("emits one record per model when modelUsage has several entries", () => {
81 const records = parseGrokLine(
82 turnCompleted({
83 modelUsage: {
84 "grok-4.5": {
85 inputTokens: 1000,
86 outputTokens: 50,
87 cachedReadTokens: 400,
88 reasoningTokens: 20,
89 costUsdTicks: 50_000_000,
90 },
91 "grok-composer-2.5-fast": {
92 inputTokens: 200,
93 outputTokens: 30,
94 cachedReadTokens: 100,
95 reasoningTokens: 0,
96 costUsdTicks: 10_000_000,
97 },
98 },
99 }),
100 );
101
102 expect(records.map((record) => record.model).toSorted()).toEqual([
103 "grok-4.5",
104 "grok-composer-2.5-fast",
105 ]);
106 expect(records.every((record) => record.provider === "grok")).toBe(true);
107 expect(records.find((record) => record.model === "grok-4.5")?.reportedCostUsd).toBeCloseTo(
108 0.005,
109 12,
110 );
111 });
112
113 it("inherits top-level cost ticks for a single model without its own ticks", () => {
114 const records = parseGrokLine(
115 turnCompleted({
116 modelUsage: {
117 "grok-4.5-build": {
118 inputTokens: 1000,
119 outputTokens: 10,
120 cachedReadTokens: 0,
121 reasoningTokens: 0,
122 },
123 },
124 usage: { costUsdTicks: GROK_COST_USD_TICKS_PER_DOLLAR },
125 }),
126 );
127
128 expect(records).toHaveLength(1);
129 expect(records[0]?.reportedCostUsd).toBe(1);
130 });
131
132 it("falls back to a generic grok model when modelUsage is absent", () => {
133 const records = parseGrokLine(turnCompleted({ modelUsage: null }));
134
135 expect(records).toHaveLength(1);
136 const [record] = records;
137 expect(record?.provider).toBe("grok");
138 expect(record?.model).toBe("grok");
139 expect(record?.totals).toEqual({
140 uncachedInputTokens: 20_272 - 11_264,
141 cachedInputTokens: 11_264,
142 cacheCreationTokens: 0,
143 outputTokens: 272,
144 reasoningTokens: 180,
145 });
146 expect(record?.reportedCostUsd).toBeCloseTo(230_272_000 / GROK_COST_USD_TICKS_PER_DOLLAR, 12);
147 expect(record?.dedupeKey).toBe(
148 "019fec1a-12f7-72f2-9b1f-7778a00aea3c:prompt-1:1786372566485:grok",
149 );
150 });
151
152 it("pro-rates top-level cost ticks across multi-model turns without per-model ticks", () => {
153 const records = parseGrokLine(
154 turnCompleted({
155 modelUsage: {
156 "grok-4.5": {
157 inputTokens: 300,
158 outputTokens: 0,
159 cachedReadTokens: 0,
160 reasoningTokens: 0,
161 },
162 "grok-composer-2.5-fast": {
163 inputTokens: 100,
164 outputTokens: 0,
165 cachedReadTokens: 0,
166 reasoningTokens: 0,
167 },
168 },
169 usage: { costUsdTicks: GROK_COST_USD_TICKS_PER_DOLLAR },
170 }),
171 );
172
173 expect(records).toHaveLength(2);
174 const byModel = Object.fromEntries(records.map((record) => [record.model, record]));
175 expect(byModel["grok-4.5"]?.reportedCostUsd).toBeCloseTo(0.75, 12);
176 expect(byModel["grok-composer-2.5-fast"]?.reportedCostUsd).toBeCloseTo(0.25, 12);
177 const sum =
178 (byModel["grok-4.5"]?.reportedCostUsd ?? 0) +
179 (byModel["grok-composer-2.5-fast"]?.reportedCostUsd ?? 0);
180 expect(sum).toBeCloseTo(1, 12);
181 });
182
183 it("allocates leftover aggregate ticks to models that omit per-model ticks", () => {
184 const records = parseGrokLine(
185 turnCompleted({
186 modelUsage: {
187 "grok-4.5": {
188 inputTokens: 300,
189 outputTokens: 0,
190 cachedReadTokens: 0,
191 reasoningTokens: 0,
192 costUsdTicks: 0.4 * GROK_COST_USD_TICKS_PER_DOLLAR,
193 },
194 "grok-composer-2.5-fast": {
195 inputTokens: 100,
196 outputTokens: 0,
197 cachedReadTokens: 0,
198 reasoningTokens: 0,
199 },
200 },
201 usage: { costUsdTicks: GROK_COST_USD_TICKS_PER_DOLLAR },
202 }),
203 );
204
205 expect(records).toHaveLength(2);
206 const byModel = Object.fromEntries(records.map((record) => [record.model, record]));
207 expect(byModel["grok-4.5"]?.reportedCostUsd).toBeCloseTo(0.4, 12);
208 expect(byModel["grok-composer-2.5-fast"]?.reportedCostUsd).toBeCloseTo(0.6, 12);
209 const sum =
210 (byModel["grok-4.5"]?.reportedCostUsd ?? 0) +
211 (byModel["grok-composer-2.5-fast"]?.reportedCostUsd ?? 0);
212 expect(sum).toBeCloseTo(1, 12);
213 });
214
215 it("does not invent a colliding dedupe key when prompt_id is missing", () => {
216 const line = JSON.stringify({
217 timestamp: 1_786_372_566,
218 method: "_x.ai/session/update",
219 params: {
220 sessionId: "s1",
221 update: {
222 sessionUpdate: "turn_completed",
223 usage: {
224 inputTokens: 10,
225 outputTokens: 2,
226 modelUsage: {
227 "grok-4.5": { inputTokens: 10, outputTokens: 2 },
228 },
229 },
230 },
231 },
232 });
233
234 expect(parseGrokLine(line)[0]?.dedupeKey).toBeNull();
235 });
236
237 it("keeps later turns that reuse a prompt id after the session resumes in a new process", () => {
238 const first = parseGrokLine(turnCompleted({ promptId: "t3-xai-prompt-1" }))[0];
239 const resumed = parseGrokLine(
240 turnCompleted({ promptId: "t3-xai-prompt-1", agentTimestampMs: 1_786_459_000_000 }),
241 )[0];
242 const copied = parseGrokLine(turnCompleted({ promptId: "t3-xai-prompt-1" }))[0];
243
244 expect(resumed?.dedupeKey).not.toBe(first?.dedupeKey);
245 expect(copied?.dedupeKey).toBe(first?.dedupeKey);
246 });
247
248 it("ignores non-turn lines and empty usage", () => {
249 expect(parseGrokLine(JSON.stringify({ method: "session/update", params: {} }))).toEqual([]);
250 expect(parseGrokLine("not json")).toEqual([]);
251 expect(
252 parseGrokLine(
253 turnCompleted({
254 modelUsage: {
255 "grok-4.5-build": {
256 inputTokens: 0,
257 outputTokens: 0,
258 cachedReadTokens: 0,
259 reasoningTokens: 0,
260 costUsdTicks: 0,
261 },
262 },
263 }),
264 ),
265 ).toEqual([]);
266 });
267
268 it("falls back to the outer unix-seconds timestamp when agent meta is missing", () => {
269 const line = JSON.stringify({
270 timestamp: 1_786_372_566,
271 method: "_x.ai/session/update",
272 params: {
273 sessionId: "s1",
274 update: {
275 sessionUpdate: "turn_completed",
276 prompt_id: "p1",
277 usage: {
278 inputTokens: 10,
279 outputTokens: 2,
280 modelUsage: {
281 "grok-4.5": { inputTokens: 10, outputTokens: 2 },
282 },
283 },
284 },
285 },
286 });
287
288 const records = parseGrokLine(line);
289 expect(records[0]?.timestampMs).toBe(1_786_372_566_000);
290 });
291});