@agent-finops/core 0.7.3 → 0.8.0

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.
@@ -1,10 +1,12 @@
1
- import { parseClaudeCodeTranscript, parseCodexRollout, readClaudeCodeFinancialFileForRegistry, readCodexFinancialFileForRegistry } from "../localAgentLogs.js";
1
+ import { parseClaudeCodeTranscript, parseCodexRollout, readClaudeCodeFinancialFileForRegistry, readCodexFinancialFileForRegistry, readGeminiFinancialFileForRegistry } from "../localAgentLogs.js";
2
2
  import { createCodexInvocationCollector } from "../toolInvocations.js";
3
3
  import { localAgentFormatDescriptors, validateLocalAgentFormatDescriptors } from "./registry.js";
4
+ import { parseGeminiSession } from "./gemini.js";
4
5
  const byId = new Map(localAgentFormatDescriptors.map((descriptor) => [descriptor.id, descriptor]));
5
6
  const claudeCode = byId.get("claude-code");
6
7
  const codex = byId.get("codex");
7
- if (!claudeCode || !codex) {
8
+ const geminiCli = byId.get("gemini-cli");
9
+ if (!claudeCode || !codex || !geminiCli) {
8
10
  throw new Error("Built-in local-agent format descriptors are incomplete.");
9
11
  }
10
12
  validateLocalAgentFormatDescriptors();
@@ -28,6 +30,27 @@ const runtimes = [
28
30
  };
29
31
  },
30
32
  parseFinancialFile: readCodexFinancialFileForRegistry
33
+ },
34
+ {
35
+ descriptor: geminiCli,
36
+ parseFull: ({ content, filePath, sinceMs, onDiagnostic }) => {
37
+ const parsed = parseGeminiSession(content, {
38
+ filePath,
39
+ ...(sinceMs !== undefined ? { sinceMs } : {})
40
+ });
41
+ for (const diagnostic of parsed.diagnostics) {
42
+ onDiagnostic({
43
+ code: diagnostic.code === "malformed_jsonl"
44
+ ? "malformed_jsonl"
45
+ : diagnostic.code === "unsupported_token_shape"
46
+ ? "unsupported_token_shape"
47
+ : "malformed_session_file",
48
+ count: diagnostic.count
49
+ });
50
+ }
51
+ return { calls: parsed.calls };
52
+ },
53
+ parseFinancialFile: readGeminiFinancialFileForRegistry
31
54
  }
32
55
  ];
33
56
  export const localAgentFormatRuntimeRegistry = Object.freeze(runtimes.map((runtime) => Object.freeze(runtime)));
@@ -5,7 +5,7 @@ import type { ParsedInvocationFile } from "../toolInvocations.js";
5
5
  * registry-owned change so existing exhaustive consumers do not see an
6
6
  * unbounded `string` in a patch release.
7
7
  */
