sentinelayer-cli 0.9.6 → 0.9.7
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/billing/ledger-entry.js +162 -0
- package/src/billing/price-book.js +123 -0
- package/src/billing/session-usage.js +80 -0
- package/src/cost/tracker.js +26 -2
- package/src/review/ai-review.js +70 -16
package/package.json
CHANGED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { redactEventPayload } from "../session/redact.js";
|
|
4
|
+
import { computeProviderCost, PRICE_BOOK_VERSION } from "./price-book.js";
|
|
5
|
+
|
|
6
|
+
export const PRICED_ACTIONS = Object.freeze([
|
|
7
|
+
"audit_run",
|
|
8
|
+
"audit_security",
|
|
9
|
+
"audit_frontend",
|
|
10
|
+
"omargate_deep",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const OMIT_METADATA_KEYS = new Set([
|
|
14
|
+
"content",
|
|
15
|
+
"input",
|
|
16
|
+
"message",
|
|
17
|
+
"messages",
|
|
18
|
+
"output",
|
|
19
|
+
"prompt",
|
|
20
|
+
"prompts",
|
|
21
|
+
"raw",
|
|
22
|
+
"rawtext",
|
|
23
|
+
"response",
|
|
24
|
+
"responses",
|
|
25
|
+
"text",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function normalizeString(value) {
|
|
29
|
+
return String(value || "").trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeTokenCount(value, field) {
|
|
33
|
+
const parsed = Number(value || 0);
|
|
34
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
35
|
+
throw new Error(`${field} must be a non-negative number.`);
|
|
36
|
+
}
|
|
37
|
+
return Math.floor(parsed);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeIsoTimestamp(value, fallbackIso = new Date().toISOString()) {
|
|
41
|
+
const normalized = normalizeString(value);
|
|
42
|
+
if (!normalized) return fallbackIso;
|
|
43
|
+
const epoch = Date.parse(normalized);
|
|
44
|
+
if (!Number.isFinite(epoch)) {
|
|
45
|
+
throw new Error("createdAt must be an ISO timestamp.");
|
|
46
|
+
}
|
|
47
|
+
return new Date(epoch).toISOString();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function stableHash(value) {
|
|
51
|
+
return crypto.createHash("sha256").update(String(value || "")).digest("hex");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sanitizeMetadataValue(value, depth = 0) {
|
|
55
|
+
if (depth > 8) return "[REDACTED]";
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
return value
|
|
58
|
+
.map((item) => sanitizeMetadataValue(item, depth + 1))
|
|
59
|
+
.filter((item) => item !== undefined);
|
|
60
|
+
}
|
|
61
|
+
if (value && typeof value === "object") {
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
64
|
+
const normalizedKey = normalizeString(key).toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
65
|
+
if (!normalizedKey || OMIT_METADATA_KEYS.has(normalizedKey)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const sanitized = sanitizeMetadataValue(inner, depth + 1);
|
|
69
|
+
if (sanitized !== undefined) {
|
|
70
|
+
out[key] = sanitized;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function sanitizeBillingMetadata(metadata = {}) {
|
|
79
|
+
const sanitized = sanitizeMetadataValue(metadata);
|
|
80
|
+
const redacted = redactEventPayload({ payload: sanitized });
|
|
81
|
+
return redacted?.payload && typeof redacted.payload === "object" ? redacted.payload : {};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function buildBillingRunId({
|
|
85
|
+
sessionId,
|
|
86
|
+
invocationTimestamp,
|
|
87
|
+
configHash,
|
|
88
|
+
} = {}) {
|
|
89
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
90
|
+
const normalizedTimestamp = normalizeString(invocationTimestamp);
|
|
91
|
+
const normalizedConfigHash = normalizeString(configHash);
|
|
92
|
+
if (!normalizedSessionId) throw new Error("sessionId is required.");
|
|
93
|
+
if (!normalizedTimestamp) throw new Error("invocationTimestamp is required.");
|
|
94
|
+
if (!normalizedConfigHash) throw new Error("configHash is required.");
|
|
95
|
+
return stableHash(`${normalizedSessionId}|${normalizedTimestamp}|${normalizedConfigHash}`).slice(0, 16);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildCallIdempotencyKey({ runId, callIndex = 0 } = {}) {
|
|
99
|
+
const normalizedRunId = normalizeString(runId);
|
|
100
|
+
if (!normalizedRunId) throw new Error("runId is required.");
|
|
101
|
+
const normalizedCallIndex = normalizeTokenCount(callIndex, "callIndex");
|
|
102
|
+
return `${normalizedRunId}:${normalizedCallIndex}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function buildLedgerEntry({
|
|
106
|
+
sessionId,
|
|
107
|
+
agentId,
|
|
108
|
+
action,
|
|
109
|
+
model,
|
|
110
|
+
inputTokens = 0,
|
|
111
|
+
outputTokens = 0,
|
|
112
|
+
idempotencyKey,
|
|
113
|
+
billingTier = "internal",
|
|
114
|
+
metadata = {},
|
|
115
|
+
createdAt = "",
|
|
116
|
+
} = {}) {
|
|
117
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
118
|
+
const normalizedAgentId = normalizeString(agentId);
|
|
119
|
+
const normalizedAction = normalizeString(action);
|
|
120
|
+
const normalizedModel = normalizeString(model);
|
|
121
|
+
const normalizedIdempotencyKey = normalizeString(idempotencyKey);
|
|
122
|
+
if (!normalizedSessionId) throw new Error("sessionId is required.");
|
|
123
|
+
if (!normalizedAgentId) throw new Error("agentId is required.");
|
|
124
|
+
if (!normalizedAction) throw new Error("action is required.");
|
|
125
|
+
if (!normalizedModel) throw new Error("model is required.");
|
|
126
|
+
if (!normalizedIdempotencyKey) throw new Error("idempotencyKey is required.");
|
|
127
|
+
|
|
128
|
+
const priced = computeProviderCost({
|
|
129
|
+
model: normalizedModel,
|
|
130
|
+
inputTokens,
|
|
131
|
+
outputTokens,
|
|
132
|
+
});
|
|
133
|
+
const createdIso = normalizeIsoTimestamp(createdAt);
|
|
134
|
+
return {
|
|
135
|
+
ledgerEntryId: `bill_${stableHash(normalizedIdempotencyKey).slice(0, 16)}`,
|
|
136
|
+
schema: "billing/v1",
|
|
137
|
+
sessionId: normalizedSessionId,
|
|
138
|
+
agentId: normalizedAgentId,
|
|
139
|
+
action: normalizedAction,
|
|
140
|
+
model: normalizedModel,
|
|
141
|
+
canonicalModel: priced.canonicalModel,
|
|
142
|
+
priceBookVersion: PRICE_BOOK_VERSION,
|
|
143
|
+
inputTokens: priced.inputTokens,
|
|
144
|
+
outputTokens: priced.outputTokens,
|
|
145
|
+
totalTokens: priced.totalTokens,
|
|
146
|
+
providerCostUsd: priced.providerCostUsd,
|
|
147
|
+
customerCostUsd: null,
|
|
148
|
+
billingTier: normalizeString(billingTier) || "internal",
|
|
149
|
+
idempotencyKey: normalizedIdempotencyKey,
|
|
150
|
+
unpriced: Boolean(priced.unpriced),
|
|
151
|
+
createdAt: createdIso,
|
|
152
|
+
metadata: sanitizeBillingMetadata(metadata),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function countPricedUsageEvents(events = [], pricedActions = PRICED_ACTIONS) {
|
|
157
|
+
const actions = new Set((Array.isArray(pricedActions) ? pricedActions : []).map(normalizeString));
|
|
158
|
+
return (Array.isArray(events) ? events : []).filter((event) => {
|
|
159
|
+
const payload = event?.payload || {};
|
|
160
|
+
return event?.event === "session_usage" && payload.schema === "billing/v1" && actions.has(normalizeString(payload.action));
|
|
161
|
+
}).length;
|
|
162
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export const PRICE_BOOK_VERSION = "2026-05-19";
|
|
2
|
+
|
|
3
|
+
export const PRICE_BOOK_SOURCES = Object.freeze({
|
|
4
|
+
openai: "https://platform.openai.com/docs/pricing",
|
|
5
|
+
openaiCodex: "https://developers.openai.com/api/docs/models/gpt-5.3-codex",
|
|
6
|
+
anthropic: "https://docs.anthropic.com/en/docs/about-claude/pricing",
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const RATE = (inputPerMTok, outputPerMTok, source, aliases = []) =>
|
|
10
|
+
Object.freeze({
|
|
11
|
+
inputPerMTok,
|
|
12
|
+
outputPerMTok,
|
|
13
|
+
currency: "USD",
|
|
14
|
+
source,
|
|
15
|
+
aliases,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const PRICE_BOOK = Object.freeze({
|
|
19
|
+
"gpt-5.4-mini": RATE(0.75, 4.5, "openai"),
|
|
20
|
+
"gpt-5.3-codex": RATE(1.75, 14, "openaiCodex"),
|
|
21
|
+
"gpt-5.2-codex": RATE(1.75, 14, "openai"),
|
|
22
|
+
"gpt-5.1-codex": RATE(1.25, 10, "openai"),
|
|
23
|
+
"gpt-5-codex": RATE(1.25, 10, "openai"),
|
|
24
|
+
"gpt-4.1-mini": RATE(0.4, 1.6, "openai"),
|
|
25
|
+
"claude-opus-4.1": RATE(15, 75, "anthropic", ["claude-opus-4-7"]),
|
|
26
|
+
"claude-opus-4": RATE(15, 75, "anthropic", ["claude-opus-4-6"]),
|
|
27
|
+
"claude-sonnet-4": RATE(3, 15, "anthropic", ["claude-sonnet-4-6"]),
|
|
28
|
+
"claude-sonnet-3.7": RATE(3, 15, "anthropic"),
|
|
29
|
+
"claude-sonnet-3.5": RATE(3, 15, "anthropic"),
|
|
30
|
+
"claude-haiku-3.5": RATE(0.8, 4, "anthropic"),
|
|
31
|
+
"claude-haiku-3": RATE(0.25, 1.25, "anthropic"),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function normalizeString(value) {
|
|
35
|
+
return String(value || "").trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeModel(value) {
|
|
39
|
+
return normalizeString(value).toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeTokenCount(value, field) {
|
|
43
|
+
const parsed = Number(value || 0);
|
|
44
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
45
|
+
throw new Error(`${field} must be a non-negative number.`);
|
|
46
|
+
}
|
|
47
|
+
return Math.floor(parsed);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function round6(value) {
|
|
51
|
+
return Math.round((Number(value || 0) + Number.EPSILON) * 1_000_000) / 1_000_000;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ALIAS_TO_MODEL = Object.freeze(
|
|
55
|
+
Object.entries(PRICE_BOOK).reduce((acc, [model, rate]) => {
|
|
56
|
+
acc[model] = model;
|
|
57
|
+
for (const alias of rate.aliases || []) {
|
|
58
|
+
acc[normalizeModel(alias)] = model;
|
|
59
|
+
}
|
|
60
|
+
return acc;
|
|
61
|
+
}, {}),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
export function resolvePriceBookRate(model) {
|
|
65
|
+
const requestedModel = normalizeString(model);
|
|
66
|
+
const normalized = normalizeModel(requestedModel);
|
|
67
|
+
const canonicalModel = ALIAS_TO_MODEL[normalized] || normalized;
|
|
68
|
+
const rate = PRICE_BOOK[canonicalModel] || null;
|
|
69
|
+
if (!rate) {
|
|
70
|
+
return {
|
|
71
|
+
model: requestedModel,
|
|
72
|
+
canonicalModel: normalized || requestedModel,
|
|
73
|
+
rate: null,
|
|
74
|
+
unpriced: true,
|
|
75
|
+
priceBookVersion: PRICE_BOOK_VERSION,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
model: requestedModel || canonicalModel,
|
|
80
|
+
canonicalModel,
|
|
81
|
+
rate,
|
|
82
|
+
unpriced: false,
|
|
83
|
+
priceBookVersion: PRICE_BOOK_VERSION,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function computeProviderCost({
|
|
88
|
+
model,
|
|
89
|
+
inputTokens = 0,
|
|
90
|
+
outputTokens = 0,
|
|
91
|
+
} = {}) {
|
|
92
|
+
const input = normalizeTokenCount(inputTokens, "inputTokens");
|
|
93
|
+
const output = normalizeTokenCount(outputTokens, "outputTokens");
|
|
94
|
+
const resolved = resolvePriceBookRate(model);
|
|
95
|
+
if (resolved.unpriced) {
|
|
96
|
+
return {
|
|
97
|
+
model: resolved.model,
|
|
98
|
+
canonicalModel: resolved.canonicalModel,
|
|
99
|
+
priceBookVersion: PRICE_BOOK_VERSION,
|
|
100
|
+
providerCostUsd: null,
|
|
101
|
+
unpriced: true,
|
|
102
|
+
inputTokens: input,
|
|
103
|
+
outputTokens: output,
|
|
104
|
+
totalTokens: input + output,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const providerCostUsd = round6(
|
|
108
|
+
(input / 1_000_000) * resolved.rate.inputPerMTok +
|
|
109
|
+
(output / 1_000_000) * resolved.rate.outputPerMTok,
|
|
110
|
+
);
|
|
111
|
+
return {
|
|
112
|
+
model: resolved.model,
|
|
113
|
+
canonicalModel: resolved.canonicalModel,
|
|
114
|
+
priceBookVersion: PRICE_BOOK_VERSION,
|
|
115
|
+
providerCostUsd,
|
|
116
|
+
unpriced: false,
|
|
117
|
+
inputTokens: input,
|
|
118
|
+
outputTokens: output,
|
|
119
|
+
totalTokens: input + output,
|
|
120
|
+
currency: resolved.rate.currency,
|
|
121
|
+
source: resolved.rate.source,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
|
|
3
|
+
import { createAgentEvent } from "../events/schema.js";
|
|
4
|
+
import { createSession } from "../session/store.js";
|
|
5
|
+
import { appendToStream } from "../session/stream.js";
|
|
6
|
+
import { buildLedgerEntry } from "./ledger-entry.js";
|
|
7
|
+
|
|
8
|
+
const SESSION_USAGE_EVENT = "session_usage";
|
|
9
|
+
|
|
10
|
+
function normalizeString(value) {
|
|
11
|
+
return String(value || "").trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function appendUsageEvent(sessionId, envelope, { targetPath, syncRemote }) {
|
|
15
|
+
return appendToStream(sessionId, envelope, {
|
|
16
|
+
targetPath,
|
|
17
|
+
syncRemote,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function recordSessionUsage(
|
|
22
|
+
sessionId,
|
|
23
|
+
params = {},
|
|
24
|
+
{
|
|
25
|
+
targetPath = process.cwd(),
|
|
26
|
+
// Billing events originate in the CLI and must reach the API for server-side
|
|
27
|
+
// ledger/quota projection; this intentionally defaults to remote sync.
|
|
28
|
+
syncRemote = true,
|
|
29
|
+
ensureLocalSession = true,
|
|
30
|
+
append = appendUsageEvent,
|
|
31
|
+
} = {},
|
|
32
|
+
) {
|
|
33
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
34
|
+
if (!normalizedSessionId) throw new Error("sessionId is required.");
|
|
35
|
+
const entry = buildLedgerEntry({
|
|
36
|
+
...params,
|
|
37
|
+
sessionId: normalizedSessionId,
|
|
38
|
+
});
|
|
39
|
+
const envelope = createAgentEvent({
|
|
40
|
+
event: SESSION_USAGE_EVENT,
|
|
41
|
+
sessionId: normalizedSessionId,
|
|
42
|
+
agentId: entry.agentId,
|
|
43
|
+
agentModel: entry.model,
|
|
44
|
+
payload: {
|
|
45
|
+
...entry,
|
|
46
|
+
costUsd: entry.providerCostUsd ?? 0,
|
|
47
|
+
usage: {
|
|
48
|
+
totalTokens: entry.totalTokens,
|
|
49
|
+
inputTokens: entry.inputTokens,
|
|
50
|
+
outputTokens: entry.outputTokens,
|
|
51
|
+
costUsd: entry.providerCostUsd ?? 0,
|
|
52
|
+
providerCostUsd: entry.providerCostUsd,
|
|
53
|
+
customerCostUsd: entry.customerCostUsd,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
ts: entry.createdAt,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await append(normalizedSessionId, envelope, { targetPath, syncRemote });
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (!ensureLocalSession || !/was not found/i.test(error?.message || "")) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
await createSession({
|
|
66
|
+
sessionId: normalizedSessionId,
|
|
67
|
+
targetPath,
|
|
68
|
+
title: `Billing usage ${normalizedSessionId}`,
|
|
69
|
+
createdAt: entry.createdAt,
|
|
70
|
+
lastInteractionAt: entry.createdAt,
|
|
71
|
+
});
|
|
72
|
+
await append(normalizedSessionId, envelope, { targetPath, syncRemote });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
event: SESSION_USAGE_EVENT,
|
|
78
|
+
ledgerEntry: entry,
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/cost/tracker.js
CHANGED
|
@@ -6,8 +6,16 @@ const DEFAULT_MODEL_PRICING = Object.freeze({
|
|
|
6
6
|
outputPerMillionUsd: 10.0,
|
|
7
7
|
}),
|
|
8
8
|
"gpt-5.3-codex": Object.freeze({
|
|
9
|
-
inputPerMillionUsd: 1.
|
|
10
|
-
outputPerMillionUsd:
|
|
9
|
+
inputPerMillionUsd: 1.75,
|
|
10
|
+
outputPerMillionUsd: 14.0,
|
|
11
|
+
}),
|
|
12
|
+
"gpt-5.4-mini": Object.freeze({
|
|
13
|
+
inputPerMillionUsd: 0.75,
|
|
14
|
+
outputPerMillionUsd: 4.5,
|
|
15
|
+
}),
|
|
16
|
+
"gpt-4.1-mini": Object.freeze({
|
|
17
|
+
inputPerMillionUsd: 0.4,
|
|
18
|
+
outputPerMillionUsd: 1.6,
|
|
11
19
|
}),
|
|
12
20
|
"claude-sonnet-4": Object.freeze({
|
|
13
21
|
inputPerMillionUsd: 3.0,
|
|
@@ -21,6 +29,14 @@ const DEFAULT_MODEL_PRICING = Object.freeze({
|
|
|
21
29
|
inputPerMillionUsd: 3.0,
|
|
22
30
|
outputPerMillionUsd: 15.0,
|
|
23
31
|
}),
|
|
32
|
+
"claude-opus-4": Object.freeze({
|
|
33
|
+
inputPerMillionUsd: 15.0,
|
|
34
|
+
outputPerMillionUsd: 75.0,
|
|
35
|
+
}),
|
|
36
|
+
"claude-opus-4.1": Object.freeze({
|
|
37
|
+
inputPerMillionUsd: 15.0,
|
|
38
|
+
outputPerMillionUsd: 75.0,
|
|
39
|
+
}),
|
|
24
40
|
"claude-opus-4-6": Object.freeze({
|
|
25
41
|
inputPerMillionUsd: 15.0,
|
|
26
42
|
outputPerMillionUsd: 75.0,
|
|
@@ -29,6 +45,14 @@ const DEFAULT_MODEL_PRICING = Object.freeze({
|
|
|
29
45
|
inputPerMillionUsd: 15.0,
|
|
30
46
|
outputPerMillionUsd: 75.0,
|
|
31
47
|
}),
|
|
48
|
+
"claude-haiku-3.5": Object.freeze({
|
|
49
|
+
inputPerMillionUsd: 0.8,
|
|
50
|
+
outputPerMillionUsd: 4.0,
|
|
51
|
+
}),
|
|
52
|
+
"claude-haiku-3": Object.freeze({
|
|
53
|
+
inputPerMillionUsd: 0.25,
|
|
54
|
+
outputPerMillionUsd: 1.25,
|
|
55
|
+
}),
|
|
32
56
|
"gemini-2.5-pro": Object.freeze({
|
|
33
57
|
inputPerMillionUsd: 2.5,
|
|
34
58
|
outputPerMillionUsd: 10.0,
|
package/src/review/ai-review.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import fsp from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
createMultiProviderApiClient,
|
|
6
7
|
resolveModel,
|
|
7
8
|
resolveProvider,
|
|
8
9
|
} from "../ai/client.js";
|
|
10
|
+
import { computeProviderCost } from "../billing/price-book.js";
|
|
11
|
+
import { buildBillingRunId, buildCallIdempotencyKey } from "../billing/ledger-entry.js";
|
|
12
|
+
import { recordSessionUsage } from "../billing/session-usage.js";
|
|
9
13
|
import { loadConfig } from "../config/service.js";
|
|
10
14
|
import { evaluateBudget } from "../cost/budget.js";
|
|
11
15
|
import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
|
|
12
|
-
import { estimateModelCost } from "../cost/tracker.js";
|
|
13
16
|
import { estimateTokens } from "../cost/tokenizer.js";
|
|
14
17
|
import { appendRunEvent, deriveStopClassFromBudget } from "../telemetry/ledger.js";
|
|
15
18
|
|
|
@@ -329,21 +332,22 @@ export function buildAiReviewPrompt({
|
|
|
329
332
|
}
|
|
330
333
|
|
|
331
334
|
function maybeEstimateModelCost({ modelId, inputTokens, outputTokens }) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
335
|
+
const priced = computeProviderCost({
|
|
336
|
+
model: modelId,
|
|
337
|
+
inputTokens,
|
|
338
|
+
outputTokens,
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
costUsd: priced.providerCostUsd ?? 0,
|
|
342
|
+
providerCostUsd: priced.providerCostUsd,
|
|
343
|
+
pricingFound: !priced.unpriced,
|
|
344
|
+
priceBookVersion: priced.priceBookVersion,
|
|
345
|
+
canonicalModel: priced.canonicalModel,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function stableConfigHash(value = {}) {
|
|
350
|
+
return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
347
351
|
}
|
|
348
352
|
|
|
349
353
|
function composeAiReviewMarkdown({
|
|
@@ -529,6 +533,7 @@ export async function runAiReviewLayer({
|
|
|
529
533
|
});
|
|
530
534
|
|
|
531
535
|
const startedAt = Date.now();
|
|
536
|
+
const startedAtIso = new Date(startedAt).toISOString();
|
|
532
537
|
const responseText = dryRun
|
|
533
538
|
? buildDryRunResponse({
|
|
534
539
|
deterministicSummary: deterministic?.summary || {},
|
|
@@ -643,6 +648,53 @@ export async function runAiReviewLayer({
|
|
|
643
648
|
}
|
|
644
649
|
);
|
|
645
650
|
|
|
651
|
+
let sessionUsageLedger = null;
|
|
652
|
+
try {
|
|
653
|
+
const billingRunId = buildBillingRunId({
|
|
654
|
+
sessionId: normalizedSessionId,
|
|
655
|
+
invocationTimestamp: startedAtIso,
|
|
656
|
+
configHash: stableConfigHash({
|
|
657
|
+
sourceCommand: "review",
|
|
658
|
+
layer: "ai_reasoning",
|
|
659
|
+
provider: resolvedProvider,
|
|
660
|
+
model: resolvedModel,
|
|
661
|
+
mode: normalizedMode,
|
|
662
|
+
runId: normalizedRunId,
|
|
663
|
+
dryRun: Boolean(dryRun),
|
|
664
|
+
}),
|
|
665
|
+
});
|
|
666
|
+
sessionUsageLedger = await recordSessionUsage(
|
|
667
|
+
normalizedSessionId,
|
|
668
|
+
{
|
|
669
|
+
agentId: "audit-orchestrator",
|
|
670
|
+
action: "audit_run",
|
|
671
|
+
model: resolvedModel,
|
|
672
|
+
inputTokens,
|
|
673
|
+
outputTokens,
|
|
674
|
+
idempotencyKey: buildCallIdempotencyKey({ runId: billingRunId, callIndex: 0 }),
|
|
675
|
+
billingTier: "internal",
|
|
676
|
+
createdAt: startedAtIso,
|
|
677
|
+
metadata: {
|
|
678
|
+
sourceCommand: "review",
|
|
679
|
+
layer: "ai_reasoning",
|
|
680
|
+
provider: resolvedProvider,
|
|
681
|
+
mode: normalizedMode,
|
|
682
|
+
runId: normalizedRunId,
|
|
683
|
+
dryRun: Boolean(dryRun),
|
|
684
|
+
pricingFound: modelCost.pricingFound,
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
targetPath: normalizedTargetPath,
|
|
689
|
+
},
|
|
690
|
+
);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
sessionUsageLedger = {
|
|
693
|
+
ok: false,
|
|
694
|
+
reason: error instanceof Error ? error.message : String(error || "session_usage_failed"),
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
646
698
|
let stopTelemetry = null;
|
|
647
699
|
if (budget.blocking) {
|
|
648
700
|
stopTelemetry = await appendRunEvent(
|
|
@@ -704,6 +756,7 @@ export async function runAiReviewLayer({
|
|
|
704
756
|
dryRun: Boolean(dryRun),
|
|
705
757
|
usage,
|
|
706
758
|
pricingFound: modelCost.pricingFound,
|
|
759
|
+
sessionUsageLedger,
|
|
707
760
|
budget,
|
|
708
761
|
deterministicSummary,
|
|
709
762
|
aiSummary,
|
|
@@ -756,6 +809,7 @@ export async function runAiReviewLayer({
|
|
|
756
809
|
usageEventId: usageTelemetry.event.eventId,
|
|
757
810
|
stopEventId: stopTelemetry?.event?.eventId || null,
|
|
758
811
|
},
|
|
812
|
+
billing: sessionUsageLedger,
|
|
759
813
|
};
|
|
760
814
|
}
|
|
761
815
|
|