@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 +4 -4
- package/bin/coding-agent-langfuse.mjs +0 -0
- package/dist/backfill.d.ts +3 -1
- package/dist/backfill.js +26 -69
- package/package.json +1 -1
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
|
|
7
|
-
as
|
|
8
|
-
|
|
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
|
|
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
|
package/dist/backfill.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
429
|
-
const
|
|
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.
|
|
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,
|