@agent-finops/core 0.5.9 → 0.6.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 +6 -0
- package/dist/discovery.d.ts +6 -2
- package/dist/discovery.js +36 -14
- package/dist/glance.d.ts +6 -2
- package/dist/glance.js +62 -13
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/localAgentLogs.d.ts +38 -2
- package/dist/localAgentLogs.js +184 -20
- package/dist/modelPricing.js +0 -1
- package/dist/providerConnectors.d.ts +3 -2
- package/dist/providerConnectors.js +665 -89
- package/dist/sampleData.js +4 -3
- package/dist/schema.d.ts +31 -29
- package/dist/schema.js +27 -3
- package/dist/sourceRegistry.d.ts +30 -5
- package/dist/sourceRegistry.js +250 -21
- package/dist/sourceStatus.d.ts +65 -0
- package/dist/sourceStatus.js +147 -0
- package/dist/stateTrust.d.ts +37 -0
- package/dist/stateTrust.js +277 -0
- package/package.json +1 -1
- package/samples/openai-usage.csv +2 -2
package/dist/localAgentLogs.js
CHANGED
|
@@ -53,7 +53,7 @@ function totalUsageTokens(usage) {
|
|
|
53
53
|
(usage.cacheWrite1hTokens ?? 0);
|
|
54
54
|
}
|
|
55
55
|
/** Parse one Claude Code transcript (JSONL). Exported for tests. */
|
|
56
|
-
export function parseClaudeCodeTranscript(content, filePath = "", sinceMs) {
|
|
56
|
+
export function parseClaudeCodeTranscript(content, filePath = "", sinceMs, onDiagnostic) {
|
|
57
57
|
const calls = [];
|
|
58
58
|
const seen = new Set();
|
|
59
59
|
const pendingPrompts = [];
|
|
@@ -63,6 +63,7 @@ export function parseClaudeCodeTranscript(content, filePath = "", sinceMs) {
|
|
|
63
63
|
let latestActivityKey;
|
|
64
64
|
let isSubagent = filePath.split(sep).includes("subagents");
|
|
65
65
|
let parentSessionId;
|
|
66
|
+
let malformedLines = 0;
|
|
66
67
|
for (const line of content.split("\n")) {
|
|
67
68
|
if (!line.trim())
|
|
68
69
|
continue;
|
|
@@ -71,6 +72,7 @@ export function parseClaudeCodeTranscript(content, filePath = "", sinceMs) {
|
|
|
71
72
|
entry = JSON.parse(line);
|
|
72
73
|
}
|
|
73
74
|
catch {
|
|
75
|
+
malformedLines += 1;
|
|
74
76
|
continue;
|
|
75
77
|
}
|
|
76
78
|
if (!isRecord(entry))
|
|
@@ -179,10 +181,13 @@ export function parseClaudeCodeTranscript(content, filePath = "", sinceMs) {
|
|
|
179
181
|
for (const call of calls) {
|
|
180
182
|
call.activity = activities.get(localActivityScopeKey(call.sessionId, call.workingDirectory, call.project));
|
|
181
183
|
}
|
|
184
|
+
if (malformedLines > 0) {
|
|
185
|
+
onDiagnostic?.({ code: "malformed_jsonl", count: malformedLines });
|
|
186
|
+
}
|
|
182
187
|
return calls;
|
|
183
188
|
}
|
|
184
189
|
/** Parse one Codex rollout file (JSONL event stream). Exported for tests. */
|
|
185
|
-
export function parseCodexRollout(content, onEntry) {
|
|
190
|
+
export function parseCodexRollout(content, onEntry, onDiagnostic) {
|
|
186
191
|
let model;
|
|
187
192
|
let rootCwd;
|
|
188
193
|
const toolWorkdirs = new Map();
|
|
@@ -201,6 +206,7 @@ export function parseCodexRollout(content, onEntry) {
|
|
|
201
206
|
let toolCallCount = 0;
|
|
202
207
|
let isSubagent = false;
|
|
203
208
|
let parentSessionId;
|
|
209
|
+
let malformedLines = 0;
|
|
204
210
|
for (const line of content.split("\n")) {
|
|
205
211
|
if (!line.trim())
|
|
206
212
|
continue;
|
|
@@ -209,6 +215,7 @@ export function parseCodexRollout(content, onEntry) {
|
|
|
209
215
|
entry = JSON.parse(line);
|
|
210
216
|
}
|
|
211
217
|
catch {
|
|
218
|
+
malformedLines += 1;
|
|
212
219
|
continue;
|
|
213
220
|
}
|
|
214
221
|
if (!isRecord(entry))
|
|
@@ -300,14 +307,33 @@ export function parseCodexRollout(content, onEntry) {
|
|
|
300
307
|
// safer than charging the parent cumulative counter again. Likewise, a
|
|
301
308
|
// recognized boundary with no later total_token_usage is not a financial
|
|
302
309
|
// call yet.
|
|
310
|
+
if (malformedLines > 0) {
|
|
311
|
+
onDiagnostic?.({ code: "malformed_jsonl", count: malformedLines });
|
|
312
|
+
}
|
|
303
313
|
if (!lastTotal || isSubagent && !rootTaskStarted)
|
|
304
314
|
return [];
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
const
|
|
315
|
+
const rawInput = numberOf(lastTotal.input_tokens);
|
|
316
|
+
const rawOutput = numberOf(lastTotal.output_tokens);
|
|
317
|
+
const rawCached = numberOf(lastTotal.cached_input_tokens);
|
|
318
|
+
const rawReportedTotal = numberOf(lastTotal.total_tokens);
|
|
319
|
+
const baselineInput = numberOf(inheritedUsageBaseline?.input_tokens);
|
|
320
|
+
const baselineOutput = numberOf(inheritedUsageBaseline?.output_tokens);
|
|
321
|
+
const baselineReportedTotal = numberOf(inheritedUsageBaseline?.total_tokens);
|
|
322
|
+
const currentComponentsComplete = rawInput !== undefined && rawOutput !== undefined && !((rawReportedTotal ?? 0) > 0 && rawInput === 0 && rawOutput === 0);
|
|
323
|
+
const baselineComponentsComplete = !inheritedUsageBaseline || (baselineInput !== undefined && baselineOutput !== undefined && !((baselineReportedTotal ?? 0) > 0 && baselineInput === 0 && baselineOutput === 0));
|
|
324
|
+
const usageSupport = currentComponentsComplete && baselineComponentsComplete
|
|
325
|
+
? "complete"
|
|
326
|
+
: "unsupported_token_shape";
|
|
327
|
+
if (usageSupport === "unsupported_token_shape") {
|
|
328
|
+
onDiagnostic?.({ code: "unsupported_token_shape", count: 1 });
|
|
329
|
+
}
|
|
330
|
+
const input = Math.max(0, (rawInput ?? 0) - (baselineInput ?? 0));
|
|
331
|
+
const cached = Math.max(0, (rawCached ?? 0) -
|
|
308
332
|
(numberOf(inheritedUsageBaseline?.cached_input_tokens) ?? 0));
|
|
309
|
-
const output = Math.max(0, (
|
|
310
|
-
|
|
333
|
+
const output = Math.max(0, (rawOutput ?? 0) - (baselineOutput ?? 0));
|
|
334
|
+
const reportedTotalTokens = rawReportedTotal === undefined
|
|
335
|
+
? undefined
|
|
336
|
+
: Math.max(0, rawReportedTotal - (baselineReportedTotal ?? 0));
|
|
311
337
|
const latestTurnUsage = lastTurn
|
|
312
338
|
? codexTurnUsage(lastTurn)
|
|
313
339
|
: undefined;
|
|
@@ -333,6 +359,8 @@ export function parseCodexRollout(content, onEntry) {
|
|
|
333
359
|
activity,
|
|
334
360
|
latestTurnUsage,
|
|
335
361
|
usageScope: "session_cumulative",
|
|
362
|
+
usageSupport,
|
|
363
|
+
...(reportedTotalTokens !== undefined ? { reportedTotalTokens } : {}),
|
|
336
364
|
usage: {
|
|
337
365
|
// Codex input_tokens INCLUDES cached tokens; split them out.
|
|
338
366
|
inputTokens: Math.max(0, input - cached),
|
|
@@ -417,24 +445,51 @@ export async function loadLocalAgentUsage(options = {}) {
|
|
|
417
445
|
let filesParsed = 0;
|
|
418
446
|
const since = options.sinceIso ? Date.parse(options.sinceIso) : undefined;
|
|
419
447
|
const sinceMs = typeof since === "number" && Number.isFinite(since) ? since : undefined;
|
|
420
|
-
|
|
421
|
-
|
|
448
|
+
const diagnostics = [];
|
|
449
|
+
const sourceScans = [
|
|
450
|
+
emptySourceScan("claude-code"),
|
|
451
|
+
emptySourceScan("codex")
|
|
452
|
+
];
|
|
453
|
+
const claudeScan = sourceScans[0];
|
|
454
|
+
const codexScan = sourceScans[1];
|
|
455
|
+
for (const file of await listJsonlFiles(claudeDir, claudeScan, diagnostics)) {
|
|
456
|
+
let content;
|
|
457
|
+
try {
|
|
458
|
+
content = await readFile(file, "utf8");
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
recordUnreadableFile("claude-code", claudeScan, diagnostics, error);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
422
464
|
if (!content)
|
|
423
465
|
continue;
|
|
424
466
|
filesParsed += 1;
|
|
425
|
-
|
|
467
|
+
claudeScan.filesParsed += 1;
|
|
468
|
+
calls.push(...parseClaudeCodeTranscript(content, file, sinceMs, (diagnostic) => {
|
|
469
|
+
recordParseDiagnostic("claude-code", claudeScan, diagnostics, diagnostic);
|
|
470
|
+
}));
|
|
426
471
|
}
|
|
427
|
-
for (const file of await listJsonlFiles(codexDir)) {
|
|
472
|
+
for (const file of await listJsonlFiles(codexDir, codexScan, diagnostics)) {
|
|
428
473
|
if (!basename(file).startsWith("rollout-"))
|
|
429
474
|
continue;
|
|
430
|
-
|
|
475
|
+
let content;
|
|
476
|
+
try {
|
|
477
|
+
content = await readFile(file, "utf8");
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
recordUnreadableFile("codex", codexScan, diagnostics, error);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
431
483
|
if (!content)
|
|
432
484
|
continue;
|
|
433
485
|
filesParsed += 1;
|
|
486
|
+
codexScan.filesParsed += 1;
|
|
434
487
|
const collector = codexInvocationFiles
|
|
435
488
|
? createCodexInvocationCollector(sinceMs)
|
|
436
489
|
: undefined;
|
|
437
|
-
calls.push(...parseCodexRollout(content, collector?.consume)
|
|
490
|
+
calls.push(...parseCodexRollout(content, collector?.consume, (diagnostic) => {
|
|
491
|
+
recordParseDiagnostic("codex", codexScan, diagnostics, diagnostic);
|
|
492
|
+
}));
|
|
438
493
|
if (collector)
|
|
439
494
|
codexInvocationFiles.push(collector.finish());
|
|
440
495
|
}
|
|
@@ -447,6 +502,8 @@ export async function loadLocalAgentUsage(options = {}) {
|
|
|
447
502
|
calls: filtered,
|
|
448
503
|
filesParsed,
|
|
449
504
|
agentsDetected: [...new Set(filtered.map((call) => call.agent))],
|
|
505
|
+
sourceScans,
|
|
506
|
+
diagnostics,
|
|
450
507
|
...(codexInvocationFiles ? { codexInvocationFiles } : {})
|
|
451
508
|
};
|
|
452
509
|
}
|
|
@@ -468,8 +525,9 @@ export function aggregateCalls(calls) {
|
|
|
468
525
|
cacheWrite5mTokens: sum(groupCalls, (c) => c.usage.cacheWrite5mTokens ?? 0),
|
|
469
526
|
cacheWrite1hTokens: sum(groupCalls, (c) => c.usage.cacheWrite1hTokens ?? 0)
|
|
470
527
|
};
|
|
471
|
-
const
|
|
472
|
-
const
|
|
528
|
+
const usageSupported = groupCalls.every((call) => call.usageSupport !== "unsupported_token_shape");
|
|
529
|
+
const amountUsd = usageSupported ? estimateTokenCostUsd(model, usage) : undefined;
|
|
530
|
+
const priced = usageSupported && typeof amountUsd === "number";
|
|
473
531
|
records.push({
|
|
474
532
|
id: slug(["local", agent, day, model, project].join("-")),
|
|
475
533
|
timestamp: new Date(`${day}T00:00:00Z`).toISOString(),
|
|
@@ -498,25 +556,131 @@ export function aggregateCalls(calls) {
|
|
|
498
556
|
}
|
|
499
557
|
return records.sort((left, right) => left.id.localeCompare(right.id));
|
|
500
558
|
}
|
|
501
|
-
async function listJsonlFiles(root) {
|
|
502
|
-
|
|
503
|
-
|
|
559
|
+
async function listJsonlFiles(root, scan, diagnostics) {
|
|
560
|
+
let rootStat;
|
|
561
|
+
try {
|
|
562
|
+
rootStat = await stat(root);
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
if (isNodeError(error, "ENOENT")) {
|
|
566
|
+
scan.directoryStatus = "missing";
|
|
567
|
+
diagnostics.push({
|
|
568
|
+
agent: scan.agent,
|
|
569
|
+
code: "directory_missing",
|
|
570
|
+
severity: "info",
|
|
571
|
+
message: `${agentLabel(scan.agent)} transcript directory was not found.`,
|
|
572
|
+
count: 1
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
scan.directoryStatus = "unreadable";
|
|
577
|
+
diagnostics.push({
|
|
578
|
+
agent: scan.agent,
|
|
579
|
+
code: "directory_unreadable",
|
|
580
|
+
severity: "error",
|
|
581
|
+
message: `${agentLabel(scan.agent)} transcript directory could not be read${errorCodeSuffix(error)}.`,
|
|
582
|
+
count: 1
|
|
583
|
+
});
|
|
584
|
+
}
|
|
504
585
|
return [];
|
|
586
|
+
}
|
|
587
|
+
if (!rootStat.isDirectory()) {
|
|
588
|
+
scan.directoryStatus = "unreadable";
|
|
589
|
+
diagnostics.push({
|
|
590
|
+
agent: scan.agent,
|
|
591
|
+
code: "directory_unreadable",
|
|
592
|
+
severity: "error",
|
|
593
|
+
message: `${agentLabel(scan.agent)} transcript path is not a readable directory.`,
|
|
594
|
+
count: 1
|
|
595
|
+
});
|
|
596
|
+
return [];
|
|
597
|
+
}
|
|
598
|
+
scan.directoryStatus = "readable";
|
|
505
599
|
const out = [];
|
|
506
600
|
const queue = [root];
|
|
507
601
|
while (queue.length > 0) {
|
|
508
602
|
const dir = queue.pop();
|
|
509
|
-
|
|
603
|
+
let entries;
|
|
604
|
+
try {
|
|
605
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
scan.directoryStatus = "unreadable";
|
|
609
|
+
diagnostics.push({
|
|
610
|
+
agent: scan.agent,
|
|
611
|
+
code: "directory_unreadable",
|
|
612
|
+
severity: "error",
|
|
613
|
+
message: `${agentLabel(scan.agent)} transcript directory could not be read${errorCodeSuffix(error)}.`,
|
|
614
|
+
count: 1
|
|
615
|
+
});
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
510
618
|
for (const entry of entries) {
|
|
511
619
|
const path = join(dir, entry.name);
|
|
512
620
|
if (entry.isDirectory())
|
|
513
621
|
queue.push(path);
|
|
514
|
-
else if (entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
622
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
515
623
|
out.push(path);
|
|
624
|
+
scan.filesDiscovered += 1;
|
|
625
|
+
}
|
|
516
626
|
}
|
|
517
627
|
}
|
|
518
628
|
return out;
|
|
519
629
|
}
|
|
630
|
+
function emptySourceScan(agent) {
|
|
631
|
+
return {
|
|
632
|
+
agent,
|
|
633
|
+
directoryStatus: "readable",
|
|
634
|
+
filesDiscovered: 0,
|
|
635
|
+
filesParsed: 0,
|
|
636
|
+
malformedLines: 0,
|
|
637
|
+
unreadableFiles: 0,
|
|
638
|
+
unsupportedUsageSnapshots: 0
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function recordUnreadableFile(agent, scan, diagnostics, error) {
|
|
642
|
+
scan.unreadableFiles += 1;
|
|
643
|
+
diagnostics.push({
|
|
644
|
+
agent,
|
|
645
|
+
code: "file_unreadable",
|
|
646
|
+
severity: "error",
|
|
647
|
+
message: `${agentLabel(agent)} transcript file could not be read${errorCodeSuffix(error)}.`,
|
|
648
|
+
count: 1
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
function recordParseDiagnostic(agent, scan, diagnostics, diagnostic) {
|
|
652
|
+
if (diagnostic.code === "malformed_jsonl") {
|
|
653
|
+
scan.malformedLines += diagnostic.count;
|
|
654
|
+
diagnostics.push({
|
|
655
|
+
agent,
|
|
656
|
+
code: diagnostic.code,
|
|
657
|
+
severity: "warning",
|
|
658
|
+
message: `${diagnostic.count} malformed JSONL line(s) were skipped in ${agentLabel(agent)} transcripts.`,
|
|
659
|
+
count: diagnostic.count
|
|
660
|
+
});
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
scan.unsupportedUsageSnapshots += diagnostic.count;
|
|
664
|
+
diagnostics.push({
|
|
665
|
+
agent,
|
|
666
|
+
code: diagnostic.code,
|
|
667
|
+
severity: "warning",
|
|
668
|
+
message: `${diagnostic.count} ${agentLabel(agent)} token snapshot(s) lacked the input/output components required for pricing.`,
|
|
669
|
+
count: diagnostic.count
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
function agentLabel(agent) {
|
|
673
|
+
return agent === "claude-code" ? "Claude Code" : "Codex";
|
|
674
|
+
}
|
|
675
|
+
function errorCodeSuffix(error) {
|
|
676
|
+
const code = error instanceof Error
|
|
677
|
+
? error.code
|
|
678
|
+
: undefined;
|
|
679
|
+
return code && /^[A-Z0-9_]+$/.test(code) ? ` (${code})` : "";
|
|
680
|
+
}
|
|
681
|
+
function isNodeError(error, code) {
|
|
682
|
+
return error instanceof Error && error.code === code;
|
|
683
|
+
}
|
|
520
684
|
function projectFromCwd(cwd) {
|
|
521
685
|
if (!cwd)
|
|
522
686
|
return undefined;
|
package/dist/modelPricing.js
CHANGED
|
@@ -52,7 +52,6 @@ const pricingRules = [
|
|
|
52
52
|
// Open-weight models with NO canonical price (llama, qwen, mistral, glm):
|
|
53
53
|
// hosting rates vary several-fold by provider, so we deliberately return
|
|
54
54
|
// undefined -> costConfidence "missing" instead of inventing a number.
|
|
55
|
-
{ match: /codex/i, inputPerM: 1.25, outputPerM: 10, cacheReadPerM: 0.125 }
|
|
56
55
|
];
|
|
57
56
|
export function findPricingRule(model) {
|
|
58
57
|
return pricingRules.find((rule) => rule.match.test(model));
|
|
@@ -8,6 +8,7 @@ type ProviderResponse = {
|
|
|
8
8
|
get: (name: string) => string | null;
|
|
9
9
|
};
|
|
10
10
|
json: () => Promise<unknown>;
|
|
11
|
+
text?: () => Promise<string>;
|
|
11
12
|
};
|
|
12
13
|
export type ProviderQaPagination = {
|
|
13
14
|
label: string;
|
|
@@ -73,9 +74,9 @@ export type CreateProviderConnectionInput = {
|
|
|
73
74
|
sourceId?: string;
|
|
74
75
|
authReference: string;
|
|
75
76
|
verifiedRecordCount: number;
|
|
76
|
-
totalUsd: number;
|
|
77
|
+
totalUsd: number | null;
|
|
77
78
|
fetchedAt?: Date;
|
|
78
|
-
/** Record-derived completeness;
|
|
79
|
+
/** Record-derived completeness; this controls financial evidence, not connector validation. */
|
|
79
80
|
completeness?: ProviderConnectorResult["completeness"];
|
|
80
81
|
};
|
|
81
82
|
export type TokenResolver = (reference: string) => string;
|