@agent-finops/core 0.9.7 → 0.9.9

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.
@@ -416,7 +416,10 @@ export declare const SAFE_QUALITATIVE_SCAN_POLICY: Readonly<LocalAgentQualitativ
416
416
  * intentionally unavailable: this path reads only the evidence needed for a
417
417
  * financial snapshot and transcript-reported plan limits.
418
418
  */
419
- export type LocalAgentFinancialLogOptions = Omit<LocalAgentLogOptions, "collectCodexInvocationEvidence" | "qualitativeScan" | "qualitativeIndex" | "ownershipIndex" | "coverageProjectRef">;
419
+ export type LocalAgentFinancialLogOptions = Omit<LocalAgentLogOptions, "collectCodexInvocationEvidence" | "qualitativeScan" | "qualitativeIndex" | "ownershipIndex" | "coverageProjectRef"> & {
420
+ /** Explicit Workspace lane: Codex counter deltas at each UTC event, never lifetime totals on the last day. */
421
+ workspaceDailyFacts?: boolean;
422
+ };
420
423
  export type LocalAgentLogDiagnosticCode = "directory_missing" | "directory_unreadable" | "file_unreadable" | "malformed_jsonl" | "malformed_session_file" | "unsupported_token_shape" | "qualitative_scan_incomplete" | "qualitative_index_error";
