@ramarivera/coding-agent-langfuse 0.1.55 → 0.1.57

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
@@ -135,15 +135,27 @@ records a total cost, that recorded value wins. Otherwise, the importer
135
135
  calculates per-usage-type USD costs from a model catalog using rates in USD per
136
136
  1M tokens.
137
137
 
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
140
- models already used in local configuration, including Fireworks Kimi K2.6,
141
- Fireworks DeepSeek V4 Pro, MiniMax-M3, Together DeepSeek/Kimi/GLM/MiniMax, and
142
- Zai GLM. `gpt-5.5` is charged at current standard API list price by default:
143
- `$5.00` input, `$0.50` cached input, and `$30.00` output per 1M tokens. GPT-5.5
144
- Pro defaults to `$30.00` input and `$180.00` output per 1M tokens. Claude Opus 4
145
- models default to `$15.00` input, `$1.50` cache hits, `$18.75` 5-minute cache
146
- writes, `$30.00` 1-hour cache writes, and `$75.00` output per 1M tokens.
138
+ By default, the importer refreshes model pricing from `https://models.dev/api.json`
139
+ and caches it locally. The cache is used for both one-shot backfills and
140
+ `--follow` services, so long-running collectors do not hit the network for every
141
+ scan. The built-in catalog remains only as an offline fallback for known local
142
+ models; explicit `--cost-rates` and `--cost-rates-json` overrides always win.
143
+
144
+ Control the pricing cache with:
145
+
146
+ ```sh
147
+ npx @ramarivera/coding-agent-langfuse@latest \
148
+ --models-dev-cache "$HOME/.cache/coding-agent-langfuse/models-dev-v1.json" \
149
+ --models-dev-ttl-ms 86400000
150
+ ```
151
+
152
+ Use `--models-dev` to force-enable the cache if an environment disabled it. Use
153
+ `--no-models-dev` or `CODING_AGENT_LANGFUSE_MODELS_DEV=0` for fully static
154
+ offline pricing. Advanced deployments can set `--models-dev-url`,
155
+ `CODING_AGENT_LANGFUSE_MODELS_DEV_CACHE`,
156
+ `CODING_AGENT_LANGFUSE_MODELS_DEV_TTL_MS`, or
157
+ `CODING_AGENT_LANGFUSE_MODELS_DEV_FETCH_TIMEOUT_MS`.
158
+
147
159
  When a billable generation source only records a total token count without
148
160
  input/output/cache breakdown, the importer charges that total at the model input
149
161
  rate and marks the cost source as `calculated_total_as_input`.
@@ -228,6 +240,12 @@ with a conservative cross-platform PATH. Use `--npx-path` and `--path` when a
228
240
  host keeps Node.js somewhere outside the normal npm/Homebrew/system locations,
229
241
  including nvm, fnm, Volta, asdf, mise, or another shell manager.
230
242
 
243
+ The public API exposes `serviceCreators` for macOS LaunchAgent, Linux systemd
244
+ user units, and Windows Scheduled Task scripts, plus `agentProcessors` for
245
+ Claude Code, Codex, Grok, OpenCode, and Pi. These registries are the extension
246
+ points for adding another host service target or coding-agent history reader
247
+ without adding more platform or agent conditionals to the CLI internals.
248
+
231
249
  ## Backfill windows
232
250
 
233
251
  Backfill only a timeframe when repairing a host or replaying a recent window:
@@ -50,6 +50,8 @@ type BackfillOptions = {
50
50
  maxFieldBytes: number;
51
51
  postDelayMs: number;
52
52
  costRates: CostCatalog;
53
+ costRateOverrides: CostCatalog;
54
+ modelsDev: ModelsDevOptions;
53
55
  pathTagsConfigPath?: string;
54
56
  pathTagsConfig: PathTagsConfig;
55
57
  };
@@ -63,6 +65,13 @@ type CostRates = {
63
65
  cacheWrite1h?: number;
64
66
  };
65
67
  type CostCatalog = Record<string, CostRates>;
