@ramarivera/coding-agent-langfuse 0.1.42 → 0.1.43

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
@@ -3,9 +3,10 @@
3
3
  Universal coding-agent Langfuse backfiller and OTLP exporter helpers.
4
4
 
5
5
  It imports local histories from Codex, Claude Code, Grok, OpenCode, and Pi into
6
- Langfuse as session traces with child observations. LLM usage records are kept
7
- as observation metadata so historical imports do not create Langfuse billing or
8
- cost rows. Tool calls remain child spans under the same session.
6
+ Langfuse as session traces with child observations. LLM generations include
7
+ Langfuse canonical `usage_details` and `cost_details` attributes so historical
8
+ backfills participate in Langfuse model-usage and cost dashboards. Tool calls
9
+ remain child spans under the same session.
9
10
 
10
11
  ```sh
11
12
  coding-agent-langfuse-backfill --agents codex,claude,grok,pi,opencode
@@ -35,6 +36,48 @@ npx @ramarivera/coding-agent-langfuse@latest \
35
36
  The importer is fail-fast: the first failed OTLP POST stops the run, prints the
36
37
  real network cause, and preserves local state so reruns resume cleanly.
37
38
 
39
+ ## Cost calculation
40
+
41
+ Backfill cost calculation follows Langfuse's OpenTelemetry mapping: generation
42
+ spans receive `langfuse.observation.usage_details` and
43
+ `langfuse.observation.cost_details` JSON attributes. If a source history already
44
+ records a total cost, that recorded value wins. Otherwise, the importer
45
+ calculates per-usage-type USD costs from a model catalog using rates in USD per
46
+ 1M tokens.
47
+
48
+ The built-in catalog covers OpenAI GPT-5.5 API list pricing plus the toolbox/Pi
49
+ models already used in local configuration, including Fireworks Kimi K2.6,
50
+ Fireworks DeepSeek V4 Pro, MiniMax-M3, Together DeepSeek/Kimi/GLM/MiniMax, and
51
+ Zai GLM. `gpt-5.5` is charged at current standard API list price by default:
52
+ `$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.
54
+
55
+ Use an override only when you intentionally want a different accounting policy:
56
+
57
+ ```sh
58
+ npx @ramarivera/coding-agent-langfuse@latest \
59
+ --agents codex \
60
+ --cost-rates-json '{"gpt-5.5":{"input":1,"output":2,"cacheRead":0.1,"cacheWrite":0}}'
61
+ ```
62
+
63
+ You can also keep the policy in a JSON file and pass `--cost-rates PATH`, or set
64
+ `CODING_AGENT_LANGFUSE_COST_RATES_PATH` /
65
+ `CODING_AGENT_LANGFUSE_COST_RATES_JSON` for both manual backfills and generated
66
+ services. A file can be either a direct model map or `{ "rates": { ... } }`:
67
+
68
+ ```json
69
+ {
70
+ "rates": {
71
+ "gpt-5.5": {
72
+ "input": 1,
73
+ "output": 2,
74
+ "cacheRead": 0.1,
75
+ "cacheWrite": 0
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
38
81
  ## Follow as a host service
39
82
 
40
83
  Install a live follower directly from npm. The generated service keeps inference
@@ -112,7 +155,7 @@ npm run test:e2e
112
155
  The e2e suite verifies:
113
156
 
114
157
  - Codex full session traces with messages, reasoning, tool calls, tool results,
115
- and usage metadata
158
+ usage details, and cost details
116
159
  - Follow mode picking up newly written Codex events
117
160
  - One CLI run posting reconstructable traces for Claude Code, Codex, Grok,
118
161
  OpenCode, and Pi
@@ -8,6 +8,7 @@ type Usage = {
8
8
  cacheWrite?: number;
9
9
  total?: number;
10
10
  cost?: number;
11
+ inputIncludesCache?: boolean;
11
12
  };
12
13
  type BackfillEvent = {
13
14
  agent: AgentName;
@@ -44,6 +45,19 @@ type BackfillOptions = {
44
45
  maxRequestBytes: number;
45
46
  maxFieldBytes: number;
46
47
  postDelayMs: number;
48
+ costRates: CostCatalog;
49
+ };
50
+ type CostRates = {
51
+ input?: number;
52
+ output?: number;
53
+ reasoning?: number;
54
+ cacheRead?: number;
55
+ cacheWrite?: number;
56
+ };
57
+ type CostCatalog = Record<string, CostRates>;
58
+ type OtlpOptions = {
59
+ maxFieldBytes?: number;
60
+ costRates?: CostCatalog;
47
61
  };
48
62
  type RunSummary = {
49
63
  discovered: Record<string, number>;
@@ -77,9 +91,7 @@ declare function opencodeEvents(homeDir: string, options?: {
77
91
  untilMs?: number;
78
92
  }): BackfillEvent[];
79
93
  declare function fingerprint(event: BackfillEvent): string;
80
- declare function toOtlp(events: BackfillEvent[], options?: {
81
- maxFieldBytes?: number;
82
- }): Record<string, unknown>;
94
+ declare function toOtlp(events: BackfillEvent[], options?: OtlpOptions): Record<string, unknown>;
83
95
  declare function discoverEvents(options: BackfillOptions): BackfillEvent[];
84
96
  declare function run(options: BackfillOptions): Promise<RunSummary>;
85
97
  declare function follow(options: BackfillOptions): Promise<FollowSummary>;
package/dist/backfill.js CHANGED
@@ -5,13 +5,13 @@ 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 importIdentityVersion = "v8-cached-input-token-split";
8
+ const importIdentityVersion = "v9-cost-details";
9
9
  const importIdentityVersions = {
10
- claude: "v11-tool-results",
11
- codex: "v9-codex-conversation-events",
12
- grok: "v11-chat-history-only",
13
- opencode: "v10-opencode-message-parts",
14
- pi: "v11-tool-results",
10
+ claude: "v12-cost-details",
11
+ codex: "v10-cost-details",
12
+ grok: "v12-cost-details",
13
+ opencode: "v11-cost-details",
14
+ pi: "v12-cost-details",
15
15
  };
16
16
  const defaultEndpoint = "https://langfuse.ai.roxasroot.net/otel/v1/traces";
17
17
  const deadRemoteEndpoint = "http://langfuse.ai.roxasroot.net:14318/v1/traces";
@@ -19,6 +19,79 @@ const defaultMaxRequestBytes = 12 * 1024 * 1024;
19
19
  const defaultMaxFieldBytes = 512 * 1024;
20
20
  const defaultStatePath = join(homedir(), ".local/state/coding-agent-langfuse/backfill-v6.json");
21
21
  const currentHost = hostname();
22
+ const kimiFirepassRates = {
23
+ input: 2,
24
+ output: 8,
25
+ cacheRead: 0.3,
26
+ cacheWrite: 0,
27
+ };
28
+ const deepseekFireworksRates = {
29
+ input: 1.74,
30
+ output: 3.48,
31
+ cacheRead: 0.15,
32
+ cacheWrite: 0,
33
+ };
34
+ const miniMaxM3Rates = {
35
+ input: 0.6,
36
+ output: 2.4,
37
+ cacheRead: 0.12,
38
+ cacheWrite: 0,
39
+ };
40
+ const gpt55Rates = {
41
+ input: 5,
42
+ output: 30,
43
+ cacheRead: 0.5,
44
+ cacheWrite: 5,
45
+ };
46
+ const gpt55ProRates = {
47
+ input: 30,
48
+ output: 180,
49
+ cacheRead: 30,
50
+ cacheWrite: 30,
51
+ };
52
+ const defaultCostRates = {
53
+ "gpt-5.5": gpt55Rates,
54
+ "openai/gpt-5.5": gpt55Rates,
55
+ "gpt-5.5-pro": gpt55ProRates,
56
+ "openai/gpt-5.5-pro": gpt55ProRates,
57
+ "accounts/fireworks/routers/kimi-k2p6-turbo": kimiFirepassRates,
58
+ "fireworks-firepass/accounts/fireworks/routers/kimi-k2p6-turbo": kimiFirepassRates,
59
+ "kimi-for-coding": kimiFirepassRates,
60
+ "accounts/fireworks/models/deepseek-v4-pro": deepseekFireworksRates,
61
+ "fireworks/accounts/fireworks/models/deepseek-v4-pro": deepseekFireworksRates,
62
+ "MiniMax-M3": miniMaxM3Rates,
63
+ "minimax/MiniMax-M3": miniMaxM3Rates,
64
+ "together/deepseek-ai/DeepSeek-V4-Pro": {
65
+ input: 2.1,
66
+ output: 4.4,
67
+ cacheRead: 0.2,
68
+ cacheWrite: 0,
69
+ },
70
+ "together/zai-org/GLM-5.1": {
71
+ input: 1.4,
72
+ output: 4.4,
73
+ cacheRead: 0.2,
74
+ cacheWrite: 0,
75
+ },
76
+ "together/moonshotai/Kimi-K2.6": {
77
+ input: 1.2,
78
+ output: 4.5,
79
+ cacheRead: 0.2,
80
+ cacheWrite: 0,
81
+ },
82
+ "together/MiniMaxAI/MiniMax-M2.7": {
83
+ input: 0.3,
84
+ output: 1.2,
85
+ cacheRead: 0.06,
86
+ cacheWrite: 0,
87
+ },
88
+ "zai/glm-5.1": {
89
+ input: 1.4,
90
+ output: 4.4,
91
+ cacheRead: 0.2,
92
+ cacheWrite: 0,
93
+ },
94
+ };
22
95
  function projectMetadata(cwd) {
23
96
  if (!cwd)
24
97
  return {};
@@ -46,6 +119,8 @@ Options:
46
119
  --max-request-bytes N Split OTLP POSTs below this JSON byte size (default: ${defaultMaxRequestBytes})
47
120
  --max-field-bytes N Truncate individual input/output fields above this byte size (default: ${defaultMaxFieldBytes})
48
121
  --post-delay-ms N Delay after each successful OTLP POST (default: 0)
122
+ --cost-rates PATH JSON model cost-rate overrides in USD per 1M tokens
123
+ --cost-rates-json JSON Inline JSON model cost-rate overrides in USD per 1M tokens
49
124
  --follow Keep scanning and sending newly written events
50
125
  --poll-interval-ms N Delay between --follow scans (default: 5000)
51
126
  --idle-exit-after-ms N Stop --follow after this much time without new sends
@@ -71,6 +146,7 @@ function parseArgs(argv) {
71
146
  let postDelayMs = Number.parseInt(process.env.LANGFUSE_BACKFILL_POST_DELAY_MS ?? "", 10);
72
147
  if (!Number.isFinite(postDelayMs))
73
148
  postDelayMs = 0;
149
+ let costRates = loadCostCatalogFromEnv();
74
150
  let follow = false;
75
151
  let pollIntervalMs = 5_000;
76
152
  let idleExitAfterMs;
@@ -125,6 +201,12 @@ function parseArgs(argv) {
125
201
  else if (arg === "--post-delay-ms") {
126
202
  postDelayMs = Number.parseInt(next(), 10);
127
203
  }
204
+ else if (arg === "--cost-rates") {
205
+ costRates = mergeCostCatalog(costRates, loadCostCatalogFile(next()));
206
+ }
207
+ else if (arg === "--cost-rates-json") {
208
+ costRates = mergeCostCatalog(costRates, parseCostCatalogJson(next()));
209
+ }
128
210
  else if (arg === "--follow") {
129
211
  follow = true;
130
212
  }
@@ -190,7 +272,82 @@ function parseArgs(argv) {
190
272
  maxRequestBytes,
191
273
  maxFieldBytes,
192
274
  postDelayMs,
275
+ costRates,
276
+ };
277
+ }
278
+ function loadCostCatalogFromEnv() {
279
+ let catalog = { ...defaultCostRates };
280
+ const path = process.env.CODING_AGENT_LANGFUSE_COST_RATES_PATH ??
281
+ process.env.LANGFUSE_BACKFILL_COST_RATES_PATH;
282
+ const inlineJson = process.env.CODING_AGENT_LANGFUSE_COST_RATES_JSON ??
283
+ process.env.LANGFUSE_BACKFILL_COST_RATES_JSON;
284
+ if (path)
285
+ catalog = mergeCostCatalog(catalog, loadCostCatalogFile(path));
286
+ if (inlineJson)
287
+ catalog = mergeCostCatalog(catalog, parseCostCatalogJson(inlineJson));
288
+ return catalog;
289
+ }
290
+ function loadCostCatalogFile(path) {
291
+ return parseCostCatalogJson(readFileSync(path, "utf8"), path);
292
+ }
293
+ function parseCostCatalogJson(json, source = "inline JSON") {
294
+ let parsed;
295
+ try {
296
+ parsed = JSON.parse(json);
297
+ }
298
+ catch (error) {
299
+ throw new Error(`Invalid cost rates ${source}: ${describeError(error)}`);
300
+ }
301
+ const root = asRecord(parsed);
302
+ const nestedRates = asRecord(root.rates);
303
+ const entries = Object.keys(nestedRates).length > 0 ? nestedRates : root;
304
+ const catalog = {};
305
+ for (const [modelKey, rawRates] of Object.entries(entries)) {
306
+ const rates = normalizeCostRates(rawRates, modelKey, source);
307
+ if (rates)
308
+ catalog[modelKey] = rates;
309
+ }
310
+ return catalog;
311
+ }
312
+ function normalizeCostRates(value, modelKey, source) {
313
+ const record = asRecord(value);
314
+ const rates = {
315
+ input: asNumber(record.input) ??
316
+ asNumber(record.input_cost) ??
317
+ asNumber(record.inputPerMillion),
318
+ output: asNumber(record.output) ??
319
+ asNumber(record.output_cost) ??
320
+ asNumber(record.outputPerMillion),
321
+ reasoning: asNumber(record.reasoning) ??
322
+ asNumber(record.reasoning_cost) ??
323
+ asNumber(record.reasoningPerMillion),
324
+ cacheRead: asNumber(record.cacheRead) ??
325
+ asNumber(record.cache_read) ??
326
+ asNumber(record.cachedInput) ??
327
+ asNumber(record.input_cached_tokens),
328
+ cacheWrite: asNumber(record.cacheWrite) ??
329
+ asNumber(record.cache_write) ??
330
+ asNumber(record.inputCacheCreation) ??
331
+ asNumber(record.input_cache_creation),
193
332
  };
333
+ const values = Object.entries(rates).filter(([, rate]) => rate !== undefined);
334
+ if (values.length === 0)
335
+ return undefined;
336
+ const cleanRates = {};
337
+ for (const [name, rate] of values) {
338
+ if (rate === undefined || rate < 0) {
339
+ throw new Error(`Invalid ${name} cost rate for '${modelKey}' in ${source}; rates must be non-negative USD per 1M tokens.`);
340
+ }
341
+ cleanRates[name] = rate;
342
+ }
343
+ return cleanRates;
344
+ }
345
+ function mergeCostCatalog(base, override) {
346
+ const merged = { ...base };
347
+ for (const [modelKey, rates] of Object.entries(override)) {
348
+ merged[modelKey] = { ...(merged[modelKey] ?? {}), ...rates };
349
+ }
350
+ return merged;
194
351
  }
195
352
  function normalizeEndpoint(endpoint) {
196
353
  if (endpoint !== deadRemoteEndpoint)
@@ -312,10 +469,11 @@ function normalizeUsage(value) {
312
469
  const cache = asRecord(record.cache);
313
470
  const inputDetails = asRecord(record.input_tokens_details);
314
471
  const outputDetails = asRecord(record.output_tokens_details);
472
+ const directInput = asNumber(record.input);
473
+ const aggregateInput = asNumber(record.input_tokens) ??
474
+ asNumber(record.prompt_tokens);
315
475
  const usage = {
316
- input: asNumber(record.input) ??
317
- asNumber(record.input_tokens) ??
318
- asNumber(record.prompt_tokens),
476
+ input: directInput ?? aggregateInput,
319
477
  output: asNumber(record.output) ??
320
478
  asNumber(record.output_tokens) ??
321
479
  asNumber(record.completion_tokens),
@@ -340,11 +498,16 @@ function normalizeUsage(value) {
340
498
  asNumber(record.total_cost) ??
341
499
  asNumber(record.cost),
342
500
  };
501
+ if (usage.input !== undefined) {
502
+ usage.inputIncludesCache = directInput === undefined;
503
+ }
343
504
  if (usage.total === undefined) {
505
+ const cacheRead = usage.inputIncludesCache === false ? (usage.cacheRead ?? 0) : 0;
344
506
  const total = (usage.input ?? 0) +
345
507
  (usage.output ?? 0) +
346
508
  (usage.reasoning ?? 0) +
347
- (usage.cacheWrite ?? 0);
509
+ (usage.cacheWrite ?? 0) +
510
+ cacheRead;
348
511
  if (total > 0)
349
512
  usage.total = total;
350
513
  }
@@ -365,8 +528,8 @@ function usageDetails(usage) {
365
528
  if (!usage)
366
529
  return undefined;
367
530
  const details = {};
368
- const cachedInput = usage.cacheRead ?? 0;
369
- const cacheWrite = usage.cacheWrite ?? 0;
531
+ const cachedInput = usage.inputIncludesCache === false ? 0 : (usage.cacheRead ?? 0);
532
+ const cacheWrite = usage.inputIncludesCache === false ? 0 : (usage.cacheWrite ?? 0);
370
533
  const regularInput = usage.input === undefined
371
534
  ? undefined
372
535
  : Math.max(usage.input - cachedInput - cacheWrite, 0);
@@ -384,6 +547,62 @@ function usageDetails(usage) {
384
547
  details.total = usage.total;
385
548
  return Object.keys(details).length > 0 ? details : undefined;
386
549
  }
550
+ function calculateCost(event, usage, costRates) {
551
+ if (!usage)
552
+ return undefined;
553
+ if (event.usage?.cost !== undefined) {
554
+ return {
555
+ details: { total: roundCost(event.usage.cost) },
556
+ source: "recorded",
557
+ };
558
+ }
559
+ const match = findCostRates(event, costRates);
560
+ if (!match)
561
+ return undefined;
562
+ const { rates, modelKey } = match;
563
+ const details = {};
564
+ setCostPart(details, "input", usage.input, rates.input);
565
+ setCostPart(details, "output", usage.output, rates.output);
566
+ setCostPart(details, "output_reasoning", usage.output_reasoning, rates.reasoning ?? rates.output);
567
+ setCostPart(details, "input_cached_tokens", usage.input_cached_tokens, rates.cacheRead ?? rates.input);
568
+ setCostPart(details, "input_cache_creation", usage.input_cache_creation, rates.cacheWrite ?? rates.input);
569
+ if (Object.keys(details).length === 0)
570
+ return undefined;
571
+ details.total = roundCost(Object.values(details).reduce((sum, value) => sum + value, 0));
572
+ return {
573
+ details,
574
+ source: "calculated",
575
+ modelKey,
576
+ rates,
577
+ };
578
+ }
579
+ function findCostRates(event, costRates) {
580
+ const modelName = normalizeModelName(event.model);
581
+ const candidates = [
582
+ event.provider && event.model ? `${event.provider}/${event.model}` : undefined,
583
+ event.provider && modelName ? `${event.provider}/${modelName}` : undefined,
584
+ event.model,
585
+ modelName,
586
+ ];
587
+ const seen = new Set();
588
+ for (const candidate of candidates) {
589
+ if (!candidate || seen.has(candidate))
590
+ continue;
591
+ seen.add(candidate);
592
+ const rates = costRates[candidate];
593
+ if (rates)
594
+ return { modelKey: candidate, rates };
595
+ }
596
+ return undefined;
597
+ }
598
+ function setCostPart(details, key, tokens, usdPerMillionTokens) {
599
+ if (tokens === undefined || usdPerMillionTokens === undefined)
600
+ return;
601
+ details[key] = roundCost((tokens * usdPerMillionTokens) / 1_000_000);
602
+ }
603
+ function roundCost(value) {
604
+ return Number(value.toFixed(12));
605
+ }
387
606
  function isGenerationEvent(event) {
388
607
  return event.usage !== undefined && event.role !== "user" &&
389
608
  event.role !== "developer" && event.role !== "system";
@@ -1189,6 +1408,7 @@ function limitEventPayload(event, maxFieldBytes) {
1189
1408
  }
1190
1409
  function toOtlp(events, options = {}) {
1191
1410
  const maxFieldBytes = options.maxFieldBytes ?? defaultMaxFieldBytes;
1411
+ const costRates = options.costRates ?? loadCostCatalogFromEnv();
1192
1412
  const spansByTrace = new Map();
1193
1413
  for (const rawEvent of events) {
1194
1414
  const event = limitEventPayload(rawEvent, maxFieldBytes);
@@ -1259,6 +1479,7 @@ function toOtlp(events, options = {}) {
1259
1479
  const modelName = normalizeModelName(event.model);
1260
1480
  const generation = isGenerationEvent(event);
1261
1481
  const usage = usageDetails(event.usage);
1482
+ const cost = generation ? calculateCost(event, usage, costRates) : undefined;
1262
1483
  const eventProject = projectMetadata(event.cwd);
1263
1484
  const attributes = [
1264
1485
  attr("service.name", `agent.${event.agent}`),
@@ -1286,7 +1507,13 @@ function toOtlp(events, options = {}) {
1286
1507
  attr("langfuse.observation.metadata.project_folder", eventProject.projectFolder),
1287
1508
  attr("langfuse.observation.metadata.model", modelName ?? event.model),
1288
1509
  attr("langfuse.observation.metadata.provider", event.provider),
1510
+ attr("langfuse.observation.usage_details", generation ? usage : undefined),
1511
+ attr("langfuse.observation.cost_details", cost?.details),
1289
1512
  attr("langfuse.observation.metadata.usage_details", usage),
1513
+ attr("langfuse.observation.metadata.cost_details", cost?.details),
1514
+ attr("langfuse.observation.metadata.cost_source", cost?.source),
1515
+ attr("langfuse.observation.metadata.cost_model_key", cost?.modelKey),
1516
+ attr("langfuse.observation.metadata.cost_rates", cost?.rates),
1290
1517
  attr("langfuse.observation.metadata.recorded_cost", event.usage?.cost),
1291
1518
  attr("langfuse.observation.input", event.input),
1292
1519
  attr("langfuse.observation.output", event.output),
@@ -1465,6 +1692,7 @@ async function run(options) {
1465
1692
  batchSize: options.batchSize,
1466
1693
  maxRequestBytes: options.maxRequestBytes,
1467
1694
  maxFieldBytes: options.maxFieldBytes,
1695
+ costRates: options.costRates,
1468
1696
  });
1469
1697
  }
1470
1698
  catch (error) {
@@ -1490,6 +1718,7 @@ async function run(options) {
1490
1718
  try {
1491
1719
  await postOtlp(options.endpoint, batch, {
1492
1720
  maxFieldBytes: options.maxFieldBytes,
1721
+ costRates: options.costRates,
1493
1722
  });
1494
1723
  for (const event of batch) {
1495
1724
  state.sent[fingerprint(event)] = new Date().toISOString();
package/dist/service.d.ts CHANGED
@@ -13,6 +13,8 @@ type ServiceOptions = {
13
13
  batchSize: number;
14
14
  pollIntervalMs: number;
15
15
  postDelayMs: number;
16
+ costRatesPath?: string;
17
+ costRatesJson?: string;
16
18
  npxPath: string;
17
19
  since?: string;
18
20
  dryRun: boolean;
package/dist/service.js CHANGED
@@ -24,6 +24,8 @@ Service options:
24
24
  --batch-size N OTLP spans per POST (default: 10)
25
25
  --poll-interval-ms N Delay between --follow scans (default: 5000)
26
26
  --post-delay-ms N Delay after each successful OTLP POST (default: 0)
27
+ --cost-rates PATH JSON model cost-rate overrides in USD per 1M tokens
28
+ --cost-rates-json JSON Inline JSON model cost-rate overrides in USD per 1M tokens
27
29
  --since ISO_OR_MS Optional lower bound for events the follower may send
28
30
  --working-directory DIR Directory the service starts in (default: --home)
29
31
  --path VALUE PATH value injected into the service environment
@@ -49,6 +51,10 @@ function parseServiceArgs(argv) {
49
51
  let batchSize = 10;
50
52
  let pollIntervalMs = 5_000;
51
53
  let postDelayMs = 0;
54
+ let costRatesPath = process.env.CODING_AGENT_LANGFUSE_COST_RATES_PATH ??
55
+ process.env.LANGFUSE_BACKFILL_COST_RATES_PATH;
56
+ let costRatesJson = process.env.CODING_AGENT_LANGFUSE_COST_RATES_JSON ??
57
+ process.env.LANGFUSE_BACKFILL_COST_RATES_JSON;
52
58
  let since;
53
59
  let dryRun = false;
54
60
  let start = true;
@@ -95,6 +101,12 @@ function parseServiceArgs(argv) {
95
101
  else if (arg === "--post-delay-ms") {
96
102
  postDelayMs = parseNonNegativeInt(arg, next());
97
103
  }
104
+ else if (arg === "--cost-rates") {
105
+ costRatesPath = next();
106
+ }
107
+ else if (arg === "--cost-rates-json") {
108
+ costRatesJson = next();
109
+ }
98
110
  else if (arg === "--since") {
99
111
  since = next();
100
112
  }
@@ -134,6 +146,8 @@ function parseServiceArgs(argv) {
134
146
  batchSize,
135
147
  pollIntervalMs,
136
148
  postDelayMs,
149
+ costRatesPath,
150
+ costRatesJson,
137
151
  since,
138
152
  dryRun,
139
153
  start,
@@ -282,6 +296,10 @@ function buildFollowCommand(options) {
282
296
  ];
283
297
  if (options.since)
284
298
  command.push("--since", options.since);
299
+ if (options.costRatesPath)
300
+ command.push("--cost-rates", options.costRatesPath);
301
+ if (options.costRatesJson)
302
+ command.push("--cost-rates-json", options.costRatesJson);
285
303
  return command;
286
304
  }
287
305
  function renderSystemdUnit(options, command) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramarivera/coding-agent-langfuse",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
4
4
  "description": "Universal coding-agent Langfuse backfiller and live OTLP helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",