@ramarivera/coding-agent-langfuse 0.1.52 → 0.1.54
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 +19 -7
- package/dist/backfill.d.ts +2 -1
- package/dist/backfill.js +68 -12
- 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
|
|
@@ -63,13 +65,14 @@ There are two config layers:
|
|
|
63
65
|
- Optional project-local overlays: every `.langfuse-ca.json` found while
|
|
64
66
|
walking from the session cwd up to the scanned home directory
|
|
65
67
|
|
|
66
|
-
Global config uses prefix rules:
|
|
68
|
+
Global config uses path-prefix or Git-remote rules:
|
|
67
69
|
|
|
68
70
|
```json
|
|
69
71
|
{
|
|
70
72
|
"rules": [
|
|
71
73
|
{
|
|
72
74
|
"pathPrefix": "/Users/example/dev/acme",
|
|
75
|
+
"gitRemoteIncludes": ["github.com/acme"],
|
|
73
76
|
"tags": ["acme", "client:acme"],
|
|
74
77
|
"metadata": {
|
|
75
78
|
"project_group": "acme",
|
|
@@ -82,6 +85,11 @@ Global config uses prefix rules:
|
|
|
82
85
|
}
|
|
83
86
|
```
|
|
84
87
|
|
|
88
|
+
Use `pathPrefix` for stable checkout roots. Use `gitRemoteIncludes` for
|
|
89
|
+
temporary agent worktrees, where the cwd may live under a random directory such
|
|
90
|
+
as `.codex/worktrees/<id>/repo` but the Git remote still identifies the real
|
|
91
|
+
project.
|
|
92
|
+
|
|
85
93
|
Project-local config is intentionally smaller and overrides/extends the matched
|
|
86
94
|
global rule. Parent overlays are applied before child overlays, so a config at
|
|
87
95
|
`~/work/client-a/.langfuse-ca.json` can tag a whole workstream while nested
|
|
@@ -127,8 +135,8 @@ records a total cost, that recorded value wins. Otherwise, the importer
|
|
|
127
135
|
calculates per-usage-type USD costs from a model catalog using rates in USD per
|
|
128
136
|
1M tokens.
|
|
129
137
|
|
|
130
|
-
The built-in catalog covers OpenAI GPT-5.5, GPT-5.4,
|
|
131
|
-
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
|
|
132
140
|
models already used in local configuration, including Fireworks Kimi K2.6,
|
|
133
141
|
Fireworks DeepSeek V4 Pro, MiniMax-M3, Together DeepSeek/Kimi/GLM/MiniMax, and
|
|
134
142
|
Zai GLM. `gpt-5.5` is charged at current standard API list price by default:
|
|
@@ -139,6 +147,10 @@ writes, `$30.00` 1-hour cache writes, and `$75.00` output per 1M tokens.
|
|
|
139
147
|
When a billable generation source only records a total token count without
|
|
140
148
|
input/output/cache breakdown, the importer charges that total at the model input
|
|
141
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.
|
|
142
154
|
|
|
143
155
|
Use an override only when you intentionally want a different accounting policy:
|
|
144
156
|
|
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: "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,
|
|
@@ -196,13 +204,20 @@ function projectMetadata(cwd) {
|
|
|
196
204
|
projectFolder: projectName,
|
|
197
205
|
};
|
|
198
206
|
}
|
|
199
|
-
function matchPathTags(cwd, config, homeDir) {
|
|
200
|
-
if (!cwd)
|
|
207
|
+
function matchPathTags(cwd, config, homeDir, git) {
|
|
208
|
+
if (!cwd && !git?.remoteUrls?.length)
|
|
201
209
|
return { tags: [], metadata: {} };
|
|
202
|
-
const normalizedCwd = normalizeRulePath(cwd);
|
|
210
|
+
const normalizedCwd = cwd ? normalizeRulePath(cwd) : undefined;
|
|
211
|
+
const normalizedRemotes = (git?.remoteUrls ?? []).map((remote) => remote.toLowerCase());
|
|
203
212
|
const matched = config.rules.filter((rule) => {
|
|
204
|
-
const
|
|
205
|
-
|
|
213
|
+
const pathMatch = rule.pathPrefix && normalizedCwd
|
|
214
|
+
? (() => {
|
|
215
|
+
const prefix = normalizeRulePath(rule.pathPrefix);
|
|
216
|
+
return normalizedCwd === prefix || normalizedCwd.startsWith(`${prefix}/`);
|
|
217
|
+
})()
|
|
218
|
+
: false;
|
|
219
|
+
const gitRemoteMatch = (rule.gitRemoteIncludes ?? []).some((needle) => normalizedRemotes.some((remote) => remote.includes(needle.toLowerCase())));
|
|
220
|
+
return pathMatch || gitRemoteMatch;
|
|
206
221
|
});
|
|
207
222
|
const globalMatch = matched.reduce((acc, rule) => ({
|
|
208
223
|
tags: [...new Set([...acc.tags, ...rule.tags])],
|
|
@@ -238,14 +253,15 @@ function matchPathTags(cwd, config, homeDir) {
|
|
|
238
253
|
}
|
|
239
254
|
function mergeProjectMetadata(cwd, config, homeDir) {
|
|
240
255
|
const project = projectMetadata(cwd);
|
|
241
|
-
const
|
|
256
|
+
const git = loadGitMetadata(cwd);
|
|
257
|
+
const pathTags = matchPathTags(cwd, config, homeDir, git);
|
|
242
258
|
return {
|
|
243
259
|
...project,
|
|
244
260
|
projectName: pathTags.projectName ?? project.projectName,
|
|
245
261
|
projectFolder: pathTags.projectFolder ?? project.projectFolder,
|
|
246
262
|
tags: pathTags.tags,
|
|
247
263
|
metadata: pathTags.metadata,
|
|
248
|
-
git
|
|
264
|
+
git,
|
|
249
265
|
};
|
|
250
266
|
}
|
|
251
267
|
function loadGitMetadata(cwd) {
|
|
@@ -263,10 +279,16 @@ function loadGitMetadata(cwd) {
|
|
|
263
279
|
const branch = gitOutput(gitCwd, ["branch", "--show-current"]) ??
|
|
264
280
|
gitOutput(gitCwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
265
281
|
const commit = gitOutput(gitCwd, ["rev-parse", "--verify", "HEAD"]);
|
|
282
|
+
const remoteUrls = gitOutput(gitCwd, ["remote", "-v"])
|
|
283
|
+
?.split("\n")
|
|
284
|
+
.map((line) => line.trim().split(/\s+/)[1])
|
|
285
|
+
.filter((remote) => Boolean(remote))
|
|
286
|
+
.filter((remote, index, remotes) => remotes.indexOf(remote) === index);
|
|
266
287
|
const metadata = {
|
|
267
288
|
worktreePath,
|
|
268
289
|
branch: branch === "HEAD" ? undefined : branch,
|
|
269
290
|
commit,
|
|
291
|
+
remoteUrls,
|
|
270
292
|
};
|
|
271
293
|
gitMetadataCache.set(gitCwd, metadata);
|
|
272
294
|
return metadata;
|
|
@@ -511,8 +533,10 @@ function loadPathTagsConfig(path) {
|
|
|
511
533
|
const rules = rawRules.map((rawRule, index) => {
|
|
512
534
|
const rule = asRecord(rawRule);
|
|
513
535
|
const pathPrefix = asString(rule.pathPrefix) ?? asString(rule.path_prefix);
|
|
514
|
-
|
|
515
|
-
|
|
536
|
+
const gitRemoteIncludes = stringArray(rule.gitRemoteIncludes ?? rule.git_remote_includes ?? rule.gitRemoteInclude ??
|
|
537
|
+
rule.git_remote_include);
|
|
538
|
+
if (!pathPrefix && gitRemoteIncludes.length === 0) {
|
|
539
|
+
throw new Error(`Invalid path tags config ${path}: rules[${index}] requires pathPrefix or gitRemoteIncludes`);
|
|
516
540
|
}
|
|
517
541
|
const tags = Array.isArray(rule.tags)
|
|
518
542
|
? rule.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0)
|
|
@@ -520,6 +544,7 @@ function loadPathTagsConfig(path) {
|
|
|
520
544
|
const metadata = asRecord(rule.metadata);
|
|
521
545
|
return {
|
|
522
546
|
pathPrefix,
|
|
547
|
+
gitRemoteIncludes,
|
|
523
548
|
tags,
|
|
524
549
|
metadata,
|
|
525
550
|
projectName: asString(rule.projectName) ?? asString(rule.project_name),
|
|
@@ -764,6 +789,13 @@ function getPath(value, keys) {
|
|
|
764
789
|
function asString(value) {
|
|
765
790
|
return typeof value === "string" ? value : undefined;
|
|
766
791
|
}
|
|
792
|
+
function stringArray(value) {
|
|
793
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
794
|
+
return [value];
|
|
795
|
+
if (!Array.isArray(value))
|
|
796
|
+
return [];
|
|
797
|
+
return value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
798
|
+
}
|
|
767
799
|
function asNumber(value) {
|
|
768
800
|
return typeof value === "number" && Number.isFinite(value)
|
|
769
801
|
? value
|
|
@@ -925,6 +957,28 @@ function usageTokenTotal(usage) {
|
|
|
925
957
|
(usage.cacheWrite5m ?? 0) +
|
|
926
958
|
(usage.cacheWrite1h ?? 0);
|
|
927
959
|
}
|
|
960
|
+
function codexBillableTokenUsage(usage) {
|
|
961
|
+
if (!usage)
|
|
962
|
+
return undefined;
|
|
963
|
+
const hasTokenBuckets = [
|
|
964
|
+
usage.input,
|
|
965
|
+
usage.output,
|
|
966
|
+
usage.reasoning,
|
|
967
|
+
usage.cacheRead,
|
|
968
|
+
usage.cacheWrite,
|
|
969
|
+
usage.cacheWrite5m,
|
|
970
|
+
usage.cacheWrite1h,
|
|
971
|
+
].some((value) => value !== undefined && value > 0);
|
|
972
|
+
if (!hasTokenBuckets)
|
|
973
|
+
return undefined;
|
|
974
|
+
return {
|
|
975
|
+
...usage,
|
|
976
|
+
output: usage.output ?? usage.reasoning,
|
|
977
|
+
// Codex token_count rows report reasoning as a subset of output tokens.
|
|
978
|
+
// Keep reasoning in metadata, but do not charge it a second time.
|
|
979
|
+
reasoning: undefined,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
928
982
|
function textLength(value) {
|
|
929
983
|
if (typeof value === "string")
|
|
930
984
|
return value.length;
|
|
@@ -1190,6 +1244,7 @@ function codexEvents(homeDir, options = {}) {
|
|
|
1190
1244
|
const info = asRecord(rowPayload.info);
|
|
1191
1245
|
const usage = normalizeUsage(asRecord(info.last_token_usage)) ??
|
|
1192
1246
|
normalizeUsage(asRecord(info.total_token_usage));
|
|
1247
|
+
const billableUsage = codexBillableTokenUsage(usage);
|
|
1193
1248
|
const tokenKey = JSON.stringify({
|
|
1194
1249
|
model: asString(info.model) ?? currentModel,
|
|
1195
1250
|
usage,
|
|
@@ -1207,9 +1262,10 @@ function codexEvents(homeDir, options = {}) {
|
|
|
1207
1262
|
cwd: currentCwd,
|
|
1208
1263
|
startMs: timestamp,
|
|
1209
1264
|
parentRecordId: "session",
|
|
1265
|
+
usage: billableUsage,
|
|
1210
1266
|
metadata: {
|
|
1211
1267
|
...pick(info, ["model_context_window"]),
|
|
1212
|
-
token_usage_billable:
|
|
1268
|
+
token_usage_billable: billableUsage !== undefined,
|
|
1213
1269
|
token_usage_source: "codex_event_msg_token_count",
|
|
1214
1270
|
token_usage_details: usageDetails(usage),
|
|
1215
1271
|
},
|