@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 CHANGED
@@ -2,12 +2,45 @@
2
2
 
3
3
  The canonical local-first evidence and decision engine for
4
4
  [aibill](https://github.com/futurastudio/ai-spend-agent): Claude Code/Codex
5
- activity ingestion, provider cost semantics, attribution, provenance, runway,
6
- Context Health, and the shared Glance contract.
5
+ activity ingestion, experimental Gemini CLI financial ingestion, provider cost
6
+ semantics, attribution, provenance, runway, Context Health, and the shared
7
+ Glance contract. Gemini support is fixture-verified and does not enter the
8
+ Glance/status-line or Context Health contracts.
7
9
 
8
10
  Most users should run `npx aibill`. This package is for integrations that
9
11
  need the same evidence-labeled calculations as the CLI and MCP server.
10
12
 
13
+ ## Supported library preview
14
+
15
+ Use Node.js 22+ ESM and import only from the package root:
16
+
17
+ ```ts
18
+ import { analyzeSpend, parseUsageRecord } from "@agent-finops/core";
19
+ ```
20
+
21
+ The supported 0.x subset covers usage-record validation/parsing,
22
+ `analyzeSpend`, Receipt v0 creation/parsing, opaque source-record references,
23
+ and the explicitly version-pinned FOCUS, OpenTelemetry GenAI, and Tokenomics
24
+ projections. Tokenomics remains a `not_published`, zero-row tracking stub.
25
+
26
+ An explicit export map blocks unsupported deep imports. Other historical root
27
+ exports remain available for compatibility but are not stabilized by this
28
+ narrow preview. See the complete
29
+ [library contract](https://github.com/futurastudio/ai-spend-agent/blob/main/docs/LIBRARY.md),
30
+ including the runnable JavaScript/TypeScript example and 0.x deprecation
31
+ policy.
32
+
33
+ The supported functions are local, deterministic operations on caller-supplied
34
+ objects for a given package version. Importing this package does not read
35
+ transcripts or credentials, write state, call a provider, upload evidence, or
36
+ send telemetry.
37
+
38
+ Schema parsing checks structure and internal consistency; it does not
39
+ authenticate a source, verify a price, or reconcile an invoice. Callers must
40
+ keep provenance and confidence labels truthful: reserve `verified` for
41
+ official provider-reported financial evidence, keep modeled/local value
42
+ `estimated` or `missing`, and leave unvalidated adapters `untested`.
43
+
11
44
  This is the open foundation for aibill's financial-accountability mission. It
12
45
  does not yet implement company-wide ownership, accepted outcomes, approvals,
13
46
  invoice reconciliation, or ROI.
@@ -4,8 +4,8 @@ import type { LocalAgentFormatId } from "./localAgentFormats/types.js";
4
4
  import type { DetectedPlan } from "./planDetection.js";
5
5
  import { type UsageRecord } from "./schema.js";
6
6
  import { type SourceValidationCoverage } from "./sourceStatus.js";
7
- export declare const activitySnapshotAgentValues: readonly LocalAgentFormatId[];
8
- export type ActivitySnapshotAgent = LocalAgentFormatId;
7
+ export type ActivitySnapshotAgent = Extract<LocalAgentFormatId, "claude-code" | "codex">;
8
+ export declare const activitySnapshotAgentValues: readonly ActivitySnapshotAgent[];
9
9
  export declare const activitySnapshotPlanIdValues: readonly ["claude-pro", "claude-max-5x", "claude-max-20x", "chatgpt-plus", "chatgpt-pro"];
10
10
  export type ActivitySnapshotPlanId = typeof activitySnapshotPlanIdValues[number];
11
11
  export declare const activitySnapshotProviderValues: readonly ["openai", "anthropic", "cursor", "github-copilot", "other"];
@@ -178,7 +178,7 @@ export declare const activitySnapshotLimitSchema: z.ZodObject<{
178
178
  }, z.core.$strict>;
179
179
  export type ActivitySnapshotLimit = z.infer<typeof activitySnapshotLimitSchema>;
180
180
  export declare const activitySnapshotSubscriptionAgentSchema: z.ZodObject<{
181
- agent: z.ZodString & z.ZodType<LocalAgentFormatId, string, z.core.$ZodTypeInternals<LocalAgentFormatId, string>>;
181
+ agent: z.ZodString & z.ZodType<ActivitySnapshotAgent, string, z.core.$ZodTypeInternals<ActivitySnapshotAgent, string>>;
182
182
  billing: z.ZodLiteral<"subscription">;
183
183
  planId: z.ZodNullable<z.ZodEnum<{
184
184
  "claude-max-20x": "claude-max-20x";
@@ -258,7 +258,7 @@ export declare const activitySnapshotOverageSchema: z.ZodObject<{
258
258
  export type ActivitySnapshotOverage = z.infer<typeof activitySnapshotOverageSchema>;
259
259
  export declare const activitySnapshotCoverageSchema: z.ZodObject<{
260
260
  agents: z.ZodArray<z.ZodObject<{
261
- agent: z.ZodString & z.ZodType<LocalAgentFormatId, string, z.core.$ZodTypeInternals<LocalAgentFormatId, string>>;
261
+ agent: z.ZodString & z.ZodType<ActivitySnapshotAgent, string, z.core.$ZodTypeInternals<ActivitySnapshotAgent, string>>;
262
262
  directoryStatus: z.ZodEnum<{
263
263
  missing: "missing";
264
264
  readable: "readable";
@@ -349,7 +349,7 @@ export declare const activitySnapshotSchema: z.ZodObject<{
349
349
  }>;
350
350
  subscription: z.ZodNullable<z.ZodObject<{
351
351
  agents: z.ZodArray<z.ZodObject<{
352
- agent: z.ZodString & z.ZodType<LocalAgentFormatId, string, z.core.$ZodTypeInternals<LocalAgentFormatId, string>>;
352
+ agent: z.ZodString & z.ZodType<ActivitySnapshotAgent, string, z.core.$ZodTypeInternals<ActivitySnapshotAgent, string>>;
353
353
  billing: z.ZodLiteral<"subscription">;
354
354
  planId: z.ZodNullable<z.ZodEnum<{
355
355
  "claude-max-20x": "claude-max-20x";
@@ -420,7 +420,7 @@ export declare const activitySnapshotSchema: z.ZodObject<{
420
420
  }, z.core.$strict>>;
421
421
  metered: z.ZodNullable<z.ZodObject<{
422
422
  agents: z.ZodArray<z.ZodObject<{
423
- agent: z.ZodString & z.ZodType<LocalAgentFormatId, string, z.core.$ZodTypeInternals<LocalAgentFormatId, string>>;
423
+ agent: z.ZodString & z.ZodType<ActivitySnapshotAgent, string, z.core.$ZodTypeInternals<ActivitySnapshotAgent, string>>;
424
424
  billing: z.ZodLiteral<"api_key">;
425
425
  planId: z.ZodNullable<z.ZodEnum<{
426
426
  "claude-max-20x": "claude-max-20x";
@@ -521,7 +521,7 @@ export declare const activitySnapshotSchema: z.ZodObject<{
521
521
  }, z.core.$strict>>;
522
522
  unresolved: z.ZodNullable<z.ZodObject<{
523
523
  agents: z.ZodArray<z.ZodObject<{
524
- agent: z.ZodString & z.ZodType<LocalAgentFormatId, string, z.core.$ZodTypeInternals<LocalAgentFormatId, string>>;
524
+ agent: z.ZodString & z.ZodType<ActivitySnapshotAgent, string, z.core.$ZodTypeInternals<ActivitySnapshotAgent, string>>;
525
525
  billing: z.ZodLiteral<"unknown">;
526
526
  planId: z.ZodNullable<z.ZodEnum<{
527
527
  "claude-max-20x": "claude-max-20x";
@@ -586,7 +586,7 @@ export declare const activitySnapshotSchema: z.ZodObject<{
586
586
  }, z.core.$strict>>;
587
587
  coverage: z.ZodObject<{
588
588
  agents: z.ZodArray<z.ZodObject<{
589
- agent: z.ZodString & z.ZodType<LocalAgentFormatId, string, z.core.$ZodTypeInternals<LocalAgentFormatId, string>>;
589
+ agent: z.ZodString & z.ZodType<ActivitySnapshotAgent, string, z.core.$ZodTypeInternals<ActivitySnapshotAgent, string>>;
590
590
  directoryStatus: z.ZodEnum<{
591
591
  missing: "missing";
592
592
  readable: "readable";
@@ -1,11 +1,13 @@
1
1
  import { z } from "zod";
2
2
  import { aggregateCalls, dedupeCumulativeSessionCalls } from "./localAgentLogs.js";
3
3
  import { localAgentFormatDescriptors } from "./localAgentFormats/registry.js";
4
- import { estimateTokenCostUsd, PRICING_TABLE_AS_OF } from "./modelPricing.js";
4
+ import { canPriceTokenUsageAtScope, estimateTokenCostUsd, PRICING_TABLE_AS_OF } from "./modelPricing.js";
5
5
  import { isBundledSampleUsage } from "./schema.js";
6
6
  import { sourceValidationCoverageValues } from "./sourceStatus.js";
7
7
  const DAY_MS = 24 * 60 * 60 * 1_000;
8
- export const activitySnapshotAgentValues = Object.freeze(localAgentFormatDescriptors.map((descriptor) => descriptor.id));
8
+ export const activitySnapshotAgentValues = Object.freeze(localAgentFormatDescriptors
9
+ .filter((descriptor) => descriptor.capabilities.statuslineSnapshot)
10
+ .map((descriptor) => descriptor.id));
9
11
  const activitySnapshotAgentValueSet = new Set(activitySnapshotAgentValues);
10
12
  const activitySnapshotAgentLimit = activitySnapshotAgentValues.length;
11
13
  export const activitySnapshotPlanIdValues = [
@@ -459,7 +461,12 @@ export function buildActivitySnapshot(input) {
459
461
  const horizonStartMs = asOfMs - 30 * DAY_MS;
460
462
  // Horizon filtering must precede ID conflict detection: stale history must
461
463
  // neither activate a cohort nor suppress a current row that reused an ID.
462
- const horizonRecords = input.records.filter((record) => validRecordTimestampInHorizon(record, horizonStartMs, asOfMs));
464
+ const horizonRecords = input.records.filter((record) => validRecordTimestampInHorizon(record, horizonStartMs, asOfMs) &&
465
+ // The statusline cache is a deliberately narrower contract than the full
466
+ // parser registry. Registry-native sources join only after their snapshot
467
+ // capability has its own host/UI verification.
468
+ (record.providerCostType !== "local_agent_logs" ||
469
+ record.agentId === undefined || isSnapshotAgent(record.agentId)));
463
470
  const deduplicated = deduplicateRecords(horizonRecords);
464
471
  const classified = deduplicated.records
465
472
  .map((record) => ({
@@ -468,10 +475,10 @@ export function buildActivitySnapshot(input) {
468
475
  }));
469
476
  // Keep later same-day calls available only for proportionally splitting a
470
477
  // daily aggregate at asOf; activity and limit selection remain <= asOf.
471
- const allCalls = dedupeCumulativeSessionCalls([...(input.calls ?? [])].filter((call) =>
472
- // One preceding bucket is needed to preserve the denominator when a
473
- // daily aggregate straddles the 30-day cutoff.
474
- validCallAtOrAfter(call, horizonStartMs - DAY_MS)));
478
+ const allCalls = dedupeCumulativeSessionCalls([...(input.calls ?? [])].filter((call) => isSnapshotAgent(call.agent) &&
479
+ // One preceding bucket is needed to preserve the denominator when a
480
+ // daily aggregate straddles the 30-day cutoff.
481
+ validCallAtOrAfter(call, horizonStartMs - DAY_MS)));
475
482
  const calls = allCalls.filter((call) => validCallAtOrBefore(call, asOfMs));
476
483
  const subscriptionAgents = activitySubscriptionAgents(classified, calls, allCalls, plans, scans, trustedIds, asOfMs);
477
484
  const meteredApiRecords = classified
@@ -794,10 +801,14 @@ function apiEquivalentObservations(records, calls, trustedProviderIds) {
794
801
  function allocateAggregateAmount(amountUsd, calls) {
795
802
  if (amountUsd === null)
796
803
  return calls.map(() => null);
804
+ const priceable = calls.map((call) => canPriceTokenUsageAtScope(call.model, call.usage, call.usageScope === "turn" ? "request" : "aggregate") && estimateTokenCostUsd(call.model, call.usage) !== undefined);
805
+ if (priceable.some((supported) => !supported))
806
+ return calls.map(() => null);
797
807
  const weights = calls.map((call) => estimateTokenCostUsd(call.model, call.usage) ?? 0);
798
808
  const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
799
- if (totalWeight <= 0)
800
- return calls.map(() => amountUsd / calls.length);
809
+ if (totalWeight <= 0) {
810
+ return amountUsd === 0 ? calls.map(() => 0) : calls.map(() => null);
811
+ }
801
812
  return weights.map((weight) => amountUsd * weight / totalWeight);
802
813
  }
803
814
  function observationsForWindow(observations, asOfMs, days) {
package/dist/cutList.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { hasCallLevelProvenance, hasModeledWorkloadEvidence, hasPricedEvidence } from "./schema.js";
2
+ import { localAgentFormatSupports } from "./localAgentFormats/registry.js";
2
3
  /**
3
4
  * Select a non-overlapping subset of cut actions, highest-savings first. An
4
5
  * action is added only if none of its records were already claimed by a
@@ -76,7 +77,7 @@ export function generateCutList(records) {
76
77
  // Local transcript aggregates remain eligible only for the observed-only
77
78
  // context exposure path below; they never earn a modeled savings number.
78
79
  const callLevelRecords = records.filter(hasModeledWorkloadEvidence);
79
- const contextEvidenceRecords = records.filter((record) => isLocalAgentRecord(record) || (hasCallLevelProvenance(record) && hasPricedEvidence(record)));
80
+ const contextEvidenceRecords = records.filter((record) => (hasPricedEvidence(record) && (isActionPlanningLocalAgentRecord(record) || hasCallLevelProvenance(record))));
80
81
  const actions = [
81
82
  ...modelDowngradeActions(callLevelRecords),
82
83
  ...contextTrimActions(contextEvidenceRecords),
@@ -90,6 +91,13 @@ export function generateCutList(records) {
90
91
  right.affectedSpendUsd - left.affectedSpendUsd ||
91
92
  left.id.localeCompare(right.id));
92
93
  }
94
+ function isActionPlanningLocalAgentRecord(record) {
95
+ if (!isLocalAgentRecord(record))
96
+ return false;
97
+ // Preserve old local caches that predate agentId, but make every registered
98
+ // source opt into recommendation/Apply semantics explicitly.
99
+ return !record.agentId || localAgentFormatSupports(record.agentId, "actionPlanning");
100
+ }
93
101
  /** Sum of all per-action estimated monthly savings. */
94
102
  export function totalEstimatedMonthlySavingsUsd(actions) {
95
103
  return roundMoney(actions.reduce((total, action) => total + action.estimatedMonthlySavingsUsd, 0));
package/dist/glance.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { dedupeCumulativeSessionCalls, sanitizeLocalActivityText } from "./localAgentLogs.js";
2
- import { estimateTokenCostUsd, PRICING_TABLE_AS_OF } from "./modelPricing.js";
2
+ import { canPriceTokenUsageAtScope, estimateTokenCostUsd, PRICING_TABLE_AS_OF } from "./modelPricing.js";
3
3
  import { subscriptionPlans } from "./planMath.js";
4
4
  import { buildContextHealth } from "./contextHealth.js";
5
5
  import { localAgentFormatDescriptors, localAgentFormatSupports } from "./localAgentFormats/registry.js";
@@ -612,6 +612,8 @@ function limitActionName(limit) {
612
612
  function callCost(call) {
613
613
  if (call.usageSupport === "unsupported_token_shape")
614
614
  return undefined;
615
+ if (!canPriceTokenUsageAtScope(call.model, call.usage, call.usageScope === "turn" ? "request" : "aggregate"))
616
+ return undefined;
615
617
  return estimateTokenCostUsd(call.model, call.usage);
616
618
  }
617
619
  function inputSideTokens(call) {
@@ -0,0 +1,55 @@
1
+ import type { TokenUsage } from "../modelPricing.js";
2
+ export type GeminiCacheAccounting = "included" | "none" | "unknown";
3
+ export type GeminiTokenEvidence = {
4
+ /** Raw provider fields. Invalid or absent fields remain absent. */
5
+ readonly input?: number;
6
+ readonly output?: number;
7
+ readonly cached?: number;
8
+ readonly thoughts?: number;
9
+ readonly tool?: number;
10
+ readonly total?: number;
11
+ /** Whether the reported input count includes the cached count. */
12
+ readonly cacheAccounting: GeminiCacheAccounting;
13
+ };
14
+ /**
15
+ * Structural call type that can be wired into LocalAgentCall once the Gemini
16
+ * registry descriptor is enabled. It deliberately exposes no prompt content
17
+ * or raw project hash.
18
+ */
19
+ export type GeminiParsedCall = {
20
+ readonly agent: "gemini-cli";
21
+ readonly callId: string;
22
+ readonly model: string;
23
+ readonly timestamp: string;
24
+ readonly startedAt?: string;
25
+ readonly project?: string;
26
+ readonly workingDirectory?: string;
27
+ readonly sessionId?: string;
28
+ readonly usageScope: "turn";
29
+ readonly usageSupport: "complete" | "unsupported_token_shape";
30
+ readonly reportedTotalTokens?: number;
31
+ readonly sourceVersion?: string;
32
+ readonly usage: TokenUsage;
33
+ readonly geminiTokenEvidence: GeminiTokenEvidence;
34
+ };
35
+ export type GeminiParseDiagnosticCode = "malformed_json" | "malformed_jsonl" | "unsupported_token_shape" | "missing_timestamp";
36
+ export type GeminiParseDiagnostic = {
37
+ readonly code: GeminiParseDiagnosticCode;
38
+ readonly count: number;
39
+ };
40
+ export type GeminiParseOptions = {
41
+ /** Caller-supplied path, including recursive chats/subagent paths. */
42
+ readonly filePath: string;
43
+ readonly sinceMs?: number;
44
+ };
45
+ export type GeminiParseResult = {
46
+ readonly calls: GeminiParsedCall[];
47
+ readonly diagnostics: GeminiParseDiagnostic[];
48
+ };
49
+ /** Parse a Gemini chat file according to its caller-supplied extension. */
50
+ export declare function parseGeminiSession(content: string, options: GeminiParseOptions): GeminiParseResult;
51
+ /** Parse the legacy whole-conversation JSON representation. */
52
+ export declare function parseGeminiJsonSession(content: string, options: GeminiParseOptions): GeminiParseResult;
53
+ /** Parse the append-only current JSONL message representation. */
54
+ export declare function parseGeminiJsonlSession(content: string, options: GeminiParseOptions): GeminiParseResult;
55
+ //# sourceMappingURL=gemini.d.ts.map