68
+ type ModelsDevOptions = {
69
+ enabled: boolean;
70
+ url: string;
71
+ cachePath: string;
72
+ ttlMs: number;
73
+ fetchTimeoutMs: number;
74
+ };
66
75
  type OtlpOptions = {
67
76
  maxFieldBytes?: number;
68
77
  costRates?: CostCatalog;
@@ -88,6 +97,10 @@ type ProjectTagOverlay = {
88
97
  projectFolder?: string;
89
98
  sourcePath?: string;
90
99
  };
100
+ type AgentProcessor = {
101
+ name: AgentName;
102
+ discover(options: BackfillOptions): BackfillEvent[];
103
+ };
91
104
  type RunSummary = {
92
105
  discovered: Record<string, number>;
93
106
  sent: number;
@@ -107,6 +120,7 @@ type FollowSummary = RunSummary & {
107
120
  declare const allAgents: AgentName[];
108
121
  declare const defaultEndpoint = "https://langfuse.ai.roxasroot.net/otel/v1/traces";
109
122
  declare function parseArgs(argv: string[]): BackfillOptions;
123
+ declare function parseModelsDevCostCatalog(value: unknown, source?: string): CostCatalog;
110
124
  declare function codexEvents(homeDir: string, options?: {
111
125
  sessionIds?: Set<string>;
112
126
  sinceMs?: number;
@@ -121,8 +135,9 @@ declare function opencodeEvents(homeDir: string, options?: {
121
135
  }): BackfillEvent[];
122
136
  declare function fingerprint(event: BackfillEvent): string;
123
137
  declare function toOtlp(events: BackfillEvent[], options?: OtlpOptions): Record<string, unknown>;
138
+ declare const agentProcessors: Record<AgentName, AgentProcessor>;
124
139
  declare function discoverEvents(options: BackfillOptions): BackfillEvent[];
125
140
  declare function run(options: BackfillOptions): Promise<RunSummary>;
126
141
  declare function follow(options: BackfillOptions): Promise<FollowSummary>;
127
142
  declare function main(argv?: string[]): Promise<RunSummary | FollowSummary>;
128
- export { type BackfillEvent, type BackfillOptions, type AgentName, allAgents, claudeEvents, codexEvents, defaultEndpoint, discoverEvents, fingerprint, follow, grokEvents, main, opencodeEvents, parseArgs, piEvents, run, toOtlp, };
143
+ export { type BackfillEvent, type BackfillOptions, type AgentName, type AgentProcessor, type CostCatalog, type CostRates, type ModelsDevOptions, agentProcessors, allAgents, claudeEvents, codexEvents, defaultEndpoint, discoverEvents, fingerprint, follow, grokEvents, parseModelsDevCostCatalog, main, opencodeEvents, parseArgs, piEvents, run, toOtlp, };
package/dist/backfill.js CHANGED
@@ -2,16 +2,16 @@
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
4
  import { closeSync, existsSync, mkdirSync, openSync, renameSync, readdirSync, readFileSync, readSync, statSync, writeFileSync, } from "node:fs";
5
- import { hostname, homedir } from "node:os";
5
+ import { hostname, homedir, platform as osPlatform } from "node:os";
6
6
  import { dirname, join } from "node:path";
7
7
  const allAgents = ["claude", "codex", "grok", "opencode", "pi"];
8
- const importStateIdentityVersion = "v9-cost-details";
8
+ const importStateIdentityVersion = "v10-models-dev-cost-catalog";
9
9
  const importStateIdentityVersions = {
10
- claude: "v13-claude-message-snapshot-dedupe",
11
- codex: "v12-codex-token-accounting-priced",
12
- grok: "v12-cost-details",
13
- opencode: "v11-cost-details",
14
- pi: "v12-cost-details",
10
+ claude: "v14-models-dev-cost-catalog",
11
+ codex: "v13-models-dev-cost-catalog",
12
+ grok: "v13-models-dev-cost-catalog",
13
+ opencode: "v12-models-dev-cost-catalog",
14
+ pi: "v13-models-dev-cost-catalog",
15
15
  };
16
16
  const langfuseIdIdentityVersion = "v8-cached-input-token-split";
17
17
  const langfuseIdIdentityVersions = {
@@ -21,10 +21,10 @@ const langfuseIdIdentityVersions = {
21
21
  opencode: "v10-opencode-message-parts",
22
22
  pi: "v11-tool-results",
23
23
  };
24
- const importPayloadVersion = "v10-cost-details";
24
+ const importPayloadVersion = "v11-models-dev-cost-catalog";
25
25
  const importPayloadVersions = {
26
- claude: "v11-claude-message-snapshot-dedupe",
27
- codex: "v12-codex-token-accounting-priced",
26
+ claude: "v12-models-dev-cost-catalog",
27
+ codex: "v13-models-dev-cost-catalog",
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";
@@ -32,6 +32,10 @@ const defaultMaxRequestBytes = 12 * 1024 * 1024;
32
32
  const defaultMaxFieldBytes = 512 * 1024;
33
33
  const defaultStatePath = join(homedir(), ".local/state/coding-agent-langfuse/backfill-v6.json");
34
34
  const defaultPathTagsConfigPath = join(homedir(), ".config/coding-agent-langfuse/path-tags.json");
35
+ const defaultModelsDevUrl = "https://models.dev/api.json";
36
+ const defaultModelsDevTtlMs = 24 * 60 * 60 * 1000;
37
+ const defaultModelsDevFetchTimeoutMs = 5_000;
38
+ const defaultModelsDevCachePath = join(defaultCacheDir(), "models-dev-v1.json");
35
39
  const currentHost = hostname();
36
40
  const projectLocalConfigFile = ".langfuse-ca.json";
37
41
  const projectLocalConfigCache = new Map();
@@ -115,6 +119,8 @@ const defaultCostRates = {
115
119
  "anthropic/claude-opus-4": claudeOpus4Rates,
116
120
  "claude-opus-4-1": claudeOpus4Rates,
117
121
  "anthropic/claude-opus-4-1": claudeOpus4Rates,
122
+ "claude-opus-4-5": claudeOpus4Rates,
123
+ "anthropic/claude-opus-4-5": claudeOpus4Rates,
118
124
  "claude-opus-4-6": claudeOpus4Rates,
119
125
  "anthropic/claude-opus-4-6": claudeOpus4Rates,
120
126
  "claude-opus-4-7": claudeOpus4Rates,
@@ -338,6 +344,11 @@ Options:
338
344
  --max-request-bytes N Split OTLP POSTs below this JSON byte size (default: ${defaultMaxRequestBytes})
339
345
  --max-field-bytes N Truncate individual input/output fields above this byte size (default: ${defaultMaxFieldBytes})
340
346
  --post-delay-ms N Delay after each successful OTLP POST (default: 0)
347
+ --models-dev Enable models.dev pricing cache refresh
348
+ --no-models-dev Disable models.dev pricing cache refresh
349
+ --models-dev-url URL models.dev-compatible pricing catalog URL (default: ${defaultModelsDevUrl})
350
+ --models-dev-cache PATH Local pricing cache path (default: ${defaultModelsDevCachePath})
351
+ --models-dev-ttl-ms N Refresh models.dev cache after this many ms (default: ${defaultModelsDevTtlMs})
341
352
  --cost-rates PATH JSON model cost-rate overrides in USD per 1M tokens
342
353
  --cost-rates-json JSON Inline JSON model cost-rate overrides in USD per 1M tokens
343
354
  --path-tags-config PATH JSON path-prefix rules that add Langfuse tags/metadata
@@ -370,7 +381,8 @@ function parseArgs(argv) {
370
381
  let postDelayMs = Number.parseInt(process.env.LANGFUSE_BACKFILL_POST_DELAY_MS ?? "", 10);
371
382
  if (!Number.isFinite(postDelayMs))
372
383
  postDelayMs = 0;
373
- let costRates = loadCostCatalogFromEnv();
384
+ let modelsDev = loadModelsDevOptionsFromEnv();
385
+ let costRateOverrides = loadCostCatalogOverridesFromEnv();
374
386
  let pathTagsConfigPath = process.env.CODING_AGENT_LANGFUSE_PATH_TAGS_CONFIG ??
375
387
  process.env.LANGFUSE_BACKFILL_PATH_TAGS_CONFIG ??
376
388
  defaultPathTagsConfigPath;
@@ -434,11 +446,26 @@ function parseArgs(argv) {
434
446
  else if (arg === "--post-delay-ms") {
435
447
  postDelayMs = Number.parseInt(next(), 10);
436
448
  }
449
+ else if (arg === "--models-dev") {
450
+ modelsDev = { ...modelsDev, enabled: true };
451
+ }
452
+ else if (arg === "--no-models-dev") {
453
+ modelsDev = { ...modelsDev, enabled: false };
454
+ }
455
+ else if (arg === "--models-dev-url") {
456
+ modelsDev = { ...modelsDev, url: next() };
457
+ }
458
+ else if (arg === "--models-dev-cache") {
459
+ modelsDev = { ...modelsDev, cachePath: next() };
460
+ }
461
+ else if (arg === "--models-dev-ttl-ms") {
462
+ modelsDev = { ...modelsDev, ttlMs: Number.parseInt(next(), 10) };
463
+ }
437
464
  else if (arg === "--cost-rates") {
438
- costRates = mergeCostCatalog(costRates, loadCostCatalogFile(next()));
465
+ costRateOverrides = mergeCostCatalog(costRateOverrides, loadCostCatalogFile(next()));
439
466
  }
440
467
  else if (arg === "--cost-rates-json") {
441
- costRates = mergeCostCatalog(costRates, parseCostCatalogJson(next()));
468
+ costRateOverrides = mergeCostCatalog(costRateOverrides, parseCostCatalogJson(next()));
442
469
  }
443
470
  else if (arg === "--path-tags-config") {
444
471
  pathTagsConfigPath = next();
@@ -481,6 +508,13 @@ function parseArgs(argv) {
481
508
  if (!Number.isFinite(postDelayMs) || postDelayMs < 0) {
482
509
  throw new Error("--post-delay-ms must be a non-negative integer");
483
510
  }
511
+ if (!Number.isFinite(modelsDev.ttlMs) || modelsDev.ttlMs < 0) {
512
+ throw new Error("--models-dev-ttl-ms must be a non-negative integer");
513
+ }
514
+ if (!Number.isFinite(modelsDev.fetchTimeoutMs) ||
515
+ modelsDev.fetchTimeoutMs < 1) {
516
+ throw new Error("CODING_AGENT_LANGFUSE_MODELS_DEV_FETCH_TIMEOUT_MS must be a positive integer");
517
+ }
484
518
  if (!Number.isFinite(pollIntervalMs) || pollIntervalMs < 1) {
485
519
  throw new Error("--poll-interval-ms must be a positive integer");
486
520
  }
@@ -494,6 +528,7 @@ function parseArgs(argv) {
494
528
  if (auth !== undefined && auth.trim().length === 0) {
495
529
  throw new Error("--auth must not be empty");
496
530
  }
531
+ const costRates = resolveCostCatalogSync(modelsDev, costRateOverrides);
497
532
  return {
498
533
  agents,
499
534
  endpoint,
@@ -514,6 +549,8 @@ function parseArgs(argv) {
514
549
  maxFieldBytes,
515
550
  postDelayMs,
516
551
  costRates,
552
+ costRateOverrides,
553
+ modelsDev,
517
554
  pathTagsConfigPath,
518
555
  pathTagsConfig: loadPathTagsConfig(pathTagsConfigPath),
519
556
  };
@@ -628,7 +665,22 @@ function isSameOrChildPath(child, parent) {
628
665
  normalizedChild.startsWith(`${normalizedParent}/`);
629
666
  }
630
667
  function loadCostCatalogFromEnv() {
631
- let catalog = { ...defaultCostRates };
668
+ return resolveCostCatalogSync(loadModelsDevOptionsFromEnv(), loadCostCatalogOverridesFromEnv());
669
+ }
670
+ function resolveCostCatalogSync(modelsDev, overrides = {}) {
671
+ const modelsDevCatalog = modelsDev.enabled
672
+ ? loadModelsDevCostCatalogFromCache(modelsDev)
673
+ : {};
674
+ return mergeCostCatalog(mergeCostCatalog(defaultCostRates, modelsDevCatalog), overrides);
675
+ }
676
+ async function resolveCostCatalog(options) {
677
+ const modelsDevCatalog = options.modelsDev.enabled
678
+ ? await loadModelsDevCostCatalog(options.modelsDev)
679
+ : {};
680
+ return mergeCostCatalog(mergeCostCatalog(defaultCostRates, modelsDevCatalog), options.costRateOverrides);
681
+ }
682
+ function loadCostCatalogOverridesFromEnv() {
683
+ let catalog = {};
632
684
  const path = process.env.CODING_AGENT_LANGFUSE_COST_RATES_PATH ??
633
685
  process.env.LANGFUSE_BACKFILL_COST_RATES_PATH;
634
686
  const inlineJson = process.env.CODING_AGENT_LANGFUSE_COST_RATES_JSON ??
@@ -639,6 +691,184 @@ function loadCostCatalogFromEnv() {
639
691
  catalog = mergeCostCatalog(catalog, parseCostCatalogJson(inlineJson));
640
692
  return catalog;
641
693
  }
694
+ function loadModelsDevOptionsFromEnv() {
695
+ const enabledValue = process.env.CODING_AGENT_LANGFUSE_MODELS_DEV ??
696
+ process.env.LANGFUSE_BACKFILL_MODELS_DEV;
697
+ const ttlMs = Number.parseInt(process.env.CODING_AGENT_LANGFUSE_MODELS_DEV_TTL_MS ??
698
+ process.env.LANGFUSE_BACKFILL_MODELS_DEV_TTL_MS ??
699
+ `${defaultModelsDevTtlMs}`, 10);
700
+ const fetchTimeoutMs = Number.parseInt(process.env.CODING_AGENT_LANGFUSE_MODELS_DEV_FETCH_TIMEOUT_MS ??
701
+ `${defaultModelsDevFetchTimeoutMs}`, 10);
702
+ return {
703
+ enabled: !isFalseLike(enabledValue),
704
+ url: process.env.CODING_AGENT_LANGFUSE_MODELS_DEV_URL ??
705
+ process.env.LANGFUSE_BACKFILL_MODELS_DEV_URL ??
706
+ defaultModelsDevUrl,
707
+ cachePath: process.env.CODING_AGENT_LANGFUSE_MODELS_DEV_CACHE ??
708
+ process.env.LANGFUSE_BACKFILL_MODELS_DEV_CACHE ??
709
+ defaultModelsDevCachePath,
710
+ ttlMs,
711
+ fetchTimeoutMs,
712
+ };
713
+ }
714
+ function isFalseLike(value) {
715
+ return value !== undefined && /^(0|false|off|no)$/i.test(value.trim());
716
+ }
717
+ function defaultCacheDir() {
718
+ const xdgCacheHome = process.env.XDG_CACHE_HOME;
719
+ if (xdgCacheHome)
720
+ return join(xdgCacheHome, "coding-agent-langfuse");
721
+ if (osPlatform() === "darwin") {
722
+ return join(homedir(), "Library/Caches/coding-agent-langfuse");
723
+ }
724
+ return join(homedir(), ".cache/coding-agent-langfuse");
725
+ }
726
+ async function loadModelsDevCostCatalog(options) {
727
+ const cached = readModelsDevCache(options.cachePath);
728
+ if (cached && !modelsDevCacheExpired(cached.fetchedAt, options.ttlMs)) {
729
+ return cached.catalog;
730
+ }
731
+ try {
732
+ const rawCatalog = await fetchModelsDevCatalog(options);
733
+ writeModelsDevCache(options.cachePath, options.url, rawCatalog);
734
+ return parseModelsDevCostCatalog(rawCatalog, options.url);
735
+ }
736
+ catch (error) {
737
+ if (cached) {
738
+ console.warn(`Could not refresh models.dev pricing from ${options.url}; using cached pricing from ${options.cachePath}: ${describeError(error)}`);
739
+ return cached.catalog;
740
+ }
741
+ console.warn(`Could not load models.dev pricing from ${options.url}; using built-in fallback rates: ${describeError(error)}`);
742
+ return {};
743
+ }
744
+ }
745
+ function loadModelsDevCostCatalogFromCache(options) {
746
+ return readModelsDevCache(options.cachePath)?.catalog ?? {};
747
+ }
748
+ function readModelsDevCache(path) {
749
+ if (!existsSync(path))
750
+ return undefined;
751
+ try {
752
+ const json = readFileSync(path, "utf8");
753
+ const parsed = JSON.parse(json);
754
+ const record = asRecord(parsed);
755
+ const fetchedAt = asString(record.fetchedAt) ??
756
+ asString(record.fetched_at) ??
757
+ new Date(statSync(path).mtimeMs).toISOString();
758
+ return {
759
+ fetchedAt,
760
+ catalog: parseModelsDevCostCatalog(parsed, path),
761
+ };
762
+ }
763
+ catch (error) {
764
+ console.warn(`Ignoring invalid models.dev pricing cache ${path}: ${describeError(error)}`);
765
+ return undefined;
766
+ }
767
+ }
768
+ function modelsDevCacheExpired(fetchedAt, ttlMs) {
769
+ if (ttlMs === 0)
770
+ return true;
771
+ const fetchedAtMs = Date.parse(fetchedAt);
772
+ if (!Number.isFinite(fetchedAtMs))
773
+ return true;
774
+ return Date.now() - fetchedAtMs > ttlMs;
775
+ }
776
+ async function fetchModelsDevCatalog(options) {
777
+ const controller = new AbortController();
778
+ const timeout = setTimeout(() => controller.abort(), options.fetchTimeoutMs);
779
+ try {
780
+ const response = await fetch(options.url, {
781
+ headers: { accept: "application/json" },
782
+ signal: controller.signal,
783
+ });
784
+ if (!response.ok) {
785
+ throw new Error(`HTTP ${response.status} ${await response.text()}`);
786
+ }
787
+ return await response.json();
788
+ }
789
+ finally {
790
+ clearTimeout(timeout);
791
+ }
792
+ }
793
+ function writeModelsDevCache(path, sourceUrl, catalog) {
794
+ mkdirSync(dirname(path), { recursive: true });
795
+ const tempPath = `${path}.${process.pid}.tmp`;
796
+ writeFileSync(tempPath, `${JSON.stringify({
797
+ version: 1,
798
+ sourceUrl,
799
+ fetchedAt: new Date().toISOString(),
800
+ catalog,
801
+ }, null, 2)}\n`);
802
+ renameSync(tempPath, path);
803
+ }
804
+ function parseModelsDevCostCatalog(value, source = "models.dev catalog") {
805
+ const root = asRecord(value);
806
+ const catalogRoot = asRecord(root.catalog);
807
+ const providerRoot = asRecord(catalogRoot.providers);
808
+ const providers = Object.keys(providerRoot).length > 0
809
+ ? providerRoot
810
+ : Object.keys(catalogRoot).length > 0
811
+ ? catalogRoot
812
+ : root;
813
+ const out = {};
814
+ for (const [providerId, rawProvider] of Object.entries(providers)) {
815
+ const provider = asRecord(rawProvider);
816
+ const models = asRecord(provider.models);
817
+ for (const [modelId, rawModel] of Object.entries(models)) {
818
+ const rates = modelsDevCostRates(rawModel, `${providerId}/${modelId}`, source);
819
+ if (!rates)
820
+ continue;
821
+ for (const alias of modelsDevAliases(providerId, modelId)) {
822
+ out[alias] = { ...(out[alias] ?? {}), ...rates };
823
+ }
824
+ }
825
+ }
826
+ return out;
827
+ }
828
+ function modelsDevCostRates(rawModel, modelKey, source) {
829
+ const model = asRecord(rawModel);
830
+ const cost = asRecord(model.cost);
831
+ if (Object.keys(cost).length === 0)
832
+ return undefined;
833
+ const cacheWrite = asNumber(cost.cache_write);
834
+ return normalizeCostRates({
835
+ input: cost.input,
836
+ output: cost.output,
837
+ reasoning: cost.reasoning,
838
+ cacheRead: cost.cache_read,
839
+ cacheWrite,
840
+ cacheWrite5m: cacheWrite,
841
+ cacheWrite1h: cacheWrite,
842
+ }, modelKey, source);
843
+ }
844
+ function modelsDevAliases(providerId, modelId) {
845
+ const aliases = new Set([modelId, `${providerId}/${modelId}`]);
846
+ const canonicalProviderPrefixes = [
847
+ "anthropic",
848
+ "openai",
849
+ "fireworks-ai",
850
+ "fireworks",
851
+ "fireworks-firepass",
852
+ "opencode",
853
+ ];
854
+ for (const prefix of canonicalProviderPrefixes) {
855
+ if (modelId.startsWith(`${prefix}/`)) {
856
+ aliases.add(modelId.slice(prefix.length + 1));
857
+ }
858
+ }
859
+ if (providerId === "anthropic")
860
+ aliases.add(`anthropic/${modelId}`);
861
+ if (providerId === "openai")
862
+ aliases.add(`openai/${modelId}`);
863
+ if (providerId === "fireworks-ai") {
864
+ aliases.add(`fireworks/${modelId}`);
865
+ aliases.add(`fireworks-firepass/${modelId}`);
866
+ }
867
+ if (providerId === "opencode") {
868
+ aliases.add(modelId);
869
+ }
870
+ return [...aliases];
871
+ }
642
872
  function loadCostCatalogFile(path) {
643
873
  return parseCostCatalogJson(readFileSync(path, "utf8"), path);
644
874
  }
@@ -2260,32 +2490,49 @@ function describeError(error) {
2260
2490
  ? `${error.message} (${parts.join("; ")})`
2261
2491
  : error.message;
2262
2492
  }
2263
- function discoverEvents(options) {
2264
- const providers = {
2265
- claude: (inner) => claudeEvents(inner.homeDir),
2266
- codex: (inner) => codexEvents(inner.homeDir, {
2267
- sessionIds: inner.sessionIds,
2268
- sinceMs: inner.sinceMs,
2493
+ const agentProcessors = {
2494
+ claude: {
2495
+ name: "claude",
2496
+ discover: (options) => claudeEvents(options.homeDir),
2497
+ },
2498
+ codex: {
2499
+ name: "codex",
2500
+ discover: (options) => codexEvents(options.homeDir, {
2501
+ sessionIds: options.sessionIds,
2502
+ sinceMs: options.sinceMs,
2269
2503
  }),
2270
- grok: (inner) => grokEvents(inner.homeDir),
2271
- opencode: (inner) => opencodeEvents(inner.homeDir, {
2272
- rowLimit: inner.limit,
2273
- sinceMs: inner.sinceMs,
2274
- untilMs: inner.untilMs,
2504
+ },
2505
+ grok: {
2506
+ name: "grok",
2507
+ discover: (options) => grokEvents(options.homeDir),
2508
+ },
2509
+ opencode: {
2510
+ name: "opencode",
2511
+ discover: (options) => opencodeEvents(options.homeDir, {
2512
+ rowLimit: options.limit,
2513
+ sinceMs: options.sinceMs,
2514
+ untilMs: options.untilMs,
2275
2515
  }),
2276
- pi: (inner) => piEvents(inner.homeDir),
2277
- };
2516
+ },
2517
+ pi: {
2518
+ name: "pi",
2519
+ discover: (options) => piEvents(options.homeDir),
2520
+ },
2521
+ };
2522
+ function discoverEvents(options) {
2278
2523
  return allAgents
2279
2524
  .filter((agent) => options.agents.has(agent))
2280
- .flatMap((agent) => providers[agent](options))
2525
+ .flatMap((agent) => agentProcessors[agent].discover(options))
2281
2526
  .filter((event) => options.sinceMs === undefined || event.startMs >= options.sinceMs)
2282
2527
  .filter((event) => options.untilMs === undefined || event.startMs <= options.untilMs)
2283
2528
  .filter((event) => options.sessionIds.size === 0 || options.sessionIds.has(event.sessionId))
2284
2529
  .sort((a, b) => a.startMs - b.startMs);
2285
2530
  }
2286
2531
  async function run(options) {
2532
+ const costRates = await resolveCostCatalog(options);
2533
+ const runOptions = { ...options, costRates };
2287
2534
  const state = loadState(options.statePath);
2288
- const events = discoverEvents(options);
2535
+ const events = discoverEvents(runOptions);
2289
2536
  const discovered = Object.fromEntries(allAgents.map((agent) => [agent, 0]));
2290
2537
  for (const event of events) {
2291
2538
  discovered[event.agent] = (discovered[event.agent] ?? 0) + 1;
@@ -2307,7 +2554,7 @@ async function run(options) {
2307
2554
  batchSize: options.batchSize,
2308
2555
  maxRequestBytes: options.maxRequestBytes,
2309
2556
  maxFieldBytes: options.maxFieldBytes,
2310
- costRates: options.costRates,
2557
+ costRates: runOptions.costRates,
2311
2558
  pathTagsConfig: options.pathTagsConfig,
2312
2559
  homeDir: options.homeDir,
2313
2560
  });
@@ -2335,7 +2582,7 @@ async function run(options) {
2335
2582
  try {
2336
2583
  await postOtlp(options.endpoint, batch, {
2337
2584
  maxFieldBytes: options.maxFieldBytes,
2338
- costRates: options.costRates,
2585
+ costRates: runOptions.costRates,
2339
2586
  pathTagsConfig: options.pathTagsConfig,
2340
2587
  homeDir: options.homeDir,
2341
2588
  auth: options.auth,
@@ -2446,4 +2693,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
2446
2693
  process.exit(1);
2447
2694
  }
2448
2695
  }
2449
- export { allAgents, claudeEvents, codexEvents, defaultEndpoint, discoverEvents, fingerprint, follow, grokEvents, main, opencodeEvents, parseArgs, piEvents, run, toOtlp, };
2696
+ export { agentProcessors, allAgents, claudeEvents, codexEvents, defaultEndpoint, discoverEvents, fingerprint, follow, grokEvents, parseModelsDevCostCatalog, main, opencodeEvents, parseArgs, piEvents, run, toOtlp, };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { codexEvents, discoverEvents, fingerprint, parseArgs, piEvents, run, toOtlp, } from "./backfill.js";
2
- export { buildServicePlan, parseServiceArgs, serviceMain, } from "./service.js";
1
+ export { type AgentProcessor, type CostCatalog, type CostRates, type ModelsDevOptions, agentProcessors, codexEvents, discoverEvents, fingerprint, parseModelsDevCostCatalog, parseArgs, piEvents, run, toOtlp, } from "./backfill.js";
2
+ export { type ServiceCreator, buildServicePlan, parseServiceArgs, serviceMain, serviceCreators, } from "./service.js";
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { codexEvents, discoverEvents, fingerprint, parseArgs, piEvents, run, toOtlp, } from "./backfill.js";
2
- export { buildServicePlan, parseServiceArgs, serviceMain, } from "./service.js";
1
+ export { agentProcessors, codexEvents, discoverEvents, fingerprint, parseModelsDevCostCatalog, parseArgs, piEvents, run, toOtlp, } from "./backfill.js";
2
+ export { buildServicePlan, parseServiceArgs, serviceMain, serviceCreators, } from "./service.js";
package/dist/service.d.ts CHANGED
@@ -13,6 +13,11 @@ type ServiceOptions = {
13
13
  batchSize: number;
14
14
  pollIntervalMs: number;
15
15
  postDelayMs: number;
16
+ modelsDevEnabled: boolean;
17
+ modelsDevDisabled: boolean;
18
+ modelsDevUrl?: string;
19
+ modelsDevCachePath?: string;
20
+ modelsDevTtlMs?: number;
16
21
  costRatesPath?: string;
17
22
  costRatesJson?: string;
18
23
  pathTagsConfigPath?: string;
@@ -34,9 +39,14 @@ type ServicePlan = {
34
39
  uninstallCommands: string[][];
35
40
  statusCommands: string[][];
36
41
  };
42
+ type ServiceCreator = {
43
+ platform: ServicePlatform;
44
+ create(options: ServiceOptions, command: string[]): ServicePlan;
45
+ };
37
46
  declare function parseServiceArgs(argv: string[]): ServiceOptions;
47
+ declare const serviceCreators: Record<ServicePlatform, ServiceCreator>;
38
48
  declare function buildServicePlan(options: ServiceOptions): ServicePlan;
39
49
  declare function serviceMain(argv?: string[]): Promise<ServicePlan>;
40
50
  declare function redactStatusOutput(output: string): string;
41
51
  declare function lifecycleEnv(): NodeJS.ProcessEnv;
42
- export { type ServiceOptions, type ServicePlan, buildServicePlan, redactStatusOutput, parseServiceArgs, serviceMain, lifecycleEnv, };
52
+ export { type ServiceCreator, type ServiceOptions, type ServicePlan, buildServicePlan, serviceCreators, redactStatusOutput, parseServiceArgs, serviceMain, lifecycleEnv, };
package/dist/service.js CHANGED
@@ -24,6 +24,11 @@ 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
+ --models-dev Enable models.dev pricing cache refresh in the follower
28
+ --no-models-dev Disable models.dev pricing cache refresh in the follower
29
+ --models-dev-url URL models.dev-compatible pricing catalog URL
30
+ --models-dev-cache PATH Local pricing cache path
31
+ --models-dev-ttl-ms N Refresh models.dev cache after this many ms
27
32
  --cost-rates PATH JSON model cost-rate overrides in USD per 1M tokens
28
33
  --cost-rates-json JSON Inline JSON model cost-rate overrides in USD per 1M tokens
29
34
  --path-tags-config PATH JSON path-prefix rules that add Langfuse tags/metadata
@@ -52,6 +57,11 @@ function parseServiceArgs(argv) {
52
57
  let batchSize = 10;
53
58
  let pollIntervalMs = 5_000;
54
59
  let postDelayMs = 0;
60
+ let modelsDevEnabled = false;
61
+ let modelsDevDisabled = false;
62
+ let modelsDevUrl;
63
+ let modelsDevCachePath;
64
+ let modelsDevTtlMs;
55
65
  let costRatesPath = process.env.CODING_AGENT_LANGFUSE_COST_RATES_PATH ??
56
66
  process.env.LANGFUSE_BACKFILL_COST_RATES_PATH;
57
67
  let costRatesJson = process.env.CODING_AGENT_LANGFUSE_COST_RATES_JSON ??
@@ -103,6 +113,23 @@ function parseServiceArgs(argv) {
103
113
  else if (arg === "--post-delay-ms") {
104
114
  postDelayMs = parseNonNegativeInt(arg, next());
105
115
  }
116
+ else if (arg === "--models-dev") {
117
+ modelsDevEnabled = true;
118
+ modelsDevDisabled = false;
119
+ }
120
+ else if (arg === "--no-models-dev") {
121
+ modelsDevEnabled = false;
122
+ modelsDevDisabled = true;
123
+ }
124
+ else if (arg === "--models-dev-url") {
125
+ modelsDevUrl = next();
126
+ }
127
+ else if (arg === "--models-dev-cache") {
128
+ modelsDevCachePath = next();
129
+ }
130
+ else if (arg === "--models-dev-ttl-ms") {
131
+ modelsDevTtlMs = parseNonNegativeInt(arg, next());
132
+ }
106
133
  else if (arg === "--cost-rates") {
107
134
  costRatesPath = next();
108
135
  }
@@ -151,6 +178,11 @@ function parseServiceArgs(argv) {
151
178
  batchSize,
152
179
  pollIntervalMs,
153
180
  postDelayMs,
181
+ modelsDevEnabled,
182
+ modelsDevDisabled,
183
+ modelsDevUrl,
184
+ modelsDevCachePath,
185
+ modelsDevTtlMs,
154
186
  costRatesPath,
155
187
  costRatesJson,
156
188
  pathTagsConfigPath,
@@ -161,96 +193,109 @@ function parseServiceArgs(argv) {
161
193
  pathEnv,
162
194
  };
163
195
  }
164
- function buildServicePlan(options) {
165
- const command = buildFollowCommand(options);
166
- if (options.platform === "darwin") {
167
- const path = join(options.homeDir, "Library/LaunchAgents", `${options.name}.plist`);
168
- return {
169
- platform: options.platform,
170
- action: options.action,
171
- name: options.name,
172
- path,
173
- content: renderLaunchdPlist(options, command),
174
- command,
175
- postInstallCommands: options.start
176
- ? [
177
- ["launchctl", "bootstrap", `gui/${process.getuid?.() ?? 501}`, path],
178
- ["launchctl", "kickstart", "-k", `gui/${process.getuid?.() ?? 501}/${options.name}`],
179
- ]
180
- : [],
181
- uninstallCommands: [
182
- ["launchctl", "bootout", `gui/${process.getuid?.() ?? 501}`, path],
183
- ],
184
- statusCommands: [
185
- ["launchctl", "print", `gui/${process.getuid?.() ?? 501}/${options.name}`],
186
- ],
187
- };
188
- }
189
- if (options.platform === "linux") {
190
- const path = join(options.homeDir, ".config/systemd/user", `${options.name}.service`);
191
- return {
192
- platform: options.platform,
193
- action: options.action,
194
- name: options.name,
195
- path,
196
- content: renderSystemdUnit(options, command),
197
- command,
198
- postInstallCommands: options.start
199
- ? [
196
+ const serviceCreators = {
197
+ darwin: {
198
+ platform: "darwin",
199
+ create(options, command) {
200
+ const path = join(options.homeDir, "Library/LaunchAgents", `${options.name}.plist`);
201
+ return {
202
+ platform: options.platform,
203
+ action: options.action,
204
+ name: options.name,
205
+ path,
206
+ content: renderLaunchdPlist(options, command),
207
+ command,
208
+ postInstallCommands: options.start
209
+ ? [
210
+ ["launchctl", "bootstrap", `gui/${process.getuid?.() ?? 501}`, path],
211
+ ["launchctl", "kickstart", "-k", `gui/${process.getuid?.() ?? 501}/${options.name}`],
212
+ ]
213
+ : [],
214
+ uninstallCommands: [
215
+ ["launchctl", "bootout", `gui/${process.getuid?.() ?? 501}`, path],
216
+ ],
217
+ statusCommands: [
218
+ ["launchctl", "print", `gui/${process.getuid?.() ?? 501}/${options.name}`],
219
+ ],
220
+ };
221
+ },
222
+ },
223
+ linux: {
224
+ platform: "linux",
225
+ create(options, command) {
226
+ const path = join(options.homeDir, ".config/systemd/user", `${options.name}.service`);
227
+ return {
228
+ platform: options.platform,
229
+ action: options.action,
230
+ name: options.name,
231
+ path,
232
+ content: renderSystemdUnit(options, command),
233
+ command,
234
+ postInstallCommands: options.start
235
+ ? [
236
+ ["systemctl", "--user", "daemon-reload"],
237
+ ["systemctl", "--user", "enable", "--now", `${options.name}.service`],
238
+ ]
239
+ : [["systemctl", "--user", "daemon-reload"]],
240
+ uninstallCommands: [
241
+ ["systemctl", "--user", "disable", "--now", `${options.name}.service`],
200
242
  ["systemctl", "--user", "daemon-reload"],
201
- ["systemctl", "--user", "enable", "--now", `${options.name}.service`],
202
- ]
203
- : [["systemctl", "--user", "daemon-reload"]],
204
- uninstallCommands: [
205
- ["systemctl", "--user", "disable", "--now", `${options.name}.service`],
206
- ["systemctl", "--user", "daemon-reload"],
207
- ],
208
- statusCommands: [
209
- ["systemctl", "--user", "status", `${options.name}.service`],
210
- ],
211
- };
212
- }
213
- const path = join(process.env.APPDATA ?? join(options.homeDir, "AppData/Roaming"), "coding-agent-langfuse", `${options.name}.ps1`);
214
- const taskName = `CodingAgentLangfuse-${options.name}`;
215
- return {
216
- platform: options.platform,
217
- action: options.action,
218
- name: options.name,
219
- path,
220
- content: renderWindowsScript(command),
221
- command,
222
- postInstallCommands: options.start
223
- ? [
224
- [
225
- "powershell.exe",
226
- "-NoProfile",
227
- "-ExecutionPolicy",
228
- "Bypass",
229
- "-File",
230
- path,
231
- "-Install",
232
- "-TaskName",
233
- taskName,
234
243
  ],
235
- ]
236
- : [],
237
- uninstallCommands: [
238
- [
239
- "powershell.exe",
240
- "-NoProfile",
241
- "-Command",
242
- `Unregister-ScheduledTask -TaskName ${powershellString(taskName)} -Confirm:$false -ErrorAction SilentlyContinue`,
243
- ],
244
- ],
245
- statusCommands: [
246
- [
247
- "powershell.exe",
248
- "-NoProfile",
249
- "-Command",
250
- `Get-ScheduledTask -TaskName ${powershellString(taskName)} | Format-List *`,
251
- ],
252
- ],
253
- };
244
+ statusCommands: [
245
+ ["systemctl", "--user", "status", `${options.name}.service`],
246
+ ],
247
+ };
248
+ },
249
+ },
250
+ win32: {
251
+ platform: "win32",
252
+ create(options, command) {
253
+ const path = join(process.env.APPDATA ?? join(options.homeDir, "AppData/Roaming"), "coding-agent-langfuse", `${options.name}.ps1`);
254
+ const taskName = `CodingAgentLangfuse-${options.name}`;
255
+ return {
256
+ platform: options.platform,
257
+ action: options.action,
258
+ name: options.name,
259
+ path,
260
+ content: renderWindowsScript(command),
261
+ command,
262
+ postInstallCommands: options.start
263
+ ? [
264
+ [
265
+ "powershell.exe",
266
+ "-NoProfile",
267
+ "-ExecutionPolicy",
268
+ "Bypass",
269
+ "-File",
270
+ path,
271
+ "-Install",
272
+ "-TaskName",
273
+ taskName,
274
+ ],
275
+ ]
276
+ : [],
277
+ uninstallCommands: [
278
+ [
279
+ "powershell.exe",
280
+ "-NoProfile",
281
+ "-Command",
282
+ `Unregister-ScheduledTask -TaskName ${powershellString(taskName)} -Confirm:$false -ErrorAction SilentlyContinue`,
283
+ ],
284
+ ],
285
+ statusCommands: [
286
+ [
287
+ "powershell.exe",
288
+ "-NoProfile",
289
+ "-Command",
290
+ `Get-ScheduledTask -TaskName ${powershellString(taskName)} | Format-List *`,
291
+ ],
292
+ ],
293
+ };
294
+ },
295
+ },
296
+ };
297
+ function buildServicePlan(options) {
298
+ return serviceCreators[options.platform].create(options, buildFollowCommand(options));
254
299
  }
255
300
  async function serviceMain(argv = process.argv.slice(2)) {
256
301
  const options = parseServiceArgs(argv);
@@ -302,6 +347,18 @@ function buildFollowCommand(options) {
302
347
  ];
303
348
  if (options.since)
304
349
  command.push("--since", options.since);
350
+ if (options.modelsDevEnabled)
351
+ command.push("--models-dev");
352
+ if (options.modelsDevDisabled)
353
+ command.push("--no-models-dev");
354
+ if (options.modelsDevUrl)
355
+ command.push("--models-dev-url", options.modelsDevUrl);
356
+ if (options.modelsDevCachePath) {
357
+ command.push("--models-dev-cache", options.modelsDevCachePath);
358
+ }
359
+ if (options.modelsDevTtlMs !== undefined) {
360
+ command.push("--models-dev-ttl-ms", String(options.modelsDevTtlMs));
361
+ }
305
362
  if (options.costRatesPath)
306
363
  command.push("--cost-rates", options.costRatesPath);
307
364
  if (options.costRatesJson)
@@ -368,15 +425,30 @@ function renderWindowsScript(command) {
368
425
  const commandArray = command.map(powershellString).join(", ");
369
426
  return `param(
370
427
  [switch]$Install,
428
+ [switch]$Run,
371
429
  [string]$TaskName = "CodingAgentLangfuse"
372
430
  )
373
431
 
374
432
  $Command = @(${commandArray})
375
- $Action = New-ScheduledTaskAction -Execute $Command[0] -Argument (($Command | Select-Object -Skip 1) -join " ")
376
- $Trigger = New-ScheduledTaskTrigger -AtLogOn
377
- $Settings = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)
433
+
434
+ function Quote-PowerShellLiteral([string]$Value) {
435
+ return "'" + $Value.Replace("'", "''") + "'"
436
+ }
437
+
438
+ if ($Run) {
439
+ & $Command[0] @($Command | Select-Object -Skip 1)
440
+ exit $LASTEXITCODE
441
+ }
378
442
 
379
443
  if ($Install) {
444
+ $ScriptPath = if ($PSCommandPath) { $PSCommandPath } else { $MyInvocation.MyCommand.Path }
445
+ $RunCommand = "& " + (Quote-PowerShellLiteral $ScriptPath) + " -Run"
446
+ $EncodedRunCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($RunCommand))
447
+ $Action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand $EncodedRunCommand"
448
+ $Trigger = New-ScheduledTaskTrigger -AtLogOn
449
+ $Settings = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -Hidden
450
+
451
+ Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
380
452
  Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Settings $Settings -Force | Out-Null
381
453
  Start-ScheduledTask -TaskName $TaskName
382
454
  }
@@ -557,4 +629,4 @@ function escapeXml(value) {
557
629
  .replaceAll('"', "&quot;")
558
630
  .replaceAll("'", "&apos;");
559
631
  }
560
- export { buildServicePlan, redactStatusOutput, parseServiceArgs, serviceMain, lifecycleEnv, };
632
+ export { buildServicePlan, serviceCreators, redactStatusOutput, parseServiceArgs, serviceMain, lifecycleEnv, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramarivera/coding-agent-langfuse",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
4
4
  "description": "Universal coding-agent Langfuse backfiller and live OTLP helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",