authorgravatar for admin@snappeh.comTristan Knight <admin@snappeh.com> 2026-10-12 01:39:55+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-12 00:39:55+00:00
log7f02b849584332368a5489631d3286409eed90a8
tree6c30b755624619a6bad42664c6c56de910f56eba
parent499a53c307ec6a0fbff5c83efde355729bd7d343
signature Signed by PGP key B5690EEEBB952194

perf(usage): the first usage scan skips decoding transcript lines that carry no usage (#17255)


6 files changed, 93 insertions(+), 19 deletions(-)

apps/server/src/provider/Drivers/claudeUsage.ts+1-1
...@@ -98,7 +98,7 @@ export const claudeUsageFormat: TranscriptUsageFormat<void> = {...@@ -98,7 +98,7 @@ export const claudeUsageFormat: TranscriptUsageFormat<void> = {
98 costUSD: true,98 costUSD: true,
99 message: { id: true, model: true, usage: true },99 message: { id: true, model: true, usage: true },
100 },100 },
101 mightCarryUsage: (line) => line.includes('"usage"'),101 usageMarkers: ['"usage"'],
102 parseLine: (line) => orEmpty(parseClaudeLine(line)),102 parseLine: (line) => orEmpty(parseClaudeLine(line)),
103 parseProjected: (projected) => orEmpty(parseClaudeRecord(projected)),103 parseProjected: (projected) => orEmpty(parseClaudeRecord(projected)),
104};104};
apps/server/src/provider/Drivers/codexUsage.ts+1-5
...@@ -231,11 +231,7 @@ export const codexUsageFormat: TranscriptUsageFormat<CodexScanState> = {...@@ -231,11 +231,7 @@ export const codexUsageFormat: TranscriptUsageFormat<CodexScanState> = {
231 },231 },
232 // `turn_context`, `thread_settings_applied` and `session_meta` lines hold no232 // `turn_context`, `thread_settings_applied` and `session_meta` lines hold no
233 // usage, but they carry the model, tier and session the reducer needs.233 // usage, but they carry the model, tier and session the reducer needs.
234 mightCarryUsage: (line) =>234 usageMarkers: ['"token_count"', '"turn_context"', '"thread_settings_applied"', '"session_meta"'],
235 line.includes('"token_count"') ||
236 line.includes('"turn_context"') ||
237 line.includes('"thread_settings_applied"') ||
238 line.includes('"session_meta"'),
239 parseLine: (line, state) => orEmpty(parseCodexLine(line, state)),235 parseLine: (line, state) => orEmpty(parseCodexLine(line, state)),
240 parseProjected: (projected, state) => orEmpty(parseCodexRecord(projected, state)),236 parseProjected: (projected, state) => orEmpty(parseCodexRecord(projected, state)),
241 state: { initial: initialCodexScanState, schema: CodexScanState },237 state: { initial: initialCodexScanState, schema: CodexScanState },
apps/server/src/usage/usageTranscriptReader.test.ts+55-1
...@@ -8,7 +8,7 @@ import * as NodePath from "node:path";...@@ -8,7 +8,7 @@ import * as NodePath from "node:path";
8import { afterEach, assert, beforeEach, describe, it } from "@effect/vitest";8import { afterEach, assert, beforeEach, describe, it } from "@effect/vitest";
99
10import { TEST_FORMATS } from "./usageTestFormats.ts";10import { TEST_FORMATS } from "./usageTestFormats.ts";
11import { readTranscriptRecords } from "./usageTranscriptReader.ts";11import { readTranscriptRecords, transcriptLineGate } from "./usageTranscriptReader.ts";
1212
13let dir: string;13let dir: string;
1414
...@@ -218,9 +218,63 @@ describe("readTranscriptRecords resume", () => {...@@ -218,9 +218,63 @@ describe("readTranscriptRecords resume", () => {
218 );218 );
219 });219 });
220220
221 it("keeps a CRLF Codex reducer line whose marker straddles a stream chunk", async () => {
222 const path = NodePath.join(dir, "rollout.jsonl");
223 const crlf = (line: string) => line.replace("\n", "\r\n");
224 const modelLine = crlf(codexModelLine("gpt-5.2-codex"));
225 // Multi-byte tool output that fails the gate, sized so the 256 KiB chunk
226 // boundary falls inside the model line's `"turn_context"` marker.
227 const head = crlf(codexMetaLine());
228 const fillerBytes =
229 256 * 1024 - Buffer.byteLength(head) - modelLine.indexOf("turn_context") - 4;
230 const fillerEnvelope = crlf(`${JSON.stringify({ type: "response_item", output: "" })}\n`);
231 const padding = "é".repeat((fillerBytes - Buffer.byteLength(fillerEnvelope)) / 2);
232 const filler = crlf(`${JSON.stringify({ type: "response_item", output: padding })}\n`);
233 const content = head + filler + modelLine + crlf(codexUsageLine(40, 2));
234 await NodeFSP.writeFile(path, content);
235
236 const result = await readTranscriptRecords(path, TEST_FORMATS.codex);
237 assert.isNotNull(result);
238 assert.strictEqual(result.records.length, 1);
239 assert.strictEqual(result.records[0]?.model, "gpt-5.2-codex");
240 assert.strictEqual(result.records[0]?.sessionId, "codex-session-1");
241 assert.strictEqual(result.position.resumeOffset, Buffer.byteLength(content));
242 });
243
221 it("returns null for an unreadable file", async () => {244 it("returns null for an unreadable file", async () => {
222 assert.isNull(245 assert.isNull(
223 await readTranscriptRecords(NodePath.join(dir, "missing.jsonl"), TEST_FORMATS.claude),246 await readTranscriptRecords(NodePath.join(dir, "missing.jsonl"), TEST_FORMATS.claude),
224 );247 );
225 });248 });
226});249});
250
251describe("transcriptLineGate", () => {
252 const gates = {
253 one: transcriptLineGate(['"usage"']),
254 several: transcriptLineGate(['"token_count"', '"session_meta"', '"a.b(c)"']),
255 };
256 const markers = { one: ['"usage"'], several: ['"token_count"', '"session_meta"', '"a.b(c)"'] };
257
258 it("passes only lines containing one of its markers", () => {
259 for (const kind of ["one", "several"] as const) {
260 for (const marker of markers[kind]) {
261 assert.isTrue(gates[kind](Buffer.from(`{"payload":{${marker}:1}}`)));
262 // The same text without its quotes, or with one character changed.
263 assert.isFalse(gates[kind](Buffer.from(`{"text":"see ${marker.slice(1, -1)} here"}`)));
264 assert.isFalse(gates[kind](Buffer.from(`{${marker.replace(/[._]|usage/, "x")}:1}`)));
265 }
266 assert.isFalse(gates[kind](Buffer.alloc(0)));
267 }
268 });
269
270 it("matches markers beside multi-byte and malformed UTF-8", () => {
271 const invalid = Buffer.from([0xff, 0xc3]);
272 for (const kind of ["one", "several"] as const) {
273 for (const marker of markers[kind]) {
274 assert.isTrue(gates[kind](Buffer.from(`{"text":"日本語 é",${marker}:1}`)));
275 assert.isTrue(gates[kind](Buffer.concat([invalid, Buffer.from(marker), invalid])));
276 assert.isFalse(gates[kind](Buffer.from(`{"text":"日本語 é ${marker.slice(1, -1)}"}`)));
277 }
278 }
279 });
280});
apps/server/src/usage/usageTranscriptReader.ts+30-7
...@@ -206,6 +206,26 @@ async function guardMatches(...@@ -206,6 +206,26 @@ async function guardMatches(
206 }206 }
207}207}
208208
209/**
210 * Builds the byte-level test for a format's `usageMarkers`: whether a line
211 * contains any of them.
212 *
213 * One marker is a single `Buffer` search. Several would be one pass over the
214 * line each, so they are matched together as one alternation over the line's
215 * latin1 view instead: latin1 maps every byte to one character without
216 * validating it, so an ASCII marker matches exactly where its bytes do.
217 */
218export function transcriptLineGate(markers: readonly [string, ...string[]]) {
219 if (markers.length === 1) {
220 const marker = Buffer.from(markers[0]);
221 return (line: Buffer) => line.includes(marker);
222 }
223 const pattern = new RegExp(
224 markers.map((marker) => marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"),
225 );
226 return (line: Buffer) => pattern.test(line.toString("latin1"));
227}
228
209/**229/**
210 * Streams one transcript and returns the usage records it contains, or `null`230 * Streams one transcript and returns the usage records it contains, or `null`
211 * when the file could not be read.231 * when the file could not be read.
...@@ -259,11 +279,6 @@ export async function readTranscriptRecords<State>(...@@ -259,11 +279,6 @@ export async function readTranscriptRecords<State>(
259 resumed = true;279 resumed = true;
260 }280 }
261281
262 const parseLine = (line: string, lineState: State, out: UsageRecord[]): void => {
263 if (!format.mightCarryUsage(line)) return;
264 for (const record of format.parseLine(line, lineState)) out.push(record);
265 };
266
267 const toLineString = (lineBuffer: Buffer): string => {282 const toLineString = (lineBuffer: Buffer): string => {
268 const content =283 const content =
269 lineBuffer.length > 0 && lineBuffer[lineBuffer.length - 1] === CARRIAGE_RETURN284 lineBuffer.length > 0 && lineBuffer[lineBuffer.length - 1] === CARRIAGE_RETURN
...@@ -272,6 +287,14 @@ export async function readTranscriptRecords<State>(...@@ -272,6 +287,14 @@ export async function readTranscriptRecords<State>(
272 return content.toString("utf8");287 return content.toString("utf8");
273 };288 };
274289
290 // Gate on the raw bytes so the lines that fail it, which is most of them,
291 // are never decoded.
292 const mightCarryUsage = transcriptLineGate(format.usageMarkers);
293 const parseLine = (lineBuffer: Buffer, lineState: State, out: UsageRecord[]): void => {
294 if (!mightCarryUsage(lineBuffer)) return;
295 for (const record of format.parseLine(toLineString(lineBuffer), lineState)) out.push(record);
296 };
297
275 const records: UsageRecord[] = [];298 const records: UsageRecord[] = [];
276 // Byte offsets remain independent of UTF-8 decoding. Only complete lines299 // Byte offsets remain independent of UTF-8 decoding. Only complete lines
277 // commit the resume point; an unfinished tail is replayed on the next scan.300 // commit the resume point; an unfinished tail is replayed on the next scan.
...@@ -310,7 +333,7 @@ export async function readTranscriptRecords<State>(...@@ -310,7 +333,7 @@ export async function readTranscriptRecords<State>(
310 pendingChunks.length === 1333 pendingChunks.length === 1
311 ? pendingChunks[0]!334 ? pendingChunks[0]!
312 : Buffer.concat(pendingChunks, pendingBytes);335 : Buffer.concat(pendingChunks, pendingBytes);
313 parseLine(toLineString(line), lineState, out);336 parseLine(line, lineState, out);
314 }337 }
315 pendingChunks = [];338 pendingChunks = [];
316 pendingBytes = 0;339 pendingBytes = 0;
...@@ -333,7 +356,7 @@ export async function readTranscriptRecords<State>(...@@ -333,7 +356,7 @@ export async function readTranscriptRecords<State>(
333 // Most lines fit in the current chunk. Avoid buffering/streaming356 // Most lines fit in the current chunk. Avoid buffering/streaming
334 // machinery on this hot path.357 // machinery on this hot path.
335 if (!streaming && pendingBytes === 0) {358 if (!streaming && pendingBytes === 0) {
336 parseLine(toLineString(chunk.subarray(lineStart, newlineIndex)), state, records);359 parseLine(chunk.subarray(lineStart, newlineIndex), state, records);
337 } else {360 } else {
338 append(chunk.subarray(lineStart, newlineIndex));361 append(chunk.subarray(lineStart, newlineIndex));
339 finish(state, records);362 finish(state, records);
packages/provider-core/src/server/usage.ts+5-4
...@@ -90,11 +90,12 @@ export type SelectedFields = { readonly [key: string]: true | SelectedFields };...@@ -90,11 +90,12 @@ export type SelectedFields = { readonly [key: string]: true | SelectedFields };
90export interface TranscriptUsageFormat<State> {90export interface TranscriptUsageFormat<State> {
91 readonly selectFields: SelectedFields;91 readonly selectFields: SelectedFields;
92 /**92 /**
93 * Cheap substring gate applied before `JSON.parse`. Transcripts are mostly93 * ASCII substrings, at least one of which every line that matters contains.
94 * tool output; skipping lines that cannot matter is worth an order of94 * Transcripts are mostly tool output, so the engine matches these against a
95 * magnitude. Lines that only update `State` must pass too.95 * line's raw bytes and neither decodes nor parses the rest, which is worth an
96 * order of magnitude. Lines that only update `State` must match too.
96 */97 */
97 mightCarryUsage(line: string): boolean;98 readonly usageMarkers: readonly [string, ...string[]];
98 parseLine(line: string, state: State): readonly UsageRecord[];99 parseLine(line: string, state: State): readonly UsageRecord[];
99 parseProjected(projected: unknown, state: State): readonly UsageRecord[];100 parseProjected(projected: unknown, state: State): readonly UsageRecord[];
100 /**101 /**
packages/provider-grok/src/server/usage.ts+1-1
...@@ -218,7 +218,7 @@ export const grokUsageFormat: TranscriptUsageFormat<void> = {...@@ -218,7 +218,7 @@ export const grokUsageFormat: TranscriptUsageFormat<void> = {
218 update: { sessionUpdate: true, prompt_id: true, usage: true },218 update: { sessionUpdate: true, prompt_id: true, usage: true },
219 },219 },
220 },220 },
221 mightCarryUsage: (line) => line.includes('"turn_completed"'),221 usageMarkers: ['"turn_completed"'],
222 parseLine: (line) => parseGrokLine(line),222 parseLine: (line) => parseGrokLine(line),
223 parseProjected: (projected) => parseGrokRecord(projected),223 parseProjected: (projected) => parseGrokRecord(projected),
224};224};