@ramarivera/coding-agent-langfuse 0.1.46 → 0.1.47
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 +62 -0
- package/dist/backfill.d.ts +21 -0
- package/dist/backfill.js +187 -2
- package/dist/service.d.ts +1 -0
- package/dist/service.js +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,6 +51,68 @@ npx @ramarivera/coding-agent-langfuse@latest \
|
|
|
51
51
|
The importer is fail-fast: the first failed OTLP POST stops the run, prints the
|
|
52
52
|
real network cause, and preserves local state so reruns resume cleanly.
|
|
53
53
|
|
|
54
|
+
## Project tags and metadata
|
|
55
|
+
|
|
56
|
+
Backfills and live followers can attach stable Langfuse dimensions based on the
|
|
57
|
+
session cwd. Use this for client/workstream dashboards like ITSynch without
|
|
58
|
+
guessing from host names or Windows paths in the Langfuse UI.
|
|
59
|
+
|
|
60
|
+
There are two config layers:
|
|
61
|
+
|
|
62
|
+
- Global host config: `~/.config/coding-agent-langfuse/path-tags.json`
|
|
63
|
+
- Optional project-local overlay: `.langfuse-ca.json` in the project directory
|
|
64
|
+
or any parent of the session cwd
|
|
65
|
+
|
|
66
|
+
Global config uses prefix rules:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"rules": [
|
|
71
|
+
{
|
|
72
|
+
"pathPrefix": "/Users/ramarivera/dev/itsynch",
|
|
73
|
+
"tags": ["itsynch", "client:itsynch"],
|
|
74
|
+
"metadata": {
|
|
75
|
+
"project_group": "itsynch",
|
|
76
|
+
"project_owner": "ramiro"
|
|
77
|
+
},
|
|
78
|
+
"projectName": "itsynch",
|
|
79
|
+
"projectFolder": "itsynch"
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Project-local config is intentionally smaller and overrides/extends the matched
|
|
86
|
+
global rule:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"tags": ["repo:portal"],
|
|
91
|
+
"metadata": {
|
|
92
|
+
"service": "portal"
|
|
93
|
+
},
|
|
94
|
+
"projectName": "itsynch-portal"
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Matched values are emitted on root trace metadata, observation metadata, and
|
|
99
|
+
top-level OTLP attributes such as `project.tags`, `project.group`, and
|
|
100
|
+
`project.owner`. The root span also sets `langfuse.trace.tags`, so Langfuse
|
|
101
|
+
custom dashboards can filter/group by tags or metadata. Historical repair runs
|
|
102
|
+
use the same OTLP builder as live follow mode, so rerunning with `--force` can
|
|
103
|
+
backfill newly added tags for existing sessions.
|
|
104
|
+
|
|
105
|
+
Use a non-default global config path with:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
npx @ramarivera/coding-agent-langfuse@latest \
|
|
109
|
+
--path-tags-config "$HOME/.config/coding-agent-langfuse/path-tags.json"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Generated services pass `--path-tags-config` when provided to
|
|
113
|
+
`service install`; otherwise the package reads the default global config path if
|
|
114
|
+
it exists.
|
|
115
|
+
|
|
54
116
|
## Cost calculation
|
|
55
117
|
|
|
56
118
|
Backfill cost calculation follows Langfuse's OpenTelemetry mapping: generation
|
package/dist/backfill.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ type BackfillOptions = {
|
|
|
50
50
|
maxFieldBytes: number;
|
|
51
51
|
postDelayMs: number;
|
|
52
52
|
costRates: CostCatalog;
|
|
53
|
+
pathTagsConfigPath?: string;
|
|
54
|
+
pathTagsConfig: PathTagsConfig;
|
|
53
55
|
};
|
|
54
56
|
type CostRates = {
|
|
55
57
|
input?: number;
|
|
@@ -64,6 +66,25 @@ type CostCatalog = Record<string, CostRates>;
|
|
|
64
66
|
type OtlpOptions = {
|
|
65
67
|
maxFieldBytes?: number;
|
|
66
68
|
costRates?: CostCatalog;
|
|
69
|
+
pathTagsConfig?: PathTagsConfig;
|
|
70
|
+
};
|
|
71
|
+
type PathTagRule = {
|
|
72
|
+
pathPrefix: string;
|
|
73
|
+
tags: string[];
|
|
74
|
+
metadata: Record<string, unknown>;
|
|
75
|
+
projectName?: string;
|
|
76
|
+
projectFolder?: string;
|
|
77
|
+
};
|
|
78
|
+
type PathTagsConfig = {
|
|
79
|
+
rules: PathTagRule[];
|
|
80
|
+
project?: ProjectTagOverlay;
|
|
81
|
+
};
|
|
82
|
+
type ProjectTagOverlay = {
|
|
83
|
+
tags: string[];
|
|
84
|
+
metadata: Record<string, unknown>;
|
|
85
|
+
projectName?: string;
|
|
86
|
+
projectFolder?: string;
|
|
87
|
+
sourcePath?: string;
|
|
67
88
|
};
|
|
68
89
|
type RunSummary = {
|
|
69
90
|
discovered: Record<string, number>;
|
package/dist/backfill.js
CHANGED
|
@@ -31,7 +31,10 @@ const deadRemoteEndpoint = "http://langfuse.ai.roxasroot.net:14318/v1/traces";
|
|
|
31
31
|
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
|
+
const defaultPathTagsConfigPath = join(homedir(), ".config/coding-agent-langfuse/path-tags.json");
|
|
34
35
|
const currentHost = hostname();
|
|
36
|
+
const projectLocalConfigFile = ".langfuse-ca.json";
|
|
37
|
+
const projectLocalConfigCache = new Map();
|
|
35
38
|
const kimiFirepassRates = {
|
|
36
39
|
input: 2,
|
|
37
40
|
output: 8,
|
|
@@ -192,6 +195,57 @@ function projectMetadata(cwd) {
|
|
|
192
195
|
projectFolder: projectName,
|
|
193
196
|
};
|
|
194
197
|
}
|
|
198
|
+
function matchPathTags(cwd, config) {
|
|
199
|
+
if (!cwd)
|
|
200
|
+
return { tags: [], metadata: {} };
|
|
201
|
+
const normalizedCwd = normalizeRulePath(cwd);
|
|
202
|
+
const matched = config.rules.filter((rule) => {
|
|
203
|
+
const prefix = normalizeRulePath(rule.pathPrefix);
|
|
204
|
+
return normalizedCwd === prefix || normalizedCwd.startsWith(`${prefix}/`);
|
|
205
|
+
});
|
|
206
|
+
const globalMatch = matched.reduce((acc, rule) => ({
|
|
207
|
+
tags: [...new Set([...acc.tags, ...rule.tags])],
|
|
208
|
+
metadata: { ...acc.metadata, ...rule.metadata },
|
|
209
|
+
projectName: rule.projectName ?? acc.projectName,
|
|
210
|
+
projectFolder: rule.projectFolder ?? acc.projectFolder,
|
|
211
|
+
}), {
|
|
212
|
+
tags: [],
|
|
213
|
+
metadata: {},
|
|
214
|
+
projectName: undefined,
|
|
215
|
+
projectFolder: undefined,
|
|
216
|
+
});
|
|
217
|
+
const overlays = [
|
|
218
|
+
globalMatch,
|
|
219
|
+
config.project,
|
|
220
|
+
loadProjectLocalOverlay(cwd),
|
|
221
|
+
].filter((overlay) => overlay !== undefined);
|
|
222
|
+
return overlays.reduce((acc, overlay) => {
|
|
223
|
+
if (!overlay)
|
|
224
|
+
return acc;
|
|
225
|
+
return {
|
|
226
|
+
tags: [...new Set([...acc.tags, ...overlay.tags])],
|
|
227
|
+
metadata: { ...acc.metadata, ...overlay.metadata },
|
|
228
|
+
projectName: overlay.projectName ?? acc.projectName,
|
|
229
|
+
projectFolder: overlay.projectFolder ?? acc.projectFolder,
|
|
230
|
+
};
|
|
231
|
+
}, {
|
|
232
|
+
tags: [],
|
|
233
|
+
metadata: {},
|
|
234
|
+
projectName: undefined,
|
|
235
|
+
projectFolder: undefined,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function mergeProjectMetadata(cwd, config) {
|
|
239
|
+
const project = projectMetadata(cwd);
|
|
240
|
+
const pathTags = matchPathTags(cwd, config);
|
|
241
|
+
return {
|
|
242
|
+
...project,
|
|
243
|
+
projectName: pathTags.projectName ?? project.projectName,
|
|
244
|
+
projectFolder: pathTags.projectFolder ?? project.projectFolder,
|
|
245
|
+
tags: pathTags.tags,
|
|
246
|
+
metadata: pathTags.metadata,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
195
249
|
function usage() {
|
|
196
250
|
return `Usage: coding-agent-langfuse-backfill [options]
|
|
197
251
|
|
|
@@ -211,6 +265,7 @@ Options:
|
|
|
211
265
|
--post-delay-ms N Delay after each successful OTLP POST (default: 0)
|
|
212
266
|
--cost-rates PATH JSON model cost-rate overrides in USD per 1M tokens
|
|
213
267
|
--cost-rates-json JSON Inline JSON model cost-rate overrides in USD per 1M tokens
|
|
268
|
+
--path-tags-config PATH JSON path-prefix rules that add Langfuse tags/metadata
|
|
214
269
|
--follow Keep scanning and sending newly written events
|
|
215
270
|
--poll-interval-ms N Delay between --follow scans (default: 5000)
|
|
216
271
|
--idle-exit-after-ms N Stop --follow after this much time without new sends
|
|
@@ -241,6 +296,9 @@ function parseArgs(argv) {
|
|
|
241
296
|
if (!Number.isFinite(postDelayMs))
|
|
242
297
|
postDelayMs = 0;
|
|
243
298
|
let costRates = loadCostCatalogFromEnv();
|
|
299
|
+
let pathTagsConfigPath = process.env.CODING_AGENT_LANGFUSE_PATH_TAGS_CONFIG ??
|
|
300
|
+
process.env.LANGFUSE_BACKFILL_PATH_TAGS_CONFIG ??
|
|
301
|
+
defaultPathTagsConfigPath;
|
|
244
302
|
let follow = false;
|
|
245
303
|
let pollIntervalMs = 5_000;
|
|
246
304
|
let idleExitAfterMs;
|
|
@@ -307,6 +365,9 @@ function parseArgs(argv) {
|
|
|
307
365
|
else if (arg === "--cost-rates-json") {
|
|
308
366
|
costRates = mergeCostCatalog(costRates, parseCostCatalogJson(next()));
|
|
309
367
|
}
|
|
368
|
+
else if (arg === "--path-tags-config") {
|
|
369
|
+
pathTagsConfigPath = next();
|
|
370
|
+
}
|
|
310
371
|
else if (arg === "--follow") {
|
|
311
372
|
follow = true;
|
|
312
373
|
}
|
|
@@ -378,8 +439,110 @@ function parseArgs(argv) {
|
|
|
378
439
|
maxFieldBytes,
|
|
379
440
|
postDelayMs,
|
|
380
441
|
costRates,
|
|
442
|
+
pathTagsConfigPath,
|
|
443
|
+
pathTagsConfig: loadPathTagsConfig(pathTagsConfigPath),
|
|
381
444
|
};
|
|
382
445
|
}
|
|
446
|
+
function loadPathTagsConfig(path) {
|
|
447
|
+
if (!path || !existsSync(path))
|
|
448
|
+
return { rules: [] };
|
|
449
|
+
let parsed;
|
|
450
|
+
try {
|
|
451
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
throw new Error(`Invalid path tags config ${path}: ${describeError(error)}`);
|
|
455
|
+
}
|
|
456
|
+
const root = asRecord(parsed);
|
|
457
|
+
const rawRules = Array.isArray(root.rules) ? root.rules : [];
|
|
458
|
+
const rules = rawRules.map((rawRule, index) => {
|
|
459
|
+
const rule = asRecord(rawRule);
|
|
460
|
+
const pathPrefix = asString(rule.pathPrefix) ?? asString(rule.path_prefix);
|
|
461
|
+
if (!pathPrefix) {
|
|
462
|
+
throw new Error(`Invalid path tags config ${path}: rules[${index}].pathPrefix is required`);
|
|
463
|
+
}
|
|
464
|
+
const tags = Array.isArray(rule.tags)
|
|
465
|
+
? rule.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0)
|
|
466
|
+
: [];
|
|
467
|
+
const metadata = asRecord(rule.metadata);
|
|
468
|
+
return {
|
|
469
|
+
pathPrefix,
|
|
470
|
+
tags,
|
|
471
|
+
metadata,
|
|
472
|
+
projectName: asString(rule.projectName) ?? asString(rule.project_name),
|
|
473
|
+
projectFolder: asString(rule.projectFolder) ?? asString(rule.project_folder),
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
return { rules, project: normalizeProjectTagOverlay(root, path) };
|
|
477
|
+
}
|
|
478
|
+
function loadProjectLocalOverlay(cwd) {
|
|
479
|
+
const configPath = findProjectLocalConfig(cwd);
|
|
480
|
+
if (!configPath)
|
|
481
|
+
return undefined;
|
|
482
|
+
const cached = projectLocalConfigCache.get(configPath);
|
|
483
|
+
if (projectLocalConfigCache.has(configPath))
|
|
484
|
+
return cached;
|
|
485
|
+
let overlay;
|
|
486
|
+
try {
|
|
487
|
+
overlay = normalizeProjectTagOverlay(JSON.parse(readFileSync(configPath, "utf8")), configPath);
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
throw new Error(`Invalid project Langfuse config ${configPath}: ${describeError(error)}`);
|
|
491
|
+
}
|
|
492
|
+
projectLocalConfigCache.set(configPath, overlay);
|
|
493
|
+
return overlay;
|
|
494
|
+
}
|
|
495
|
+
function findProjectLocalConfig(cwd) {
|
|
496
|
+
if (!cwd)
|
|
497
|
+
return undefined;
|
|
498
|
+
let current = cwd;
|
|
499
|
+
while (true) {
|
|
500
|
+
try {
|
|
501
|
+
current = statSync(current).isDirectory() ? current : dirname(current);
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
const parent = dirname(current);
|
|
506
|
+
if (parent === current)
|
|
507
|
+
return undefined;
|
|
508
|
+
current = parent;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
while (true) {
|
|
512
|
+
const candidate = join(current, projectLocalConfigFile);
|
|
513
|
+
if (existsSync(candidate))
|
|
514
|
+
return candidate;
|
|
515
|
+
const parent = dirname(current);
|
|
516
|
+
if (parent === current)
|
|
517
|
+
return undefined;
|
|
518
|
+
current = parent;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function normalizeProjectTagOverlay(value, sourcePath) {
|
|
522
|
+
const root = asRecord(value);
|
|
523
|
+
const rawTags = Array.isArray(root.tags) ? root.tags : [];
|
|
524
|
+
const tags = rawTags.filter((tag) => typeof tag === "string" && tag.trim().length > 0);
|
|
525
|
+
const metadata = asRecord(root.metadata);
|
|
526
|
+
const projectName = asString(root.projectName) ?? asString(root.project_name);
|
|
527
|
+
const projectFolder = asString(root.projectFolder) ?? asString(root.project_folder);
|
|
528
|
+
if (tags.length === 0 &&
|
|
529
|
+
Object.keys(metadata).length === 0 &&
|
|
530
|
+
!projectName &&
|
|
531
|
+
!projectFolder) {
|
|
532
|
+
return undefined;
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
tags,
|
|
536
|
+
metadata,
|
|
537
|
+
projectName,
|
|
538
|
+
projectFolder,
|
|
539
|
+
sourcePath,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function normalizeRulePath(path) {
|
|
543
|
+
const normalized = path.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
544
|
+
return /^[a-z]:\//i.test(normalized) ? normalized.toLowerCase() : normalized;
|
|
545
|
+
}
|
|
383
546
|
function loadCostCatalogFromEnv() {
|
|
384
547
|
let catalog = { ...defaultCostRates };
|
|
385
548
|
const path = process.env.CODING_AGENT_LANGFUSE_COST_RATES_PATH ??
|
|
@@ -1613,6 +1776,12 @@ function attr(key, value) {
|
|
|
1613
1776
|
return { key, value: { stringValue: value } };
|
|
1614
1777
|
return { key, value: { stringValue: JSON.stringify(value).slice(0, 8000) } };
|
|
1615
1778
|
}
|
|
1779
|
+
function nonEmptyArray(value) {
|
|
1780
|
+
return value.length > 0 ? value : undefined;
|
|
1781
|
+
}
|
|
1782
|
+
function nonEmptyRecord(value) {
|
|
1783
|
+
return Object.keys(value).length > 0 ? value : undefined;
|
|
1784
|
+
}
|
|
1616
1785
|
function utf8Bytes(value) {
|
|
1617
1786
|
return Buffer.byteLength(value, "utf8");
|
|
1618
1787
|
}
|
|
@@ -1645,6 +1814,7 @@ function limitEventPayload(event, maxFieldBytes) {
|
|
|
1645
1814
|
function toOtlp(events, options = {}) {
|
|
1646
1815
|
const maxFieldBytes = options.maxFieldBytes ?? defaultMaxFieldBytes;
|
|
1647
1816
|
const costRates = options.costRates ?? loadCostCatalogFromEnv();
|
|
1817
|
+
const pathTagsConfig = options.pathTagsConfig ?? { rules: [] };
|
|
1648
1818
|
const spansByTrace = new Map();
|
|
1649
1819
|
for (const rawEvent of events) {
|
|
1650
1820
|
const event = limitEventPayload(rawEvent, maxFieldBytes);
|
|
@@ -1659,7 +1829,7 @@ function toOtlp(events, options = {}) {
|
|
|
1659
1829
|
const traceStartMs = sortedEvents[0]?.startMs ?? Date.now();
|
|
1660
1830
|
const traceEndMs = Math.max(...sortedEvents.map((event) => event.endMs ?? event.startMs + 1), traceStartMs + 1);
|
|
1661
1831
|
const shouldEmitRootSpan = sortedEvents.some((event) => event.recordId === "session");
|
|
1662
|
-
const firstProject =
|
|
1832
|
+
const firstProject = mergeProjectMetadata(first.cwd, pathTagsConfig);
|
|
1663
1833
|
const rootAttributes = [
|
|
1664
1834
|
attr("service.name", `agent.${first.agent}`),
|
|
1665
1835
|
attr("deployment.environment", "local"),
|
|
@@ -1682,6 +1852,9 @@ function toOtlp(events, options = {}) {
|
|
|
1682
1852
|
attr("langfuse.trace.metadata.project_path", firstProject.projectPath),
|
|
1683
1853
|
attr("langfuse.trace.metadata.project_name", firstProject.projectName),
|
|
1684
1854
|
attr("langfuse.trace.metadata.project_folder", firstProject.projectFolder),
|
|
1855
|
+
attr("langfuse.trace.metadata.project_tags", nonEmptyArray(firstProject.tags)),
|
|
1856
|
+
attr("langfuse.trace.metadata.path_tags", nonEmptyRecord(firstProject.metadata)),
|
|
1857
|
+
attr("langfuse.trace.tags", nonEmptyArray(firstProject.tags)),
|
|
1685
1858
|
attr("langfuse.trace.metadata.import_payload_version", payloadVersion(first)),
|
|
1686
1859
|
attr("langfuse.trace.metadata.import_state_identity", importIdentity(first)),
|
|
1687
1860
|
attr("langfuse.trace.metadata.langfuse_id_identity", langfuseIdIdentity(first)),
|
|
@@ -1695,6 +1868,8 @@ function toOtlp(events, options = {}) {
|
|
|
1695
1868
|
attr("langfuse.observation.metadata.project_path", firstProject.projectPath),
|
|
1696
1869
|
attr("langfuse.observation.metadata.project_name", firstProject.projectName),
|
|
1697
1870
|
attr("langfuse.observation.metadata.project_folder", firstProject.projectFolder),
|
|
1871
|
+
attr("langfuse.observation.metadata.project_tags", nonEmptyArray(firstProject.tags)),
|
|
1872
|
+
attr("langfuse.observation.metadata.path_tags", nonEmptyRecord(firstProject.metadata)),
|
|
1698
1873
|
attr("langfuse.observation.metadata.import_payload_version", payloadVersion(first)),
|
|
1699
1874
|
attr("langfuse.observation.metadata.import_state_identity", importIdentity(first)),
|
|
1700
1875
|
attr("langfuse.observation.metadata.langfuse_id_identity", langfuseIdIdentity(first)),
|
|
@@ -1703,6 +1878,9 @@ function toOtlp(events, options = {}) {
|
|
|
1703
1878
|
attr("project.path", firstProject.projectPath),
|
|
1704
1879
|
attr("project.name", firstProject.projectName),
|
|
1705
1880
|
attr("project.folder", firstProject.projectFolder),
|
|
1881
|
+
attr("project.tags", nonEmptyArray(firstProject.tags)),
|
|
1882
|
+
attr("project.group", firstProject.metadata.project_group),
|
|
1883
|
+
attr("project.owner", firstProject.metadata.project_owner),
|
|
1706
1884
|
].filter((item) => Boolean(item));
|
|
1707
1885
|
const rootSpan = {
|
|
1708
1886
|
traceId: traceId(first),
|
|
@@ -1722,7 +1900,7 @@ function toOtlp(events, options = {}) {
|
|
|
1722
1900
|
const generation = isGenerationEvent(event);
|
|
1723
1901
|
const usage = usageDetails(event.usage);
|
|
1724
1902
|
const cost = generation ? calculateCost(event, usage, costRates) : undefined;
|
|
1725
|
-
const eventProject =
|
|
1903
|
+
const eventProject = mergeProjectMetadata(event.cwd, pathTagsConfig);
|
|
1726
1904
|
const attributes = [
|
|
1727
1905
|
attr("service.name", `agent.${event.agent}`),
|
|
1728
1906
|
attr("deployment.environment", "local"),
|
|
@@ -1747,6 +1925,8 @@ function toOtlp(events, options = {}) {
|
|
|
1747
1925
|
attr("langfuse.observation.metadata.project_path", eventProject.projectPath),
|
|
1748
1926
|
attr("langfuse.observation.metadata.project_name", eventProject.projectName),
|
|
1749
1927
|
attr("langfuse.observation.metadata.project_folder", eventProject.projectFolder),
|
|
1928
|
+
attr("langfuse.observation.metadata.project_tags", nonEmptyArray(eventProject.tags)),
|
|
1929
|
+
attr("langfuse.observation.metadata.path_tags", nonEmptyRecord(eventProject.metadata)),
|
|
1750
1930
|
attr("langfuse.observation.metadata.model", modelName ?? event.model),
|
|
1751
1931
|
attr("langfuse.observation.metadata.provider", event.provider),
|
|
1752
1932
|
attr("langfuse.observation.metadata.import_payload_version", payloadVersion(event)),
|
|
@@ -1770,6 +1950,9 @@ function toOtlp(events, options = {}) {
|
|
|
1770
1950
|
attr("project.path", eventProject.projectPath),
|
|
1771
1951
|
attr("project.name", eventProject.projectName),
|
|
1772
1952
|
attr("project.folder", eventProject.projectFolder),
|
|
1953
|
+
attr("project.tags", nonEmptyArray(eventProject.tags)),
|
|
1954
|
+
attr("project.group", eventProject.metadata.project_group),
|
|
1955
|
+
attr("project.owner", eventProject.metadata.project_owner),
|
|
1773
1956
|
attr("role", event.role),
|
|
1774
1957
|
attr("agent.model", event.model),
|
|
1775
1958
|
attr("agent.provider", event.provider),
|
|
@@ -1953,6 +2136,7 @@ async function run(options) {
|
|
|
1953
2136
|
maxRequestBytes: options.maxRequestBytes,
|
|
1954
2137
|
maxFieldBytes: options.maxFieldBytes,
|
|
1955
2138
|
costRates: options.costRates,
|
|
2139
|
+
pathTagsConfig: options.pathTagsConfig,
|
|
1956
2140
|
});
|
|
1957
2141
|
}
|
|
1958
2142
|
catch (error) {
|
|
@@ -1979,6 +2163,7 @@ async function run(options) {
|
|
|
1979
2163
|
await postOtlp(options.endpoint, batch, {
|
|
1980
2164
|
maxFieldBytes: options.maxFieldBytes,
|
|
1981
2165
|
costRates: options.costRates,
|
|
2166
|
+
pathTagsConfig: options.pathTagsConfig,
|
|
1982
2167
|
auth: options.auth,
|
|
1983
2168
|
});
|
|
1984
2169
|
for (const event of batch) {
|
package/dist/service.d.ts
CHANGED
package/dist/service.js
CHANGED
|
@@ -26,6 +26,7 @@ Service options:
|
|
|
26
26
|
--post-delay-ms N Delay after each successful OTLP POST (default: 0)
|
|
27
27
|
--cost-rates PATH JSON model cost-rate overrides in USD per 1M tokens
|
|
28
28
|
--cost-rates-json JSON Inline JSON model cost-rate overrides in USD per 1M tokens
|
|
29
|
+
--path-tags-config PATH JSON path-prefix rules that add Langfuse tags/metadata
|
|
29
30
|
--since ISO_OR_MS Optional lower bound for events the follower may send
|
|
30
31
|
--working-directory DIR Directory the service starts in (default: --home)
|
|
31
32
|
--path VALUE PATH value injected into the service environment
|
|
@@ -55,6 +56,8 @@ function parseServiceArgs(argv) {
|
|
|
55
56
|
process.env.LANGFUSE_BACKFILL_COST_RATES_PATH;
|
|
56
57
|
let costRatesJson = process.env.CODING_AGENT_LANGFUSE_COST_RATES_JSON ??
|
|
57
58
|
process.env.LANGFUSE_BACKFILL_COST_RATES_JSON;
|
|
59
|
+
let pathTagsConfigPath = process.env.CODING_AGENT_LANGFUSE_PATH_TAGS_CONFIG ??
|
|
60
|
+
process.env.LANGFUSE_BACKFILL_PATH_TAGS_CONFIG;
|
|
58
61
|
let since;
|
|
59
62
|
let dryRun = false;
|
|
60
63
|
let start = true;
|
|
@@ -107,6 +110,9 @@ function parseServiceArgs(argv) {
|
|
|
107
110
|
else if (arg === "--cost-rates-json") {
|
|
108
111
|
costRatesJson = next();
|
|
109
112
|
}
|
|
113
|
+
else if (arg === "--path-tags-config") {
|
|
114
|
+
pathTagsConfigPath = next();
|
|
115
|
+
}
|
|
110
116
|
else if (arg === "--since") {
|
|
111
117
|
since = next();
|
|
112
118
|
}
|
|
@@ -148,6 +154,7 @@ function parseServiceArgs(argv) {
|
|
|
148
154
|
postDelayMs,
|
|
149
155
|
costRatesPath,
|
|
150
156
|
costRatesJson,
|
|
157
|
+
pathTagsConfigPath,
|
|
151
158
|
since,
|
|
152
159
|
dryRun,
|
|
153
160
|
start,
|
|
@@ -300,6 +307,8 @@ function buildFollowCommand(options) {
|
|
|
300
307
|
command.push("--cost-rates", options.costRatesPath);
|
|
301
308
|
if (options.costRatesJson)
|
|
302
309
|
command.push("--cost-rates-json", options.costRatesJson);
|
|
310
|
+
if (options.pathTagsConfigPath)
|
|
311
|
+
command.push("--path-tags-config", options.pathTagsConfigPath);
|
|
303
312
|
return command;
|
|
304
313
|
}
|
|
305
314
|
function renderSystemdUnit(options, command) {
|