@ramarivera/coding-agent-langfuse 0.1.44 → 0.1.45

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
@@ -8,6 +8,11 @@ Langfuse canonical `usage_details` and `cost_details` attributes so historical
8
8
  backfills participate in Langfuse model-usage and cost dashboards. Tool calls
9
9
  remain child spans under the same session.
10
10
 
11
+ Codex `event_msg` `token_count` rows are imported as non-billable accounting
12
+ spans. They are rolling/snapshot telemetry from the Codex session log, not
13
+ individual model generations, so the importer preserves their token details in
14
+ metadata without sending Langfuse generation `usage_details` or `cost_details`.
15
+
11
16
  ```sh
12
17
  coding-agent-langfuse-backfill --agents codex,claude,grok,pi,opencode
13
18
  ```
@@ -22,6 +27,16 @@ npx @ramarivera/coding-agent-langfuse@latest \
22
27
  --batch-size 10
23
28
  ```
24
29
 
30
+ For a Langfuse endpoint that requires project API keys, pass Basic auth
31
+ credentials as `publicKey:secretKey` or set `LANGFUSE_BACKFILL_AUTH`:
32
+
33
+ ```sh
34
+ npx @ramarivera/coding-agent-langfuse@latest \
35
+ --agents claude,codex,grok,pi,opencode \
36
+ --endpoint http://127.0.0.1:3000/api/public/otel/v1/traces \
37
+ --auth pk-lf-example:sk-lf-example
38
+ ```
39
+
25
40
  Run live incremental forwarding without putting inference behind a gateway:
26
41
 
27
42
  ```sh
@@ -54,9 +69,9 @@ Zai GLM. `gpt-5.5` is charged at current standard API list price by default:
54
69
  Pro defaults to `$30.00` input and `$180.00` output per 1M tokens. Claude Opus 4
55
70
  models default to `$15.00` input, `$1.50` cache hits, `$18.75` 5-minute cache
56
71
  writes, `$30.00` 1-hour cache writes, and `$75.00` output per 1M tokens.
57
- When a source only records a total token count without input/output/cache
58
- breakdown, the importer charges that total at the model input rate and marks the
59
- cost source as `calculated_total_as_input`.
72
+ When a billable generation source only records a total token count without
73
+ input/output/cache breakdown, the importer charges that total at the model input
74
+ rate and marks the cost source as `calculated_total_as_input`.
60
75
 
61
76
  Use an override only when you intentionally want a different accounting policy:
62
77
 
@@ -176,6 +191,7 @@ CLI against a local OTLP collector.
176
191
  npm run check
177
192
  npm test
178
193
  npm run test:e2e
