@ramarivera/coding-agent-langfuse 0.1.39 → 0.1.41

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
@@ -3,9 +3,9 @@
3
3
  Universal coding-agent Langfuse backfiller and OTLP exporter helpers.
4
4
 
5
5
  It imports local histories from Codex, Claude Code, Grok, OpenCode, and Pi into
6
- Langfuse as session traces with child observations. LLM usage records are emitted
7
- as Langfuse generations with model names, usage details, and recorded or
8
- estimated cost details. Tool calls remain child spans under the same session.
6
+ Langfuse as session traces with child observations. LLM usage records are kept
7
+ as observation metadata so historical imports do not create Langfuse billing or
8
+ cost rows. Tool calls remain child spans under the same session.
9
9
 
10
10
  ```sh
11
11
  coding-agent-langfuse-backfill --agents codex,claude,grok,pi,opencode
@@ -112,7 +112,7 @@ npm run test:e2e
112
112
  The e2e suite verifies:
113
113
 
114
114
  - Codex full session traces with messages, reasoning, tool calls, tool results,
115
- usage, and costs
115
+ and usage metadata
116
116
  - Follow mode picking up newly written Codex events
117
117
  - One CLI run posting reconstructable traces for Claude Code, Codex, Grok,
118
118
  OpenCode, and Pi
File without changes
@@ -63,7 +63,9 @@ type FollowSummary = RunSummary & {
63
63
  declare const allAgents: AgentName[];
64
64
  declare const defaultEndpoint = "https://langfuse.ai.roxasroot.net/otel/v1/traces";
65
65
  declare function parseArgs(argv: string[]): BackfillOptions;
66
- declare function codexEvents(homeDir: string): BackfillEvent[];
66
+ declare function codexEvents(homeDir: string, options?: {
67
+ sinceMs?: number;
68
+ }): BackfillEvent[];
67
69
  declare function claudeEvents(homeDir: string): BackfillEvent[];
68
70
  declare function piEvents(homeDir: string): BackfillEvent[];
69
71
  declare function grokEvents(homeDir: string): BackfillEvent[];
package/dist/backfill.js CHANGED
@@ -235,7 +235,7 @@ function listFiles(root, predicate) {
235
235
  }
236
236
  if (stat.isDirectory())
237
237
  stack.push(path);
238
- else if (stat.isFile() && predicate(path))
238
+ else if (stat.isFile() && predicate(path, stat))
239
239
  out.push(path);
240
240
  }
241
241
  }
@@ -374,59 +374,32 @@ function usageDetails(usage) {
374
374
  details.total = usage.total;
375
375
  return Object.keys(details).length > 0 ? details : undefined;
376
376
  }
377
- function pricingForModel(model) {
378
- if (!model)
379
- return undefined;
380
- const normalized = normalizeModelName(model) ?? model;
381
- if (normalized === "kimi-for-coding") {
382
- return { input: 0.95, output: 4.0, cacheRead: 0.16, cacheWrite: 0 };
383
- }
384
- if (normalized.includes("accounts/fireworks/routers/kimi-k2p6-turbo")) {
385
- return { input: 2.0, output: 8.0, cacheRead: 0.30, cacheWrite: 0 };
386
- }
387
- if (normalized.includes("accounts/fireworks/models/deepseek-v4-pro")) {
388
- return { input: 1.74, output: 3.48, cacheRead: 0.15, cacheWrite: 0 };
389
- }
390
- if (normalized.includes("DeepSeek-V4-Pro")) {
391
- return { input: 2.1, output: 4.4, cacheRead: 0.2, cacheWrite: 0 };
392
- }
393
- if (normalized.includes("Kimi-K2.6")) {
394
- return { input: 1.2, output: 4.5, cacheRead: 0.2, cacheWrite: 0 };
395
- }
396
- if (normalized.includes("MiniMax-M2.7")) {
397
- return { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 };
398
- }
399
- return undefined;
400
- }
401
- function costDetails(usage, model) {
402
- if (!usage)
403
- return undefined;
404
- const rates = pricingForModel(model);
405
- if (rates) {
406
- const cachedInput = usage.cacheRead ?? 0;
407
- const cacheWriteTokens = usage.cacheWrite ?? 0;
408
- const regularInput = Math.max((usage.input ?? 0) - cachedInput - cacheWriteTokens, 0);
409
- const input = (regularInput * rates.input) / 1_000_000;
410
- const output = (((usage.output ?? 0) + (usage.reasoning ?? 0)) * rates.output) /
411
- 1_000_000;
412
- const cache_read = (cachedInput * rates.cacheRead) / 1_000_000;
413
- const cache_write = (cacheWriteTokens * rates.cacheWrite) / 1_000_000;
414
- const total = input + output + cache_read + cache_write;
415
- if (total > 0) {
416
- return { input, output, cache_read, cache_write, total, source: "estimated" };
417
- }
418
- }
419
- if (usage.cost !== undefined && usage.cost > 0) {
420
- return { total: usage.cost, source: "recorded" };
421
- }
422
- return undefined;
423
- }
424
377
  function isGenerationEvent(event) {
425
378
  return event.usage !== undefined && event.role !== "user" &&
426
379
  event.role !== "developer" && event.role !== "system";
427
380
  }
428
- function codexEvents(homeDir) {
429
- const files = listFiles(join(homeDir, ".codex/sessions"), (path) => path.endsWith(".jsonl"));
381
+ function codexSessionPathTimeMs(path) {
382
+ const rolloutMatch = path.match(/rollout-(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})/);
383
+ if (rolloutMatch) {
384
+ const [, year, month, day, hour, minute, second] = rolloutMatch;
385
+ return Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
386
+ }
387
+ const folderMatch = path.match(/[\\/]sessions[\\/](\d{4})[\\/](\d{2})[\\/](\d{2})[\\/]/);
388
+ if (!folderMatch)
389
+ return undefined;
390
+ const [, year, month, day] = folderMatch;
391
+ return Date.UTC(Number(year), Number(month) - 1, Number(day));
392
+ }
393
+ function fileMayContainCodexWindow(path, stat, options) {
394
+ if (options.sinceMs === undefined)
395
+ return true;
396
+ if (stat.mtimeMs >= options.sinceMs)
397
+ return true;
398
+ const pathTimeMs = codexSessionPathTimeMs(path);
399
+ return pathTimeMs === undefined || pathTimeMs >= options.sinceMs;
400
+ }
401
+ function codexEvents(homeDir, options = {}) {
402
+ const files = listFiles(join(homeDir, ".codex/sessions"), (path, stat) => path.endsWith(".jsonl") && fileMayContainCodexWindow(path, stat, options));
430
403
  const seenTokenCounts = new Set();
431
404
  return files.flatMap((path) => {
432
405
  const rows = parseJsonl(path).map(asRecord);
@@ -1271,17 +1244,7 @@ function toOtlp(events, options = {}) {
1271
1244
  const modelName = normalizeModelName(event.model);
1272
1245
  const generation = isGenerationEvent(event);
1273
1246
  const usage = usageDetails(event.usage);
1274
- const cost = costDetails(event.usage, modelName);
1275
1247
  const eventProject = projectMetadata(event.cwd);
1276
- const costForLangfuse = cost === undefined
1277
- ? undefined
1278
- : {
1279
- ...(cost.input !== undefined ? { input: cost.input } : {}),
1280
- ...(cost.output !== undefined ? { output: cost.output } : {}),
1281
- ...(cost.cache_read !== undefined ? { cache_read: cost.cache_read } : {}),
1282
- ...(cost.cache_write !== undefined ? { cache_write: cost.cache_write } : {}),
1283
- total: cost.total,
1284
- };
1285
1248
  const attributes = [
1286
1249
  attr("service.name", `agent.${event.agent}`),
1287
1250
  attr("deployment.environment", "local"),
@@ -1290,13 +1253,6 @@ function toOtlp(events, options = {}) {
1290
1253
  attr("session.id", event.sessionId),
1291
1254
  attr("langfuse.observation.type", generation ? "generation" : "span"),
1292
1255
  attr("langfuse.observation.model.name", generation ? modelName : undefined),
1293
- attr("langfuse.observation.usage_details", usage),
1294
- attr("langfuse.observation.cost_details", costForLangfuse),
1295
- attr("gen_ai.response.model", generation ? modelName : undefined),
1296
- attr("gen_ai.usage.input_tokens", usage?.input),
1297
- attr("gen_ai.usage.output_tokens", usage?.output),
1298
- attr("gen_ai.usage.total_tokens", usage?.total),
1299
- attr("gen_ai.usage.cost", cost?.total),
1300
1256
  attr("agent.name", event.agent),
1301
1257
  attr("host.name", currentHost),
1302
1258
  attr("agent.session_id", event.sessionId),
@@ -1315,7 +1271,8 @@ function toOtlp(events, options = {}) {
1315
1271
  attr("langfuse.observation.metadata.project_folder", eventProject.projectFolder),
1316
1272
  attr("langfuse.observation.metadata.model", modelName ?? event.model),
1317
1273
  attr("langfuse.observation.metadata.provider", event.provider),
1318
- attr("langfuse.observation.metadata.cost_source", cost?.source),
1274
+ attr("langfuse.observation.metadata.usage_details", usage),
1275
+ attr("langfuse.observation.metadata.recorded_cost", event.usage?.cost),
1319
1276
  attr("langfuse.observation.input", event.input),
1320
1277
  attr("langfuse.observation.output", event.output),
1321
1278
  attr("source.path", event.sourcePath),
@@ -1451,7 +1408,7 @@ function describeError(error) {
1451
1408
  function discoverEvents(options) {
1452
1409
  const providers = {
1453
1410
  claude: (inner) => claudeEvents(inner.homeDir),
1454
- codex: (inner) => codexEvents(inner.homeDir),
1411
+ codex: (inner) => codexEvents(inner.homeDir, { sinceMs: inner.sinceMs }),
1455
1412
  grok: (inner) => grokEvents(inner.homeDir),
1456
1413
  opencode: (inner) => opencodeEvents(inner.homeDir, {
1457
1414
  rowLimit: inner.limit,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramarivera/coding-agent-langfuse",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "Universal coding-agent Langfuse backfiller and live OTLP helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",