8
- export type LocalAgentFormatId = "claude-code" | "codex";
8
+ export type LocalAgentFormatId = "claude-code" | "codex" | "gemini-cli";
9
9
  export type LocalAgentFormatDescriptor = {
10
10
  readonly schemaVersion: 1;
11
11
  readonly id: LocalAgentFormatId;
@@ -13,11 +13,16 @@ export type LocalAgentFormatDescriptor = {
13
13
  readonly label: string;
14
14
  readonly provider: string;
15
15
  readonly defaultHomeRelative: readonly string[];
16
- readonly legacyDirectoryOption?: "claudeProjectsDir" | "codexSessionsDir";
16
+ readonly legacyDirectoryOption?: "claudeProjectsDir" | "codexSessionsDir" | "geminiSessionsDir";
17
17
  readonly discovery: {
18
18
  readonly extension?: string;
19
+ readonly extensions?: readonly string[];
19
20
  readonly basename?: string;
20
21
  readonly basenamePrefix?: string;
22
+ /** Required directory name somewhere above a financial evidence file. */
23
+ readonly ancestorBasename?: string;
24
+ /** Presence signal only. This file must never reach a financial parser. */
25
+ readonly detectionBasename?: string;
21
26
  };
22
27
  readonly confidenceDefaults: {
23
28
  readonly validationCoverage: "live_verified" | "fixture_verified" | "untested" | "failed";
@@ -34,6 +39,8 @@ export type LocalAgentFormatDescriptor = {
34
39
  readonly operation: string;
35
40
  };
36
41
  readonly capabilities: {
42
+ /** May local rows from this source feed recommendation or Apply logic? */
43
+ readonly actionPlanning: boolean;
37
44
  readonly activity: boolean;
38
45
  readonly contextHealth: boolean;
39
46
  readonly financialFastPath: boolean;
@@ -41,8 +48,10 @@ export type LocalAgentFormatDescriptor = {
41
48
  readonly invocationEvidence: boolean;
42
49
  readonly planContext: boolean;
43
50
  readonly rateLimits: boolean;
51
+ /** Whether this source is allowed into the fixed Glance/statusline cache. */
52
+ readonly statuslineSnapshot: boolean;
44
53
  };
45
- readonly financialRead: "full_jsonl" | "bounded_event_jsonl";
54
+ readonly financialRead: "full_jsonl" | "bounded_event_jsonl" | "full_session_files";
46
55
  readonly validationNote: string;
47
56
  readonly docs: {
48
57
  readonly format: string;
@@ -62,7 +71,7 @@ export type LocalAgentFormatParseContext = {
62
71
  sinceMs?: number;
63
72
  collectInvocationEvidence: boolean;
64
73
  onDiagnostic: (diagnostic: {
65
- code: "malformed_jsonl" | "unsupported_token_shape";
74
+ code: "malformed_jsonl" | "malformed_session_file" | "unsupported_token_shape";
66
75
  count: number;
67
76
  }) => void;
68
77
  };
@@ -20,6 +20,8 @@ import type { LocalAgentFormatDescriptor, LocalAgentFormatFinancialFileContext,
20
20
  */
21
21
  export type LocalAgentCall = {
22
22
  agent: LocalAgentFormatId;
23
+ /** Stable source call/message identity when the format safely exposes one. */
24
+ callId?: string;
23
25
  model: string;
24
26
  /** ISO timestamp of this call, or the latest cumulative usage event. */
25
27
  timestamp: string;
@@ -50,6 +52,18 @@ export type LocalAgentCall = {
50
52
  usageSupport?: "complete" | "unsupported_token_shape";
51
53
  /** Provider-reported total retained when component fields are unavailable. */
52
54
  reportedTotalTokens?: number;
55
+ /** Optional parser/source version when the evolving session format reports it. */
56
+ sourceVersion?: string;
57
+ /** Raw Gemini token split retained for evidence/debugging, never prompt content. */
58
+ geminiTokenEvidence?: {
59
+ input?: number;
60
+ output?: number;
61
+ cached?: number;
62
+ thoughts?: number;
63
+ tool?: number;
64
+ total?: number;
65
+ cacheAccounting: "included" | "none" | "unknown";
66
+ };
53
67
  usage: TokenUsage;
54
68
  sessionId?: string;
55
69
  /** Provider-reported plan windows embedded in the transcript, when present. */
@@ -105,6 +119,8 @@ export type LocalAgentLogOptions = {
105
119
  claudeProjectsDir?: string;
106
120
  /** Default: ~/.codex/sessions */
107
121
  codexSessionsDir?: string;
122
+ /** Default: ~/.gemini/tmp (financial evidence is bounded to chats files). */
123
+ geminiSessionsDir?: string;
108
124
  /** Registry-native source-root overrides, keyed by format id. */
109
125
  sourceDirectories?: Readonly<Partial<Record<LocalAgentFormatId, string>>>;
110
126
  /** Only include calls at/after this ISO timestamp. */
@@ -118,7 +134,7 @@ export type LocalAgentLogOptions = {
118
134
  * financial snapshot and transcript-reported plan limits.
119
135
  */
120
136
  export type LocalAgentFinancialLogOptions = Omit<LocalAgentLogOptions, "collectCodexInvocationEvidence">;
121
- export type LocalAgentLogDiagnosticCode = "directory_missing" | "directory_unreadable" | "file_unreadable" | "malformed_jsonl" | "unsupported_token_shape";
137
+ export type LocalAgentLogDiagnosticCode = "directory_missing" | "directory_unreadable" | "file_unreadable" | "malformed_jsonl" | "malformed_session_file" | "unsupported_token_shape";
122
138
  export type LocalAgentLogDiagnostic = {
123
139
  agent: LocalAgentCall["agent"];
124
140
  code: LocalAgentLogDiagnosticCode;
@@ -136,6 +152,8 @@ export type LocalAgentSourceScan = {
136
152
  malformedLines: number;
137
153
  unreadableFiles: number;
138
154
  unsupportedUsageSnapshots: number;
155
+ /** Presence-only files such as Gemini CLI logs.json; never financial rows. */
156
+ detectionSignals?: number;
139
157
  /** Regular files safely excluded because their metadata predates `sinceIso`. */
140
158
  filesSkippedBeforeWindow?: number;
141
159
  /** Codex files resolved from bounded head/tail financial evidence. */
@@ -164,7 +182,7 @@ export type LocalAgentLogResult = {
164
182
  codexInvocationFiles?: ParsedInvocationFile[];
165
183
  };
166
184
  type TranscriptParseDiagnostic = {
167
- code: "malformed_jsonl" | "unsupported_token_shape";
185
+ code: "malformed_jsonl" | "malformed_session_file" | "unsupported_token_shape";
168
186
  count: number;
169
187
  };
170
188
  type TranscriptParseDiagnosticHandler = (diagnostic: TranscriptParseDiagnostic) => void;
@@ -172,9 +190,10 @@ type TranscriptParseDiagnosticHandler = (diagnostic: TranscriptParseDiagnostic)
172
190
  * Codex rollout/compaction files can repeat the same session's cumulative
173
191
  * token counter. Keep only the latest snapshot per session so financial value,
174
192
  * Glance, and project totals never add cumulative checkpoints together.
175
- * Turn-scoped Claude calls and calls without a stable session id are retained.
193
+ * Turn-scoped calls with a stable session+call identity are also deduplicated
194
+ * across copied/checkpointed files. Calls without that proof are retained.
176
195
  */
177
- export declare function dedupeCumulativeSessionCalls(calls: LocalAgentCall[]): LocalAgentCall[];
196
+ export declare function dedupeCumulativeSessionCalls(calls: LocalAgentCall[], onStableTurnConflict?: (agent: LocalAgentFormatId) => void): LocalAgentCall[];
178
197
  /** Parse one Claude Code transcript (JSONL). Exported for tests. */
179
198
  export declare function parseClaudeCodeTranscript(content: string, filePath?: string, sinceMs?: number, onDiagnostic?: TranscriptParseDiagnosticHandler): LocalAgentCall[];
180
199
  /** Parse one Codex rollout file (JSONL event stream). Exported for tests. */
@@ -204,6 +223,8 @@ export declare function loadLocalAgentFinancialUsageWithFormats(registry: readon
204
223
  export declare function readClaudeCodeFinancialFileForRegistry(context: LocalAgentFormatFinancialFileContext): Promise<LocalAgentCall[]>;
205
224
  /** @internal Runtime hook owned by the Codex registry entry. */
206
225
  export declare function readCodexFinancialFileForRegistry(context: LocalAgentFormatFinancialFileContext): Promise<LocalAgentCall[]>;
226
+ /** @internal Runtime hook owned by the Gemini CLI registry entry. */
227
+ export declare function readGeminiFinancialFileForRegistry(context: LocalAgentFormatFinancialFileContext): Promise<LocalAgentCall[]>;
207
228
  /** Aggregate per-call usage into one UsageRecord per day+agent+model+project. */
208
229
  export declare function aggregateCalls(calls: LocalAgentCall[]): UsageRecord[];
209
230
  /** @internal Registry-aware aggregation used by the extensible ingestion engine. */
@@ -3,9 +3,10 @@ import { lstat, open, readdir, readFile, stat } from "node:fs/promises";
3
3
  import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { createInterface } from "node:readline";
6
- import { estimateTokenCostUsd } from "./modelPricing.js";
6
+ import { canPriceTokenUsageAtScope, estimateTokenCostUsd, estimateTokenCostsUsd, promptTierThreshold, usesPromptTieredPricing } from "./modelPricing.js";
7
7
  import { redactSecrets } from "./discovery.js";
8
- import { localAgentFormatDescriptors, localAgentFormatLabel, matchesLocalAgentFormatFile, validateLocalAgentFormatDescriptors } from "./localAgentFormats/registry.js";
8
+ import { localAgentFormatDescriptors, localAgentFormatLabel, matchesLocalAgentDetectionFile, matchesLocalAgentFormatFile, validateLocalAgentFormatDescriptors } from "./localAgentFormats/registry.js";
9
+ import { parseGeminiSession } from "./localAgentFormats/gemini.js";
9
10
  /**
10
11
  * Resolve the repository root most recently observed in transcript metadata.
11
12
  *
@@ -23,12 +24,42 @@ export function latestObservedWorkingDirectory(calls) {
23
24
  * Codex rollout/compaction files can repeat the same session's cumulative
24
25
  * token counter. Keep only the latest snapshot per session so financial value,
25
26
  * Glance, and project totals never add cumulative checkpoints together.
26
- * Turn-scoped Claude calls and calls without a stable session id are retained.
27
+ * Turn-scoped calls with a stable session+call identity are also deduplicated
28
+ * across copied/checkpointed files. Calls without that proof are retained.
27
29
  */
28
- export function dedupeCumulativeSessionCalls(calls) {
30
+ export function dedupeCumulativeSessionCalls(calls, onStableTurnConflict) {
29
31
  const retained = [];
30
32
  const cumulative = new Map();
33
+ const stableTurns = new Map();
31
34
  for (const call of calls) {
35
+ if (call.usageScope === "turn" && call.sessionId && call.callId) {
36
+ const key = `${call.agent}:${call.sessionId}:${call.callId}`;
37
+ const prior = stableTurns.get(key);
38
+ if (!prior) {
39
+ stableTurns.set(key, call);
40
+ }
41
+ else if (isStableTurnConflict(prior)) {
42
+ continue;
43
+ }
44
+ else if (isCompleteStableTurn(prior) && isCompleteStableTurn(call) &&
45
+ stableTurnEvidenceFingerprint(call) !== stableTurnEvidenceFingerprint(prior)) {
46
+ stableTurns.set(key, conflictingStableTurn(prior, call));
47
+ onStableTurnConflict?.(call.agent);
48
+ }
49
+ else if (!isCompleteStableTurn(prior) && isCompleteStableTurn(call)) {
50
+ // JSONL/checkpoint copies can preserve an early tokenless snapshot
51
+ // beside its later complete update. Complete evidence supersedes only
52
+ // unsupported evidence for the same stable identity.
53
+ stableTurns.set(key, call);
54
+ }
55
+ else if (!isCompleteStableTurn(call) && isCompleteStableTurn(prior)) {
56
+ continue;
57
+ }
58
+ else if (isLaterCumulativeSnapshot(call, prior)) {
59
+ stableTurns.set(key, call);
60
+ }
61
+ continue;
62
+ }
32
63
  if (call.usageScope !== "session_cumulative" || !call.sessionId) {
33
64
  retained.push(call);
34
65
  continue;
@@ -39,7 +70,47 @@ export function dedupeCumulativeSessionCalls(calls) {
39
70
  cumulative.set(key, call);
40
71
  }
41
72
  }
42
- return [...retained, ...cumulative.values()];
73
+ return [...retained, ...stableTurns.values(), ...cumulative.values()];
74
+ }
75
+ function isCompleteStableTurn(call) {
76
+ return call.usageSupport !== "unsupported_token_shape";
77
+ }
78
+ function stableTurnEvidenceFingerprint(call) {
79
+ return JSON.stringify({
80
+ timestamp: call.timestamp,
81
+ model: call.model,
82
+ project: call.project ?? null,
83
+ workingDirectory: call.workingDirectory ?? null,
84
+ usageSupport: call.usageSupport ?? null,
85
+ reportedTotalTokens: call.reportedTotalTokens ?? null,
86
+ usage: {
87
+ inputTokens: call.usage.inputTokens,
88
+ outputTokens: call.usage.outputTokens,
89
+ cacheReadTokens: call.usage.cacheReadTokens ?? null,
90
+ cacheWrite5mTokens: call.usage.cacheWrite5mTokens ?? null,
91
+ cacheWrite1hTokens: call.usage.cacheWrite1hTokens ?? null,
92
+ thoughtTokens: call.usage.thoughtTokens ?? null,
93
+ toolTokens: call.usage.toolTokens ?? null
94
+ },
95
+ geminiTokenEvidence: call.geminiTokenEvidence ?? null
96
+ });
97
+ }
98
+ function isStableTurnConflict(call) {
99
+ return call.model === "conflicting-local-evidence" &&
100
+ call.usageSupport === "unsupported_token_shape";
101
+ }
102
+ function conflictingStableTurn(left, right) {
103
+ const call = left.timestamp.localeCompare(right.timestamp) <= 0 ? left : right;
104
+ return {
105
+ agent: call.agent,
106
+ ...(call.callId ? { callId: call.callId } : {}),
107
+ ...(call.sessionId ? { sessionId: call.sessionId } : {}),
108
+ model: "conflicting-local-evidence",
109
+ timestamp: call.timestamp,
110
+ usageScope: "turn",
111
+ usageSupport: "unsupported_token_shape",
112
+ usage: { inputTokens: 0, outputTokens: 0 }
113
+ };
43
114
  }
44
115
  function isLaterCumulativeSnapshot(candidate, prior) {
45
116
  const timestampOrder = candidate.timestamp.localeCompare(prior.timestamp);
@@ -52,7 +123,9 @@ function totalUsageTokens(usage) {
52
123
  usage.outputTokens +
53
124
  (usage.cacheReadTokens ?? 0) +
54
125
  (usage.cacheWrite5mTokens ?? 0) +
55
- (usage.cacheWrite1hTokens ?? 0);
126
+ (usage.cacheWrite1hTokens ?? 0) +
127
+ (usage.thoughtTokens ?? 0) +
128
+ (usage.toolTokens ?? 0);
56
129
  }
57
130
  function parseClaudeFinancialUsage(value, onDiagnostic) {
58
131
  const inputTokens = tokenComponentOf(value.input_tokens);
@@ -609,7 +682,15 @@ export async function loadLocalAgentUsageWithFormats(registry, options = {}) {
609
682
  }
610
683
  }
611
684
  }
612
- const normalizedCalls = dedupeCumulativeSessionCalls(calls);
685
+ const normalizedCalls = dedupeCumulativeSessionCalls(calls, (agent) => {
686
+ const scan = sourceScans.find((entry) => entry.agent === agent);
687
+ if (scan) {
688
+ recordParseDiagnostic(agent, scan, diagnostics, {
689
+ code: "unsupported_token_shape",
690
+ count: 1
691
+ });
692
+ }
693
+ });
613
694
  const filtered = typeof sinceMs === "number"
614
695
  ? normalizedCalls.filter((call) => Date.parse(call.timestamp) >= sinceMs)
615
696
  : normalizedCalls;
@@ -666,7 +747,15 @@ export async function loadLocalAgentFinancialUsageWithFormats(registry, options
666
747
  // Sources scan concurrently, but flatten in registry order to preserve the
667
748
  // long-standing Claude-then-Codex output and diagnostic contract.
668
749
  const calls = scanned.flatMap((entry) => entry.calls);
669
- const normalizedCalls = dedupeCumulativeSessionCalls(calls);
750
+ const normalizedCalls = dedupeCumulativeSessionCalls(calls, (agent) => {
751
+ const source = scanned.find((entry) => entry.scan.agent === agent);
752
+ if (source) {
753
+ recordParseDiagnostic(agent, source.scan, source.diagnostics, {
754
+ code: "unsupported_token_shape",
755
+ count: 1
756
+ });
757
+ }
758
+ });
670
759
  const filtered = typeof sinceMs === "number"
671
760
  ? normalizedCalls.filter((call) => Date.parse(call.timestamp) >= sinceMs)
672
761
  : normalizedCalls;
@@ -756,6 +845,38 @@ export async function readCodexFinancialFileForRegistry(context) {
756
845
  });
757
846
  return call ? [call] : [];
758
847
  }
848
+ /** @internal Runtime hook owned by the Gemini CLI registry entry. */
849
+ export async function readGeminiFinancialFileForRegistry(context) {
850
+ const { filePath, sinceMs, scan, diagnostics } = context;
851
+ if (!await shouldStreamFile(filePath, sinceMs, "gemini-cli", scan, diagnostics)) {
852
+ return [];
853
+ }
854
+ let content;
855
+ try {
856
+ content = await readFile(filePath, "utf8");
857
+ }
858
+ catch (error) {
859
+ recordUnreadableFile("gemini-cli", scan, diagnostics, error);
860
+ return [];
861
+ }
862
+ if (!content)
863
+ return [];
864
+ scan.filesParsed += 1;
865
+ const parsed = parseGeminiSession(content, { filePath, ...(sinceMs !== undefined ? { sinceMs } : {}) });
866
+ for (const diagnostic of parsed.diagnostics) {
867
+ recordParseDiagnostic("gemini-cli", scan, diagnostics, normalizeGeminiDiagnostic(diagnostic));
868
+ }
869
+ return parsed.calls;
870
+ }
871
+ function normalizeGeminiDiagnostic(diagnostic) {
872
+ if (diagnostic.code === "malformed_jsonl") {
873
+ return { code: "malformed_jsonl", count: diagnostic.count };
874
+ }
875
+ if (diagnostic.code === "unsupported_token_shape") {
876
+ return { code: "unsupported_token_shape", count: diagnostic.count };
877
+ }
878
+ return { code: "malformed_session_file", count: diagnostic.count };
879
+ }
759
880
  async function localAgentFormatRuntimes() {
760
881
  const module = await import("./localAgentFormats/runtimeRegistry.js");
761
882
  return module.localAgentFormatRuntimeRegistry;
@@ -770,6 +891,9 @@ function localAgentFormatRoot(descriptor, options, home) {
770
891
  if (descriptor.legacyDirectoryOption === "codexSessionsDir" && options.codexSessionsDir) {
771
892
  return options.codexSessionsDir;
772
893
  }
894
+ if (descriptor.legacyDirectoryOption === "geminiSessionsDir" && options.geminiSessionsDir) {
895
+ return options.geminiSessionsDir;
896
+ }
773
897
  const canonicalHome = resolve(home);
774
898
  const root = resolve(canonicalHome, ...descriptor.defaultHomeRelative);
775
899
  const fromHome = relative(canonicalHome, root);
@@ -1341,10 +1465,19 @@ export function aggregateCallsForFormats(calls, descriptors) {
1341
1465
  outputTokens: sum(groupCalls, (c) => c.usage.outputTokens),
1342
1466
  cacheReadTokens: sum(groupCalls, (c) => c.usage.cacheReadTokens ?? 0),
1343
1467
  cacheWrite5mTokens: sum(groupCalls, (c) => c.usage.cacheWrite5mTokens ?? 0),
1344
- cacheWrite1hTokens: sum(groupCalls, (c) => c.usage.cacheWrite1hTokens ?? 0)
1468
+ cacheWrite1hTokens: sum(groupCalls, (c) => c.usage.cacheWrite1hTokens ?? 0),
1469
+ thoughtTokens: sum(groupCalls, (c) => c.usage.thoughtTokens ?? 0),
1470
+ toolTokens: sum(groupCalls, (c) => c.usage.toolTokens ?? 0)
1345
1471
  };
1346
1472
  const usageSupported = groupCalls.every((call) => call.usageSupport !== "unsupported_token_shape");
1347
- const amountUsd = usageSupported && format ? estimateTokenCostUsd(model, usage) : undefined;
1473
+ const sourceVersions = [...new Set(groupCalls.flatMap((call) => call.sourceVersion ? [call.sourceVersion] : []))].sort().slice(0, 8);
1474
+ const tieredPricingEvidenceSupported = !usesPromptTieredPricing(model) ||
1475
+ groupCalls.every((call) => canPriceTokenUsageAtScope(model, call.usage, call.usageScope === "turn" ? "request" : "aggregate") && (agent !== "gemini-cli" || hasCompleteGeminiPromptEvidence(call)));
1476
+ const amountUsd = usageSupported && tieredPricingEvidenceSupported && format
1477
+ ? usesPromptTieredPricing(model)
1478
+ ? estimateTokenCostsUsd(model, groupCalls.map((call) => call.usage))
1479
+ : estimateTokenCostUsd(model, usage)
1480
+ : undefined;
1348
1481
  const priced = usageSupported && typeof amountUsd === "number";
1349
1482
  records.push({
1350
1483
  id: slug(["local", agent, day, model, project].join("-")),
@@ -1357,8 +1490,27 @@ export function aggregateCallsForFormats(calls, descriptors) {
1357
1490
  observedFrom: format?.sourceRecord.observedFrom ?? "unregistered local transcript (this machine)"
1358
1491
  },
1359
1492
  model,
1360
- inputTokens: usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWrite5mTokens ?? 0) + (usage.cacheWrite1hTokens ?? 0),
1361
- outputTokens: usage.outputTokens,
1493
+ inputTokens: usage.inputTokens + (usage.cacheReadTokens ?? 0) +
1494
+ (usage.cacheWrite5mTokens ?? 0) + (usage.cacheWrite1hTokens ?? 0) +
1495
+ (usage.toolTokens ?? 0),
1496
+ outputTokens: usage.outputTokens + (usage.thoughtTokens ?? 0),
1497
+ ...(agent === "gemini-cli"
1498
+ ? {
1499
+ ...(groupCalls.every((call) => call.usage.cacheReadTokens !== undefined)
1500
+ ? { cacheReadTokens: usage.cacheReadTokens ?? 0 }
1501
+ : {}),
1502
+ ...(groupCalls.every((call) => call.usage.thoughtTokens !== undefined)
1503
+ ? { thoughtTokens: usage.thoughtTokens ?? 0 }
1504
+ : {}),
1505
+ ...(groupCalls.every((call) => call.usage.toolTokens !== undefined)
1506
+ ? { toolTokens: usage.toolTokens ?? 0 }
1507
+ : {}),
1508
+ ...(groupCalls.every((call) => call.reportedTotalTokens !== undefined)
1509
+ ? { reportedTotalTokens: sum(groupCalls, (call) => call.reportedTotalTokens ?? 0) }
1510
+ : {}),
1511
+ ...(sourceVersions.length > 0 ? { sourceVersions } : {})
1512
+ }
1513
+ : {}),
1362
1514
  amountUsd: priced ? amountUsd : null,
1363
1515
  costConfidence: priced
1364
1516
  ? format?.confidenceDefaults.pricedFinancialEvidence ?? "estimated"
@@ -1376,6 +1528,47 @@ export function aggregateCallsForFormats(calls, descriptors) {
1376
1528
  }
1377
1529
  return records.sort((left, right) => left.id.localeCompare(right.id));
1378
1530
  }
1531
+ function hasCompleteGeminiPromptEvidence(call) {
1532
+ const evidence = call.geminiTokenEvidence;
1533
+ if (call.usageSupport !== "complete" || !evidence ||
1534
+ evidence.cacheAccounting === "unknown") {
1535
+ return false;
1536
+ }
1537
+ const components = [
1538
+ evidence.input,
1539
+ evidence.output,
1540
+ evidence.cached,
1541
+ evidence.thoughts,
1542
+ evidence.tool,
1543
+ evidence.total
1544
+ ];
1545
+ if (!components.every((value) => Number.isSafeInteger(value) && (value ?? -1) >= 0)) {
1546
+ return false;
1547
+ }
1548
+ const input = evidence.input;
1549
+ const output = evidence.output;
1550
+ const cached = evidence.cached;
1551
+ const thoughts = evidence.thoughts;
1552
+ const tool = evidence.tool;
1553
+ const freshInput = evidence.cacheAccounting === "included"
1554
+ ? input - cached
1555
+ : input;
1556
+ const expectedTotal = input + output + thoughts + tool;
1557
+ const threshold = promptTierThreshold(call.model);
1558
+ // Gemini exposes promptTokenCount and toolUsePromptTokenCount separately,
1559
+ // while the published >200k rule does not resolve which side owns tool
1560
+ // prompt tokens. Only price when both interpretations select the same tier.
1561
+ const promptTierIsUnambiguous = threshold === undefined ||
1562
+ input > threshold || input + tool <= threshold;
1563
+ return freshInput >= 0 && Number.isSafeInteger(expectedTotal) &&
1564
+ promptTierIsUnambiguous &&
1565
+ evidence.total === expectedTotal &&
1566
+ call.usage.inputTokens === freshInput &&
1567
+ call.usage.outputTokens === output &&
1568
+ call.usage.cacheReadTokens === cached &&
1569
+ call.usage.thoughtTokens === thoughts &&
1570
+ call.usage.toolTokens === tool;
1571
+ }
1379
1572
  async function listFormatCandidateFiles(root, descriptor, scan, diagnostics) {
1380
1573
  let rootStat;
1381
1574
  try {
@@ -1439,13 +1632,16 @@ async function listFormatCandidateFiles(root, descriptor, scan, diagnostics) {
1439
1632
  const path = join(dir, entry.name);
1440
1633
  if (entry.isDirectory())
1441
1634
  queue.push(path);
1442
- else if (entry.isFile() && (!descriptor.discovery.extension || entry.name.endsWith(descriptor.discovery.extension))) {
1635
+ else if (entry.isFile() && matchesLocalAgentDetectionFile(descriptor, path)) {
1636
+ scan.detectionSignals = (scan.detectionSignals ?? 0) + 1;
1637
+ }
1638
+ else if (entry.isFile() && matchesLocalAgentFormatFile(descriptor, path)) {
1443
1639
  out.push(path);
1444
1640
  scan.filesDiscovered += 1;
1445
1641
  }
1446
1642
  }
1447
1643
  }
1448
- return out;
1644
+ return out.sort((left, right) => left.localeCompare(right));
1449
1645
  }
1450
1646
  function emptySourceScan(agent) {
1451
1647
  return {
@@ -1469,13 +1665,15 @@ function recordUnreadableFile(agent, scan, diagnostics, error) {
1469
1665
  });
1470
1666
  }
1471
1667
  function recordParseDiagnostic(agent, scan, diagnostics, diagnostic) {
1472
- if (diagnostic.code === "malformed_jsonl") {
1668
+ if (diagnostic.code === "malformed_jsonl" || diagnostic.code === "malformed_session_file") {
1473
1669
  scan.malformedLines += diagnostic.count;
1474
1670
  diagnostics.push({
1475
1671
  agent,
1476
1672
  code: diagnostic.code,
1477
1673
  severity: "warning",
1478
- message: `${diagnostic.count} malformed JSONL line(s) were skipped in ${agentLabel(agent)} transcripts.`,
1674
+ message: diagnostic.code === "malformed_jsonl"
1675
+ ? `${diagnostic.count} malformed JSONL line(s) were skipped in ${agentLabel(agent)} transcripts.`
1676
+ : `${diagnostic.count} malformed session file(s) were skipped in ${agentLabel(agent)} transcripts.`,
1479
1677
  count: diagnostic.count
1480
1678
  });
1481
1679
  return;
@@ -1485,7 +1683,7 @@ function recordParseDiagnostic(agent, scan, diagnostics, diagnostic) {
1485
1683
  agent,
1486
1684
  code: diagnostic.code,
1487
1685
  severity: "warning",
1488
- message: `${diagnostic.count} ${agentLabel(agent)} token snapshot(s) lacked the input/output components required for pricing.`,
1686
+ message: `${diagnostic.count} ${agentLabel(agent)} token snapshot(s) lacked the complete, internally consistent fields required for safe normalization and pricing.`,
1489
1687
  count: diagnostic.count
1490
1688
  });
1491
1689
  }
@@ -7,7 +7,7 @@
7
7
  * matched top-down; first match wins. Unknown models return undefined so
8
8
  * callers can label the record "missing" instead of inventing a number.
9
9
  */
10
- export declare const PRICING_TABLE_AS_OF = "2026-07-28";
10
+ export declare const PRICING_TABLE_AS_OF = "2026-08-13";
11
11
  export type TokenUsage = {
12
12
  /** Billable, uncached input tokens. */
13
13
  inputTokens: number;
@@ -15,6 +15,10 @@ export type TokenUsage = {
15
15
  cacheReadTokens?: number;
16
16
  cacheWrite5mTokens?: number;
17
17
  cacheWrite1hTokens?: number;
18
+ /** Explicit reasoning/thought tokens, priced on the output side when supported. */
19
+ thoughtTokens?: number;
20
+ /** Explicit tool prompt tokens, priced on the input side when supported. */
21
+ toolTokens?: number;
18
22
  };
19
23
  type PricingRule = {
20
24
  match: RegExp;
@@ -25,6 +29,15 @@ type PricingRule = {
25
29
  cacheReadPerM?: number;
26
30
  cacheWrite5mPerM?: number;
27
31
  cacheWrite1hPerM?: number;
32
+ /** Some providers select one rate for the whole request from prompt size. */
33
+ abovePromptTokens?: {
34
+ threshold: number;
35
+ inputPerM: number;
36
+ outputPerM: number;
37
+ cacheReadPerM?: number;
38
+ cacheWrite5mPerM?: number;
39
+ cacheWrite1hPerM?: number;
40
+ };
28
41
  };
29
42
  export declare function findPricingRule(model: string): PricingRule | undefined;
30
43
  /**
@@ -32,5 +45,24 @@ export declare function findPricingRule(model: string): PricingRule | undefined;
32
45
  * published price we recognize.
33
46
  */
34
47
  export declare function estimateTokenCostUsd(model: string, usage: TokenUsage): number | undefined;
48
+ /**
49
+ * Price request-scoped usage before aggregating it. This is required for
50
+ * models whose entire request moves to a higher rate above a prompt-size
51
+ * threshold; pricing a daily token sum would incorrectly treat many small
52
+ * requests as one large request.
53
+ */
54
+ export declare function estimateTokenCostsUsd(model: string, usages: readonly TokenUsage[]): number | undefined;
55
+ /** Whether this model's rate selection depends on each request's prompt size. */
56
+ export declare function usesPromptTieredPricing(model: string): boolean;
57
+ /** Prompt-size threshold for tiered request pricing, when one is published. */
58
+ export declare function promptTierThreshold(model: string): number | undefined;
59
+ /**
60
+ * Tiered prices are selected per request, never from a multi-request sum.
61
+ * An aggregate is still unambiguous when its entire non-negative prompt-side
62
+ * total is at or below the threshold; then no constituent request can have
63
+ * crossed it. Larger aggregates fail closed until request-level evidence is
64
+ * available.
65
+ */
66
+ export declare function canPriceTokenUsageAtScope(model: string, usage: TokenUsage, scope: "request" | "aggregate"): boolean;
35
67
  export {};
36
68
  //# sourceMappingURL=modelPricing.d.ts.map