praxis-agent 0.16.1 → 0.17.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/README.md +2 -1
- package/dist/application/background-agent-manager.d.ts +7 -1
- package/dist/application/background-agent-manager.js +148 -5
- package/dist/application/metered-model-completion.d.ts +15 -0
- package/dist/application/metered-model-completion.js +76 -0
- package/dist/application/model-compactor.js +25 -1
- package/dist/application/session-cost-tracker.d.ts +50 -0
- package/dist/application/session-cost-tracker.js +254 -0
- package/dist/application/session-service.d.ts +13 -0
- package/dist/application/session-service.js +594 -69
- package/dist/application/subagent-service.d.ts +11 -1
- package/dist/application/subagent-service.js +41 -1
- package/dist/application/tool-use-summary.d.ts +7 -2
- package/dist/application/tool-use-summary.js +42 -10
- package/dist/application/workflow-manager.d.ts +9 -4
- package/dist/application/workflow-manager.js +151 -7
- package/dist/cli/interactive.d.ts +5 -1
- package/dist/cli/interactive.js +244 -15
- package/dist/cli/protocol.d.ts +11 -0
- package/dist/cli/protocol.js +16 -1
- package/dist/cli/tui/claude-command-inventory.d.ts +2 -2
- package/dist/cli/tui/claude-command-inventory.js +2 -2
- package/dist/cli/tui/config-dashboard.d.ts +2 -9
- package/dist/cli/tui/config-dashboard.js +6 -6
- package/dist/cli/tui/cost-summary.d.ts +34 -0
- package/dist/cli/tui/cost-summary.js +192 -0
- package/dist/cli/tui/doctor-dashboard.d.ts +15 -0
- package/dist/cli/tui/doctor-dashboard.js +81 -0
- package/dist/cli/tui/slash-commands.js +10 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +192 -15
- package/dist/core/compaction.d.ts +2 -0
- package/dist/core/runtime.d.ts +38 -1
- package/dist/core/runtime.js +265 -11
- package/dist/persistence/claude-cost-state-store.d.ts +48 -0
- package/dist/persistence/claude-cost-state-store.js +335 -0
- package/dist/providers/anthropic-compatible.js +34 -9
- package/dist/providers/fallback-provider.d.ts +2 -1
- package/dist/providers/fallback-provider.js +28 -3
- package/dist/tools/line-changes.d.ts +8 -0
- package/dist/tools/line-changes.js +35 -0
- package/dist/tools/local-tools.js +30 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -103,7 +103,8 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
103
103
|
skills/hooks flow, provider-free per-session `/color` prompt-bar styling,
|
|
104
104
|
`/mcp`, `/memory` shared instruction and auto-memory
|
|
105
105
|
access, and live extension-reload controls,
|
|
106
|
-
cursor/history composer,
|
|
106
|
+
cursor/history composer, provider-free `/cost` usage and pricing summaries,
|
|
107
|
+
interactive `/doctor` diagnostics, per-session model/effort/permission controls,
|
|
107
108
|
context/status/skill/task dashboards, prompt stash and continuation shortcuts,
|
|
108
109
|
filterable `@` file and agent references, composer undo, `Ctrl+G` external
|
|
109
110
|
editing, shared `/keybindings` creation/editing and supported-action remapping,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { ModelUsage } from '../core/runtime.js';
|
|
1
|
+
import type { ModelUsage, ModelUsageByModel } from '../core/runtime.js';
|
|
2
2
|
export interface BackgroundAgentRunResult {
|
|
3
3
|
text: string;
|
|
4
4
|
usage: ModelUsage;
|
|
5
|
+
modelUsage?: ModelUsageByModel;
|
|
6
|
+
durationApiMs?: number;
|
|
7
|
+
durationApiWithoutRetriesMs?: number;
|
|
5
8
|
toolUseCount: number;
|
|
6
9
|
durationMs: number;
|
|
7
10
|
isolationPath?: string;
|
|
@@ -57,6 +60,9 @@ export declare class BackgroundAgentManager {
|
|
|
57
60
|
}): Promise<{
|
|
58
61
|
messages: string[];
|
|
59
62
|
usage: ModelUsage;
|
|
63
|
+
modelUsage?: ModelUsageByModel;
|
|
64
|
+
durationApiMs?: number;
|
|
65
|
+
durationApiWithoutRetriesMs?: number;
|
|
60
66
|
}>;
|
|
61
67
|
private start;
|
|
62
68
|
private resolveOptional;
|
|
@@ -34,6 +34,118 @@ function waitBounded(operation, milliseconds) {
|
|
|
34
34
|
});
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
+
const modelUsageCounterFields = [
|
|
38
|
+
'inputTokens',
|
|
39
|
+
'outputTokens',
|
|
40
|
+
'cacheReadInputTokens',
|
|
41
|
+
'cacheCreationInputTokens',
|
|
42
|
+
'webSearchRequests',
|
|
43
|
+
];
|
|
44
|
+
const modelUsageMetadataFields = ['contextWindow', 'maxOutputTokens'];
|
|
45
|
+
function assertValidModelUsageEntry(model, usage) {
|
|
46
|
+
if (model.trim() === '') {
|
|
47
|
+
throw new Error('Model usage breakdown contains a blank model name');
|
|
48
|
+
}
|
|
49
|
+
for (const field of modelUsageCounterFields) {
|
|
50
|
+
const value = usage[field];
|
|
51
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
|
|
52
|
+
throw new Error(`Model usage for "${model}" has an invalid ${field} counter`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const field of modelUsageMetadataFields) {
|
|
56
|
+
const value = usage[field];
|
|
57
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 1)) {
|
|
58
|
+
throw new Error(`Model usage for "${model}" has an invalid ${field} metadata value`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function mergeModelUsageMetadata(model, left, right) {
|
|
63
|
+
const contextWindow = mergeModelUsageMetadataField(model, 'contextWindow', left.contextWindow, right.contextWindow);
|
|
64
|
+
const maxOutputTokens = mergeModelUsageMetadataField(model, 'maxOutputTokens', left.maxOutputTokens, right.maxOutputTokens);
|
|
65
|
+
return {
|
|
66
|
+
...(contextWindow === undefined ? {} : { contextWindow }),
|
|
67
|
+
...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function mergeModelUsageMetadataField(model, field, left, right) {
|
|
71
|
+
if (left === undefined)
|
|
72
|
+
return right;
|
|
73
|
+
if (right === undefined)
|
|
74
|
+
return left;
|
|
75
|
+
if (left !== right) {
|
|
76
|
+
throw new Error(`Model usage for "${model}" has conflicting ${field} values: ${left} vs ${right}`);
|
|
77
|
+
}
|
|
78
|
+
return left;
|
|
79
|
+
}
|
|
80
|
+
function addUsageChecked(model, left, right) {
|
|
81
|
+
const inputTokens = left.inputTokens + right.inputTokens;
|
|
82
|
+
const outputTokens = left.outputTokens + right.outputTokens;
|
|
83
|
+
const cacheReadInputTokens = (left.cacheReadInputTokens ?? 0) + (right.cacheReadInputTokens ?? 0);
|
|
84
|
+
const cacheCreationInputTokens = (left.cacheCreationInputTokens ?? 0) + (right.cacheCreationInputTokens ?? 0);
|
|
85
|
+
const webSearchRequests = (left.webSearchRequests ?? 0) + (right.webSearchRequests ?? 0);
|
|
86
|
+
if (!Number.isSafeInteger(inputTokens) ||
|
|
87
|
+
!Number.isSafeInteger(outputTokens) ||
|
|
88
|
+
!Number.isSafeInteger(cacheReadInputTokens) ||
|
|
89
|
+
!Number.isSafeInteger(cacheCreationInputTokens) ||
|
|
90
|
+
!Number.isSafeInteger(webSearchRequests)) {
|
|
91
|
+
throw new Error('Model usage total overflow');
|
|
92
|
+
}
|
|
93
|
+
// Aggregates without a model stay counter-only; per-model rows merge their
|
|
94
|
+
// capability metadata with conflict rejection.
|
|
95
|
+
const metadata = model === undefined ? {} : mergeModelUsageMetadata(model, left, right);
|
|
96
|
+
return {
|
|
97
|
+
inputTokens,
|
|
98
|
+
outputTokens,
|
|
99
|
+
...(cacheReadInputTokens === 0 ? {} : { cacheReadInputTokens }),
|
|
100
|
+
...(cacheCreationInputTokens === 0 ? {} : { cacheCreationInputTokens }),
|
|
101
|
+
...(webSearchRequests === 0 ? {} : { webSearchRequests }),
|
|
102
|
+
...metadata,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function assertValidResultUsage(usage) {
|
|
106
|
+
for (const field of modelUsageCounterFields) {
|
|
107
|
+
const value = usage[field];
|
|
108
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
|
|
109
|
+
throw new Error(`Model usage total has an invalid ${field} counter`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function addApiDuration(value, total, field) {
|
|
114
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
115
|
+
throw new TypeError(`${field} must be a finite nonnegative number`);
|
|
116
|
+
}
|
|
117
|
+
const next = total + value;
|
|
118
|
+
if (!Number.isFinite(next) || next < 0) {
|
|
119
|
+
throw new TypeError(`${field} total overflow`);
|
|
120
|
+
}
|
|
121
|
+
return next;
|
|
122
|
+
}
|
|
123
|
+
function mergeModelUsageEntry(map, model, usage) {
|
|
124
|
+
assertValidModelUsageEntry(model, usage);
|
|
125
|
+
const existing = map.get(model);
|
|
126
|
+
if (existing === undefined) {
|
|
127
|
+
map.set(model, { ...usage });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
map.set(model, addUsageChecked(model, existing, usage));
|
|
131
|
+
}
|
|
132
|
+
function mergeToolModelUsage(map, breakdown) {
|
|
133
|
+
const entries = Object.entries(breakdown);
|
|
134
|
+
if (entries.length === 0)
|
|
135
|
+
return;
|
|
136
|
+
// Validate every key, counter, metadata, and merged sum before adding any
|
|
137
|
+
// entry from this breakdown so malformed or conflicting data never merges
|
|
138
|
+
// partially.
|
|
139
|
+
for (const [model, usage] of entries) {
|
|
140
|
+
assertValidModelUsageEntry(model, usage);
|
|
141
|
+
const existing = map.get(model);
|
|
142
|
+
if (existing !== undefined)
|
|
143
|
+
addUsageChecked(model, existing, usage);
|
|
144
|
+
}
|
|
145
|
+
for (const [model, usage] of entries) {
|
|
146
|
+
mergeModelUsageEntry(map, model, usage);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
37
149
|
export class BackgroundAgentManager {
|
|
38
150
|
tasks = new Map();
|
|
39
151
|
names = new Map();
|
|
@@ -203,15 +315,46 @@ export class BackgroundAgentManager {
|
|
|
203
315
|
await Promise.race(running);
|
|
204
316
|
}
|
|
205
317
|
const notifications = [];
|
|
206
|
-
|
|
318
|
+
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
319
|
+
const modelUsageByModel = new Map();
|
|
320
|
+
let durationApiMs = 0;
|
|
321
|
+
let durationApiWithoutRetriesMs = 0;
|
|
322
|
+
let durationSeen = false;
|
|
323
|
+
const consumedTasks = [];
|
|
207
324
|
for (const [, task] of eligibleTasks) {
|
|
208
|
-
|
|
325
|
+
if (task.notifications.length === 0)
|
|
326
|
+
continue;
|
|
327
|
+
for (const notification of task.notifications) {
|
|
209
328
|
notifications.push(this.formatNotification(task, notification));
|
|
210
|
-
|
|
211
|
-
|
|
329
|
+
if (notification.result) {
|
|
330
|
+
assertValidResultUsage(notification.result.usage);
|
|
331
|
+
usage = addUsageChecked(undefined, usage, notification.result.usage);
|
|
332
|
+
}
|
|
333
|
+
if (notification.result?.modelUsage) {
|
|
334
|
+
mergeToolModelUsage(modelUsageByModel, notification.result.modelUsage);
|
|
335
|
+
}
|
|
336
|
+
if (notification.result?.durationApiMs !== undefined ||
|
|
337
|
+
notification.result?.durationApiWithoutRetriesMs !== undefined) {
|
|
338
|
+
durationSeen = true;
|
|
339
|
+
const total = notification.result.durationApiMs ?? 0;
|
|
340
|
+
durationApiMs = addApiDuration(total, durationApiMs, 'durationApiMs');
|
|
341
|
+
durationApiWithoutRetriesMs = addApiDuration(notification.result.durationApiWithoutRetriesMs ?? total, durationApiWithoutRetriesMs, 'durationApiWithoutRetriesMs');
|
|
342
|
+
}
|
|
212
343
|
}
|
|
344
|
+
consumedTasks.push(task);
|
|
213
345
|
}
|
|
214
|
-
|
|
346
|
+
const modelUsage = modelUsageByModel.size === 0
|
|
347
|
+
? undefined
|
|
348
|
+
: Object.fromEntries(modelUsageByModel);
|
|
349
|
+
const result = {
|
|
350
|
+
messages: notifications,
|
|
351
|
+
usage,
|
|
352
|
+
...(modelUsage === undefined ? {} : { modelUsage }),
|
|
353
|
+
...(durationSeen ? { durationApiMs, durationApiWithoutRetriesMs } : {}),
|
|
354
|
+
};
|
|
355
|
+
for (const task of consumedTasks)
|
|
356
|
+
task.notifications.splice(0);
|
|
357
|
+
return result;
|
|
215
358
|
}
|
|
216
359
|
start(task, message, continuation, toolUseId) {
|
|
217
360
|
task.generation += 1;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ModelProvider, type ModelRequest, type ModelToolCall, type ModelUsage } from '../core/runtime.js';
|
|
2
|
+
export interface MeteredModelCompletion {
|
|
3
|
+
text: string;
|
|
4
|
+
toolCalls: readonly ModelToolCall[];
|
|
5
|
+
usage: ModelUsage;
|
|
6
|
+
model?: string;
|
|
7
|
+
durationApiMs: number;
|
|
8
|
+
durationApiWithoutRetriesMs: number;
|
|
9
|
+
}
|
|
10
|
+
export interface MeteredModelRequestOptions {
|
|
11
|
+
onTextDelta?: (delta: string) => void;
|
|
12
|
+
onMetrics?: (metrics: MeteredModelCompletion) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function completeMeteredModelRequest(provider: ModelProvider, request: ModelRequest, options?: MeteredModelRequestOptions): Promise<MeteredModelCompletion>;
|
|
15
|
+
//# sourceMappingURL=metered-model-completion.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { AgentRunCancelledError, } from '../core/runtime.js';
|
|
2
|
+
function nonblankModel(provider) {
|
|
3
|
+
if (provider.model === undefined)
|
|
4
|
+
return undefined;
|
|
5
|
+
return provider.model.trim() === '' ? undefined : provider.model;
|
|
6
|
+
}
|
|
7
|
+
function enrichCapabilityMetadata(usage, value, field) {
|
|
8
|
+
if (usage[field] !== undefined)
|
|
9
|
+
return usage;
|
|
10
|
+
if (value === undefined || !Number.isSafeInteger(value) || value < 1) {
|
|
11
|
+
return usage;
|
|
12
|
+
}
|
|
13
|
+
return { ...usage, [field]: value };
|
|
14
|
+
}
|
|
15
|
+
function enrichedUsage(provider, usage) {
|
|
16
|
+
return enrichCapabilityMetadata(enrichCapabilityMetadata(usage, provider.capabilities.contextWindowTokens, 'contextWindow'), provider.capabilities.maxOutputTokens, 'maxOutputTokens');
|
|
17
|
+
}
|
|
18
|
+
export async function completeMeteredModelRequest(provider, request, options = {}) {
|
|
19
|
+
const { onTextDelta, onMetrics } = options;
|
|
20
|
+
if (request.signal?.aborted)
|
|
21
|
+
throw new AgentRunCancelledError();
|
|
22
|
+
let text = '';
|
|
23
|
+
const toolCalls = [];
|
|
24
|
+
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
25
|
+
let attemptDurationMs;
|
|
26
|
+
let attemptDurationSeen = false;
|
|
27
|
+
const apiStartedAt = performance.now();
|
|
28
|
+
let metrics;
|
|
29
|
+
try {
|
|
30
|
+
for await (const event of provider.complete(request)) {
|
|
31
|
+
if (request.signal?.aborted)
|
|
32
|
+
throw new AgentRunCancelledError();
|
|
33
|
+
if (event.type === 'api-retry')
|
|
34
|
+
continue;
|
|
35
|
+
if (event.type === 'api-attempt-duration') {
|
|
36
|
+
if (attemptDurationSeen) {
|
|
37
|
+
throw new Error('Provider emitted multiple api-attempt-duration events in one attempt');
|
|
38
|
+
}
|
|
39
|
+
attemptDurationSeen = true;
|
|
40
|
+
const { durationMs } = event;
|
|
41
|
+
if (typeof durationMs !== 'number' ||
|
|
42
|
+
!Number.isFinite(durationMs) ||
|
|
43
|
+
durationMs < 0) {
|
|
44
|
+
throw new TypeError('api-attempt-duration durationMs must be a finite nonnegative number');
|
|
45
|
+
}
|
|
46
|
+
attemptDurationMs = durationMs;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (event.type === 'text-delta') {
|
|
50
|
+
text += event.delta;
|
|
51
|
+
onTextDelta?.(event.delta);
|
|
52
|
+
}
|
|
53
|
+
else if (event.type === 'usage') {
|
|
54
|
+
usage = event.usage;
|
|
55
|
+
}
|
|
56
|
+
else if (event.type === 'tool-call') {
|
|
57
|
+
toolCalls.push(event.call);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
const model = nonblankModel(provider);
|
|
63
|
+
const durationApiMs = Math.max(0, performance.now() - apiStartedAt);
|
|
64
|
+
metrics = {
|
|
65
|
+
text,
|
|
66
|
+
toolCalls,
|
|
67
|
+
usage: enrichedUsage(provider, usage),
|
|
68
|
+
...(model === undefined ? {} : { model }),
|
|
69
|
+
durationApiMs,
|
|
70
|
+
durationApiWithoutRetriesMs: attemptDurationMs ?? durationApiMs,
|
|
71
|
+
};
|
|
72
|
+
onMetrics?.(metrics);
|
|
73
|
+
}
|
|
74
|
+
return metrics;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=metered-model-completion.js.map
|
|
@@ -41,6 +41,8 @@ export class ModelCompactor {
|
|
|
41
41
|
const startedAt = Date.now();
|
|
42
42
|
let summary = '';
|
|
43
43
|
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
44
|
+
let attemptDurationMs;
|
|
45
|
+
let attemptDurationSeen = false;
|
|
44
46
|
const providerRequest = {
|
|
45
47
|
messages,
|
|
46
48
|
...(request.signal ? { signal: request.signal } : {}),
|
|
@@ -51,6 +53,20 @@ export class ModelCompactor {
|
|
|
51
53
|
if (event.type === 'tool-call') {
|
|
52
54
|
throw new Error('Compaction model must not call tools');
|
|
53
55
|
}
|
|
56
|
+
if (event.type === 'api-attempt-duration') {
|
|
57
|
+
if (attemptDurationSeen) {
|
|
58
|
+
throw new Error('Provider emitted multiple api-attempt-duration events in one attempt');
|
|
59
|
+
}
|
|
60
|
+
attemptDurationSeen = true;
|
|
61
|
+
const { durationMs } = event;
|
|
62
|
+
if (typeof durationMs !== 'number' ||
|
|
63
|
+
!Number.isFinite(durationMs) ||
|
|
64
|
+
durationMs < 0) {
|
|
65
|
+
throw new TypeError('api-attempt-duration durationMs must be a finite nonnegative number');
|
|
66
|
+
}
|
|
67
|
+
attemptDurationMs = durationMs;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
54
70
|
if (event.type === 'text-delta')
|
|
55
71
|
summary += event.delta;
|
|
56
72
|
else if (event.type === 'usage')
|
|
@@ -64,7 +80,15 @@ export class ModelCompactor {
|
|
|
64
80
|
if (summaryTokens > targetTokens) {
|
|
65
81
|
throw new Error(`Compaction summary exceeded ${targetTokens} tokens (estimated ${summaryTokens})`);
|
|
66
82
|
}
|
|
67
|
-
|
|
83
|
+
const durationMs = Date.now() - startedAt;
|
|
84
|
+
const model = this.provider.model;
|
|
85
|
+
return {
|
|
86
|
+
summary,
|
|
87
|
+
usage,
|
|
88
|
+
durationMs,
|
|
89
|
+
durationWithoutRetriesMs: attemptDurationMs ?? durationMs,
|
|
90
|
+
...(model !== undefined && model.trim() !== '' ? { model } : {}),
|
|
91
|
+
};
|
|
68
92
|
}
|
|
69
93
|
}
|
|
70
94
|
//# sourceMappingURL=model-compactor.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ModelUsage } from '../core/runtime.js';
|
|
2
|
+
import type { ClaudeSessionCostState } from '../persistence/claude-cost-state-store.js';
|
|
3
|
+
export interface ClaudeSessionCostTrackerOptions {
|
|
4
|
+
readonly sessionId: string;
|
|
5
|
+
readonly restored?: ClaudeSessionCostState | null;
|
|
6
|
+
readonly now?: () => number;
|
|
7
|
+
}
|
|
8
|
+
export interface ClaudeSessionTurnInput {
|
|
9
|
+
readonly model: string;
|
|
10
|
+
readonly usage: ModelUsage;
|
|
11
|
+
readonly costUsd?: number;
|
|
12
|
+
readonly webSearchRequests?: number;
|
|
13
|
+
readonly apiDurationMs?: number;
|
|
14
|
+
readonly apiDurationWithoutRetriesMs?: number;
|
|
15
|
+
readonly toolDurationMs?: number;
|
|
16
|
+
readonly linesAdded?: number;
|
|
17
|
+
readonly linesRemoved?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ClaudeLineChangesInput {
|
|
20
|
+
readonly linesAdded?: number;
|
|
21
|
+
readonly linesRemoved?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ClaudeSessionDurationsInput {
|
|
24
|
+
readonly apiDurationMs?: number;
|
|
25
|
+
readonly apiDurationWithoutRetriesMs?: number;
|
|
26
|
+
readonly toolDurationMs?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface ClaudeSessionCostSnapshot extends ClaudeSessionCostState {
|
|
29
|
+
readonly hasUnknownModelCost: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare class ClaudeSessionCostTracker {
|
|
32
|
+
private readonly sessionId;
|
|
33
|
+
private readonly now;
|
|
34
|
+
private readonly modelUsage;
|
|
35
|
+
private totalCostUsd;
|
|
36
|
+
private apiDurationMs;
|
|
37
|
+
private apiDurationWithoutRetriesMs;
|
|
38
|
+
private toolDurationMs;
|
|
39
|
+
private linesAdded;
|
|
40
|
+
private linesRemoved;
|
|
41
|
+
private hasUnknownModelCost;
|
|
42
|
+
private startTime;
|
|
43
|
+
constructor(options: ClaudeSessionCostTrackerOptions);
|
|
44
|
+
recordTurn(input: ClaudeSessionTurnInput): void;
|
|
45
|
+
recordDurations(input: ClaudeSessionDurationsInput): void;
|
|
46
|
+
private durationTotals;
|
|
47
|
+
recordLineChanges(input: ClaudeLineChangesInput): void;
|
|
48
|
+
snapshot(): ClaudeSessionCostSnapshot;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=session-cost-tracker.d.ts.map
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
function requireNonBlank(value, field) {
|
|
2
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
3
|
+
throw new TypeError(`${field} must not be blank`);
|
|
4
|
+
}
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
function requireCounter(value, field) {
|
|
8
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
9
|
+
throw new TypeError(`${field} must be a nonnegative safe integer`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function requireMetric(value, field) {
|
|
14
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
15
|
+
throw new TypeError(`${field} must be a finite nonnegative number`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function requireCounterTotal(value, field) {
|
|
20
|
+
if (!Number.isSafeInteger(value)) {
|
|
21
|
+
throw new TypeError(`${field} total must be a safe integer`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function requireMetricTotal(value, field) {
|
|
25
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
26
|
+
throw new TypeError(`${field} total must be a finite nonnegative number`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function emptyModelUsage() {
|
|
30
|
+
return {
|
|
31
|
+
inputTokens: 0,
|
|
32
|
+
outputTokens: 0,
|
|
33
|
+
cacheReadInputTokens: 0,
|
|
34
|
+
cacheCreationInputTokens: 0,
|
|
35
|
+
webSearchRequests: 0,
|
|
36
|
+
costUsd: 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export class ClaudeSessionCostTracker {
|
|
40
|
+
sessionId;
|
|
41
|
+
now;
|
|
42
|
+
modelUsage = new Map();
|
|
43
|
+
totalCostUsd;
|
|
44
|
+
apiDurationMs;
|
|
45
|
+
apiDurationWithoutRetriesMs;
|
|
46
|
+
toolDurationMs;
|
|
47
|
+
linesAdded;
|
|
48
|
+
linesRemoved;
|
|
49
|
+
hasUnknownModelCost = false;
|
|
50
|
+
startTime;
|
|
51
|
+
constructor(options) {
|
|
52
|
+
this.sessionId = requireNonBlank(options.sessionId, 'sessionId');
|
|
53
|
+
this.now = options.now ?? Date.now;
|
|
54
|
+
const restored = options.restored ?? null;
|
|
55
|
+
if (restored === null) {
|
|
56
|
+
this.totalCostUsd = 0;
|
|
57
|
+
this.apiDurationMs = 0;
|
|
58
|
+
this.apiDurationWithoutRetriesMs = 0;
|
|
59
|
+
this.toolDurationMs = 0;
|
|
60
|
+
this.linesAdded = 0;
|
|
61
|
+
this.linesRemoved = 0;
|
|
62
|
+
this.startTime = this.now();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (restored.sessionId !== this.sessionId) {
|
|
66
|
+
throw new TypeError('restored sessionId does not match the tracker sessionId');
|
|
67
|
+
}
|
|
68
|
+
this.totalCostUsd = restored.totalCostUsd;
|
|
69
|
+
this.apiDurationMs = restored.apiDurationMs;
|
|
70
|
+
this.apiDurationWithoutRetriesMs = restored.apiDurationWithoutRetriesMs;
|
|
71
|
+
this.toolDurationMs = restored.toolDurationMs;
|
|
72
|
+
this.linesAdded = restored.linesAdded;
|
|
73
|
+
this.linesRemoved = restored.linesRemoved;
|
|
74
|
+
for (const [model, usage] of Object.entries(restored.modelUsage)) {
|
|
75
|
+
this.modelUsage.set(model, {
|
|
76
|
+
inputTokens: usage.inputTokens,
|
|
77
|
+
outputTokens: usage.outputTokens,
|
|
78
|
+
cacheReadInputTokens: usage.cacheReadInputTokens,
|
|
79
|
+
cacheCreationInputTokens: usage.cacheCreationInputTokens,
|
|
80
|
+
webSearchRequests: usage.webSearchRequests,
|
|
81
|
+
costUsd: usage.costUsd,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const current = this.now();
|
|
85
|
+
this.startTime =
|
|
86
|
+
restored.wallDurationMs !== 0
|
|
87
|
+
? current - restored.wallDurationMs
|
|
88
|
+
: current;
|
|
89
|
+
}
|
|
90
|
+
recordTurn(input) {
|
|
91
|
+
const model = requireNonBlank(input.model, 'model');
|
|
92
|
+
const inputTokens = requireCounter(input.usage.inputTokens, 'usage.inputTokens');
|
|
93
|
+
const outputTokens = requireCounter(input.usage.outputTokens, 'usage.outputTokens');
|
|
94
|
+
const cacheReadInputTokens = input.usage.cacheReadInputTokens === undefined
|
|
95
|
+
? 0
|
|
96
|
+
: requireCounter(input.usage.cacheReadInputTokens, 'usage.cacheReadInputTokens');
|
|
97
|
+
const cacheCreationInputTokens = input.usage.cacheCreationInputTokens === undefined
|
|
98
|
+
? 0
|
|
99
|
+
: requireCounter(input.usage.cacheCreationInputTokens, 'usage.cacheCreationInputTokens');
|
|
100
|
+
const webSearchRequests = input.webSearchRequests === undefined
|
|
101
|
+
? 0
|
|
102
|
+
: requireCounter(input.webSearchRequests, 'webSearchRequests');
|
|
103
|
+
const costUsd = input.costUsd === undefined
|
|
104
|
+
? undefined
|
|
105
|
+
: requireMetric(input.costUsd, 'costUsd');
|
|
106
|
+
const apiDurationMs = input.apiDurationMs === undefined
|
|
107
|
+
? 0
|
|
108
|
+
: requireMetric(input.apiDurationMs, 'apiDurationMs');
|
|
109
|
+
const apiDurationWithoutRetriesMs = input.apiDurationWithoutRetriesMs === undefined
|
|
110
|
+
? apiDurationMs
|
|
111
|
+
: requireMetric(input.apiDurationWithoutRetriesMs, 'apiDurationWithoutRetriesMs');
|
|
112
|
+
const toolDurationMs = input.toolDurationMs === undefined
|
|
113
|
+
? 0
|
|
114
|
+
: requireMetric(input.toolDurationMs, 'toolDurationMs');
|
|
115
|
+
const linesAdded = input.linesAdded === undefined
|
|
116
|
+
? 0
|
|
117
|
+
: requireCounter(input.linesAdded, 'linesAdded');
|
|
118
|
+
const linesRemoved = input.linesRemoved === undefined
|
|
119
|
+
? 0
|
|
120
|
+
: requireCounter(input.linesRemoved, 'linesRemoved');
|
|
121
|
+
const existing = this.modelUsage.get(model) ?? emptyModelUsage();
|
|
122
|
+
const newInputTokens = existing.inputTokens + inputTokens;
|
|
123
|
+
const newOutputTokens = existing.outputTokens + outputTokens;
|
|
124
|
+
const newCacheReadInputTokens = existing.cacheReadInputTokens + cacheReadInputTokens;
|
|
125
|
+
const newCacheCreationInputTokens = existing.cacheCreationInputTokens + cacheCreationInputTokens;
|
|
126
|
+
const newWebSearchRequests = existing.webSearchRequests + webSearchRequests;
|
|
127
|
+
requireCounterTotal(newInputTokens, 'inputTokens');
|
|
128
|
+
requireCounterTotal(newOutputTokens, 'outputTokens');
|
|
129
|
+
requireCounterTotal(newCacheReadInputTokens, 'cacheReadInputTokens');
|
|
130
|
+
requireCounterTotal(newCacheCreationInputTokens, 'cacheCreationInputTokens');
|
|
131
|
+
requireCounterTotal(newWebSearchRequests, 'webSearchRequests');
|
|
132
|
+
const hasUsage = inputTokens > 0 ||
|
|
133
|
+
outputTokens > 0 ||
|
|
134
|
+
cacheReadInputTokens > 0 ||
|
|
135
|
+
cacheCreationInputTokens > 0 ||
|
|
136
|
+
webSearchRequests > 0;
|
|
137
|
+
const newCostUsd = costUsd === undefined ? existing.costUsd : existing.costUsd + costUsd;
|
|
138
|
+
requireMetricTotal(newCostUsd, 'costUsd');
|
|
139
|
+
const newTotalCostUsd = costUsd === undefined ? this.totalCostUsd : this.totalCostUsd + costUsd;
|
|
140
|
+
requireMetricTotal(newTotalCostUsd, 'totalCostUsd');
|
|
141
|
+
const durationTotals = this.durationTotals({
|
|
142
|
+
apiDurationMs,
|
|
143
|
+
apiDurationWithoutRetriesMs,
|
|
144
|
+
toolDurationMs,
|
|
145
|
+
});
|
|
146
|
+
const newLinesAdded = this.linesAdded + linesAdded;
|
|
147
|
+
const newLinesRemoved = this.linesRemoved + linesRemoved;
|
|
148
|
+
requireCounterTotal(newLinesAdded, 'linesAdded');
|
|
149
|
+
requireCounterTotal(newLinesRemoved, 'linesRemoved');
|
|
150
|
+
// Every metric validated; commit atomically.
|
|
151
|
+
if (costUsd === undefined && hasUsage) {
|
|
152
|
+
this.hasUnknownModelCost = true;
|
|
153
|
+
}
|
|
154
|
+
this.modelUsage.set(model, {
|
|
155
|
+
inputTokens: newInputTokens,
|
|
156
|
+
outputTokens: newOutputTokens,
|
|
157
|
+
cacheReadInputTokens: newCacheReadInputTokens,
|
|
158
|
+
cacheCreationInputTokens: newCacheCreationInputTokens,
|
|
159
|
+
webSearchRequests: newWebSearchRequests,
|
|
160
|
+
costUsd: newCostUsd,
|
|
161
|
+
});
|
|
162
|
+
this.totalCostUsd = newTotalCostUsd;
|
|
163
|
+
this.apiDurationMs = durationTotals.apiDurationMs;
|
|
164
|
+
this.apiDurationWithoutRetriesMs =
|
|
165
|
+
durationTotals.apiDurationWithoutRetriesMs;
|
|
166
|
+
this.toolDurationMs = durationTotals.toolDurationMs;
|
|
167
|
+
this.linesAdded = newLinesAdded;
|
|
168
|
+
this.linesRemoved = newLinesRemoved;
|
|
169
|
+
}
|
|
170
|
+
recordDurations(input) {
|
|
171
|
+
const apiDurationMs = input.apiDurationMs === undefined
|
|
172
|
+
? undefined
|
|
173
|
+
: requireMetric(input.apiDurationMs, 'apiDurationMs');
|
|
174
|
+
const apiDurationWithoutRetriesMs = input.apiDurationWithoutRetriesMs === undefined
|
|
175
|
+
? undefined
|
|
176
|
+
: requireMetric(input.apiDurationWithoutRetriesMs, 'apiDurationWithoutRetriesMs');
|
|
177
|
+
const toolDurationMs = input.toolDurationMs === undefined
|
|
178
|
+
? undefined
|
|
179
|
+
: requireMetric(input.toolDurationMs, 'toolDurationMs');
|
|
180
|
+
// Legacy fallback: a supplied API duration implies the retry-free total
|
|
181
|
+
// when the retry-free value is absent.
|
|
182
|
+
const resolvedApiDurationWithoutRetriesMs = apiDurationMs !== undefined && apiDurationWithoutRetriesMs === undefined
|
|
183
|
+
? apiDurationMs
|
|
184
|
+
: apiDurationWithoutRetriesMs;
|
|
185
|
+
const durationTotals = this.durationTotals({
|
|
186
|
+
apiDurationMs,
|
|
187
|
+
apiDurationWithoutRetriesMs: resolvedApiDurationWithoutRetriesMs,
|
|
188
|
+
toolDurationMs,
|
|
189
|
+
});
|
|
190
|
+
this.apiDurationMs = durationTotals.apiDurationMs;
|
|
191
|
+
this.apiDurationWithoutRetriesMs =
|
|
192
|
+
durationTotals.apiDurationWithoutRetriesMs;
|
|
193
|
+
this.toolDurationMs = durationTotals.toolDurationMs;
|
|
194
|
+
}
|
|
195
|
+
durationTotals(options) {
|
|
196
|
+
const { apiDurationMs, apiDurationWithoutRetriesMs, toolDurationMs } = options;
|
|
197
|
+
const newApiDurationMs = apiDurationMs === undefined
|
|
198
|
+
? this.apiDurationMs
|
|
199
|
+
: this.apiDurationMs + apiDurationMs;
|
|
200
|
+
const newApiDurationWithoutRetriesMs = apiDurationWithoutRetriesMs === undefined
|
|
201
|
+
? this.apiDurationWithoutRetriesMs
|
|
202
|
+
: this.apiDurationWithoutRetriesMs + apiDurationWithoutRetriesMs;
|
|
203
|
+
const newToolDurationMs = toolDurationMs === undefined
|
|
204
|
+
? this.toolDurationMs
|
|
205
|
+
: this.toolDurationMs + toolDurationMs;
|
|
206
|
+
if (apiDurationMs !== undefined) {
|
|
207
|
+
requireMetricTotal(newApiDurationMs, 'apiDurationMs');
|
|
208
|
+
}
|
|
209
|
+
if (apiDurationWithoutRetriesMs !== undefined) {
|
|
210
|
+
requireMetricTotal(newApiDurationWithoutRetriesMs, 'apiDurationWithoutRetriesMs');
|
|
211
|
+
}
|
|
212
|
+
if (toolDurationMs !== undefined) {
|
|
213
|
+
requireMetricTotal(newToolDurationMs, 'toolDurationMs');
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
apiDurationMs: newApiDurationMs,
|
|
217
|
+
apiDurationWithoutRetriesMs: newApiDurationWithoutRetriesMs,
|
|
218
|
+
toolDurationMs: newToolDurationMs,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
recordLineChanges(input) {
|
|
222
|
+
const linesAdded = input.linesAdded === undefined
|
|
223
|
+
? 0
|
|
224
|
+
: requireCounter(input.linesAdded, 'linesAdded');
|
|
225
|
+
const linesRemoved = input.linesRemoved === undefined
|
|
226
|
+
? 0
|
|
227
|
+
: requireCounter(input.linesRemoved, 'linesRemoved');
|
|
228
|
+
const newLinesAdded = this.linesAdded + linesAdded;
|
|
229
|
+
const newLinesRemoved = this.linesRemoved + linesRemoved;
|
|
230
|
+
requireCounterTotal(newLinesAdded, 'linesAdded');
|
|
231
|
+
requireCounterTotal(newLinesRemoved, 'linesRemoved');
|
|
232
|
+
this.linesAdded = newLinesAdded;
|
|
233
|
+
this.linesRemoved = newLinesRemoved;
|
|
234
|
+
}
|
|
235
|
+
snapshot() {
|
|
236
|
+
const modelUsage = {};
|
|
237
|
+
for (const [model, usage] of this.modelUsage) {
|
|
238
|
+
modelUsage[model] = { ...usage };
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
sessionId: this.sessionId,
|
|
242
|
+
totalCostUsd: this.totalCostUsd,
|
|
243
|
+
apiDurationMs: this.apiDurationMs,
|
|
244
|
+
apiDurationWithoutRetriesMs: this.apiDurationWithoutRetriesMs,
|
|
245
|
+
toolDurationMs: this.toolDurationMs,
|
|
246
|
+
wallDurationMs: this.now() - this.startTime,
|
|
247
|
+
linesAdded: this.linesAdded,
|
|
248
|
+
linesRemoved: this.linesRemoved,
|
|
249
|
+
modelUsage,
|
|
250
|
+
hasUnknownModelCost: this.hasUnknownModelCost,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=session-cost-tracker.js.map
|