Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/server/src/usage/UsageService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2687,7 +2687,7 @@ describe("UsageService", () => {
60 + 20 * (version === 4 ? 1 : 2),
);
yield* upgraded.awaitPersisted;
assert.strictEqual(SCAN_CACHE_FILE_NAME, "usage-scan-cache-v11.json");
assert.strictEqual(SCAN_CACHE_FILE_NAME, "usage-scan-cache-v12.json");
assert.deepStrictEqual(
(yield* Effect.promise(() => NodeFSP.readdir(stateDir)))
.filter((name) => name.startsWith("usage-scan-cache"))
Expand Down
26 changes: 20 additions & 6 deletions apps/server/src/usage/usageScanCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,15 +248,29 @@ describe("scan cache round trip", () => {
},
);

it("re-parses a Claude transcript cached before advisor usage was counted", () => {
it("re-parses Claude and Grok transcripts cached before their usage keys changed", () => {
const original = cacheWith([["/claude.jsonl", 100, [record()]]]);
const v9 = { ...parse(makeScanCacheWriter()(original, {})), version: 9 };

const restored = decodeScanCache(JSON.parse(JSON.stringify(v9)), TEST_FORMAT_MAP);
expect(restored.get("/claude.jsonl")).toEqual({
...original.get("/claude.jsonl"),
original.set("/grok/updates.jsonl", {
...original.get("/claude.jsonl")!,
provider: "grok",
records: [record({ provider: "grok", model: "grok-4.5", dedupeKey: "s:p:grok-4.5" })],
});
const encoded = parse(makeScanCacheWriter()(original, {}));
const reparse = {
size: -1,
position: { resumeOffset: 0, guardLength: 0, guardHash: 0, state: null },
};

const v9 = decodeScanCache({ ...encoded, version: 9 }, TEST_FORMAT_MAP);
expect(v9.get("/claude.jsonl")).toEqual({ ...original.get("/claude.jsonl"), ...reparse });
const v11 = decodeScanCache({ ...encoded, version: 11 }, TEST_FORMAT_MAP);
expect(v11.get("/claude.jsonl")).toEqual(original.get("/claude.jsonl"));
expect(v11.get("/grok/updates.jsonl")).toEqual({
...original.get("/grok/updates.jsonl"),
...reparse,
records: [
record({ provider: "grok", model: "grok-4.5", dedupeKey: "s:p:1786000000000:grok-4.5" }),
],
});
});

Expand Down
15 changes: 12 additions & 3 deletions apps/server/src/usage/usageScanCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ import * as Schema from "effect/Schema";
import { CLAUDE_SYNTHETIC_MODEL } from "../provider/Drivers/claudeUsage.ts";
import { GUARD_LENGTH, type TranscriptParsePosition } from "./usageTranscriptReader.ts";

const USAGE_SCAN_CACHE_VERSION = 11 as const;
const USAGE_SCAN_CACHE_VERSION = 12 as const;
const SPEED_COMPATIBLE_SINCE_VERSION = 4;

/**
* An older server sharing that directory cannot read a newer cache and would
* replace it, dropping saved usage for deleted transcripts. Separate files keep
* the newer one; older files are removed once it is saved.
*/
export const SCAN_CACHE_FILE_NAME = "usage-scan-cache-v11.json";
export const SCAN_CACHE_FILE_NAME = "usage-scan-cache-v12.json";
Comment thread
saphid marked this conversation as resolved.
Resolved
export const LEGACY_SCAN_CACHE_FILE_NAMES = [
"usage-scan-cache-v11.json",
"usage-scan-cache-v10.json",
"usage-scan-cache-v9.json",
"usage-scan-cache-v8.json",
Expand Down Expand Up @@ -253,7 +254,13 @@ export function decodeScanCache(
...(version >= 7 && unresolvedServiceTierTurnId !== undefined
? { unresolvedServiceTierTurnId }
: {}),
dedupeKey: typeof dedupeKey === "string" ? dedupeKey : null,
dedupeKey:
typeof dedupeKey !== "string"
? null
: // Saved copies of moved transcripts must still match the re-parsed v12 key.
provider === "grok" && version < 12 && dedupeKey.endsWith(`:${model}`)
? `${dedupeKey.slice(0, -model.length)}${timestampMs}:${model}`
: dedupeKey,
});
}
return records;
Expand Down Expand Up @@ -290,8 +297,10 @@ export function decodeScanCache(
// Keep history for deleted rollouts, but re-parse incompatible reducer state.
// v9 and v11 repair skipped Codex requests; clean v8+ entries stay served while unchanged.
// v10 adds Claude advisor calls, which no older Claude entry holds.
// v12 keys Grok turns by time, restoring turns that older entries dropped as repeats.
const legacy =
(entry.p === "claude" && version < 10) ||
(entry.p === "grok" && version < 12) ||
(format.state !== undefined &&
(version < 6 ||
(entry.p === "codex" && (version < 8 || (version < 11 && malformedRecords > 0)))));
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/usage/usageTranscriptStreaming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ describe("large usage records", () => {
outputTokens: 99,
reasoningTokens: 10,
},
dedupeKey: "s1:p1:grok-4.5-build",
dedupeKey: "s1:p1:1785578400123:grok-4.5-build",
reportedCostUsd: 0.25,
});
});
Expand Down
19 changes: 17 additions & 2 deletions packages/provider-grok/src/server/usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ describe("parseGrokLine", () => {
reasoningTokens: 180,
});
expect(record?.reportedCostUsd).toBeCloseTo(230_272_000 / GROK_COST_USD_TICKS_PER_DOLLAR, 12);
expect(record?.dedupeKey).toBe("019fec1a-12f7-72f2-9b1f-7778a00aea3c:prompt-1:grok-4.5-build");
expect(record?.dedupeKey).toBe(
"019fec1a-12f7-72f2-9b1f-7778a00aea3c:prompt-1:1786372566485:grok-4.5-build",
);
});

it("emits one record per model when modelUsage has several entries", () => {
Expand Down Expand Up @@ -142,7 +144,9 @@ describe("parseGrokLine", () => {
reasoningTokens: 180,
});
expect(record?.reportedCostUsd).toBeCloseTo(230_272_000 / GROK_COST_USD_TICKS_PER_DOLLAR, 12);
expect(record?.dedupeKey).toBe("019fec1a-12f7-72f2-9b1f-7778a00aea3c:prompt-1:grok");
expect(record?.dedupeKey).toBe(
"019fec1a-12f7-72f2-9b1f-7778a00aea3c:prompt-1:1786372566485:grok",
);
});

it("pro-rates top-level cost ticks across multi-model turns without per-model ticks", () => {
Expand Down Expand Up @@ -230,6 +234,17 @@ describe("parseGrokLine", () => {
expect(parseGrokLine(line)[0]?.dedupeKey).toBeNull();
});

it("keeps later turns that reuse a prompt id after the session resumes in a new process", () => {
const first = parseGrokLine(turnCompleted({ promptId: "t3-xai-prompt-1" }))[0];
const resumed = parseGrokLine(
turnCompleted({ promptId: "t3-xai-prompt-1", agentTimestampMs: 1_786_459_000_000 }),
)[0];
const copied = parseGrokLine(turnCompleted({ promptId: "t3-xai-prompt-1" }))[0];

expect(resumed?.dedupeKey).not.toBe(first?.dedupeKey);
expect(copied?.dedupeKey).toBe(first?.dedupeKey);
});

it("ignores non-turn lines and empty usage", () => {
expect(parseGrokLine(JSON.stringify({ method: "session/update", params: {} }))).toEqual([]);
expect(parseGrokLine("not json")).toEqual([]);
Expand Down
6 changes: 4 additions & 2 deletions packages/provider-grok/src/server/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ function parseGrokRecord(parsed: unknown): readonly UsageRecord[] {
const usageRecord = usage as Record<string, unknown>;

const sessionId = typeof paramsRecord["sessionId"] === "string" ? paramsRecord["sessionId"] : "";
// T3's prompt ids restart with each Grok process, so a resumed session reuses them.
const promptId = typeof updateRecord["prompt_id"] === "string" ? updateRecord["prompt_id"] : null;

const meta = paramsRecord["_meta"];
Expand Down Expand Up @@ -136,7 +137,7 @@ function parseGrokRecord(parsed: unknown): readonly UsageRecord[] {
reportedCostUsd: grokCostTicksToUsd(topLevel.costUsdTicks),
speed: "standard",
// No prompt id means we cannot tell two same-second updates apart.
dedupeKey: promptId === null ? null : `${sessionId}:${promptId}:grok`,
dedupeKey: promptId === null ? null : `${sessionId}:${promptId}:${timestampMs}:grok`,
},
];
}
Expand Down Expand Up @@ -174,7 +175,8 @@ function parseGrokRecord(parsed: unknown): readonly UsageRecord[] {
totals,
reportedCostUsd,
speed: "standard",
dedupeKey: promptId === null ? null : `${sessionId}:${promptId}:${entry.model}`,
dedupeKey:
promptId === null ? null : `${sessionId}:${promptId}:${timestampMs}:${entry.model}`,
});
}
return results;
Expand Down
Loading