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 +1 -1
- package/src/cost/tracker.js +3 -1
- package/src/legacy-cli.js +8 -1
- package/src/scan/generator.js +8 -1
- package/src/session/pricing-ledger.js +272 -0
- package/src/session/usage.js +85 -54
package/package.json
CHANGED
package/src/cost/tracker.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { estimateTokens } from "./tokenizer.js";
|
|
2
2
|
|
|
3
|
-
const
|
|
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@
|
|
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:
|
package/src/scan/generator.js
CHANGED
|
@@ -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@
|
|
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 };
|
package/src/session/usage.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Session usage emitter — records
|
|
3
|
-
*
|
|
4
|
-
* download,
|
|
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
|
|
59
|
-
return
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
193
|
-
|
|
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 +=
|
|
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 +=
|
|
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;
|