@ramarivera/coding-agent-langfuse 0.1.15 → 0.1.17
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 +12 -1
- package/dist/backfill.d.ts +10 -2
- package/dist/backfill.js +98 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,18 @@ npx @ramarivera/coding-agent-langfuse@latest \
|
|
|
18
18
|
--agents claude,codex,grok,pi,opencode \
|
|
19
19
|
--endpoint https://langfuse.ai.roxasroot.net/otel/v1/traces \
|
|
20
20
|
--state "$HOME/.local/state/coding-agent-langfuse/backfill-v6.json" \
|
|
21
|
-
--batch-size
|
|
21
|
+
--batch-size 10
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Run live incremental forwarding without putting inference behind a gateway:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npx @ramarivera/coding-agent-langfuse@latest \
|
|
28
|
+
--agents codex \
|
|
29
|
+
--endpoint https://langfuse.ai.roxasroot.net/otel/v1/traces \
|
|
30
|
+
--state "$HOME/.local/state/coding-agent-langfuse/live-codex.json" \
|
|
31
|
+
--batch-size 10 \
|
|
32
|
+
--follow
|
|
22
33
|
```
|
|
23
34
|
|
|
24
35
|
The importer is fail-fast: the first failed OTLP POST stops the run, prints the
|
package/dist/backfill.d.ts
CHANGED
|
@@ -33,6 +33,9 @@ type BackfillOptions = {
|
|
|
33
33
|
statePath: string;
|
|
34
34
|
homeDir: string;
|
|
35
35
|
dryRun: boolean;
|
|
36
|
+
follow: boolean;
|
|
37
|
+
pollIntervalMs: number;
|
|
38
|
+
idleExitAfterMs?: number;
|
|
36
39
|
limit?: number;
|
|
37
40
|
sinceMs?: number;
|
|
38
41
|
untilMs?: number;
|
|
@@ -50,6 +53,10 @@ type RunSummary = {
|
|
|
50
53
|
endpoint: string;
|
|
51
54
|
statePath: string;
|
|
52
55
|
};
|
|
56
|
+
type FollowSummary = RunSummary & {
|
|
57
|
+
iterations: number;
|
|
58
|
+
follow: true;
|
|
59
|
+
};
|
|
53
60
|
declare function parseArgs(argv: string[]): BackfillOptions;
|
|
54
61
|
declare function codexEvents(homeDir: string): BackfillEvent[];
|
|
55
62
|
declare function claudeEvents(homeDir: string): BackfillEvent[];
|
|
@@ -60,5 +67,6 @@ declare function fingerprint(event: BackfillEvent): string;
|
|
|
60
67
|
declare function toOtlp(events: BackfillEvent[]): Record<string, unknown>;
|
|
61
68
|
declare function discoverEvents(options: BackfillOptions): BackfillEvent[];
|
|
62
69
|
declare function run(options: BackfillOptions): Promise<RunSummary>;
|
|
63
|
-
declare function
|
|
64
|
-
|
|
70
|
+
declare function follow(options: BackfillOptions): Promise<FollowSummary>;
|
|
71
|
+
declare function main(argv?: string[]): Promise<RunSummary | FollowSummary>;
|
|
72
|
+
export { type BackfillEvent, type BackfillOptions, claudeEvents, codexEvents, discoverEvents, fingerprint, follow, grokEvents, main, opencodeEvents, parseArgs, piEvents, run, toOtlp, };
|
package/dist/backfill.js
CHANGED
|
@@ -5,7 +5,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSy
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
const allAgents = ["claude", "codex", "grok", "opencode", "pi"];
|
|
8
|
-
const importIdentityVersion = "
|
|
8
|
+
const importIdentityVersion = "v8-cached-input-token-split";
|
|
9
9
|
const defaultEndpoint = "https://langfuse.ai.roxasroot.net/otel/v1/traces";
|
|
10
10
|
const deadRemoteEndpoint = "http://langfuse.ai.roxasroot.net:14318/v1/traces";
|
|
11
11
|
const defaultStatePath = join(homedir(), ".local/state/coding-agent-langfuse/backfill-v6.json");
|
|
@@ -21,6 +21,9 @@ Options:
|
|
|
21
21
|
--until ISO_OR_MS Only import events before or at this timestamp
|
|
22
22
|
--limit N Stop after N unsent events
|
|
23
23
|
--batch-size N OTLP spans per POST (default: 50)
|
|
24
|
+
--follow Keep scanning and sending newly written events
|
|
25
|
+
--poll-interval-ms N Delay between --follow scans (default: 5000)
|
|
26
|
+
--idle-exit-after-ms N Stop --follow after this much time without new sends
|
|
24
27
|
--dry-run Discover and dedupe without sending or mutating state
|
|
25
28
|
--help Show this help
|
|
26
29
|
`;
|
|
@@ -34,6 +37,9 @@ function parseArgs(argv) {
|
|
|
34
37
|
let sinceMs;
|
|
35
38
|
let untilMs;
|
|
36
39
|
let batchSize = 50;
|
|
40
|
+
let follow = false;
|
|
41
|
+
let pollIntervalMs = 5_000;
|
|
42
|
+
let idleExitAfterMs;
|
|
37
43
|
const agents = new Set(allAgents);
|
|
38
44
|
for (let i = 0; i < argv.length; i++) {
|
|
39
45
|
const arg = argv[i];
|
|
@@ -75,6 +81,15 @@ function parseArgs(argv) {
|
|
|
75
81
|
else if (arg === "--batch-size") {
|
|
76
82
|
batchSize = Number.parseInt(next(), 10);
|
|
77
83
|
}
|
|
84
|
+
else if (arg === "--follow") {
|
|
85
|
+
follow = true;
|
|
86
|
+
}
|
|
87
|
+
else if (arg === "--poll-interval-ms") {
|
|
88
|
+
pollIntervalMs = Number.parseInt(next(), 10);
|
|
89
|
+
}
|
|
90
|
+
else if (arg === "--idle-exit-after-ms") {
|
|
91
|
+
idleExitAfterMs = Number.parseInt(next(), 10);
|
|
92
|
+
}
|
|
78
93
|
else if (arg === "--since") {
|
|
79
94
|
sinceMs = parseTime(next());
|
|
80
95
|
}
|
|
@@ -88,6 +103,13 @@ function parseArgs(argv) {
|
|
|
88
103
|
if (!Number.isFinite(batchSize) || batchSize < 1) {
|
|
89
104
|
throw new Error("--batch-size must be a positive integer");
|
|
90
105
|
}
|
|
106
|
+
if (!Number.isFinite(pollIntervalMs) || pollIntervalMs < 1) {
|
|
107
|
+
throw new Error("--poll-interval-ms must be a positive integer");
|
|
108
|
+
}
|
|
109
|
+
if (idleExitAfterMs !== undefined &&
|
|
110
|
+
(!Number.isFinite(idleExitAfterMs) || idleExitAfterMs < 1)) {
|
|
111
|
+
throw new Error("--idle-exit-after-ms must be a positive integer");
|
|
112
|
+
}
|
|
91
113
|
if (limit !== undefined && (!Number.isFinite(limit) || limit < 1)) {
|
|
92
114
|
throw new Error("--limit must be a positive integer");
|
|
93
115
|
}
|
|
@@ -97,6 +119,9 @@ function parseArgs(argv) {
|
|
|
97
119
|
statePath,
|
|
98
120
|
homeDir,
|
|
99
121
|
dryRun,
|
|
122
|
+
follow,
|
|
123
|
+
pollIntervalMs,
|
|
124
|
+
idleExitAfterMs,
|
|
100
125
|
limit,
|
|
101
126
|
sinceMs,
|
|
102
127
|
untilMs,
|
|
@@ -227,9 +252,11 @@ function normalizeUsage(value) {
|
|
|
227
252
|
asNumber(record.completion_tokens),
|
|
228
253
|
reasoning: asNumber(record.reasoning) ??
|
|
229
254
|
asNumber(record.reasoning_tokens) ??
|
|
255
|
+
asNumber(record.reasoning_output_tokens) ??
|
|
230
256
|
asNumber(outputDetails.reasoning_tokens),
|
|
231
257
|
cacheRead: asNumber(record.cacheRead) ??
|
|
232
258
|
asNumber(record.cache_read_input_tokens) ??
|
|
259
|
+
asNumber(record.cached_input_tokens) ??
|
|
233
260
|
asNumber(record.cached_tokens) ??
|
|
234
261
|
asNumber(inputDetails.cached_tokens) ??
|
|
235
262
|
asNumber(cache.read),
|
|
@@ -248,7 +275,6 @@ function normalizeUsage(value) {
|
|
|
248
275
|
const total = (usage.input ?? 0) +
|
|
249
276
|
(usage.output ?? 0) +
|
|
250
277
|
(usage.reasoning ?? 0) +
|
|
251
|
-
(usage.cacheRead ?? 0) +
|
|
252
278
|
(usage.cacheWrite ?? 0);
|
|
253
279
|
if (total > 0)
|
|
254
280
|
usage.total = total;
|
|
@@ -270,8 +296,13 @@ function usageDetails(usage) {
|
|
|
270
296
|
if (!usage)
|
|
271
297
|
return undefined;
|
|
272
298
|
const details = {};
|
|
273
|
-
|
|
274
|
-
|
|
299
|
+
const cachedInput = usage.cacheRead ?? 0;
|
|
300
|
+
const cacheWrite = usage.cacheWrite ?? 0;
|
|
301
|
+
const regularInput = usage.input === undefined
|
|
302
|
+
? undefined
|
|
303
|
+
: Math.max(usage.input - cachedInput - cacheWrite, 0);
|
|
304
|
+
if (regularInput !== undefined)
|
|
305
|
+
details.input = regularInput;
|
|
275
306
|
if (usage.output !== undefined)
|
|
276
307
|
details.output = usage.output;
|
|
277
308
|
if (usage.reasoning !== undefined)
|
|
@@ -313,12 +344,14 @@ function costDetails(usage, model) {
|
|
|
313
344
|
return undefined;
|
|
314
345
|
const rates = pricingForModel(model);
|
|
315
346
|
if (rates) {
|
|
316
|
-
const
|
|
347
|
+
const cachedInput = usage.cacheRead ?? 0;
|
|
348
|
+
const cacheWriteTokens = usage.cacheWrite ?? 0;
|
|
349
|
+
const regularInput = Math.max((usage.input ?? 0) - cachedInput - cacheWriteTokens, 0);
|
|
350
|
+
const input = (regularInput * rates.input) / 1_000_000;
|
|
317
351
|
const output = (((usage.output ?? 0) + (usage.reasoning ?? 0)) * rates.output) /
|
|
318
352
|
1_000_000;
|
|
319
|
-
const cache_read = (
|
|
320
|
-
const cache_write = (
|
|
321
|
-
1_000_000;
|
|
353
|
+
const cache_read = (cachedInput * rates.cacheRead) / 1_000_000;
|
|
354
|
+
const cache_write = (cacheWriteTokens * rates.cacheWrite) / 1_000_000;
|
|
322
355
|
const total = input + output + cache_read + cache_write;
|
|
323
356
|
if (total > 0) {
|
|
324
357
|
return { input, output, cache_read, cache_write, total, source: "estimated" };
|
|
@@ -1027,8 +1060,63 @@ async function run(options) {
|
|
|
1027
1060
|
statePath: options.statePath,
|
|
1028
1061
|
};
|
|
1029
1062
|
}
|
|
1063
|
+
function mergeSummary(base, next) {
|
|
1064
|
+
return {
|
|
1065
|
+
discovered: Object.fromEntries(allAgents.map((agent) => [
|
|
1066
|
+
agent,
|
|
1067
|
+
Math.max(base.discovered[agent] ?? 0, next.discovered[agent] ?? 0),
|
|
1068
|
+
])),
|
|
1069
|
+
sent: base.sent + next.sent,
|
|
1070
|
+
skipped: next.skipped,
|
|
1071
|
+
failed: base.failed + next.failed,
|
|
1072
|
+
notAttempted: next.notAttempted,
|
|
1073
|
+
aborted: base.aborted || next.aborted,
|
|
1074
|
+
...(next.error ? { error: next.error } : base.error ? { error: base.error } : {}),
|
|
1075
|
+
dryRun: base.dryRun,
|
|
1076
|
+
endpoint: base.endpoint,
|
|
1077
|
+
statePath: base.statePath,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
function sleep(ms) {
|
|
1081
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1082
|
+
}
|
|
1083
|
+
async function follow(options) {
|
|
1084
|
+
let iterations = 0;
|
|
1085
|
+
let lastSendOrStartMs = Date.now();
|
|
1086
|
+
let aggregate;
|
|
1087
|
+
while (true) {
|
|
1088
|
+
iterations += 1;
|
|
1089
|
+
const summary = await run(options);
|
|
1090
|
+
aggregate = aggregate === undefined ? summary : mergeSummary(aggregate, summary);
|
|
1091
|
+
if (summary.sent > 0)
|
|
1092
|
+
lastSendOrStartMs = Date.now();
|
|
1093
|
+
if (summary.aborted)
|
|
1094
|
+
break;
|
|
1095
|
+
if (options.idleExitAfterMs !== undefined &&
|
|
1096
|
+
Date.now() - lastSendOrStartMs >= options.idleExitAfterMs) {
|
|
1097
|
+
break;
|
|
1098
|
+
}
|
|
1099
|
+
await sleep(options.pollIntervalMs);
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
...(aggregate ?? {
|
|
1103
|
+
discovered: Object.fromEntries(allAgents.map((agent) => [agent, 0])),
|
|
1104
|
+
sent: 0,
|
|
1105
|
+
skipped: 0,
|
|
1106
|
+
failed: 0,
|
|
1107
|
+
notAttempted: 0,
|
|
1108
|
+
aborted: false,
|
|
1109
|
+
dryRun: options.dryRun,
|
|
1110
|
+
endpoint: options.endpoint,
|
|
1111
|
+
statePath: options.statePath,
|
|
1112
|
+
}),
|
|
1113
|
+
iterations,
|
|
1114
|
+
follow: true,
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1030
1117
|
async function main(argv = process.argv.slice(2)) {
|
|
1031
|
-
const
|
|
1118
|
+
const options = parseArgs(argv);
|
|
1119
|
+
const summary = options.follow ? await follow(options) : await run(options);
|
|
1032
1120
|
console.log(JSON.stringify(summary, null, 2));
|
|
1033
1121
|
return summary;
|
|
1034
1122
|
}
|
|
@@ -1042,4 +1130,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
1042
1130
|
process.exit(1);
|
|
1043
1131
|
}
|
|
1044
1132
|
}
|
|
1045
|
-
export { claudeEvents, codexEvents, discoverEvents, fingerprint, grokEvents, main, opencodeEvents, parseArgs, piEvents, run, toOtlp, };
|
|
1133
|
+
export { claudeEvents, codexEvents, discoverEvents, fingerprint, follow, grokEvents, main, opencodeEvents, parseArgs, piEvents, run, toOtlp, };
|