194
+ npm run test:e2e:langfuse
179
195
  ```
180
196
 
181
197
  The e2e suite verifies:
@@ -185,5 +201,8 @@ The e2e suite verifies:
185
201
  - Follow mode picking up newly written Codex events
186
202
  - One CLI run posting reconstructable traces for Claude Code, Codex, Grok,
187
203
  OpenCode, and Pi
204
+ - A Docker-backed mini Langfuse import that queries the public observations API
205
+ and verifies usage/cost fields for sanitized multi-agent sessions, including
206
+ Claude Code Opus 4.7 and Opus 4.8 cache accounting
188
207
  - Service plan generation for Linux systemd user units, macOS LaunchAgents, and
189
208
  Windows Scheduled Tasks
@@ -33,6 +33,7 @@ type BackfillEvent = {
33
33
  type BackfillOptions = {
34
34
  agents: Set<AgentName>;
35
35
  endpoint: string;
36
+ auth?: string;
36
37
  statePath: string;
37
38
  homeDir: string;
38
39
  dryRun: boolean;
package/dist/backfill.js CHANGED
@@ -8,7 +8,7 @@ const allAgents = ["claude", "codex", "grok", "opencode", "pi"];
8
8
  const importStateIdentityVersion = "v9-cost-details";
9
9
  const importStateIdentityVersions = {
10
10
  claude: "v12-cost-details",
11
- codex: "v10-cost-details",
11
+ codex: "v11-codex-token-accounting-nonbillable",
12
12
  grok: "v12-cost-details",
13
13
  opencode: "v11-cost-details",
14
14
  pi: "v12-cost-details",
@@ -22,6 +22,9 @@ const langfuseIdIdentityVersions = {
22
22
  pi: "v11-tool-results",
23
23
  };
24
24
  const importPayloadVersion = "v10-cost-details";
25
+ const importPayloadVersions = {
26
+ codex: "v11-codex-token-accounting-nonbillable",
27
+ };
25
28
  const defaultEndpoint = "https://langfuse.ai.roxasroot.net/otel/v1/traces";
26
29
  const deadRemoteEndpoint = "http://langfuse.ai.roxasroot.net:14318/v1/traces";
27
30
  const defaultMaxRequestBytes = 12 * 1024 * 1024;
@@ -193,6 +196,7 @@ function usage() {
193
196
 
194
197
  Options:
195
198
  --endpoint URL OTLP HTTP traces endpoint (default: ${defaultEndpoint})
199
+ --auth USER:PASS Optional Langfuse Basic auth credentials
196
200
  --agents LIST Comma-separated agents: claude,codex,grok,opencode,pi
197
201
  --state PATH Dedupe state file (default: ${defaultStatePath})
198
202
  --home PATH Home directory to scan (default: current user home)
@@ -216,6 +220,8 @@ Options:
216
220
  }
217
221
  function parseArgs(argv) {
218
222
  let endpoint = normalizeEndpoint(process.env.LANGFUSE_BACKFILL_ENDPOINT ?? defaultEndpoint);
223
+ let auth = process.env.LANGFUSE_BACKFILL_AUTH ??
224
+ process.env.CODING_AGENT_LANGFUSE_AUTH;
219
225
  let statePath = process.env.LANGFUSE_BACKFILL_STATE ?? defaultStatePath;
220
226
  let homeDir = process.env.HOME ?? homedir();
221
227
  let dryRun = false;
@@ -260,6 +266,9 @@ function parseArgs(argv) {
260
266
  else if (arg === "--endpoint") {
261
267
  endpoint = normalizeEndpoint(next());
262
268
  }
269
+ else if (arg === "--auth") {
270
+ auth = next();
271
+ }
263
272
  else if (arg === "--state") {
264
273
  statePath = next();
265
274
  }
@@ -345,9 +354,13 @@ function parseArgs(argv) {
345
354
  if (limit !== undefined && (!Number.isFinite(limit) || limit < 1)) {
346
355
  throw new Error("--limit must be a positive integer");
347
356
  }
357
+ if (auth !== undefined && auth.trim().length === 0) {
358
+ throw new Error("--auth must not be empty");
359
+ }
348
360
  return {
349
361
  agents,
350
362
  endpoint,
363
+ auth,
351
364
  statePath,
352
365
  homeDir,
353
366
  dryRun,
@@ -952,8 +965,12 @@ function codexEvents(homeDir, options = {}) {
952
965
  cwd: currentCwd,
953
966
  startMs: timestamp,
954
967
  parentRecordId: "session",
955
- usage,
956
- metadata: pick(info, ["model_context_window"]),
968
+ metadata: {
969
+ ...pick(info, ["model_context_window"]),
970
+ token_usage_billable: false,
971
+ token_usage_source: "codex_event_msg_token_count",
972
+ token_usage_details: usageDetails(usage),
973
+ },
957
974
  });
958
975
  }
959
976
  }
@@ -1492,6 +1509,9 @@ function stableId(input) {
1492
1509
  function importIdentity(event) {
1493
1510
  return importStateIdentityVersions[event.agent] ?? importStateIdentityVersion;
1494
1511
  }
1512
+ function payloadVersion(event) {
1513
+ return importPayloadVersions[event.agent] ?? importPayloadVersion;
1514
+ }
1495
1515
  function langfuseIdIdentity(event) {
1496
1516
  return langfuseIdIdentityVersions[event.agent] ?? langfuseIdIdentityVersion;
1497
1517
  }
@@ -1599,7 +1619,7 @@ function toOtlp(events, options = {}) {
1599
1619
  attr("langfuse.trace.metadata.project_path", firstProject.projectPath),
1600
1620
  attr("langfuse.trace.metadata.project_name", firstProject.projectName),
1601
1621
  attr("langfuse.trace.metadata.project_folder", firstProject.projectFolder),
1602
- attr("langfuse.trace.metadata.import_payload_version", importPayloadVersion),
1622
+ attr("langfuse.trace.metadata.import_payload_version", payloadVersion(first)),
1603
1623
  attr("langfuse.trace.metadata.import_state_identity", importIdentity(first)),
1604
1624
  attr("langfuse.trace.metadata.langfuse_id_identity", langfuseIdIdentity(first)),
1605
1625
  attr("langfuse.observation.metadata.agent", first.agent),
@@ -1612,7 +1632,7 @@ function toOtlp(events, options = {}) {
1612
1632
  attr("langfuse.observation.metadata.project_path", firstProject.projectPath),
1613
1633
  attr("langfuse.observation.metadata.project_name", firstProject.projectName),
1614
1634
  attr("langfuse.observation.metadata.project_folder", firstProject.projectFolder),
1615
- attr("langfuse.observation.metadata.import_payload_version", importPayloadVersion),
1635
+ attr("langfuse.observation.metadata.import_payload_version", payloadVersion(first)),
1616
1636
  attr("langfuse.observation.metadata.import_state_identity", importIdentity(first)),
1617
1637
  attr("langfuse.observation.metadata.langfuse_id_identity", langfuseIdIdentity(first)),
1618
1638
  attr("source.path", first.sourcePath),
@@ -1666,7 +1686,7 @@ function toOtlp(events, options = {}) {
1666
1686
  attr("langfuse.observation.metadata.project_folder", eventProject.projectFolder),
1667
1687
  attr("langfuse.observation.metadata.model", modelName ?? event.model),
1668
1688
  attr("langfuse.observation.metadata.provider", event.provider),
1669
- attr("langfuse.observation.metadata.import_payload_version", importPayloadVersion),
1689
+ attr("langfuse.observation.metadata.import_payload_version", payloadVersion(event)),
1670
1690
  attr("langfuse.observation.metadata.import_state_identity", importIdentity(event)),
1671
1691
  attr("langfuse.observation.metadata.langfuse_id_identity", langfuseIdIdentity(event)),
1672
1692
  attr("langfuse.observation.usage_details", generation ? usage : undefined),
@@ -1677,6 +1697,9 @@ function toOtlp(events, options = {}) {
1677
1697
  attr("langfuse.observation.metadata.cost_model_key", cost?.modelKey),
1678
1698
  attr("langfuse.observation.metadata.cost_rates", cost?.rates),
1679
1699
  attr("langfuse.observation.metadata.recorded_cost", event.usage?.cost),
1700
+ attr("langfuse.observation.metadata.token_usage_billable", event.metadata?.token_usage_billable),
1701
+ attr("langfuse.observation.metadata.token_usage_source", event.metadata?.token_usage_source),
1702
+ attr("langfuse.observation.metadata.token_usage_details", event.metadata?.token_usage_details),
1680
1703
  attr("langfuse.observation.input", event.input),
1681
1704
  attr("langfuse.observation.output", event.output),
1682
1705
  attr("source.path", event.sourcePath),
@@ -1776,11 +1799,16 @@ function splitSendBatches(events, options) {
1776
1799
  }
1777
1800
  async function postOtlp(endpoint, events, options) {
1778
1801
  const body = JSON.stringify(toOtlp(events, options));
1802
+ const headers = {
1803
+ "content-type": "application/json",
1804
+ };
1805
+ if (options.auth)
1806
+ headers.Authorization = authHeader(options.auth);
1779
1807
  let response;
1780
1808
  try {
1781
1809
  response = await fetch(endpoint, {
1782
1810
  method: "POST",
1783
- headers: { "content-type": "application/json" },
1811
+ headers,
1784
1812
  body,
1785
1813
  });
1786
1814
  }
@@ -1791,6 +1819,11 @@ async function postOtlp(endpoint, events, options) {
1791
1819
  throw new Error(`OTLP POST failed: ${response.status} ${await response.text()}`);
1792
1820
  }
1793
1821
  }
1822
+ function authHeader(auth) {
1823
+ if (/^(Basic|Bearer)\s+/i.test(auth))
1824
+ return auth;
1825
+ return `Basic ${Buffer.from(auth, "utf8").toString("base64")}`;
1826
+ }
1794
1827
  function describeError(error) {
1795
1828
  if (!(error instanceof Error))
1796
1829
  return String(error);
@@ -1883,6 +1916,7 @@ async function run(options) {
1883
1916
  await postOtlp(options.endpoint, batch, {
1884
1917
  maxFieldBytes: options.maxFieldBytes,
1885
1918
  costRates: options.costRates,
1919
+ auth: options.auth,
1886
1920
  });
1887
1921
  for (const event of batch) {
1888
1922
  state.sent[fingerprint(event)] = new Date().toISOString();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramarivera/coding-agent-langfuse",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "description": "Universal coding-agent Langfuse backfiller and live OTLP helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,6 +25,7 @@
25
25
  "check": "tsc --noEmit",
26
26
  "test": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types --test test/**/*.test.ts",
27
27
  "test:e2e": "npm run build && node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types --test e2e/test/**/*.test.ts",
28
+ "test:e2e:langfuse": "npm run build && LANGFUSE_DOCKER_E2E=1 node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types --test e2e/test/langfuse-docker.test.ts",
28
29
  "pack:dry-run": "npm pack --dry-run",
29
30
  "prepack": "npm run build"
30
31
  },