@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 +22 -3
- package/dist/backfill.d.ts +1 -0
- package/dist/backfill.js +41 -7
- package/package.json +2 -1
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
|
|
58
|
-
breakdown, the importer charges that total at the model input
|
|
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
|
package/dist/backfill.d.ts
CHANGED
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: "
|
|
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
|
-
|
|
956
|
-
|
|
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",
|
|
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",
|
|
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",
|
|
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
|
|
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.
|
|
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
|
},
|