@ramarivera/coding-agent-langfuse 0.1.53 → 0.1.55

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,10 +8,12 @@ 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`.
11
+ Codex `event_msg` `token_count` rows are imported as cost-accounting
12
+ generations when they include input/output/cache buckets for a known priced
13
+ model. The importer preserves the original token snapshot in metadata and sends
14
+ Langfuse canonical `usage_details`/`cost_details` derived from the bucketed
15
+ usage. Total-only Codex snapshots and unknown/free-preview models stay
16
+ non-billable to avoid inventing cost from ambiguous telemetry.
15
17
 
16
18
  ```sh
17
19
  coding-agent-langfuse-backfill --agents codex,claude,grok,pi,opencode
@@ -133,8 +135,8 @@ records a total cost, that recorded value wins. Otherwise, the importer
133
135
  calculates per-usage-type USD costs from a model catalog using rates in USD per
134
136
  1M tokens.
135
137
 
136
- The built-in catalog covers OpenAI GPT-5.5, GPT-5.4, and GPT-5.3-Codex API list
137
- pricing, Anthropic Claude Opus/Sonnet 4 API list pricing, plus the toolbox/Pi
138
+ The built-in catalog covers OpenAI GPT-5.5, GPT-5.4, GPT-5.4 Mini, and
139
+ GPT-5.3-Codex API list pricing, Anthropic Claude Opus/Sonnet 4 API list pricing, plus the toolbox/Pi
138
140
  models already used in local configuration, including Fireworks Kimi K2.6,
139
141
  Fireworks DeepSeek V4 Pro, MiniMax-M3, Together DeepSeek/Kimi/GLM/MiniMax, and
140
142
  Zai GLM. `gpt-5.5` is charged at current standard API list price by default:
@@ -145,6 +147,10 @@ writes, `$30.00` 1-hour cache writes, and `$75.00` output per 1M tokens.
145
147
  When a billable generation source only records a total token count without
146
148
  input/output/cache breakdown, the importer charges that total at the model input
147
149
  rate and marks the cost source as `calculated_total_as_input`.
150
+ Codex token-count snapshots are the exception: they are charged only when the
151
+ snapshot has explicit input/output/cache buckets. Codex reasoning tokens are
152
+ kept in metadata, but not charged separately because Codex token-count output
153
+ totals already include reasoning tokens.
148
154
 
149
155
  Use an override only when you intentionally want a different accounting policy:
150
156
 
