sentinelayer-cli 0.17.1 → 0.18.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
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,260 @@
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
+
10
+ function n(value) {
11
+ return String(value == null ? "" : value).trim();
12
+ }
13
+
14
+ function object(value) {
15
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
16
+ }
17
+
18
+ function nonNegativeNumber(value) {
19
+ if (value == null || value === "") return null;
20
+ const parsed = Number(value);
21
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
22
+ }
23
+
24
+ function nonNegativeInt(value) {
25
+ const parsed = nonNegativeNumber(value);
26
+ return parsed == null ? null : Math.floor(parsed);
27
+ }
28
+
29
+ function money(value) {
30
+ const parsed = nonNegativeNumber(value);
31
+ return parsed == null ? null : Math.round(parsed * 1_000_000) / 1_000_000;
32
+ }
33
+
34
+ function pick(sources, keys) {
35
+ for (const source of sources) {
36
+ const bag = object(source);
37
+ for (const key of keys) {
38
+ if (bag[key] != null && bag[key] !== "") return bag[key];
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function pickText(sources, keys) {
45
+ return n(pick(sources, keys));
46
+ }
47
+
48
+ function pickInt(sources, keys) {
49
+ return nonNegativeInt(pick(sources, keys)) ?? 0;
50
+ }
51
+
52
+ function pickMoney(sources, keys) {
53
+ return money(pick(sources, keys));
54
+ }
55
+
56
+ function roundUsd(value) {
57
+ return Math.round(Number(value || 0) * 1_000_000) / 1_000_000;
58
+ }
59
+
60
+ export function createSessionUsageLedgerId({
61
+ sessionId = "",
62
+ agentId = "",
63
+ action = "",
64
+ idempotencyKey = "",
65
+ } = {}) {
66
+ const digest = createHash("sha256")
67
+ .update([sessionId, agentId, action, idempotencyKey].map(n).join("\x1f"))
68
+ .digest("hex")
69
+ .slice(0, 32);
70
+ return `bill_${digest}`;
71
+ }
72
+
73
+ function fallbackIdempotencyKey({ sessionId, event, agentId, action, model, totalTokens }) {
74
+ const sequence = n(event.sequenceId ?? event.sequence_id);
75
+ if (sequence) return `seq:${sequence}`;
76
+ const timestamp = n(event.ts || event.timestamp);
77
+ const interaction = n(object(event.payload).interactionId || object(event.payload).interaction_id);
78
+ const source = [sessionId, timestamp, agentId, action, model, totalTokens, interaction].join("\x1f");
79
+ return `event:${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
80
+ }
81
+
82
+ function providerCostFromPriceBook({ model, inputTokens, outputTokens, explicitProviderCost }) {
83
+ if (explicitProviderCost != null) {
84
+ return { providerCostUsd: explicitProviderCost, unpriced: false };
85
+ }
86
+ if (inputTokens <= 0 && outputTokens <= 0) {
87
+ return { providerCostUsd: 0, unpriced: false };
88
+ }
89
+ try {
90
+ return {
91
+ providerCostUsd: estimateModelCost({ modelId: model, inputTokens, outputTokens }),
92
+ unpriced: false,
93
+ };
94
+ } catch {
95
+ return { providerCostUsd: 0, unpriced: true };
96
+ }
97
+ }
98
+
99
+ export function buildUsageLedgerEntry(
100
+ event,
101
+ { sessionId = "", priceBookVersion = DEFAULT_PRICE_BOOK_VERSION, billingTier = "unknown" } = {},
102
+ ) {
103
+ const kind = n(event?.event || event?.type);
104
+ if (kind !== SESSION_USAGE_EVENT) return null;
105
+
106
+ const payload = object(event?.payload);
107
+ const usage = object(payload.usage);
108
+ const prompt = object(payload.prompt);
109
+ const response = object(payload.response);
110
+ const agent = object(event?.agent);
111
+ const sources = [payload, usage];
112
+
113
+ const agentId =
114
+ pickText(sources, ["agentId", "agent_id"]) ||
115
+ n(agent.id || event?.agentId) ||
116
+ "unknown";
117
+ const model =
118
+ pickText(sources, ["model", "modelId", "model_id"]) ||
119
+ n(agent.model || event?.agentModel) ||
120
+ "unknown";
121
+ const action =
122
+ pickText(sources, ["action", "operation", "kind", "billingAction", "billing_action"]) ||
123
+ "agent_message";
124
+ const resolvedPriceBook =
125
+ pickText(sources, ["priceBookVersion", "price_book_version", "pricingVersion", "pricing_version"]) ||
126
+ priceBookVersion;
127
+ const resolvedBillingTier =
128
+ pickText(sources, ["billingTier", "billing_tier", "tier"]) ||
129
+ n(billingTier) ||
130
+ "unknown";
131
+
132
+ const inputTokens =
133
+ pickInt(sources, ["inputTokens", "input_tokens", "tokensIn", "tokens_in", "promptTokens", "prompt_tokens"]) ||
134
+ pickInt([prompt], ["tokens", "tokenCount", "token_count"]);
135
+ const outputTokens =
136
+ pickInt(sources, ["outputTokens", "output_tokens", "tokensOut", "tokens_out", "completionTokens", "completion_tokens"]) ||
137
+ pickInt([response], ["tokens", "tokenCount", "token_count"]);
138
+ const explicitTotalTokens = pickInt(sources, ["totalTokens", "total_tokens", "tokens", "tokenTotal", "token_total"]);
139
+ const totalTokens = explicitTotalTokens || inputTokens + outputTokens;
140
+ const explicitProviderCost = pickMoney(sources, ["providerCostUsd", "provider_cost_usd", "costUsd", "cost_usd", "cost"]);
141
+ const customerCostUsd = pickMoney(sources, ["customerCostUsd", "customer_cost_usd", "billableCostUsd", "billable_cost_usd"]);
142
+ const { providerCostUsd, unpriced } = providerCostFromPriceBook({
143
+ model,
144
+ inputTokens,
145
+ outputTokens,
146
+ explicitProviderCost,
147
+ });
148
+
149
+ const idempotencyKey =
150
+ pickText(sources, ["idempotencyKey", "idempotency_key", "runKey", "run_key"]) ||
151
+ pickText(sources, ["interactionId", "interaction_id"]) ||
152
+ fallbackIdempotencyKey({ sessionId, event, agentId, action, model, totalTokens });
153
+ const ledgerEntryId =
154
+ pickText(sources, ["ledgerEntryId", "ledger_entry_id", "billingEventId", "billing_event_id"]) ||
155
+ createSessionUsageLedgerId({ sessionId, agentId, action, idempotencyKey });
156
+
157
+ return {
158
+ ledgerEntryId,
159
+ idempotencyKey,
160
+ sessionId: n(sessionId),
161
+ agentId,
162
+ action,
163
+ model,
164
+ priceBookVersion: resolvedPriceBook,
165
+ billingTier: resolvedBillingTier,
166
+ provider: pickText(sources, ["provider", "providerName", "provider_name"]),
167
+ inputTokens,
168
+ outputTokens,
169
+ totalTokens,
170
+ providerCostUsd: roundUsd(providerCostUsd),
171
+ customerCostUsd,
172
+ unpriced,
173
+ timestamp: n(event?.ts || event?.timestamp),
174
+ sequenceId: nonNegativeInt(event?.sequenceId ?? event?.sequence_id),
175
+ };
176
+ }
177
+
178
+ function newRollup(label) {
179
+ return {
180
+ label,
181
+ entries: 0,
182
+ inputTokens: 0,
183
+ outputTokens: 0,
184
+ totalTokens: 0,
185
+ providerCostUsd: 0,
186
+ customerCostUsd: 0,
187
+ hasCustomerCost: false,
188
+ unpriced: 0,
189
+ };
190
+ }
191
+
192
+ function addToRollup(rollup, entry) {
193
+ rollup.entries += 1;
194
+ rollup.inputTokens += entry.inputTokens;
195
+ rollup.outputTokens += entry.outputTokens;
196
+ rollup.totalTokens += entry.totalTokens;
197
+ rollup.providerCostUsd += entry.providerCostUsd;
198
+ if (entry.customerCostUsd != null) {
199
+ rollup.customerCostUsd += entry.customerCostUsd;
200
+ rollup.hasCustomerCost = true;
201
+ }
202
+ if (entry.unpriced) rollup.unpriced += 1;
203
+ }
204
+
205
+ function finalizeRollup(rollup) {
206
+ rollup.providerCostUsd = roundUsd(rollup.providerCostUsd);
207
+ rollup.customerCostUsd = roundUsd(rollup.customerCostUsd);
208
+ return rollup;
209
+ }
210
+
211
+ export function buildSessionUsageLedger(events = [], options = {}) {
212
+ if (!Array.isArray(events)) {
213
+ throw new Error("events must be an array.");
214
+ }
215
+ const entries = [];
216
+ const totals = newRollup("session");
217
+ const perAgent = new Map();
218
+ const perAction = new Map();
219
+ const priceBookVersions = new Set();
220
+ const seenKeys = new Set();
221
+ let duplicatesSkipped = 0;
222
+
223
+ for (const event of events) {
224
+ const entry = buildUsageLedgerEntry(event, options);
225
+ if (!entry) continue;
226
+ const dedupeKeys = [
227
+ entry.idempotencyKey ? `idem:${entry.idempotencyKey}` : "",
228
+ entry.ledgerEntryId ? `ledger:${entry.ledgerEntryId}` : "",
229
+ ].filter(Boolean);
230
+ if (dedupeKeys.some((dedupeKey) => seenKeys.has(dedupeKey))) {
231
+ duplicatesSkipped += 1;
232
+ continue;
233
+ }
234
+ for (const dedupeKey of dedupeKeys) seenKeys.add(dedupeKey);
235
+ entries.push(entry);
236
+ priceBookVersions.add(entry.priceBookVersion);
237
+ addToRollup(totals, entry);
238
+
239
+ if (!perAgent.has(entry.agentId)) perAgent.set(entry.agentId, newRollup(entry.agentId));
240
+ addToRollup(perAgent.get(entry.agentId), entry);
241
+
242
+ if (!perAction.has(entry.action)) perAction.set(entry.action, newRollup(entry.action));
243
+ addToRollup(perAction.get(entry.action), entry);
244
+ }
245
+
246
+ finalizeRollup(totals);
247
+ for (const rollup of perAgent.values()) finalizeRollup(rollup);
248
+ for (const rollup of perAction.values()) finalizeRollup(rollup);
249
+
250
+ return {
251
+ entries,
252
+ totals,
253
+ perAgent,
254
+ perAction,
255
+ priceBookVersions: [...priceBookVersions].sort(),
256
+ duplicatesSkipped,
257
+ };
258
+ }
259
+
260
+ export { DEFAULT_PRICE_BOOK_VERSION };
@@ -41,6 +41,12 @@ import process from "node:process";
41
41
  import { randomUUID } from "node:crypto";
42
42
 
43
43
  import { createAgentEvent } from "../events/schema.js";
44
+ import { estimateModelCost } from "../cost/tracker.js";
45
+ import {
46
+ DEFAULT_PRICE_BOOK_VERSION,
47
+ buildSessionUsageLedger,
48
+ createSessionUsageLedgerId,
49
+ } from "./pricing-ledger.js";
44
50
  import { resolveSessionPaths } from "./paths.js";
45
51
  import { appendToStream } from "./stream.js";
46
52
 
@@ -55,19 +61,8 @@ function num(value) {
55
61
  return Number.isFinite(v) && v >= 0 ? v : 0;
56
62
  }
57
63
 
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;
64
+ function money(value) {
65
+ return Math.round(num(value) * 1_000_000) / 1_000_000;
71
66
  }
72
67
 
73
68
  function clipText(text, max = 4000) {
@@ -76,6 +71,17 @@ function clipText(text, max = 4000) {
76
71
  return `${s.slice(0, max)}…`;
77
72
  }
78
73
 
74
+ function computedCost({ model, inputTokens, outputTokens }) {
75
+ try {
76
+ return {
77
+ costUsd: estimateModelCost({ modelId: model, inputTokens, outputTokens }),
78
+ unpriced: false,
79
+ };
80
+ } catch {
81
+ return { costUsd: 0, unpriced: inputTokens + outputTokens > 0 };
82
+ }
83
+ }
84
+
79
85
  /**
80
86
  * Emit a `session_usage` event into the session's NDJSON stream.
81
87
  *
@@ -87,7 +93,12 @@ function clipText(text, max = 4000) {
87
93
  * @param {number} [params.inputTokens]
88
94
  * @param {number} [params.outputTokens]
89
95
  * @param {number} [params.costUsd]
96
+ * @param {number} [params.customerCostUsd]
90
97
  * @param {number} [params.durationMs]
98
+ * @param {string} [params.action]
99
+ * @param {string} [params.provider]
100
+ * @param {string} [params.billingTier]
101
+ * @param {string} [params.priceBookVersion]
91
102
  * @param {string} [params.prompt] full prompt text (clipped)
92
103
  * @param {string} [params.response] full response text (clipped)
93
104
  * @param {string} [params.interactionId] opaque id for cross-event correlation
@@ -102,8 +113,13 @@ export async function emitLLMInteraction(
102
113
  role = "",
103
114
  inputTokens = 0,
104
115
  outputTokens = 0,
105
- costUsd = 0,
116
+ costUsd = undefined,
117
+ customerCostUsd = undefined,
106
118
  durationMs = 0,
119
+ action = "agent_message",
120
+ provider = "",
121
+ billingTier = "unknown",
122
+ priceBookVersion = DEFAULT_PRICE_BOOK_VERSION,
107
123
  prompt = "",
108
124
  response = "",
109
125
  interactionId = "",
@@ -121,20 +137,44 @@ export async function emitLLMInteraction(
121
137
  const inT = Math.floor(num(inputTokens));
122
138
  const outT = Math.floor(num(outputTokens));
123
139
  const totalT = inT + outT;
124
- const cost = Math.round(num(costUsd) * 1_000_000) / 1_000_000;
140
+ const model = n(agentModel) || "unknown";
141
+ const providedCost = costUsd != null && costUsd !== "";
142
+ const estimate = providedCost
143
+ ? { costUsd: num(costUsd), unpriced: false }
144
+ : computedCost({ model, inputTokens: inT, outputTokens: outT });
145
+ const cost = money(estimate.costUsd);
146
+ const customerCost = customerCostUsd == null || customerCostUsd === "" ? null : money(customerCostUsd);
147
+ const actionName = n(action) || "agent_message";
148
+ const tier = n(billingTier) || "unknown";
149
+ const priceBook = n(priceBookVersion) || DEFAULT_PRICE_BOOK_VERSION;
150
+ const ledgerEntryId = createSessionUsageLedgerId({
151
+ sessionId: paths.sessionId,
152
+ agentId: aid,
153
+ action: actionName,
154
+ idempotencyKey: id,
155
+ });
125
156
 
126
157
  const promptText = clipText(prompt);
127
158
  const responseText = clipText(response);
128
159
 
129
160
  const payload = {
130
161
  interactionId: id,
162
+ idempotencyKey: id,
163
+ ledgerEntryId,
131
164
  agentId: aid,
132
- model: n(agentModel) || "unknown",
165
+ model,
133
166
  role: n(role) || "observer",
167
+ action: actionName,
168
+ provider: n(provider) || undefined,
169
+ billingTier: tier,
170
+ priceBookVersion: priceBook,
134
171
  inputTokens: inT,
135
172
  outputTokens: outT,
136
173
  totalTokens: totalT,
137
174
  costUsd: cost,
175
+ providerCostUsd: cost,
176
+ customerCostUsd: customerCost ?? undefined,
177
+ unpriced: estimate.unpriced,
138
178
  durationMs: Math.max(0, Math.floor(num(durationMs))),
139
179
  prompt: { tokens: inT, chars: promptText.length },
140
180
  response: {
@@ -147,15 +187,24 @@ export async function emitLLMInteraction(
147
187
  usage: {
148
188
  totalTokens: totalT,
149
189
  costUsd: cost,
190
+ providerCostUsd: cost,
191
+ customerCostUsd: customerCost ?? undefined,
150
192
  inputTokens: inT,
151
193
  outputTokens: outT,
194
+ action: actionName,
195
+ provider: n(provider) || undefined,
196
+ billingTier: tier,
197
+ priceBookVersion: priceBook,
198
+ ledgerEntryId,
199
+ idempotencyKey: id,
200
+ unpriced: estimate.unpriced,
152
201
  },
153
202
  };
154
203
 
155
204
  const envelope = createAgentEvent({
156
205
  event: SESSION_USAGE_EVENT,
157
206
  agentId: aid,
158
- agentModel: n(agentModel) || "unknown",
207
+ agentModel: model,
159
208
  sessionId: paths.sessionId,
160
209
  payload,
161
210
  ts,
@@ -165,6 +214,7 @@ export async function emitLLMInteraction(
165
214
  return {
166
215
  event: SESSION_USAGE_EVENT,
167
216
  interactionId: id,
217
+ ledgerEntryId,
168
218
  totalTokens: totalT,
169
219
  costUsd: cost,
170
220
  };
@@ -181,6 +231,7 @@ export async function emitLLMInteraction(
181
231
  * }}
182
232
  */
183
233
  export function aggregateSessionUsage(events = []) {
234
+ const ledger = buildSessionUsageLedger(events);
184
235
  const perAgent = new Map();
185
236
  const totals = {
186
237
  totalTokens: 0,
@@ -189,37 +240,8 @@ export function aggregateSessionUsage(events = []) {
189
240
  costUsd: 0,
190
241
  interactions: 0,
191
242
  };
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
- ]);
243
+ for (const entry of ledger.entries) {
244
+ const { agentId, model, inputTokens, outputTokens, totalTokens, providerCostUsd } = entry;
223
245
  if (!perAgent.has(agentId)) {
224
246
  perAgent.set(agentId, {
225
247
  agentId,
@@ -238,13 +260,13 @@ export function aggregateSessionUsage(events = []) {
238
260
  record.totalTokens += totalTokens;
239
261
  record.inputTokens += inputTokens;
240
262
  record.outputTokens += outputTokens;
241
- record.costUsd += costUsd;
263
+ record.costUsd += providerCostUsd;
242
264
  record.interactions += 1;
243
265
 
244
266
  totals.totalTokens += totalTokens;
245
267
  totals.inputTokens += inputTokens;
246
268
  totals.outputTokens += outputTokens;
247
- totals.costUsd += costUsd;
269
+ totals.costUsd += providerCostUsd;
248
270
  totals.interactions += 1;
249
271
  }
250
272
  totals.costUsd = Math.round(totals.costUsd * 1_000_000) / 1_000_000;