@ramarivera/coding-agent-langfuse 0.1.43 → 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 +50 -5
- package/dist/backfill.d.ts +6 -0
- package/dist/backfill.js +217 -19
- 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
|
|
@@ -45,12 +60,18 @@ records a total cost, that recorded value wins. Otherwise, the importer
|
|
|
45
60
|
calculates per-usage-type USD costs from a model catalog using rates in USD per
|
|
46
61
|
1M tokens.
|
|
47
62
|
|
|
48
|
-
The built-in catalog covers OpenAI GPT-5.5
|
|
63
|
+
The built-in catalog covers OpenAI GPT-5.5, GPT-5.4, and GPT-5.3-Codex API list
|
|
64
|
+
pricing, Anthropic Claude Opus/Sonnet 4 API list pricing, plus the toolbox/Pi
|
|
49
65
|
models already used in local configuration, including Fireworks Kimi K2.6,
|
|
50
66
|
Fireworks DeepSeek V4 Pro, MiniMax-M3, Together DeepSeek/Kimi/GLM/MiniMax, and
|
|
51
67
|
Zai GLM. `gpt-5.5` is charged at current standard API list price by default:
|
|
52
68
|
`$5.00` input, `$0.50` cached input, and `$30.00` output per 1M tokens. GPT-5.5
|
|
53
|
-
Pro defaults to `$30.00` input and `$180.00` output per 1M tokens.
|
|
69
|
+
Pro defaults to `$30.00` input and `$180.00` output per 1M tokens. Claude Opus 4
|
|
70
|
+
models default to `$15.00` input, `$1.50` cache hits, `$18.75` 5-minute cache
|
|
71
|
+
writes, `$30.00` 1-hour cache writes, and `$75.00` output per 1M tokens.
|
|
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`.
|
|
54
75
|
|
|
55
76
|
Use an override only when you intentionally want a different accounting policy:
|
|
56
77
|
|
|
@@ -72,7 +93,9 @@ services. A file can be either a direct model map or `{ "rates": { ... } }`:
|
|
|
72
93
|
"input": 1,
|
|
73
94
|
"output": 2,
|
|
74
95
|
"cacheRead": 0.1,
|
|
75
|
-
"cacheWrite": 0
|
|
96
|
+
"cacheWrite": 0,
|
|
97
|
+
"cacheWrite5m": 0,
|
|
98
|
+
"cacheWrite1h": 0
|
|
76
99
|
}
|
|
77
100
|
}
|
|
78
101
|
}
|
|
@@ -138,8 +161,26 @@ npx @ramarivera/coding-agent-langfuse@latest \
|
|
|
138
161
|
--endpoint https://langfuse.ai.roxasroot.net/otel/v1/traces
|
|
139
162
|
```
|
|
140
163
|
|
|
141
|
-
Deduplication is state-file based and keyed by
|
|
142
|
-
record id. Reuse the same `--state` path for
|
|
164
|
+
Deduplication is state-file based and keyed by importer state identity, agent,
|
|
165
|
+
session id, and source record id. Reuse the same `--state` path for normal
|
|
166
|
+
incremental runs.
|
|
167
|
+
|
|
168
|
+
For an intentional repair replay, add `--force` to resend the selected window
|
|
169
|
+
even when the state file says those events were already sent:
|
|
170
|
+
|
|
171
|
+
```sh
|
|
172
|
+
npx @ramarivera/coding-agent-langfuse@latest \
|
|
173
|
+
--agents claude,codex,grok,pi,opencode \
|
|
174
|
+
--since 2026-05-01T00:00:00Z \
|
|
175
|
+
--until 2026-06-01T00:00:00Z \
|
|
176
|
+
--force \
|
|
177
|
+
--endpoint https://langfuse.ai.roxasroot.net/otel/v1/traces
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The Langfuse trace/span IDs intentionally stay pinned to the original pre-cost
|
|
181
|
+
identity, while the state-file key can advance with importer payload changes.
|
|
182
|
+
That lets cost repairs replace historical zero-cost rows instead of creating a
|
|
183
|
+
new duplicate identity for the same source event.
|
|
143
184
|
|
|
144
185
|
## Verification
|
|
145
186
|
|
|
@@ -150,6 +191,7 @@ CLI against a local OTLP collector.
|
|
|
150
191
|
npm run check
|
|
151
192
|
npm test
|
|
152
193
|
npm run test:e2e
|
|
194
|
+
npm run test:e2e:langfuse
|
|
153
195
|
```
|
|
154
196
|
|
|
155
197
|
The e2e suite verifies:
|
|
@@ -159,5 +201,8 @@ The e2e suite verifies:
|
|
|
159
201
|
- Follow mode picking up newly written Codex events
|
|
160
202
|
- One CLI run posting reconstructable traces for Claude Code, Codex, Grok,
|
|
161
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
|
|
162
207
|
- Service plan generation for Linux systemd user units, macOS LaunchAgents, and
|
|
163
208
|
Windows Scheduled Tasks
|
package/dist/backfill.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ type Usage = {
|
|
|
6
6
|
reasoning?: number;
|
|
7
7
|
cacheRead?: number;
|
|
8
8
|
cacheWrite?: number;
|
|
9
|
+
cacheWrite5m?: number;
|
|
10
|
+
cacheWrite1h?: number;
|
|
9
11
|
total?: number;
|
|
10
12
|
cost?: number;
|
|
11
13
|
inputIncludesCache?: boolean;
|
|
@@ -31,9 +33,11 @@ type BackfillEvent = {
|
|
|
31
33
|
type BackfillOptions = {
|
|
32
34
|
agents: Set<AgentName>;
|
|
33
35
|
endpoint: string;
|
|
36
|
+
auth?: string;
|
|
34
37
|
statePath: string;
|
|
35
38
|
homeDir: string;
|
|
36
39
|
dryRun: boolean;
|
|
40
|
+
force: boolean;
|
|
37
41
|
follow: boolean;
|
|
38
42
|
pollIntervalMs: number;
|
|
39
43
|
idleExitAfterMs?: number;
|
|
@@ -53,6 +57,8 @@ type CostRates = {
|
|
|
53
57
|
reasoning?: number;
|
|
54
58
|
cacheRead?: number;
|
|
55
59
|
cacheWrite?: number;
|
|
60
|
+
cacheWrite5m?: number;
|
|
61
|
+
cacheWrite1h?: number;
|
|
56
62
|
};
|
|
57
63
|
type CostCatalog = Record<string, CostRates>;
|
|
58
64
|
type OtlpOptions = {
|
package/dist/backfill.js
CHANGED
|
@@ -5,14 +5,26 @@ import { existsSync, mkdirSync, renameSync, readdirSync, readFileSync, statSync,
|
|
|
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
|
-
const
|
|
9
|
-
const
|
|
8
|
+
const importStateIdentityVersion = "v9-cost-details";
|
|
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",
|
|
15
15
|
};
|
|
16
|
+
const langfuseIdIdentityVersion = "v8-cached-input-token-split";
|
|
17
|
+
const langfuseIdIdentityVersions = {
|
|
18
|
+
claude: "v11-tool-results",
|
|
19
|
+
codex: "v9-codex-conversation-events",
|
|
20
|
+
grok: "v11-chat-history-only",
|
|
21
|
+
opencode: "v10-opencode-message-parts",
|
|
22
|
+
pi: "v11-tool-results",
|
|
23
|
+
};
|
|
24
|
+
const importPayloadVersion = "v10-cost-details";
|
|
25
|
+
const importPayloadVersions = {
|
|
26
|
+
codex: "v11-codex-token-accounting-nonbillable",
|
|
27
|
+
};
|
|
16
28
|
const defaultEndpoint = "https://langfuse.ai.roxasroot.net/otel/v1/traces";
|
|
17
29
|
const deadRemoteEndpoint = "http://langfuse.ai.roxasroot.net:14318/v1/traces";
|
|
18
30
|
const defaultMaxRequestBytes = 12 * 1024 * 1024;
|
|
@@ -49,11 +61,63 @@ const gpt55ProRates = {
|
|
|
49
61
|
cacheRead: 30,
|
|
50
62
|
cacheWrite: 30,
|
|
51
63
|
};
|
|
64
|
+
const gpt54Rates = {
|
|
65
|
+
input: 2.5,
|
|
66
|
+
output: 15,
|
|
67
|
+
cacheRead: 0.25,
|
|
68
|
+
cacheWrite: 2.5,
|
|
69
|
+
};
|
|
70
|
+
const gpt53CodexRates = {
|
|
71
|
+
input: 1.75,
|
|
72
|
+
output: 14,
|
|
73
|
+
cacheRead: 0.175,
|
|
74
|
+
cacheWrite: 1.75,
|
|
75
|
+
};
|
|
76
|
+
const claudeOpus4Rates = {
|
|
77
|
+
input: 15,
|
|
78
|
+
output: 75,
|
|
79
|
+
cacheRead: 1.5,
|
|
80
|
+
cacheWrite: 18.75,
|
|
81
|
+
cacheWrite5m: 18.75,
|
|
82
|
+
cacheWrite1h: 30,
|
|
83
|
+
};
|
|
84
|
+
const claudeSonnet4Rates = {
|
|
85
|
+
input: 3,
|
|
86
|
+
output: 15,
|
|
87
|
+
cacheRead: 0.3,
|
|
88
|
+
cacheWrite: 3.75,
|
|
89
|
+
cacheWrite5m: 3.75,
|
|
90
|
+
cacheWrite1h: 6,
|
|
91
|
+
};
|
|
52
92
|
const defaultCostRates = {
|
|
53
93
|
"gpt-5.5": gpt55Rates,
|
|
54
94
|
"openai/gpt-5.5": gpt55Rates,
|
|
55
95
|
"gpt-5.5-pro": gpt55ProRates,
|
|
56
96
|
"openai/gpt-5.5-pro": gpt55ProRates,
|
|
97
|
+
"gpt-5.4": gpt54Rates,
|
|
98
|
+
"openai/gpt-5.4": gpt54Rates,
|
|
99
|
+
"gpt-5.3-codex": gpt53CodexRates,
|
|
100
|
+
"openai/gpt-5.3-codex": gpt53CodexRates,
|
|
101
|
+
"claude-opus-4": claudeOpus4Rates,
|
|
102
|
+
"anthropic/claude-opus-4": claudeOpus4Rates,
|
|
103
|
+
"claude-opus-4-1": claudeOpus4Rates,
|
|
104
|
+
"anthropic/claude-opus-4-1": claudeOpus4Rates,
|
|
105
|
+
"claude-opus-4-6": claudeOpus4Rates,
|
|
106
|
+
"anthropic/claude-opus-4-6": claudeOpus4Rates,
|
|
107
|
+
"claude-opus-4-7": claudeOpus4Rates,
|
|
108
|
+
"anthropic/claude-opus-4-7": claudeOpus4Rates,
|
|
109
|
+
"claude-opus-4-8": claudeOpus4Rates,
|
|
110
|
+
"anthropic/claude-opus-4-8": claudeOpus4Rates,
|
|
111
|
+
"claude-sonnet-4": claudeSonnet4Rates,
|
|
112
|
+
"anthropic/claude-sonnet-4": claudeSonnet4Rates,
|
|
113
|
+
"claude-sonnet-4-5": claudeSonnet4Rates,
|
|
114
|
+
"anthropic/claude-sonnet-4-5": claudeSonnet4Rates,
|
|
115
|
+
"claude-sonnet-4.5": claudeSonnet4Rates,
|
|
116
|
+
"anthropic/claude-sonnet-4.5": claudeSonnet4Rates,
|
|
117
|
+
"claude-sonnet-4-6": claudeSonnet4Rates,
|
|
118
|
+
"anthropic/claude-sonnet-4-6": claudeSonnet4Rates,
|
|
119
|
+
"claude-sonnet-4.6": claudeSonnet4Rates,
|
|
120
|
+
"anthropic/claude-sonnet-4.6": claudeSonnet4Rates,
|
|
57
121
|
"accounts/fireworks/routers/kimi-k2p6-turbo": kimiFirepassRates,
|
|
58
122
|
"fireworks-firepass/accounts/fireworks/routers/kimi-k2p6-turbo": kimiFirepassRates,
|
|
59
123
|
"kimi-for-coding": kimiFirepassRates,
|
|
@@ -67,24 +131,48 @@ const defaultCostRates = {
|
|
|
67
131
|
cacheRead: 0.2,
|
|
68
132
|
cacheWrite: 0,
|
|
69
133
|
},
|
|
134
|
+
"deepseek-ai/DeepSeek-V4-Pro": {
|
|
135
|
+
input: 2.1,
|
|
136
|
+
output: 4.4,
|
|
137
|
+
cacheRead: 0.2,
|
|
138
|
+
cacheWrite: 0,
|
|
139
|
+
},
|
|
70
140
|
"together/zai-org/GLM-5.1": {
|
|
71
141
|
input: 1.4,
|
|
72
142
|
output: 4.4,
|
|
73
143
|
cacheRead: 0.2,
|
|
74
144
|
cacheWrite: 0,
|
|
75
145
|
},
|
|
146
|
+
"zai-org/GLM-5.1": {
|
|
147
|
+
input: 1.4,
|
|
148
|
+
output: 4.4,
|
|
149
|
+
cacheRead: 0.2,
|
|
150
|
+
cacheWrite: 0,
|
|
151
|
+
},
|
|
76
152
|
"together/moonshotai/Kimi-K2.6": {
|
|
77
153
|
input: 1.2,
|
|
78
154
|
output: 4.5,
|
|
79
155
|
cacheRead: 0.2,
|
|
80
156
|
cacheWrite: 0,
|
|
81
157
|
},
|
|
158
|
+
"moonshotai/Kimi-K2.6": {
|
|
159
|
+
input: 1.2,
|
|
160
|
+
output: 4.5,
|
|
161
|
+
cacheRead: 0.2,
|
|
162
|
+
cacheWrite: 0,
|
|
163
|
+
},
|
|
82
164
|
"together/MiniMaxAI/MiniMax-M2.7": {
|
|
83
165
|
input: 0.3,
|
|
84
166
|
output: 1.2,
|
|
85
167
|
cacheRead: 0.06,
|
|
86
168
|
cacheWrite: 0,
|
|
87
169
|
},
|
|
170
|
+
"MiniMax-M2.7": {
|
|
171
|
+
input: 0.3,
|
|
172
|
+
output: 1.2,
|
|
173
|
+
cacheRead: 0.06,
|
|
174
|
+
cacheWrite: 0,
|
|
175
|
+
},
|
|
88
176
|
"zai/glm-5.1": {
|
|
89
177
|
input: 1.4,
|
|
90
178
|
output: 4.4,
|
|
@@ -108,6 +196,7 @@ function usage() {
|
|
|
108
196
|
|
|
109
197
|
Options:
|
|
110
198
|
--endpoint URL OTLP HTTP traces endpoint (default: ${defaultEndpoint})
|
|
199
|
+
--auth USER:PASS Optional Langfuse Basic auth credentials
|
|
111
200
|
--agents LIST Comma-separated agents: claude,codex,grok,opencode,pi
|
|
112
201
|
--state PATH Dedupe state file (default: ${defaultStatePath})
|
|
113
202
|
--home PATH Home directory to scan (default: current user home)
|
|
@@ -125,14 +214,18 @@ Options:
|
|
|
125
214
|
--poll-interval-ms N Delay between --follow scans (default: 5000)
|
|
126
215
|
--idle-exit-after-ms N Stop --follow after this much time without new sends
|
|
127
216
|
--dry-run Discover and dedupe without sending or mutating state
|
|
217
|
+
--force Resend discovered events even when present in local state
|
|
128
218
|
--help Show this help
|
|
129
219
|
`;
|
|
130
220
|
}
|
|
131
221
|
function parseArgs(argv) {
|
|
132
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;
|
|
133
225
|
let statePath = process.env.LANGFUSE_BACKFILL_STATE ?? defaultStatePath;
|
|
134
226
|
let homeDir = process.env.HOME ?? homedir();
|
|
135
227
|
let dryRun = false;
|
|
228
|
+
let force = false;
|
|
136
229
|
let limit;
|
|
137
230
|
let sinceMs;
|
|
138
231
|
let untilMs;
|
|
@@ -167,9 +260,15 @@ function parseArgs(argv) {
|
|
|
167
260
|
if (arg === "--dry-run") {
|
|
168
261
|
dryRun = true;
|
|
169
262
|
}
|
|
263
|
+
else if (arg === "--force") {
|
|
264
|
+
force = true;
|
|
265
|
+
}
|
|
170
266
|
else if (arg === "--endpoint") {
|
|
171
267
|
endpoint = normalizeEndpoint(next());
|
|
172
268
|
}
|
|
269
|
+
else if (arg === "--auth") {
|
|
270
|
+
auth = next();
|
|
271
|
+
}
|
|
173
272
|
else if (arg === "--state") {
|
|
174
273
|
statePath = next();
|
|
175
274
|
}
|
|
@@ -255,12 +354,17 @@ function parseArgs(argv) {
|
|
|
255
354
|
if (limit !== undefined && (!Number.isFinite(limit) || limit < 1)) {
|
|
256
355
|
throw new Error("--limit must be a positive integer");
|
|
257
356
|
}
|
|
357
|
+
if (auth !== undefined && auth.trim().length === 0) {
|
|
358
|
+
throw new Error("--auth must not be empty");
|
|
359
|
+
}
|
|
258
360
|
return {
|
|
259
361
|
agents,
|
|
260
362
|
endpoint,
|
|
363
|
+
auth,
|
|
261
364
|
statePath,
|
|
262
365
|
homeDir,
|
|
263
366
|
dryRun,
|
|
367
|
+
force,
|
|
264
368
|
follow,
|
|
265
369
|
pollIntervalMs,
|
|
266
370
|
idleExitAfterMs,
|
|
@@ -329,6 +433,14 @@ function normalizeCostRates(value, modelKey, source) {
|
|
|
329
433
|
asNumber(record.cache_write) ??
|
|
330
434
|
asNumber(record.inputCacheCreation) ??
|
|
331
435
|
asNumber(record.input_cache_creation),
|
|
436
|
+
cacheWrite5m: asNumber(record.cacheWrite5m) ??
|
|
437
|
+
asNumber(record.cache_write_5m) ??
|
|
438
|
+
asNumber(record.inputCacheCreation5m) ??
|
|
439
|
+
asNumber(record.input_cache_creation_5m),
|
|
440
|
+
cacheWrite1h: asNumber(record.cacheWrite1h) ??
|
|
441
|
+
asNumber(record.cache_write_1h) ??
|
|
442
|
+
asNumber(record.inputCacheCreation1h) ??
|
|
443
|
+
asNumber(record.input_cache_creation_1h),
|
|
332
444
|
};
|
|
333
445
|
const values = Object.entries(rates).filter(([, rate]) => rate !== undefined);
|
|
334
446
|
if (values.length === 0)
|
|
@@ -467,11 +579,27 @@ function normalizeUsage(value) {
|
|
|
467
579
|
const record = asRecord(value);
|
|
468
580
|
const nestedCost = asRecord(record.cost);
|
|
469
581
|
const cache = asRecord(record.cache);
|
|
582
|
+
const cacheCreation = asRecord(record.cache_creation);
|
|
470
583
|
const inputDetails = asRecord(record.input_tokens_details);
|
|
471
584
|
const outputDetails = asRecord(record.output_tokens_details);
|
|
472
585
|
const directInput = asNumber(record.input);
|
|
473
586
|
const aggregateInput = asNumber(record.input_tokens) ??
|
|
474
587
|
asNumber(record.prompt_tokens);
|
|
588
|
+
const cacheWrite5m = asNumber(record.cacheWrite5m) ??
|
|
589
|
+
asNumber(record.cache_write_5m) ??
|
|
590
|
+
asNumber(record.input_cache_creation_5m) ??
|
|
591
|
+
asNumber(cacheCreation.ephemeral_5m_input_tokens);
|
|
592
|
+
const cacheWrite1h = asNumber(record.cacheWrite1h) ??
|
|
593
|
+
asNumber(record.cache_write_1h) ??
|
|
594
|
+
asNumber(record.input_cache_creation_1h) ??
|
|
595
|
+
asNumber(cacheCreation.ephemeral_1h_input_tokens);
|
|
596
|
+
const untypedCacheWrite = asNumber(record.cacheWrite) ??
|
|
597
|
+
asNumber(record.cache_creation_input_tokens) ??
|
|
598
|
+
asNumber(cache.write);
|
|
599
|
+
const hasTypedCacheWrite = cacheWrite5m !== undefined || cacheWrite1h !== undefined;
|
|
600
|
+
const hasAnthropicCacheShape = hasTypedCacheWrite ||
|
|
601
|
+
asNumber(record.cache_creation_input_tokens) !== undefined ||
|
|
602
|
+
asNumber(record.cache_read_input_tokens) !== undefined;
|
|
475
603
|
const usage = {
|
|
476
604
|
input: directInput ?? aggregateInput,
|
|
477
605
|
output: asNumber(record.output) ??
|
|
@@ -487,9 +615,9 @@ function normalizeUsage(value) {
|
|
|
487
615
|
asNumber(record.cached_tokens) ??
|
|
488
616
|
asNumber(inputDetails.cached_tokens) ??
|
|
489
617
|
asNumber(cache.read),
|
|
490
|
-
cacheWrite:
|
|
491
|
-
|
|
492
|
-
|
|
618
|
+
cacheWrite: hasTypedCacheWrite ? undefined : untypedCacheWrite,
|
|
619
|
+
cacheWrite5m,
|
|
620
|
+
cacheWrite1h,
|
|
493
621
|
total: asNumber(record.totalTokens) ?? asNumber(record.total_tokens) ??
|
|
494
622
|
asNumber(record.total),
|
|
495
623
|
cost: asNumber(nestedCost.total) ??
|
|
@@ -499,14 +627,17 @@ function normalizeUsage(value) {
|
|
|
499
627
|
asNumber(record.cost),
|
|
500
628
|
};
|
|
501
629
|
if (usage.input !== undefined) {
|
|
502
|
-
usage.inputIncludesCache = directInput === undefined;
|
|
630
|
+
usage.inputIncludesCache = directInput === undefined && !hasAnthropicCacheShape;
|
|
503
631
|
}
|
|
504
632
|
if (usage.total === undefined) {
|
|
505
633
|
const cacheRead = usage.inputIncludesCache === false ? (usage.cacheRead ?? 0) : 0;
|
|
634
|
+
const cacheWrite = (usage.cacheWrite ?? 0) +
|
|
635
|
+
(usage.cacheWrite5m ?? 0) +
|
|
636
|
+
(usage.cacheWrite1h ?? 0);
|
|
506
637
|
const total = (usage.input ?? 0) +
|
|
507
638
|
(usage.output ?? 0) +
|
|
508
639
|
(usage.reasoning ?? 0) +
|
|
509
|
-
|
|
640
|
+
cacheWrite +
|
|
510
641
|
cacheRead;
|
|
511
642
|
if (total > 0)
|
|
512
643
|
usage.total = total;
|
|
@@ -529,10 +660,13 @@ function usageDetails(usage) {
|
|
|
529
660
|
return undefined;
|
|
530
661
|
const details = {};
|
|
531
662
|
const cachedInput = usage.inputIncludesCache === false ? 0 : (usage.cacheRead ?? 0);
|
|
532
|
-
const
|
|
663
|
+
const cacheWriteTotal = (usage.cacheWrite ?? 0) +
|
|
664
|
+
(usage.cacheWrite5m ?? 0) +
|
|
665
|
+
(usage.cacheWrite1h ?? 0);
|
|
666
|
+
const cacheWriteInInput = usage.inputIncludesCache === false ? 0 : cacheWriteTotal;
|
|
533
667
|
const regularInput = usage.input === undefined
|
|
534
668
|
? undefined
|
|
535
|
-
: Math.max(usage.input - cachedInput -
|
|
669
|
+
: Math.max(usage.input - cachedInput - cacheWriteInInput, 0);
|
|
536
670
|
if (regularInput !== undefined)
|
|
537
671
|
details.input = regularInput;
|
|
538
672
|
if (usage.output !== undefined)
|
|
@@ -543,6 +677,15 @@ function usageDetails(usage) {
|
|
|
543
677
|
details.input_cached_tokens = usage.cacheRead;
|
|
544
678
|
if (usage.cacheWrite !== undefined)
|
|
545
679
|
details.input_cache_creation = usage.cacheWrite;
|
|
680
|
+
if (usage.cacheWrite5m !== undefined) {
|
|
681
|
+
details.input_cache_creation_5m = usage.cacheWrite5m;
|
|
682
|
+
}
|
|
683
|
+
if (usage.cacheWrite1h !== undefined) {
|
|
684
|
+
details.input_cache_creation_1h = usage.cacheWrite1h;
|
|
685
|
+
}
|
|
686
|
+
if (usage.cacheWrite === undefined && cacheWriteTotal > 0) {
|
|
687
|
+
details.input_cache_creation = cacheWriteTotal;
|
|
688
|
+
}
|
|
546
689
|
if (usage.total !== undefined)
|
|
547
690
|
details.total = usage.total;
|
|
548
691
|
return Object.keys(details).length > 0 ? details : undefined;
|
|
@@ -565,13 +708,30 @@ function calculateCost(event, usage, costRates) {
|
|
|
565
708
|
setCostPart(details, "output", usage.output, rates.output);
|
|
566
709
|
setCostPart(details, "output_reasoning", usage.output_reasoning, rates.reasoning ?? rates.output);
|
|
567
710
|
setCostPart(details, "input_cached_tokens", usage.input_cached_tokens, rates.cacheRead ?? rates.input);
|
|
568
|
-
|
|
711
|
+
const hasTypedCacheWrite = usage.input_cache_creation_5m !== undefined ||
|
|
712
|
+
usage.input_cache_creation_1h !== undefined;
|
|
713
|
+
setCostPart(details, "input_cache_creation_5m", usage.input_cache_creation_5m, rates.cacheWrite5m ?? rates.cacheWrite ?? rates.input);
|
|
714
|
+
setCostPart(details, "input_cache_creation_1h", usage.input_cache_creation_1h, rates.cacheWrite1h ?? rates.cacheWrite ?? rates.input);
|
|
715
|
+
if (!hasTypedCacheWrite) {
|
|
716
|
+
setCostPart(details, "input_cache_creation", usage.input_cache_creation, rates.cacheWrite ?? rates.input);
|
|
717
|
+
}
|
|
718
|
+
const calculatedTotal = Object.values(details).reduce((sum, value) => sum + value, 0);
|
|
719
|
+
let source = "calculated";
|
|
720
|
+
if (calculatedTotal === 0 &&
|
|
721
|
+
usage.total !== undefined &&
|
|
722
|
+
usage.total > 0 &&
|
|
723
|
+
rates.input !== undefined) {
|
|
724
|
+
for (const key of Object.keys(details))
|
|
725
|
+
delete details[key];
|
|
726
|
+
setCostPart(details, "input", usage.total, rates.input);
|
|
727
|
+
source = "calculated_total_as_input";
|
|
728
|
+
}
|
|
569
729
|
if (Object.keys(details).length === 0)
|
|
570
730
|
return undefined;
|
|
571
731
|
details.total = roundCost(Object.values(details).reduce((sum, value) => sum + value, 0));
|
|
572
732
|
return {
|
|
573
733
|
details,
|
|
574
|
-
source
|
|
734
|
+
source,
|
|
575
735
|
modelKey,
|
|
576
736
|
rates,
|
|
577
737
|
};
|
|
@@ -805,8 +965,12 @@ function codexEvents(homeDir, options = {}) {
|
|
|
805
965
|
cwd: currentCwd,
|
|
806
966
|
startMs: timestamp,
|
|
807
967
|
parentRecordId: "session",
|
|
808
|
-
|
|
809
|
-
|
|
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
|
+
},
|
|
810
974
|
});
|
|
811
975
|
}
|
|
812
976
|
}
|
|
@@ -1343,19 +1507,28 @@ function stableId(input) {
|
|
|
1343
1507
|
return createHash("sha256").update(input).digest("hex").slice(0, 32);
|
|
1344
1508
|
}
|
|
1345
1509
|
function importIdentity(event) {
|
|
1346
|
-
return
|
|
1510
|
+
return importStateIdentityVersions[event.agent] ?? importStateIdentityVersion;
|
|
1511
|
+
}
|
|
1512
|
+
function payloadVersion(event) {
|
|
1513
|
+
return importPayloadVersions[event.agent] ?? importPayloadVersion;
|
|
1514
|
+
}
|
|
1515
|
+
function langfuseIdIdentity(event) {
|
|
1516
|
+
return langfuseIdIdentityVersions[event.agent] ?? langfuseIdIdentityVersion;
|
|
1347
1517
|
}
|
|
1348
1518
|
function fingerprint(event) {
|
|
1349
1519
|
return `${importIdentity(event)}:${event.agent}:${event.sessionId}:${event.recordId}`;
|
|
1350
1520
|
}
|
|
1521
|
+
function langfuseFingerprint(event) {
|
|
1522
|
+
return `${langfuseIdIdentity(event)}:${event.agent}:${event.sessionId}:${event.recordId}`;
|
|
1523
|
+
}
|
|
1351
1524
|
function traceFingerprint(event) {
|
|
1352
|
-
return `${
|
|
1525
|
+
return `${langfuseIdIdentity(event)}:${event.agent}:${event.sessionId}`;
|
|
1353
1526
|
}
|
|
1354
1527
|
function traceId(event) {
|
|
1355
1528
|
return stableId(traceFingerprint(event));
|
|
1356
1529
|
}
|
|
1357
1530
|
function spanId(event) {
|
|
1358
|
-
return stableId(
|
|
1531
|
+
return stableId(langfuseFingerprint(event)).slice(0, 16);
|
|
1359
1532
|
}
|
|
1360
1533
|
function rootSpanId(event) {
|
|
1361
1534
|
return stableId(`${traceFingerprint(event)}:root`).slice(0, 16);
|
|
@@ -1446,6 +1619,9 @@ function toOtlp(events, options = {}) {
|
|
|
1446
1619
|
attr("langfuse.trace.metadata.project_path", firstProject.projectPath),
|
|
1447
1620
|
attr("langfuse.trace.metadata.project_name", firstProject.projectName),
|
|
1448
1621
|
attr("langfuse.trace.metadata.project_folder", firstProject.projectFolder),
|
|
1622
|
+
attr("langfuse.trace.metadata.import_payload_version", payloadVersion(first)),
|
|
1623
|
+
attr("langfuse.trace.metadata.import_state_identity", importIdentity(first)),
|
|
1624
|
+
attr("langfuse.trace.metadata.langfuse_id_identity", langfuseIdIdentity(first)),
|
|
1449
1625
|
attr("langfuse.observation.metadata.agent", first.agent),
|
|
1450
1626
|
attr("langfuse.observation.metadata.host", currentHost),
|
|
1451
1627
|
attr("langfuse.observation.metadata.machine", currentHost),
|
|
@@ -1456,6 +1632,9 @@ function toOtlp(events, options = {}) {
|
|
|
1456
1632
|
attr("langfuse.observation.metadata.project_path", firstProject.projectPath),
|
|
1457
1633
|
attr("langfuse.observation.metadata.project_name", firstProject.projectName),
|
|
1458
1634
|
attr("langfuse.observation.metadata.project_folder", firstProject.projectFolder),
|
|
1635
|
+
attr("langfuse.observation.metadata.import_payload_version", payloadVersion(first)),
|
|
1636
|
+
attr("langfuse.observation.metadata.import_state_identity", importIdentity(first)),
|
|
1637
|
+
attr("langfuse.observation.metadata.langfuse_id_identity", langfuseIdIdentity(first)),
|
|
1459
1638
|
attr("source.path", first.sourcePath),
|
|
1460
1639
|
attr("cwd", first.cwd),
|
|
1461
1640
|
attr("project.path", firstProject.projectPath),
|
|
@@ -1507,6 +1686,9 @@ function toOtlp(events, options = {}) {
|
|
|
1507
1686
|
attr("langfuse.observation.metadata.project_folder", eventProject.projectFolder),
|
|
1508
1687
|
attr("langfuse.observation.metadata.model", modelName ?? event.model),
|
|
1509
1688
|
attr("langfuse.observation.metadata.provider", event.provider),
|
|
1689
|
+
attr("langfuse.observation.metadata.import_payload_version", payloadVersion(event)),
|
|
1690
|
+
attr("langfuse.observation.metadata.import_state_identity", importIdentity(event)),
|
|
1691
|
+
attr("langfuse.observation.metadata.langfuse_id_identity", langfuseIdIdentity(event)),
|
|
1510
1692
|
attr("langfuse.observation.usage_details", generation ? usage : undefined),
|
|
1511
1693
|
attr("langfuse.observation.cost_details", cost?.details),
|
|
1512
1694
|
attr("langfuse.observation.metadata.usage_details", usage),
|
|
@@ -1515,6 +1697,9 @@ function toOtlp(events, options = {}) {
|
|
|
1515
1697
|
attr("langfuse.observation.metadata.cost_model_key", cost?.modelKey),
|
|
1516
1698
|
attr("langfuse.observation.metadata.cost_rates", cost?.rates),
|
|
1517
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),
|
|
1518
1703
|
attr("langfuse.observation.input", event.input),
|
|
1519
1704
|
attr("langfuse.observation.output", event.output),
|
|
1520
1705
|
attr("source.path", event.sourcePath),
|
|
@@ -1614,11 +1799,16 @@ function splitSendBatches(events, options) {
|
|
|
1614
1799
|
}
|
|
1615
1800
|
async function postOtlp(endpoint, events, options) {
|
|
1616
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);
|
|
1617
1807
|
let response;
|
|
1618
1808
|
try {
|
|
1619
1809
|
response = await fetch(endpoint, {
|
|
1620
1810
|
method: "POST",
|
|
1621
|
-
headers
|
|
1811
|
+
headers,
|
|
1622
1812
|
body,
|
|
1623
1813
|
});
|
|
1624
1814
|
}
|
|
@@ -1629,6 +1819,11 @@ async function postOtlp(endpoint, events, options) {
|
|
|
1629
1819
|
throw new Error(`OTLP POST failed: ${response.status} ${await response.text()}`);
|
|
1630
1820
|
}
|
|
1631
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
|
+
}
|
|
1632
1827
|
function describeError(error) {
|
|
1633
1828
|
if (!(error instanceof Error))
|
|
1634
1829
|
return String(error);
|
|
@@ -1677,7 +1872,9 @@ async function run(options) {
|
|
|
1677
1872
|
for (const event of events) {
|
|
1678
1873
|
discovered[event.agent] = (discovered[event.agent] ?? 0) + 1;
|
|
1679
1874
|
}
|
|
1680
|
-
const unsent =
|
|
1875
|
+
const unsent = options.force
|
|
1876
|
+
? events
|
|
1877
|
+
: events.filter((event) => state.sent[fingerprint(event)] === undefined);
|
|
1681
1878
|
const selected = options.limit === undefined
|
|
1682
1879
|
? unsent
|
|
1683
1880
|
: unsent.slice(0, options.limit);
|
|
@@ -1719,6 +1916,7 @@ async function run(options) {
|
|
|
1719
1916
|
await postOtlp(options.endpoint, batch, {
|
|
1720
1917
|
maxFieldBytes: options.maxFieldBytes,
|
|
1721
1918
|
costRates: options.costRates,
|
|
1919
|
+
auth: options.auth,
|
|
1722
1920
|
});
|
|
1723
1921
|
for (const event of batch) {
|
|
1724
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
|
},
|