@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 +12 -6
- package/dist/backfill.js +81 -8
- package/package.json +1 -1
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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,
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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:
|
|
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
|
},
|