package/dist/backfill.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
- import { existsSync, mkdirSync, renameSync, readdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
4
+ import { closeSync, existsSync, mkdirSync, openSync, renameSync, readdirSync, readFileSync, readSync, statSync, writeFileSync, } from "node:fs";
5
5
  import { hostname, homedir } from "node:os";
6
6
  import { dirname, join } from "node:path";
7
7
  const allAgents = ["claude", "codex", "grok", "opencode", "pi"];
8
8
  const importStateIdentityVersion = "v9-cost-details";
9
9
  const importStateIdentityVersions = {
10
10
  claude: "v13-claude-message-snapshot-dedupe",
11
- codex: "v11-codex-token-accounting-nonbillable",
11
+ codex: "v12-codex-token-accounting-priced",
12
12
  grok: "v12-cost-details",
13
13
  opencode: "v11-cost-details",
14
14
  pi: "v12-cost-details",
@@ -24,7 +24,7 @@ const langfuseIdIdentityVersions = {
24
24
  const importPayloadVersion = "v10-cost-details";
25
25
  const importPayloadVersions = {
26
26
  claude: "v11-claude-message-snapshot-dedupe",
27
- codex: "v11-codex-token-accounting-nonbillable",
27
+ codex: "v12-codex-token-accounting-priced",
28
28
  };
29
29
  const defaultEndpoint = "https://langfuse.ai.roxasroot.net/otel/v1/traces";
30
30
  const deadRemoteEndpoint = "http://langfuse.ai.roxasroot.net:14318/v1/traces";
@@ -72,6 +72,12 @@ const gpt54Rates = {
72
72
  cacheRead: 0.25,
73
73
  cacheWrite: 2.5,
74
74
  };
75
+ const gpt54MiniRates = {
76
+ input: 0.75,
77
+ output: 4.5,
78
+ cacheRead: 0.075,
79
+ cacheWrite: 0.75,
80
+ };
75
81
  const gpt53CodexRates = {
76
82
  input: 1.75,
77
83
  output: 14,
@@ -101,6 +107,8 @@ const defaultCostRates = {
101
107
  "openai/gpt-5.5-pro": gpt55ProRates,
102
108
  "gpt-5.4": gpt54Rates,
103
109
  "openai/gpt-5.4": gpt54Rates,
110
+ "gpt-5.4-mini": gpt54MiniRates,
111
+ "openai/gpt-5.4-mini": gpt54MiniRates,
104
112
  "gpt-5.3-codex": gpt53CodexRates,
105
113
  "openai/gpt-5.3-codex": gpt53CodexRates,
106
114
  "claude-opus-4": claudeOpus4Rates,
@@ -761,10 +769,51 @@ function listFiles(root, predicate) {
761
769
  return out.sort();
762
770
  }
763
771
  function parseJsonl(path) {
764
- return readFileSync(path, "utf8")
765
- .split(/\r?\n/)
766
- .filter((line) => line.trim().length > 0)
767
- .map((line) => JSON.parse(line));
772
+ const rows = [];
773
+ let fd;
774
+ try {
775
+ fd = openSync(path, "r");
776
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
777
+ let carry = "";
778
+ let bytesRead = 0;
779
+ do {
780
+ bytesRead = readSync(fd, buffer, 0, buffer.length, null);
781
+ if (bytesRead === 0)
782
+ break;
783
+ carry += buffer.subarray(0, bytesRead).toString("utf8");
784
+ const lines = carry.split(/\r?\n/);
785
+ carry = lines.pop() ?? "";
786
+ for (const line of lines)
787
+ pushJsonlRow(rows, line);
788
+ } while (bytesRead > 0);
789
+ pushJsonlRow(rows, carry);
790
+ }
791
+ catch {
792
+ return rows;
793
+ }
794
+ finally {
795
+ if (fd !== undefined) {
796
+ try {
797
+ closeSync(fd);
798
+ }
799
+ catch {
800
+ // Ignore close failures; the caller can still use rows parsed so far.
801
+ }
802
+ }
803
+ }
804
+ return rows;
805
+ }
806
+ function pushJsonlRow(rows, line) {
807
+ const trimmed = line.trim();
808
+ if (trimmed.length === 0)
809
+ return;
810
+ try {
811
+ rows.push(JSON.parse(trimmed));
812
+ }
813
+ catch {
814
+ // Session files may be actively written or partially corrupted.
815
+ // One bad line should not abort the whole collector.
816
+ }
768
817
  }
769
818
  function asRecord(value) {
770
819
  return value && typeof value === "object"
@@ -949,6 +998,28 @@ function usageTokenTotal(usage) {
949
998
  (usage.cacheWrite5m ?? 0) +
950
999
  (usage.cacheWrite1h ?? 0);
951
1000
  }
1001
+ function codexBillableTokenUsage(usage) {
1002
+ if (!usage)
1003
+ return undefined;
1004
+ const hasTokenBuckets = [
1005
+ usage.input,
1006
+ usage.output,
1007
+ usage.reasoning,
1008
+ usage.cacheRead,
1009
+ usage.cacheWrite,
1010
+ usage.cacheWrite5m,
1011
+ usage.cacheWrite1h,
1012
+ ].some((value) => value !== undefined && value > 0);
1013
+ if (!hasTokenBuckets)
1014
+ return undefined;
1015
+ return {
1016
+ ...usage,
1017
+ output: usage.output ?? usage.reasoning,
1018
+ // Codex token_count rows report reasoning as a subset of output tokens.
1019
+ // Keep reasoning in metadata, but do not charge it a second time.
1020
+ reasoning: undefined,
1021
+ };
1022
+ }
952
1023
  function textLength(value) {
953
1024
  if (typeof value === "string")
954
1025
  return value.length;
@@ -1214,6 +1285,7 @@ function codexEvents(homeDir, options = {}) {
1214
1285
  const info = asRecord(rowPayload.info);
1215
1286
  const usage = normalizeUsage(asRecord(info.last_token_usage)) ??
1216
1287
  normalizeUsage(asRecord(info.total_token_usage));
1288
+ const billableUsage = codexBillableTokenUsage(usage);
1217
1289
  const tokenKey = JSON.stringify({
1218
1290
  model: asString(info.model) ?? currentModel,
1219
1291
  usage,
@@ -1231,9 +1303,10 @@ function codexEvents(homeDir, options = {}) {
1231
1303
  cwd: currentCwd,
1232
1304
  startMs: timestamp,
1233
1305
  parentRecordId: "session",
1306
+ usage: billableUsage,
1234
1307
  metadata: {
1235
1308
  ...pick(info, ["model_context_window"]),
1236
- token_usage_billable: false,
1309
+ token_usage_billable: billableUsage !== undefined,
1237
1310
  token_usage_source: "codex_event_msg_token_count",
1238
1311
  token_usage_details: usageDetails(usage),
1239
1312
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramarivera/coding-agent-langfuse",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "description": "Universal coding-agent Langfuse backfiller and live OTLP helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",