1/**
2 * Usage history for Grok Build: the format of its per-session
3 * `updates.jsonl` logs and where each instance's `GROK_HOME` keeps them.
4 *
5 * @module provider-grok/server/usage
6 */
7
8import type { UsageTokenTotals } from "@t3tools/contracts";
9import * as Effect from "effect/Effect";
10import * as Path from "effect/Path";
11
12import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
13import {
14 tokenCount,
15 totalTokens,
16 type ProviderUsageReader,
17 type TranscriptUsageFormat,
18 type UsageRecord,
19} from "@t3tools/provider-core/server/usage";
20
21import type { GrokSettings } from "../settings.ts";
22import * as HostProcess from "@t3tools/shared/HostProcess";
23
24/**
25 * Grok reports cost in integer ticks where `1 USD = 10^10` ticks. See Grok
26 * headless `total_cost_usd_ticks`. Convert to dollars for pricing.
27 */
28export const GROK_COST_USD_TICKS_PER_DOLLAR = 10_000_000_000;
29
30function grokCostTicksToUsd(ticks: unknown): number | null {
31 if (typeof ticks !== "number" || !Number.isFinite(ticks) || ticks < 0) return null;
32 return ticks / GROK_COST_USD_TICKS_PER_DOLLAR;
33}
34
35interface GrokUsageTotals {
36 readonly inputTokens: number;
37 readonly outputTokens: number;
38 readonly cachedReadTokens: number;
39 readonly cacheCreationTokens: number;
40 readonly reasoningTokens: number;
41 readonly costUsdTicks: number | null;
42}
43
44function readGrokUsageTotals(value: unknown): GrokUsageTotals | null {
45 if (typeof value !== "object" || value === null) return null;
46 const record = value as Record<string, unknown>;
47 return {
48 inputTokens: tokenCount(record["inputTokens"]),
49 outputTokens: tokenCount(record["outputTokens"]),
50 cachedReadTokens: tokenCount(record["cachedReadTokens"]),
51 cacheCreationTokens: tokenCount(record["cacheCreationTokens"]),
52 reasoningTokens: tokenCount(record["reasoningTokens"]),
53 costUsdTicks:
54 typeof record["costUsdTicks"] === "number" && Number.isFinite(record["costUsdTicks"])
55 ? record["costUsdTicks"]
56 : null,
57 };
58}
59
60function grokTotalsToUsage(totals: GrokUsageTotals): UsageTokenTotals {
61 const cachedInputTokens = totals.cachedReadTokens;
62 const cacheCreationTokens = totals.cacheCreationTokens;
63 // Grok reports `inputTokens` inclusive of the cached portion, matching Codex.
64 const uncachedInputTokens = Math.max(
65 0,
66 totals.inputTokens - cachedInputTokens - cacheCreationTokens,
67 );
68 const outputTokens = totals.outputTokens;
69 return {
70 uncachedInputTokens,
71 cachedInputTokens,
72 cacheCreationTokens,
73 outputTokens,
74 reasoningTokens: Math.min(outputTokens, totals.reasoningTokens),
75 };
76}
77
78/**
79 * Parses one line of a Grok Build `updates.jsonl` session log.
80 *
81 * Usage lands on `turn_completed` session updates. Per-model breakdowns live
82 * under `usage.modelUsage`; when present each model becomes its own record.
83 *
84 * Returns every record for the line (0 or more). Callers stream line-by-line
85 * and flatten.
86 */
87export function parseGrokLine(line: string): readonly UsageRecord[] {
88 let parsed: unknown;
89 try {
90 parsed = JSON.parse(line);
91 } catch {
92 return [];
93 }
94 return parseGrokRecord(parsed);
95}
96
97function parseGrokRecord(parsed: unknown): readonly UsageRecord[] {
98 if (typeof parsed !== "object" || parsed === null) return [];
99
100 const record = parsed as Record<string, unknown>;
101 const params = record["params"];
102 if (typeof params !== "object" || params === null) return [];
103 const paramsRecord = params as Record<string, unknown>;
104
105 const update = paramsRecord["update"];
106 if (typeof update !== "object" || update === null) return [];
107 const updateRecord = update as Record<string, unknown>;
108 if (updateRecord["sessionUpdate"] !== "turn_completed") return [];
109
110 const usage = updateRecord["usage"];
111 if (typeof usage !== "object" || usage === null) return [];
112 const usageRecord = usage as Record<string, unknown>;
113
114 const sessionId = typeof paramsRecord["sessionId"] === "string" ? paramsRecord["sessionId"] : "";
115 const promptId = typeof updateRecord["prompt_id"] === "string" ? updateRecord["prompt_id"] : null;
116
117 // Prefer the high-resolution agent clock; fall back to the outer unix seconds.
118 const meta = paramsRecord["_meta"];
119 let timestampMs: number | null = null;
120 if (typeof meta === "object" && meta !== null) {
121 const agentTimestampMs = (meta as Record<string, unknown>)["agentTimestampMs"];
122 if (typeof agentTimestampMs === "number" && Number.isFinite(agentTimestampMs)) {
123 timestampMs = agentTimestampMs;
124 }
125 }
126 if (timestampMs === null) {
127 const timestamp = record["timestamp"];
128 if (typeof timestamp === "number" && Number.isFinite(timestamp)) {
129 timestampMs = timestamp > 1e12 ? timestamp : timestamp * 1000;
130 }
131 }
132 if (timestampMs === null) return [];
133
134 const topLevel = readGrokUsageTotals(usageRecord);
135 if (topLevel === null) return [];
136
137 const modelUsage = usageRecord["modelUsage"];
138 const modelEntries: Array<{ model: string; totals: GrokUsageTotals }> = [];
139 if (typeof modelUsage === "object" && modelUsage !== null) {
140 for (const [model, raw] of Object.entries(modelUsage as Record<string, unknown>)) {
141 if (model.length === 0) continue;
142 const totals = readGrokUsageTotals(raw);
143 if (totals === null) continue;
144 modelEntries.push({ model, totals });
145 }
146 }
147
148 if (modelEntries.length === 0) {
149 if (totalTokens(grokTotalsToUsage(topLevel)) === 0) return [];
150 return [
151 {
152 provider: "grok",
153 timestampMs,
154 model: "grok",
155 sessionId,
156 totals: grokTotalsToUsage(topLevel),
157 reportedCostUsd: grokCostTicksToUsd(topLevel.costUsdTicks),
158 speed: "standard",
159 // No prompt id means we cannot tell two same-second updates apart.
160 dedupeKey: promptId === null ? null : `${sessionId}:${promptId}:grok`,
161 },
162 ];
163 }
164
165 // Cost allocation:
166 // 1. Emitted models with their own costUsdTicks keep those values.
167 // 2. Remaining aggregate cost (top-level minus those per-model ticks,
168 // clamped at 0) is pro-rated across emitted models that lack ticks,
169 // by token share among the unticked models only.
170 // 3. When no model has per-model ticks, remaining equals the full
171 // aggregate and every emitted model gets a token-share slice.
172 // Zero-token rows are never emitted and never count toward used ticks.
173 const topLevelCostUsd = grokCostTicksToUsd(topLevel.costUsdTicks);
174 let usedTickedCostUsd = 0;
175 let untickedTokenDenominator = 0;
176 for (const entry of modelEntries) {
177 const tokens = totalTokens(grokTotalsToUsage(entry.totals));
178 if (tokens === 0) continue;
179 if (entry.totals.costUsdTicks !== null) {
180 usedTickedCostUsd += grokCostTicksToUsd(entry.totals.costUsdTicks) ?? 0;
181 } else {
182 untickedTokenDenominator += tokens;
183 }
184 }
185 const remainingCostUsd =
186 topLevelCostUsd === null ? null : Math.max(0, topLevelCostUsd - usedTickedCostUsd);
187
188 const results: UsageRecord[] = [];
189 for (const entry of modelEntries) {
190 const totals = grokTotalsToUsage(entry.totals);
191 if (totalTokens(totals) === 0) continue;
192
193 let reportedCostUsd = grokCostTicksToUsd(entry.totals.costUsdTicks);
194 if (reportedCostUsd === null && remainingCostUsd !== null && untickedTokenDenominator > 0) {
195 reportedCostUsd = remainingCostUsd * (totalTokens(totals) / untickedTokenDenominator);
196 }
197
198 results.push({
199 provider: "grok",
200 timestampMs,
201 model: entry.model,
202 sessionId,
203 totals,
204 reportedCostUsd,
205 speed: "standard",
206 dedupeKey: promptId === null ? null : `${sessionId}:${promptId}:${entry.model}`,
207 });
208 }
209 return results;
210}
211
212export const grokUsageFormat: TranscriptUsageFormat<void> = {
213 selectFields: {
214 timestamp: true,
215 params: {
216 sessionId: true,
217 _meta: { agentTimestampMs: true },
218 update: { sessionUpdate: true, prompt_id: true, usage: true },
219 },
220 },
221 usageMarkers: ['"turn_completed"'],
222 parseLine: (line) => parseGrokLine(line),
223 parseProjected: (projected) => parseGrokRecord(projected),
224};
225
226export const grokUsageReader: ProviderUsageReader<GrokSettings, Path.Path> = {
227 kind: "transcripts",
228 provider: "grok",
229 format: grokUsageFormat,
230 directories: Effect.fn("grokUsageReader.directories")(function* ({ environment }) {
231 const path = yield* Path.Path;
232 const homeDirectory = yield* HostProcess.HomeDirectory;
233 const home = expandHomePath(
234 environment.GROK_HOME?.trim() || path.join(homeDirectory, ".grok"),
235 homeDirectory,
236 );
237 // Sessions also ship multi-megabyte `chat_history` and `events` logs that
238 // never carry usage; only `updates.jsonl` does.
239 return [{ dir: path.resolve(home, "sessions"), fileName: "updates.jsonl" }];
240 }),
241};