sentinelayer-cli 0.12.3 → 0.12.4
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 +3 -0
- package/src/billing/llm-session-usage.js +91 -0
- package/src/commands/chat.js +35 -6
- package/src/commands/scan.js +31 -6
- package/src/commands/spec.js +29 -5
package/package.json
CHANGED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { buildBillingRunId, buildCallIdempotencyKey, stableHash } from "./ledger-entry.js";
|
|
2
|
+
import { recordSessionUsage } from "./session-usage.js";
|
|
3
|
+
|
|
4
|
+
function normalizeString(value) {
|
|
5
|
+
return String(value || "").trim();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function nonNegativeInteger(value) {
|
|
9
|
+
const parsed = Number(value || 0);
|
|
10
|
+
if (!Number.isFinite(parsed) || parsed < 0) return 0;
|
|
11
|
+
return Math.floor(parsed);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function usageNumber(value, fallback = 0) {
|
|
15
|
+
const parsed = Number(value);
|
|
16
|
+
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
|
17
|
+
const parsedFallback = Number(fallback || 0);
|
|
18
|
+
return Number.isFinite(parsedFallback) && parsedFallback >= 0 ? parsedFallback : 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function recordCliLlmSessionUsage({
|
|
22
|
+
sessionId,
|
|
23
|
+
agentId,
|
|
24
|
+
action,
|
|
25
|
+
model,
|
|
26
|
+
inputTokens = 0,
|
|
27
|
+
outputTokens = 0,
|
|
28
|
+
startedAtIso = "",
|
|
29
|
+
targetPath,
|
|
30
|
+
billingTier = "internal",
|
|
31
|
+
sourceCommand = "",
|
|
32
|
+
provider = "",
|
|
33
|
+
metadata = {},
|
|
34
|
+
} = {}) {
|
|
35
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
36
|
+
const normalizedAgentId = normalizeString(agentId);
|
|
37
|
+
const normalizedAction = normalizeString(action);
|
|
38
|
+
const normalizedModel = normalizeString(model);
|
|
39
|
+
const createdAt = normalizeString(startedAtIso) || new Date().toISOString();
|
|
40
|
+
const safeInputTokens = nonNegativeInteger(inputTokens);
|
|
41
|
+
const safeOutputTokens = nonNegativeInteger(outputTokens);
|
|
42
|
+
|
|
43
|
+
if (!normalizedSessionId || !normalizedAgentId || !normalizedAction || !normalizedModel) {
|
|
44
|
+
return { ok: false, reason: "missing_session_usage_fields" };
|
|
45
|
+
}
|
|
46
|
+
if (safeInputTokens + safeOutputTokens <= 0) {
|
|
47
|
+
return { ok: false, reason: "zero_tokens" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const configHash = stableHash(
|
|
52
|
+
JSON.stringify({
|
|
53
|
+
action: normalizedAction,
|
|
54
|
+
agentId: normalizedAgentId,
|
|
55
|
+
model: normalizedModel,
|
|
56
|
+
provider: normalizeString(provider),
|
|
57
|
+
sourceCommand: normalizeString(sourceCommand),
|
|
58
|
+
metadata,
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
const billingRunId = buildBillingRunId({
|
|
62
|
+
sessionId: normalizedSessionId,
|
|
63
|
+
invocationTimestamp: createdAt,
|
|
64
|
+
configHash,
|
|
65
|
+
});
|
|
66
|
+
return await recordSessionUsage(
|
|
67
|
+
normalizedSessionId,
|
|
68
|
+
{
|
|
69
|
+
agentId: normalizedAgentId,
|
|
70
|
+
action: normalizedAction,
|
|
71
|
+
model: normalizedModel,
|
|
72
|
+
inputTokens: safeInputTokens,
|
|
73
|
+
outputTokens: safeOutputTokens,
|
|
74
|
+
idempotencyKey: buildCallIdempotencyKey({ runId: billingRunId, callIndex: 0 }),
|
|
75
|
+
billingTier,
|
|
76
|
+
createdAt,
|
|
77
|
+
metadata: {
|
|
78
|
+
sourceCommand,
|
|
79
|
+
provider,
|
|
80
|
+
...metadata,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{ targetPath },
|
|
84
|
+
);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
reason: error instanceof Error ? error.message : String(error || "session_usage_failed"),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/commands/chat.js
CHANGED
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
resolveModel,
|
|
10
10
|
resolveProvider,
|
|
11
11
|
} from "../ai/client.js";
|
|
12
|
+
import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
|
|
12
13
|
import { resolveOutputRoot } from "../config/service.js";
|
|
14
|
+
import { estimateModelCost } from "../cost/tracker.js";
|
|
13
15
|
import { estimateTokens } from "../cost/tokenizer.js";
|
|
14
16
|
|
|
15
17
|
function shouldEmitJson(options, command) {
|
|
@@ -92,7 +94,9 @@ export function registerChatCommand(program) {
|
|
|
92
94
|
});
|
|
93
95
|
|
|
94
96
|
const startedAt = Date.now();
|
|
97
|
+
const startedAtIso = new Date(startedAt).toISOString();
|
|
95
98
|
let responseText = "";
|
|
99
|
+
let invocation = null;
|
|
96
100
|
|
|
97
101
|
if (options.dryRun) {
|
|
98
102
|
responseText = `DRY_RUN_RESPONSE: ${prompt.slice(0, 240)}`;
|
|
@@ -100,7 +104,7 @@ export function registerChatCommand(program) {
|
|
|
100
104
|
const streamEnabled = Boolean(options.stream);
|
|
101
105
|
let streamedText = "";
|
|
102
106
|
const client = createMultiProviderApiClient();
|
|
103
|
-
|
|
107
|
+
invocation = await client.invoke({
|
|
104
108
|
provider,
|
|
105
109
|
model,
|
|
106
110
|
prompt,
|
|
@@ -125,8 +129,31 @@ export function registerChatCommand(program) {
|
|
|
125
129
|
|
|
126
130
|
const durationMs = Date.now() - startedAt;
|
|
127
131
|
const generatedAt = new Date().toISOString();
|
|
128
|
-
const
|
|
129
|
-
const
|
|
132
|
+
const estimatedInputTokens = estimateTokens(prompt, { model });
|
|
133
|
+
const estimatedOutputTokens = estimateTokens(responseText, { model });
|
|
134
|
+
const inputTokens = usageNumber(invocation?.usage?.inputTokens, estimatedInputTokens);
|
|
135
|
+
const outputTokens = usageNumber(invocation?.usage?.outputTokens, estimatedOutputTokens);
|
|
136
|
+
const modelCost = estimateModelCost({ modelId: invocation?.model || model, inputTokens, outputTokens });
|
|
137
|
+
const costUsd = usageNumber(invocation?.usage?.costUsd, modelCost.costUsd);
|
|
138
|
+
|
|
139
|
+
const sessionUsageLedger = options.dryRun
|
|
140
|
+
? { ok: false, reason: "dry_run" }
|
|
141
|
+
: await recordCliLlmSessionUsage({
|
|
142
|
+
sessionId,
|
|
143
|
+
agentId: "chat-cli",
|
|
144
|
+
action: "chat_ask",
|
|
145
|
+
model: invocation?.model || model,
|
|
146
|
+
inputTokens,
|
|
147
|
+
outputTokens,
|
|
148
|
+
startedAtIso,
|
|
149
|
+
targetPath,
|
|
150
|
+
sourceCommand: "chat ask",
|
|
151
|
+
provider: invocation?.provider || provider,
|
|
152
|
+
metadata: {
|
|
153
|
+
streamed: Boolean(options.stream),
|
|
154
|
+
pricingFound: modelCost.pricingFound,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
130
157
|
|
|
131
158
|
await appendTranscriptEntries({
|
|
132
159
|
transcriptPath,
|
|
@@ -154,8 +181,8 @@ export function registerChatCommand(program) {
|
|
|
154
181
|
const payload = {
|
|
155
182
|
command: "chat ask",
|
|
156
183
|
sessionId,
|
|
157
|
-
provider,
|
|
158
|
-
model,
|
|
184
|
+
provider: invocation?.provider || provider,
|
|
185
|
+
model: invocation?.model || model,
|
|
159
186
|
dryRun: Boolean(options.dryRun),
|
|
160
187
|
streamed: Boolean(options.stream),
|
|
161
188
|
transcriptPath,
|
|
@@ -165,8 +192,10 @@ export function registerChatCommand(program) {
|
|
|
165
192
|
inputTokens,
|
|
166
193
|
outputTokens,
|
|
167
194
|
totalTokens: inputTokens + outputTokens,
|
|
195
|
+
costUsd,
|
|
168
196
|
durationMs,
|
|
169
197
|
},
|
|
198
|
+
billing: sessionUsageLedger,
|
|
170
199
|
};
|
|
171
200
|
|
|
172
201
|
if (emitJson) {
|
|
@@ -179,6 +208,6 @@ export function registerChatCommand(program) {
|
|
|
179
208
|
}
|
|
180
209
|
console.log(pc.gray(`session: ${sessionId}`));
|
|
181
210
|
console.log(pc.gray(`transcript: ${transcriptPath}`));
|
|
182
|
-
console.log(pc.gray(`usage: input=${inputTokens} output=${outputTokens} duration_ms=${durationMs}`));
|
|
211
|
+
console.log(pc.gray(`usage: input=${inputTokens} output=${outputTokens} cost_usd=${costUsd.toFixed(6)} duration_ms=${durationMs}`));
|
|
183
212
|
});
|
|
184
213
|
}
|
package/src/commands/scan.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
resolveModel,
|
|
12
12
|
resolveProvider,
|
|
13
13
|
} from "../ai/client.js";
|
|
14
|
+
import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
|
|
14
15
|
import { loadConfig, resolveOutputRoot } from "../config/service.js";
|
|
15
16
|
import { evaluateBudget } from "../cost/budget.js";
|
|
16
17
|
import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
|
|
@@ -585,6 +586,7 @@ export function registerScanCommand(program) {
|
|
|
585
586
|
profile,
|
|
586
587
|
});
|
|
587
588
|
|
|
589
|
+
const startedAtIso = new Date().toISOString();
|
|
588
590
|
const startedAtMs = Date.now();
|
|
589
591
|
const client = createMultiProviderApiClient();
|
|
590
592
|
const response = await client.invoke({
|
|
@@ -615,13 +617,16 @@ export function registerScanCommand(program) {
|
|
|
615
617
|
await fsp.mkdir(path.dirname(reportPath), { recursive: true });
|
|
616
618
|
await fsp.writeFile(reportPath, reportMarkdown, "utf-8");
|
|
617
619
|
|
|
618
|
-
const
|
|
619
|
-
const
|
|
620
|
+
const estimatedInputTokens = estimateTokens(prompt, { model: response.model });
|
|
621
|
+
const estimatedOutputTokens = estimateTokens(aiMarkdown, { model: response.model });
|
|
622
|
+
const inputTokens = usageNumber(response.usage?.inputTokens, estimatedInputTokens);
|
|
623
|
+
const outputTokens = usageNumber(response.usage?.outputTokens, estimatedOutputTokens);
|
|
620
624
|
const modelCost = maybeEstimateModelCost({
|
|
621
625
|
modelId: response.model,
|
|
622
626
|
inputTokens,
|
|
623
627
|
outputTokens,
|
|
624
628
|
});
|
|
629
|
+
const costUsd = usageNumber(response.usage?.costUsd, modelCost.costUsd);
|
|
625
630
|
const sessionId = String(options.sessionId || "scan-ai-precheck").trim() || "scan-ai-precheck";
|
|
626
631
|
|
|
627
632
|
const appendedCost = await appendCostEntry(
|
|
@@ -639,7 +644,7 @@ export function registerScanCommand(program) {
|
|
|
639
644
|
cacheWriteTokens: 0,
|
|
640
645
|
durationMs,
|
|
641
646
|
toolCalls: 1,
|
|
642
|
-
costUsd
|
|
647
|
+
costUsd,
|
|
643
648
|
progressScore: aiMarkdown ? 1 : 0,
|
|
644
649
|
}
|
|
645
650
|
);
|
|
@@ -682,7 +687,7 @@ export function registerScanCommand(program) {
|
|
|
682
687
|
outputTokens,
|
|
683
688
|
cacheReadTokens: 0,
|
|
684
689
|
cacheWriteTokens: 0,
|
|
685
|
-
costUsd
|
|
690
|
+
costUsd,
|
|
686
691
|
durationMs,
|
|
687
692
|
toolCalls: 1,
|
|
688
693
|
},
|
|
@@ -727,9 +732,28 @@ export function registerScanCommand(program) {
|
|
|
727
732
|
invocationId: appendedCost.entry.invocationId,
|
|
728
733
|
},
|
|
729
734
|
}
|
|
730
|
-
|
|
735
|
+
);
|
|
731
736
|
}
|
|
732
737
|
|
|
738
|
+
const sessionUsageLedger = await recordCliLlmSessionUsage({
|
|
739
|
+
sessionId,
|
|
740
|
+
agentId: "scan-precheck",
|
|
741
|
+
action: "scan_precheck",
|
|
742
|
+
model: response.model,
|
|
743
|
+
inputTokens,
|
|
744
|
+
outputTokens,
|
|
745
|
+
startedAtIso,
|
|
746
|
+
targetPath,
|
|
747
|
+
sourceCommand: "scan precheck",
|
|
748
|
+
provider: response.provider,
|
|
749
|
+
metadata: {
|
|
750
|
+
specPath,
|
|
751
|
+
profile,
|
|
752
|
+
policyPackId: activePolicy.selected?.id || "",
|
|
753
|
+
pricingFound: modelCost.pricingFound,
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
|
|
733
757
|
const payload = {
|
|
734
758
|
command: "scan precheck",
|
|
735
759
|
targetPath,
|
|
@@ -749,10 +773,11 @@ export function registerScanCommand(program) {
|
|
|
749
773
|
usage: {
|
|
750
774
|
inputTokens,
|
|
751
775
|
outputTokens,
|
|
752
|
-
costUsd
|
|
776
|
+
costUsd,
|
|
753
777
|
durationMs,
|
|
754
778
|
toolCalls: 1,
|
|
755
779
|
},
|
|
780
|
+
billing: sessionUsageLedger,
|
|
756
781
|
budget,
|
|
757
782
|
cost: {
|
|
758
783
|
filePath: appendedCost.filePath,
|
package/src/commands/spec.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
resolveModel,
|
|
10
10
|
resolveProvider,
|
|
11
11
|
} from "../ai/client.js";
|
|
12
|
+
import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
|
|
12
13
|
import { loadConfig } from "../config/service.js";
|
|
13
14
|
import { evaluateBudget } from "../cost/budget.js";
|
|
14
15
|
import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
|
|
@@ -273,6 +274,7 @@ async function maybeEnhanceSpecWithAi({
|
|
|
273
274
|
ingest,
|
|
274
275
|
});
|
|
275
276
|
|
|
277
|
+
const startedAtIso = new Date().toISOString();
|
|
276
278
|
const startedAtMs = Date.now();
|
|
277
279
|
const client = createMultiProviderApiClient();
|
|
278
280
|
const result = await client.invoke({
|
|
@@ -288,13 +290,16 @@ async function maybeEnhanceSpecWithAi({
|
|
|
288
290
|
const normalizedText = String(result.text || "").trim();
|
|
289
291
|
const enhancedMarkdown = normalizedText || baseSpecMarkdown;
|
|
290
292
|
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
+
const estimatedInputTokens = estimateTokens(prompt, { model: result.model });
|
|
294
|
+
const estimatedOutputTokens = estimateTokens(enhancedMarkdown, { model: result.model });
|
|
295
|
+
const inputTokens = usageNumber(result.usage?.inputTokens, estimatedInputTokens);
|
|
296
|
+
const outputTokens = usageNumber(result.usage?.outputTokens, estimatedOutputTokens);
|
|
293
297
|
const modelCost = maybeEstimateModelCost({
|
|
294
298
|
modelId: result.model,
|
|
295
299
|
inputTokens,
|
|
296
300
|
outputTokens,
|
|
297
301
|
});
|
|
302
|
+
const costUsd = usageNumber(result.usage?.costUsd, modelCost.costUsd);
|
|
298
303
|
|
|
299
304
|
const sessionId = String(options.sessionId || "spec-generate-ai").trim() || "spec-generate-ai";
|
|
300
305
|
const appendedCost = await appendCostEntry(
|
|
@@ -312,7 +317,7 @@ async function maybeEnhanceSpecWithAi({
|
|
|
312
317
|
cacheWriteTokens: 0,
|
|
313
318
|
durationMs,
|
|
314
319
|
toolCalls: 1,
|
|
315
|
-
costUsd
|
|
320
|
+
costUsd,
|
|
316
321
|
progressScore: normalizedText ? 1 : 0,
|
|
317
322
|
}
|
|
318
323
|
);
|
|
@@ -355,7 +360,7 @@ async function maybeEnhanceSpecWithAi({
|
|
|
355
360
|
outputTokens,
|
|
356
361
|
cacheReadTokens: 0,
|
|
357
362
|
cacheWriteTokens: 0,
|
|
358
|
-
costUsd
|
|
363
|
+
costUsd,
|
|
359
364
|
durationMs,
|
|
360
365
|
toolCalls: 1,
|
|
361
366
|
},
|
|
@@ -403,6 +408,24 @@ async function maybeEnhanceSpecWithAi({
|
|
|
403
408
|
);
|
|
404
409
|
}
|
|
405
410
|
|
|
411
|
+
const sessionUsageLedger = await recordCliLlmSessionUsage({
|
|
412
|
+
sessionId,
|
|
413
|
+
agentId: "spec-generator",
|
|
414
|
+
action: "spec_generate_ai",
|
|
415
|
+
model: result.model,
|
|
416
|
+
inputTokens,
|
|
417
|
+
outputTokens,
|
|
418
|
+
startedAtIso,
|
|
419
|
+
targetPath,
|
|
420
|
+
sourceCommand: "spec generate --ai",
|
|
421
|
+
provider: result.provider,
|
|
422
|
+
metadata: {
|
|
423
|
+
template: template?.id || template?.name || "",
|
|
424
|
+
projectType: template?.projectType || "",
|
|
425
|
+
pricingFound: modelCost.pricingFound,
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
|
|
406
429
|
return {
|
|
407
430
|
markdown: enhancedMarkdown,
|
|
408
431
|
ai: {
|
|
@@ -413,10 +436,11 @@ async function maybeEnhanceSpecWithAi({
|
|
|
413
436
|
usage: {
|
|
414
437
|
inputTokens,
|
|
415
438
|
outputTokens,
|
|
416
|
-
costUsd
|
|
439
|
+
costUsd,
|
|
417
440
|
durationMs,
|
|
418
441
|
toolCalls: 1,
|
|
419
442
|
},
|
|
443
|
+
billing: sessionUsageLedger,
|
|
420
444
|
budget,
|
|
421
445
|
cost: {
|
|
422
446
|
filePath: appendedCost.filePath,
|