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 +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 +260 -0
- package/src/session/usage.js +72 -50
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,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 };
|
package/src/session/usage.js
CHANGED
|
@@ -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
|
|
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;
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
]);
|
|
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 +=
|
|
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 +=
|
|
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;
|