sentinelayer-cli 0.17.1 → 0.18.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.17.1",
3
+ "version": "0.18.1",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,6 +1,8 @@
1
1
  import { estimateTokens } from "./tokenizer.js";
2
2
 
3
- const DEFAULT_MODEL_PRICING = Object.freeze({
3
+ export const DEFAULT_PRICE_BOOK_VERSION = "2026-05-24";
4
+
5
+ export const DEFAULT_MODEL_PRICING = Object.freeze({
4
6
  "gpt-4o": Object.freeze({
5
7
  inputPerMillionUsd: 2.5,
6
8
  outputPerMillionUsd: 10.0,
package/src/legacy-cli.js CHANGED
@@ -2338,11 +2338,18 @@ jobs:
2338
2338
  fi
2339
2339
  - name: Run Omar Gate
2340
2340
  id: omar
2341
- uses: mrrCarter/sentinelayer-v1-action@4cb3063e04e3b899981b25f6918b26f70d35a8d4
2341
+ uses: mrrCarter/sentinelayer-v1-action@8595c4ad41e7b710ff6b1de0603da6ad8c0c3c07
2342
2342
  with:
2343
+ github_token: \${{ github.token }}
2343
2344
  sentinelayer_token: \${{ secrets.${normalizedSecret} }}${specIdBindingLine}
2345
+ sentinelayer_managed_llm: "false"
2346
+ openai_api_key: \${{ secrets.OPENAI_API_KEY }}
2344
2347
  scan_mode: \${{ github.event_name == 'workflow_dispatch' && inputs.scan_mode || 'deep' }}
2345
2348
  severity_gate: \${{ github.event_name == 'workflow_dispatch' && inputs.severity_gate || 'P1' }}
2349
+ model: gpt-5.3-codex
2350
+ codex_model: gpt-5.3-codex
2351
+ model_fallback: gpt-5.2-codex
2352
+ llm_failure_policy: block
2346
2353
  - name: Enforce Omar reviewer merge thresholds
2347
2354
  shell: bash
2348
2355
  env:
@@ -2,7 +2,7 @@ import YAML from "yaml";
2
2
 
3
3
  export const DEFAULT_SCAN_WORKFLOW_PATH = ".github/workflows/omar-gate.yml";
4
4
  export const DEFAULT_SCAN_SECRET_NAME = "SENTINELAYER_TOKEN";
5
- export const SENTINELAYER_ACTION_REF = "mrrCarter/sentinelayer-v1-action@4cb3063e04e3b899981b25f6918b26f70d35a8d4";
5
+ export const SENTINELAYER_ACTION_REF = "mrrCarter/sentinelayer-v1-action@8595c4ad41e7b710ff6b1de0603da6ad8c0c3c07";
6
6
  export const SUPPORTED_E2E_HINTS = Object.freeze(["auto", "yes", "no"]);
7
7
  export const SUPPORTED_PLAYWRIGHT_MODES = Object.freeze(["auto", "off", "baseline", "audit"]);
8
8
 
@@ -224,9 +224,16 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
224
224
  id: "omar",
225
225
  uses: SENTINELAYER_ACTION_REF,
226
226
  with: {
227
+ github_token: "${{ github.token }}",
227
228
  sentinelayer_token: `\${{ secrets.${normalizedSecret} }}`,
229
+ sentinelayer_managed_llm: "false",
230
+ openai_api_key: "${{ secrets.OPENAI_API_KEY }}",
228
231
  scan_mode: profile.scanMode || "deep",
229
232
  severity_gate: profile.severityGate || "P1",
233
+ model: "gpt-5.3-codex",
234
+ codex_model: "gpt-5.3-codex",
235
+ model_fallback: "gpt-5.2-codex",
236
+ llm_failure_policy: "block",
230
237
  playwright_mode: profile.playwrightMode || "off",
231
238
  sbom_mode: profile.sbomMode || "off",
232
239
  wait_for_completion: "true",
@@ -0,0 +1,272 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import {
4
+ DEFAULT_PRICE_BOOK_VERSION,
5
+ estimateModelCost,
6
+ } from "../cost/tracker.js";
7
+
8
+ const SESSION_USAGE_EVENT = "session_usage";
9
+ export const BILLING_SESSION_USAGE_SCHEMA = "billing/v1";
10
+ export const LOCAL_SESSION_USAGE_SCHEMA = "session_usage/local-v1";
11
+ const LEGACY_SESSION_USAGE_SCHEMAS = new Set(["", "session_usage/v0"]);
12
+ const SUPPORTED_SESSION_USAGE_SCHEMAS = new Set([
13
+ BILLING_SESSION_USAGE_SCHEMA,
14
+ LOCAL_SESSION_USAGE_SCHEMA,
15
+ ...LEGACY_SESSION_USAGE_SCHEMAS,
16
+ ]);
17
+
18
+ function n(value) {
19
+ return String(value == null ? "" : value).trim();
20
+ }
21
+
22
+ function object(value) {
23
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
24
+ }
25
+
26
+ function nonNegativeNumber(value) {
27
+ if (value == null || value === "") return null;
28
+ const parsed = Number(value);
29
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
30
+ }
31
+
32
+ function nonNegativeInt(value) {
33
+ const parsed = nonNegativeNumber(value);
34
+ return parsed == null ? null : Math.floor(parsed);
35
+ }
36
+
37
+ function money(value) {
38
+ const parsed = nonNegativeNumber(value);
39
+ return parsed == null ? null : Math.round(parsed * 1_000_000) / 1_000_000;
40
+ }
41
+
42
+ function pick(sources, keys) {
43
+ for (const source of sources) {
44
+ const bag = object(source);
45
+ for (const key of keys) {
46
+ if (bag[key] != null && bag[key] !== "") return bag[key];
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+
52
+ function pickText(sources, keys) {
53
+ return n(pick(sources, keys));
54
+ }
55
+
56
+ function pickInt(sources, keys) {
57
+ return nonNegativeInt(pick(sources, keys)) ?? 0;
58
+ }
59
+
60
+ function pickMoney(sources, keys) {
61
+ return money(pick(sources, keys));
62
+ }
63
+
64
+ function roundUsd(value) {
65
+ return Math.round(Number(value || 0) * 1_000_000) / 1_000_000;
66
+ }
67
+
68
+ export function createSessionUsageLedgerId({
69
+ sessionId = "",
70
+ agentId = "",
71
+ action = "",
72
+ idempotencyKey = "",
73
+ } = {}) {
74
+ const digest = createHash("sha256")
75
+ .update([sessionId, agentId, action, idempotencyKey].map(n).join("\x1f"))
76
+ .digest("hex")
77
+ .slice(0, 32);
78
+ return `bill_${digest}`;
79
+ }
80
+
81
+ function fallbackIdempotencyKey({ sessionId, event, agentId, action, model, totalTokens }) {
82
+ const sequence = n(event.sequenceId ?? event.sequence_id);
83
+ if (sequence) return `seq:${sequence}`;
84
+ const timestamp = n(event.ts || event.timestamp);
85
+ const interaction = n(object(event.payload).interactionId || object(event.payload).interaction_id);
86
+ const source = [sessionId, timestamp, agentId, action, model, totalTokens, interaction].join("\x1f");
87
+ return `event:${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
88
+ }
89
+
90
+ function providerCostFromPriceBook({ model, inputTokens, outputTokens, explicitProviderCost }) {
91
+ if (explicitProviderCost != null) {
92
+ return { providerCostUsd: explicitProviderCost, unpriced: false };
93
+ }
94
+ if (inputTokens <= 0 && outputTokens <= 0) {
95
+ return { providerCostUsd: 0, unpriced: false };
96
+ }
97
+ try {
98
+ return {
99
+ providerCostUsd: estimateModelCost({ modelId: model, inputTokens, outputTokens }),
100
+ unpriced: false,
101
+ };
102
+ } catch {
103
+ return { providerCostUsd: 0, unpriced: true };
104
+ }
105
+ }
106
+
107
+ export function buildUsageLedgerEntry(
108
+ event,
109
+ { sessionId = "", priceBookVersion = DEFAULT_PRICE_BOOK_VERSION, billingTier = "unknown" } = {},
110
+ ) {
111
+ const kind = n(event?.event || event?.type);
112
+ if (kind !== SESSION_USAGE_EVENT) return null;
113
+
114
+ const payload = object(event?.payload);
115
+ const schema = n(payload.schema);
116
+ if (!SUPPORTED_SESSION_USAGE_SCHEMAS.has(schema)) return null;
117
+
118
+ const usage = object(payload.usage);
119
+ const prompt = object(payload.prompt);
120
+ const response = object(payload.response);
121
+ const agent = object(event?.agent);
122
+ const sources = [payload, usage];
123
+
124
+ const agentId =
125
+ pickText(sources, ["agentId", "agent_id"]) ||
126
+ n(agent.id || event?.agentId) ||
127
+ "unknown";
128
+ const model =
129
+ pickText(sources, ["model", "modelId", "model_id"]) ||
130
+ n(agent.model || event?.agentModel) ||
131
+ "unknown";
132
+ const action =
133
+ pickText(sources, ["action", "operation", "kind", "billingAction", "billing_action"]) ||
134
+ "agent_message";
135
+ const resolvedPriceBook =
136
+ pickText(sources, ["priceBookVersion", "price_book_version", "pricingVersion", "pricing_version"]) ||
137
+ priceBookVersion;
138
+ const resolvedBillingTier =
139
+ pickText(sources, ["billingTier", "billing_tier", "tier"]) ||
140
+ n(billingTier) ||
141
+ "unknown";
142
+
143
+ const inputTokens =
144
+ pickInt(sources, ["inputTokens", "input_tokens", "tokensIn", "tokens_in", "promptTokens", "prompt_tokens"]) ||
145
+ pickInt([prompt], ["tokens", "tokenCount", "token_count"]);
146
+ const outputTokens =
147
+ pickInt(sources, ["outputTokens", "output_tokens", "tokensOut", "tokens_out", "completionTokens", "completion_tokens"]) ||
148
+ pickInt([response], ["tokens", "tokenCount", "token_count"]);
149
+ const explicitTotalTokens = pickInt(sources, ["totalTokens", "total_tokens", "tokens", "tokenTotal", "token_total"]);
150
+ const totalTokens = explicitTotalTokens || inputTokens + outputTokens;
151
+ const explicitProviderCost = pickMoney(sources, ["providerCostUsd", "provider_cost_usd", "costUsd", "cost_usd", "cost"]);
152
+ const customerCostUsd = pickMoney(sources, ["customerCostUsd", "customer_cost_usd", "billableCostUsd", "billable_cost_usd"]);
153
+ const { providerCostUsd, unpriced } = providerCostFromPriceBook({
154
+ model,
155
+ inputTokens,
156
+ outputTokens,
157
+ explicitProviderCost,
158
+ });
159
+
160
+ const idempotencyKey =
161
+ pickText(sources, ["idempotencyKey", "idempotency_key", "runKey", "run_key"]) ||
162
+ pickText(sources, ["interactionId", "interaction_id"]) ||
163
+ fallbackIdempotencyKey({ sessionId, event, agentId, action, model, totalTokens });
164
+ const ledgerEntryId =
165
+ pickText(sources, ["ledgerEntryId", "ledger_entry_id", "billingEventId", "billing_event_id"]) ||
166
+ createSessionUsageLedgerId({ sessionId, agentId, action, idempotencyKey });
167
+
168
+ return {
169
+ ledgerEntryId,
170
+ idempotencyKey,
171
+ schema: schema || "legacy",
172
+ sessionId: n(sessionId),
173
+ agentId,
174
+ action,
175
+ model,
176
+ priceBookVersion: resolvedPriceBook,
177
+ billingTier: resolvedBillingTier,
178
+ provider: pickText(sources, ["provider", "providerName", "provider_name"]),
179
+ inputTokens,
180
+ outputTokens,
181
+ totalTokens,
182
+ providerCostUsd: roundUsd(providerCostUsd),
183
+ customerCostUsd,
184
+ unpriced,
185
+ timestamp: n(event?.ts || event?.timestamp),
186
+ sequenceId: nonNegativeInt(event?.sequenceId ?? event?.sequence_id),
187
+ };
188
+ }
189
+
190
+ function newRollup(label) {
191
+ return {
192
+ label,
193
+ entries: 0,
194
+ inputTokens: 0,
195
+ outputTokens: 0,
196
+ totalTokens: 0,
197
+ providerCostUsd: 0,
198
+ customerCostUsd: 0,
199
+ hasCustomerCost: false,
200
+ unpriced: 0,
201
+ };
202
+ }
203
+
204
+ function addToRollup(rollup, entry) {
205
+ rollup.entries += 1;
206
+ rollup.inputTokens += entry.inputTokens;
207
+ rollup.outputTokens += entry.outputTokens;
208
+ rollup.totalTokens += entry.totalTokens;
209
+ rollup.providerCostUsd += entry.providerCostUsd;
210
+ if (entry.customerCostUsd != null) {
211
+ rollup.customerCostUsd += entry.customerCostUsd;
212
+ rollup.hasCustomerCost = true;
213
+ }
214
+ if (entry.unpriced) rollup.unpriced += 1;
215
+ }
216
+
217
+ function finalizeRollup(rollup) {
218
+ rollup.providerCostUsd = roundUsd(rollup.providerCostUsd);
219
+ rollup.customerCostUsd = roundUsd(rollup.customerCostUsd);
220
+ return rollup;
221
+ }
222
+
223
+ export function buildSessionUsageLedger(events = [], options = {}) {
224
+ if (!Array.isArray(events)) {
225
+ throw new Error("events must be an array.");
226
+ }
227
+ const entries = [];
228
+ const totals = newRollup("session");
229
+ const perAgent = new Map();
230
+ const perAction = new Map();
231
+ const priceBookVersions = new Set();
232
+ const seenKeys = new Set();
233
+ let duplicatesSkipped = 0;
234
+
235
+ for (const event of events) {
236
+ const entry = buildUsageLedgerEntry(event, options);
237
+ if (!entry) continue;
238
+ const dedupeKeys = [
239
+ entry.idempotencyKey ? `idem:${entry.idempotencyKey}` : "",
240
+ entry.ledgerEntryId ? `ledger:${entry.ledgerEntryId}` : "",
241
+ ].filter(Boolean);
242
+ if (dedupeKeys.some((dedupeKey) => seenKeys.has(dedupeKey))) {
243
+ duplicatesSkipped += 1;
244
+ continue;
245
+ }
246
+ for (const dedupeKey of dedupeKeys) seenKeys.add(dedupeKey);
247
+ entries.push(entry);
248
+ priceBookVersions.add(entry.priceBookVersion);
249
+ addToRollup(totals, entry);
250
+
251
+ if (!perAgent.has(entry.agentId)) perAgent.set(entry.agentId, newRollup(entry.agentId));
252
+ addToRollup(perAgent.get(entry.agentId), entry);
253
+
254
+ if (!perAction.has(entry.action)) perAction.set(entry.action, newRollup(entry.action));
255
+ addToRollup(perAction.get(entry.action), entry);
256
+ }
257
+
258
+ finalizeRollup(totals);
259
+ for (const rollup of perAgent.values()) finalizeRollup(rollup);
260
+ for (const rollup of perAction.values()) finalizeRollup(rollup);
261
+
262
+ return {
263
+ entries,
264
+ totals,
265
+ perAgent,
266
+ perAction,
267
+ priceBookVersions: [...priceBookVersions].sort(),
268
+ duplicatesSkipped,
269
+ };
270
+ }
271
+
272
+ export { DEFAULT_PRICE_BOOK_VERSION };
@@ -1,13 +1,20 @@
1
1
  /**
2
- * Session usage emitter — records every LLM interaction inside a session
3
- * as a `session_usage` event so consumers (web dashboard, transcript
4
- * download, telemetry sync) can surface live, accurate token + cost
5
- * counters per-agent + session-wide.
2
+ * Session usage emitter — records local transcript/live-stats LLM
3
+ * interactions inside a session as `session_usage` events so consumers
4
+ * (web dashboard, transcript download, local recap) can surface live token
5
+ * + cost counters per-agent + session-wide.
6
6
  *
7
7
  * Senti orchestrator philosophy: "tokens on point every time any LLM
8
8
  * interacts." Every persona / Jules / Codex / Claude call inside a
9
9
  * session should land here so the running tally is authoritative.
10
10
  *
11
+ * Important contract split: this module is NOT the API billing ledger.
12
+ * API-durable billable rows must go through `src/billing/session-usage.js`
13
+ * and emit `payload.schema = "billing/v1"` with the API-supported price
14
+ * book. This module emits `session_usage/local-v1`, which is intentionally
15
+ * ignored by API quota/billing projection while still remaining visible in
16
+ * transcripts and local aggregate counters.
17
+ *
11
18
  * Event shape:
12
19
  *
13
20
  * {
@@ -41,6 +48,13 @@ import process from "node:process";
41
48
  import { randomUUID } from "node:crypto";
42
49
 
43
50
  import { createAgentEvent } from "../events/schema.js";
51
+ import { estimateModelCost } from "../cost/tracker.js";
52
+ import {
53
+ DEFAULT_PRICE_BOOK_VERSION,
54
+ LOCAL_SESSION_USAGE_SCHEMA,
55
+ buildSessionUsageLedger,
56
+ createSessionUsageLedgerId,
57
+ } from "./pricing-ledger.js";
44
58
  import { resolveSessionPaths } from "./paths.js";
45
59
  import { appendToStream } from "./stream.js";
46
60
 
@@ -55,19 +69,8 @@ function num(value) {
55
69
  return Number.isFinite(v) && v >= 0 ? v : 0;
56
70
  }
57
71
 
58
- function plainObject(value) {
59
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
60
- }
61
-
62
- function firstUsageNumber(payload = {}, keys = []) {
63
- const usage = plainObject(payload.usage);
64
- for (const key of keys) {
65
- const direct = num(payload[key]);
66
- if (direct > 0) return direct;
67
- const nested = num(usage[key]);
68
- if (nested > 0) return nested;
69
- }
70
- return 0;
72
+ function money(value) {
73
+ return Math.round(num(value) * 1_000_000) / 1_000_000;
71
74
  }
72
75
 
73
76
  function clipText(text, max = 4000) {
@@ -76,6 +79,17 @@ function clipText(text, max = 4000) {
76
79
  return `${s.slice(0, max)}…`;
77
80
  }
78
81
 
82
+ function computedCost({ model, inputTokens, outputTokens }) {
83
+ try {
84
+ return {
85
+ costUsd: estimateModelCost({ modelId: model, inputTokens, outputTokens }),
86
+ unpriced: false,
87
+ };
88
+ } catch {
89
+ return { costUsd: 0, unpriced: inputTokens + outputTokens > 0 };
90
+ }
91
+ }
92
+
79
93
  /**
80
94
  * Emit a `session_usage` event into the session's NDJSON stream.
81
95
  *
@@ -87,7 +101,12 @@ function clipText(text, max = 4000) {
87
101
  * @param {number} [params.inputTokens]
88
102
  * @param {number} [params.outputTokens]
89
103
  * @param {number} [params.costUsd]
104
+ * @param {number} [params.customerCostUsd]
90
105
  * @param {number} [params.durationMs]
106
+ * @param {string} [params.action]
107
+ * @param {string} [params.provider]
108
+ * @param {string} [params.billingTier]
109
+ * @param {string} [params.priceBookVersion]
91
110
  * @param {string} [params.prompt] full prompt text (clipped)
92
111
  * @param {string} [params.response] full response text (clipped)
93
112
  * @param {string} [params.interactionId] opaque id for cross-event correlation
@@ -102,8 +121,13 @@ export async function emitLLMInteraction(
102
121
  role = "",
103
122
  inputTokens = 0,
104
123
  outputTokens = 0,
105
- costUsd = 0,
124
+ costUsd = undefined,
125
+ customerCostUsd = undefined,
106
126
  durationMs = 0,
127
+ action = "agent_message",
128
+ provider = "",
129
+ billingTier = "unknown",
130
+ priceBookVersion = DEFAULT_PRICE_BOOK_VERSION,
107
131
  prompt = "",
108
132
  response = "",
109
133
  interactionId = "",
@@ -121,20 +145,45 @@ export async function emitLLMInteraction(
121
145
  const inT = Math.floor(num(inputTokens));
122
146
  const outT = Math.floor(num(outputTokens));
123
147
  const totalT = inT + outT;
124
- const cost = Math.round(num(costUsd) * 1_000_000) / 1_000_000;
148
+ const model = n(agentModel) || "unknown";
149
+ const providedCost = costUsd != null && costUsd !== "";
150
+ const estimate = providedCost
151
+ ? { costUsd: num(costUsd), unpriced: false }
152
+ : computedCost({ model, inputTokens: inT, outputTokens: outT });
153
+ const cost = money(estimate.costUsd);
154
+ const customerCost = customerCostUsd == null || customerCostUsd === "" ? null : money(customerCostUsd);
155
+ const actionName = n(action) || "agent_message";
156
+ const tier = n(billingTier) || "unknown";
157
+ const priceBook = n(priceBookVersion) || DEFAULT_PRICE_BOOK_VERSION;
158
+ const ledgerEntryId = createSessionUsageLedgerId({
159
+ sessionId: paths.sessionId,
160
+ agentId: aid,
161
+ action: actionName,
162
+ idempotencyKey: id,
163
+ });
125
164
 
126
165
  const promptText = clipText(prompt);
127
166
  const responseText = clipText(response);
128
167
 
129
168
  const payload = {
169
+ schema: LOCAL_SESSION_USAGE_SCHEMA,
130
170
  interactionId: id,
171
+ idempotencyKey: id,
172
+ ledgerEntryId,
131
173
  agentId: aid,
132
- model: n(agentModel) || "unknown",
174
+ model,
133
175
  role: n(role) || "observer",
176
+ action: actionName,
177
+ provider: n(provider) || undefined,
178
+ billingTier: tier,
179
+ priceBookVersion: priceBook,
134
180
  inputTokens: inT,
135
181
  outputTokens: outT,
136
182
  totalTokens: totalT,
137
183
  costUsd: cost,
184
+ providerCostUsd: cost,
185
+ customerCostUsd: customerCost ?? undefined,
186
+ unpriced: estimate.unpriced,
138
187
  durationMs: Math.max(0, Math.floor(num(durationMs))),
139
188
  prompt: { tokens: inT, chars: promptText.length },
140
189
  response: {
@@ -147,15 +196,24 @@ export async function emitLLMInteraction(
147
196
  usage: {
148
197
  totalTokens: totalT,
149
198
  costUsd: cost,
199
+ providerCostUsd: cost,
200
+ customerCostUsd: customerCost ?? undefined,
150
201
  inputTokens: inT,
151
202
  outputTokens: outT,
203
+ action: actionName,
204
+ provider: n(provider) || undefined,
205
+ billingTier: tier,
206
+ priceBookVersion: priceBook,
207
+ ledgerEntryId,
208
+ idempotencyKey: id,
209
+ unpriced: estimate.unpriced,
152
210
  },
153
211
  };
154
212
 
155
213
  const envelope = createAgentEvent({
156
214
  event: SESSION_USAGE_EVENT,
157
215
  agentId: aid,
158
- agentModel: n(agentModel) || "unknown",
216
+ agentModel: model,
159
217
  sessionId: paths.sessionId,
160
218
  payload,
161
219
  ts,
@@ -165,6 +223,7 @@ export async function emitLLMInteraction(
165
223
  return {
166
224
  event: SESSION_USAGE_EVENT,
167
225
  interactionId: id,
226
+ ledgerEntryId,
168
227
  totalTokens: totalT,
169
228
  costUsd: cost,
170
229
  };
@@ -181,6 +240,7 @@ export async function emitLLMInteraction(
181
240
  * }}
182
241
  */
183
242
  export function aggregateSessionUsage(events = []) {
243
+ const ledger = buildSessionUsageLedger(events);
184
244
  const perAgent = new Map();
185
245
  const totals = {
186
246
  totalTokens: 0,
@@ -189,37 +249,8 @@ export function aggregateSessionUsage(events = []) {
189
249
  costUsd: 0,
190
250
  interactions: 0,
191
251
  };
192
- for (const event of events) {
193
- if (!event || event.event !== SESSION_USAGE_EVENT) continue;
194
- const payload = event.payload || {};
195
- const agentId = n(payload.agentId || event.agent?.id);
196
- if (!agentId) continue;
197
- const model = n(payload.model || event.agent?.model) || "unknown";
198
- const inputTokens = firstUsageNumber(payload, [
199
- "inputTokens",
200
- "input_tokens",
201
- "tokensIn",
202
- "tokens_in",
203
- ]);
204
- const outputTokens = firstUsageNumber(payload, [
205
- "outputTokens",
206
- "output_tokens",
207
- "tokensOut",
208
- "tokens_out",
209
- ]);
210
- const explicitTotalTokens = firstUsageNumber(payload, [
211
- "totalTokens",
212
- "total_tokens",
213
- "tokens",
214
- ]);
215
- const totalTokens = explicitTotalTokens || inputTokens + outputTokens;
216
- const costUsd = firstUsageNumber(payload, [
217
- "costUsd",
218
- "cost_usd",
219
- "providerCostUsd",
220
- "provider_cost_usd",
221
- "cost",
222
- ]);
252
+ for (const entry of ledger.entries) {
253
+ const { agentId, model, inputTokens, outputTokens, totalTokens, providerCostUsd } = entry;
223
254
  if (!perAgent.has(agentId)) {
224
255
  perAgent.set(agentId, {
225
256
  agentId,
@@ -238,13 +269,13 @@ export function aggregateSessionUsage(events = []) {
238
269
  record.totalTokens += totalTokens;
239
270
  record.inputTokens += inputTokens;
240
271
  record.outputTokens += outputTokens;
241
- record.costUsd += costUsd;
272
+ record.costUsd += providerCostUsd;
242
273
  record.interactions += 1;
243
274
 
244
275
  totals.totalTokens += totalTokens;
245
276
  totals.inputTokens += inputTokens;
246
277
  totals.outputTokens += outputTokens;
247
- totals.costUsd += costUsd;
278
+ totals.costUsd += providerCostUsd;
248
279
  totals.interactions += 1;
249
280
  }
250
281
  totals.costUsd = Math.round(totals.costUsd * 1_000_000) / 1_000_000;