@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 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 1000
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
@@ -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 main(argv?: string[]): Promise<RunSummary>;
64
- export { type BackfillEvent, type BackfillOptions, claudeEvents, codexEvents, discoverEvents, fingerprint, grokEvents, main, opencodeEvents, parseArgs, piEvents, run, toOtlp, };
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 = "v7-host-independent-session-traces";
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
- if (usage.input !== undefined)
274
- details.input = usage.input;
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 input = ((usage.input ?? 0) * rates.input) / 1_000_000;
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 = ((usage.cacheRead ?? 0) * rates.cacheRead) / 1_000_000;
320
- const cache_write = ((usage.cacheWrite ?? 0) * rates.cacheWrite) /
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 summary = await run(parseArgs(argv));
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, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramarivera/coding-agent-langfuse",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Universal coding-agent Langfuse backfiller and live OTLP helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",