@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.
- package/README.md +35 -2
- package/dist/activitySnapshot.d.ts +8 -8
- package/dist/activitySnapshot.js +20 -9
- package/dist/cutList.js +9 -1
- package/dist/glance.js +3 -1
- package/dist/localAgentFormats/gemini.d.ts +55 -0
- package/dist/localAgentFormats/gemini.js +443 -0
- package/dist/localAgentFormats/registry.d.ts +2 -1
- package/dist/localAgentFormats/registry.js +125 -9
- package/dist/localAgentFormats/runtimeRegistry.js +25 -2
- package/dist/localAgentFormats/types.d.ts +13 -4
- package/dist/localAgentLogs.d.ts +25 -4
- package/dist/localAgentLogs.js +215 -17
- package/dist/modelPricing.d.ts +33 -1
- package/dist/modelPricing.js +117 -13
- package/dist/providerConnectors.d.ts +16 -0
- package/dist/providerConnectors.js +304 -33
- package/dist/providerContractStates.generated.d.ts +8 -0
- package/dist/providerContractStates.generated.js +9 -0
- package/dist/schema.d.ts +18 -0
- package/dist/schema.js +26 -0
- package/dist/sourceStatus.d.ts +23 -1
- package/dist/sourceStatus.js +104 -9
- package/dist/stateTrust.js +2 -2
- package/package.json +8 -1
|
@@ -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
|
-
|
|
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
|
};
|
package/dist/localAgentLogs.d.ts
CHANGED
|
@@ -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
|
|
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. */
|
package/dist/localAgentLogs.js
CHANGED
|
@@ -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
|
|
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
|
|
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) +
|
|
1361
|
-
|
|
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() && (
|
|
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:
|
|
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
|
|
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
|
}
|
package/dist/modelPricing.d.ts
CHANGED
|
@@ -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-
|
|
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
|