421
424
  export type LocalAgentLogDiagnostic = {
422
425
  agent: LocalAgentCall["agent"];
@@ -582,7 +585,7 @@ export declare function loadLocalAgentFinancialUsage(options?: LocalAgentFinanci
582
585
  /** Registry-driven financial-only engine; package-root exports stay unchanged. */
583
586
  export declare function loadLocalAgentFinancialUsageWithFormats(registry: readonly LocalAgentFormatRuntime[], options?: LocalAgentFinancialLogOptions): Promise<LocalAgentLogResult>;
584
587
  /** @internal Runtime hook owned by the Claude Code registry entry. */
585
- export declare function readClaudeCodeFinancialFileForRegistry(context: LocalAgentFormatFinancialFileContext): Promise<LocalAgentCall[]>;
588
+ export declare function readClaudeCodeFinancialFileForRegistry(context: LocalAgentFormatFinancialFileContext, workspaceDailyFacts?: boolean): Promise<LocalAgentCall[]>;
586
589
  /** @internal Runtime hook owned by the Codex registry entry. */
587
590
  export declare function readCodexFinancialFileForRegistry(context: LocalAgentFormatFinancialFileContext): Promise<LocalAgentCall[]>;
588
591
  /** @internal Runtime hook owned by the Gemini CLI registry entry. */
@@ -228,6 +228,20 @@ function parseClaudeFinancialUsage(value, onDiagnostic) {
228
228
  const write1hField = cacheCreation
229
229
  ? optionalTokenComponent(cacheCreation, "ephemeral_1h_input_tokens")
230
230
  : { present: false };
231
+ const writeTotal = writeTotalField.value;
232
+ let write5m = write5mField.value;
233
+ let write1h = write1hField.value;
234
+ const writeSplitConsistent = writeTotal === undefined || ((write5m === undefined || write5m <= writeTotal) &&
235
+ (write1h === undefined || write1h <= writeTotal) &&
236
+ (write5m === undefined || write1h === undefined || write5m + write1h === writeTotal));
237
+ // A total plus one disjoint duration determines the other duration. Never
238
+ // add the entire total to the supplied duration or invent a negative split.
239
+ if (writeTotal !== undefined && writeSplitConsistent) {
240
+ if (write5m === undefined && write1h !== undefined)
241
+ write5m = writeTotal - write1h;
242
+ else if (write1h === undefined && write5m !== undefined)
243
+ write1h = writeTotal - write5m;
244
+ }
231
245
  const componentsSupported = inputTokens !== undefined &&
232
246
  outputTokens !== undefined &&
233
247
  (!cacheReadField.present || cacheReadField.value !== undefined) &&
@@ -235,16 +249,18 @@ function parseClaudeFinancialUsage(value, onDiagnostic) {
235
249
  (!reportedTotalField.present || reportedTotalField.value !== undefined) &&
236
250
  (!cacheCreationPresent || Boolean(cacheCreation)) &&
237
251
  (!write5mField.present || write5mField.value !== undefined) &&
238
- (!write1hField.present || write1hField.value !== undefined);
252
+ (!write1hField.present || write1hField.value !== undefined) &&
253
+ writeSplitConsistent;
239
254
  const usage = {
240
255
  // Retain every valid component for partial evidence, but never let a
241
256
  // missing/invalid required field become a priceable zero-dollar call.
242
257
  inputTokens: inputTokens ?? 0,
243
258
  outputTokens: outputTokens ?? 0,
244
259
  cacheReadTokens: cacheReadField.value ?? 0,
245
- // Prefer the 5m/1h breakdown; fall back to the total as 5m (cheaper bound).
246
- cacheWrite5mTokens: write5mField.value ?? writeTotalField.value ?? 0,
247
- cacheWrite1hTokens: write1hField.value ?? 0
260
+ // With no duration split at all, keep the existing cheaper-bound estimate.
261
+ // Partial evidence without a total remains partial, never a complete sum.
262
+ cacheWrite5mTokens: write5m ?? (write1h === undefined ? writeTotal : undefined) ?? 0,
263
+ cacheWrite1hTokens: write1h ?? 0
248
264
  };
249
265
  if (componentsSupported) {
250
266
  const cacheWriteEvidence = writeTotalField.value !== undefined ||
@@ -1327,6 +1343,11 @@ export async function loadLocalAgentFinancialUsage(options = {}) {
1327
1343
  /** Registry-driven financial-only engine; package-root exports stay unchanged. */
1328
1344
  export async function loadLocalAgentFinancialUsageWithFormats(registry, options = {}) {
1329
1345
  validateLocalAgentFormatDescriptors(registry.map((entry) => entry.descriptor));
1346
+ // Cumulative financial caches cannot supply event-day deltas. Keep their semantics unchanged.
1347
+ if (options.workspaceDailyFacts) {
1348
+ options = { ...options, financialIndex: undefined };
1349
+ registry = registry.filter(runtime => runtime.descriptor.id === "claude-code" || runtime.descriptor.id === "codex");
1350
+ }
1330
1351
  const home = homedir();
1331
1352
  const since = options.sinceIso ? Date.parse(options.sinceIso) : undefined;
1332
1353
  const sinceMs = typeof since === "number" && Number.isFinite(since) ? since : undefined;
@@ -1401,12 +1422,12 @@ export async function loadLocalAgentFinancialUsageWithFormats(registry, options
1401
1422
  const diagnosticsBefore = diagnostics.length;
1402
1423
  const unreadableBefore = scan.unreadableFiles;
1403
1424
  const filesParsedBefore = scan.filesParsed;
1404
- const parsedCalls = await runtime.parseFinancialFile({
1405
- filePath: file,
1406
- sinceMs,
1407
- scan,
1408
- diagnostics
1409
- });
1425
+ const context = { filePath: file, sinceMs, scan, diagnostics };
1426
+ const parsedCalls = options.workspaceDailyFacts && descriptor.id === "codex"
1427
+ ? await readCodexDailyFinancialFile(context)
1428
+ : options.workspaceDailyFacts && descriptor.id === "claude-code"
1429
+ ? await readClaudeCodeFinancialFileForRegistry(context, true)
1430
+ : await runtime.parseFinancialFile(context);
1410
1431
  assertFormatCallOwnership(descriptor, parsedCalls);
1411
1432
  assertFinancialSourceOwnership(descriptor, scan, diagnostics);
1412
1433
  calls.push(...parsedCalls);
@@ -1588,7 +1609,7 @@ function codexHeaderAttribution(header) {
1588
1609
  };
1589
1610
  }
1590
1611
  /** @internal Runtime hook owned by the Claude Code registry entry. */
1591
- export async function readClaudeCodeFinancialFileForRegistry(context) {
1612
+ export async function readClaudeCodeFinancialFileForRegistry(context, workspaceDailyFacts = false) {
1592
1613
  const { filePath, sinceMs, scan, diagnostics } = context;
1593
1614
  if (!await shouldStreamFile(filePath, sinceMs, "claude-code", scan, diagnostics)) {
1594
1615
  return [];
@@ -1604,8 +1625,16 @@ export async function readClaudeCodeFinancialFileForRegistry(context) {
1604
1625
  // file so a narrow-window run can never truncate a wider one. The
1605
1626
  // loader's final timestamp filter performs all narrowing.
1606
1627
  undefined, seen, (diagnostic) => fileDiagnostics.push(diagnostic));
1607
- if (call)
1628
+ if (call) {
1629
+ if (workspaceDailyFacts) {
1630
+ const nativeAgent = stringOf(entry.agentId);
1631
+ const pathAgent = filePath.split(sep).includes("subagents")
1632
+ ? basename(filePath).match(/^agent-([A-Za-z0-9_-]+)\.jsonl$/)?.[1] : undefined;
1633
+ if (nativeAgent || pathAgent)
1634
+ call.subagentId = nativeAgent ?? pathAgent;
1635
+ }
1608
1636
  calls.push(call);
1637
+ }
1609
1638
  });
1610
1639
  }
1611
1640
  catch (error) {
@@ -1668,6 +1697,77 @@ export async function readCodexFinancialFileForRegistry(context) {
1668
1697
  });
1669
1698
  return call ? [call] : [];
1670
1699
  }
1700
+ /** Workspace-only event deltas. The existing snapshot reader remains cumulative. */
1701
+ async function readCodexDailyFinancialFile(context) {
1702
+ const { filePath, sinceMs, scan, diagnostics } = context;
1703
+ if (!await shouldStreamFile(filePath, sinceMs, "codex", scan, diagnostics))
1704
+ return [];
1705
+ const state = createCodexFinancialStreamState(), calls = [];
1706
+ const report = () => recordParseDiagnostic("codex", scan, diagnostics, { code: "unsupported_token_shape", count: 1 });
1707
+ let priorEventAt;
1708
+ try {
1709
+ const streamed = await streamJsonlRecords(filePath, entry => {
1710
+ const payload = isRecord(entry.payload) ? entry.payload : undefined;
1711
+ const previousTotal = state.lastTotal ?? state.inheritedUsageBaseline;
1712
+ consumeCodexFinancialEntry(state, entry);
1713
+ if (entry.type !== "event_msg" || payload?.type !== "token_count"
1714
+ || state.hasInheritedHistory && !state.rootTaskStarted)
1715
+ return;
1716
+ const info = isRecord(payload.info) ? payload.info : undefined;
1717
+ const total = info && isRecord(info.total_token_usage) ? info.total_token_usage : undefined;
1718
+ if (!total) {
1719
+ // A usage-bearing event without its cumulative endpoint cannot be
1720
+ // placed safely: a later counter may span this event's UTC day.
1721
+ if (info && isRecord(info.last_token_usage))
1722
+ report();
1723
+ return;
1724
+ }
1725
+ const timestamp = toIso(stringOf(entry.timestamp));
1726
+ if (!timestamp || !state.sessionId) {
1727
+ report();
1728
+ return;
1729
+ }
1730
+ const parsed = parseCodexCumulativeUsage(total, previousTotal);
1731
+ // Missing initial history cannot be assigned to a later day. A changed or
1732
+ // decreasing counter remains unknown at that event; the next exact pair
1733
+ // can establish a delta without pretending the reset was a zero.
1734
+ const firstDayKnown = !!previousTotal || !!state.startedAt && state.startedAt.slice(0, 10) === timestamp.slice(0, 10);
1735
+ const chronological = !priorEventAt || timestamp >= priorEventAt;
1736
+ const cachedShapeStable = !previousTotal ||
1737
+ Object.hasOwn(total, "cached_input_tokens") === Object.hasOwn(previousTotal, "cached_input_tokens");
1738
+ const supported = parsed.supported && firstDayKnown && chronological && cachedShapeStable;
1739
+ priorEventAt = timestamp;
1740
+ if (supported && previousTotal && parsed.usage.inputTokens === 0 && parsed.usage.outputTokens === 0
1741
+ && (parsed.usage.cacheReadTokens ?? 0) === 0 && (parsed.reportedTotalTokens ?? 0) === 0)
1742
+ return;
1743
+ if (!supported)
1744
+ report();
1745
+ const workingDirectory = absoluteWorkingDirectory(state.rootCwd);
1746
+ // The cumulative endpoint fingerprint is stable across copies of a
1747
+ // rollout. Different deltas for that same endpoint become a conflict in
1748
+ // dedupeCumulativeSessionCalls rather than two financial contributions.
1749
+ const callId = `callref_${createHash("sha256").update("codex-workspace-counter-event-v1\0")
1750
+ .update(JSON.stringify([state.sessionId, timestamp, total.input_tokens ?? null,
1751
+ total.output_tokens ?? null, total.cached_input_tokens ?? null, total.total_tokens ?? null])).digest("hex")}`;
1752
+ calls.push({ agent: "codex", sessionId: state.sessionId, callId,
1753
+ model: state.model ?? "codex", timestamp, startedAt: timestamp,
1754
+ project: projectFromCwd(workingDirectory), workingDirectory,
1755
+ usageScope: "turn", usageSupport: supported ? "complete" : "unsupported_token_shape",
1756
+ usage: parsed.usage,
1757
+ ...(supported && parsed.tokenComponentEvidence ? { tokenComponentEvidence: parsed.tokenComponentEvidence } : {}),
1758
+ ...(parsed.reportedTotalTokens !== undefined ? { reportedTotalTokens: parsed.reportedTotalTokens } : {}) });
1759
+ });
1760
+ if (streamed.hadContent)
1761
+ scan.filesParsed++;
1762
+ if (streamed.malformedLines)
1763
+ recordParseDiagnostic("codex", scan, diagnostics, { code: "malformed_jsonl", count: streamed.malformedLines });
1764
+ return calls;
1765
+ }
1766
+ catch (error) {
1767
+ recordUnreadableFile("codex", scan, diagnostics, error);
1768
+ return [];
1769
+ }
1770
+ }
1671
1771
  /** @internal Runtime hook owned by the Gemini CLI registry entry. */
1672
1772
  export async function readGeminiFinancialFileForRegistry(context) {
1673
1773
  const { filePath, sinceMs, scan, diagnostics } = context;
@@ -10,6 +10,9 @@
10
10
  export const PRICING_TABLE_AS_OF = "2026-08-25";
11
11
  const pricingRules = [
12
12
  // Anthropic
13
+ // Reviewed 2026-09-20: platform.claude.com/docs/en/about-claude/pricing.
14
+ // Fable/Mythos 5.1 keep base/write rates; cache reads are $0.25 per MTok.
15
+ { match: /^claude-(?:fable|mythos)-5-1$/i, inputPerM: 10, outputPerM: 50, cacheReadPerM: 0.25 },
13
16
  { match: /^claude-fable-5/i, inputPerM: 10, outputPerM: 50 },
14
17
  { match: /^claude-mythos-5/i, inputPerM: 10, outputPerM: 50 },
15
18
  { match: /^claude-opus-5/i, inputPerM: 5, outputPerM: 25 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-finops/core",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "funding": "https://asktilden.com",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",