oh-langfuse 0.1.29 → 0.1.31

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-langfuse",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
@@ -23,6 +23,7 @@
23
23
  "scripts/opencode-langfuse-setup.mjs",
24
24
  "scripts/resolve-opencode-cli.mjs",
25
25
  "scripts/real-self-verify.mjs",
26
+ "scripts/log-filter-utils.mjs",
26
27
  "scripts/metrics-utils.mjs",
27
28
  "scripts/update-langfuse-runtime.mjs",
28
29
  "scripts/update-utils.mjs",
@@ -0,0 +1,18 @@
1
+ function envShowsWarnings(env = process.env) {
2
+ return /^(1|true|yes|on)$/i.test(String(env.OH_LANGFUSE_SHOW_WARN || env.LANGFUSE_SHOW_WARN || ""));
3
+ }
4
+
5
+ export function shouldSuppressAgentLogLine(line, options = {}) {
6
+ const showWarnings = options.showWarnings ?? envShowsWarnings(options.env);
7
+ if (showWarnings) return false;
8
+ const text = String(line || "").trim();
9
+ return /(?:^|\s)(npm\s+warn|warn(?:ing)?\b|\[warn\])/i.test(text);
10
+ }
11
+
12
+ export function filterAgentLogText(text, options = {}) {
13
+ const source = String(text || "");
14
+ return source
15
+ .split(/\r?\n/)
16
+ .filter((line) => line !== "" && !shouldSuppressAgentLogLine(line, options))
17
+ .join("\n");
18
+ }
@@ -132,10 +132,10 @@ export function buildInteractionMetadata(options = {}) {
132
132
  const sessionId = String(options.sessionId || options.session_id || "unknown");
133
133
  const turnNumber = Number(options.turnNumber ?? options.turn_number ?? 0) || 0;
134
134
  const tokenMetrics = normalizeTokenMetrics(options.tokenMetrics);
135
- const toolCallCount = Number(options.toolCallCount ?? options.tool_call_count ?? 0) || 0;
136
- const toolResultCount = Number(options.toolResultCount ?? options.tool_result_count ?? 0) || 0;
137
135
  const skillNames = normalizeSkillNames(options.skillNames ?? options.skill_names);
138
136
  const skillUseCount = skillNames.length || Number(options.skillUseCount ?? options.skill_use_count ?? 0) || 0;
137
+ const toolCallCount = Math.max(Number(options.toolCallCount ?? options.tool_call_count ?? 0) || 0, skillUseCount);
138
+ const toolResultCount = Math.max(Number(options.toolResultCount ?? options.tool_result_count ?? 0) || 0, skillUseCount);
139
139
 
140
140
  const metadata = {
141
141
  source,
@@ -172,47 +172,14 @@ export function buildInteractionMetadata(options = {}) {
172
172
  export function buildOpencodeMetricAttributes(options = {}) {
173
173
  const attrs = options.attributes || {};
174
174
  const userId = String(options.userId || attrs["oh.langfuse.user_id"] || attrs["langfuse.user.id"] || "");
175
- const sessionId = String(attrs["ai.request.headers.x-opencode-session"] || options.sessionId || options.session_id || "unknown");
176
- const requestId = String(attrs["ai.request.headers.x-opencode-request"] || options.requestId || options.request_id || "unknown");
177
- const spanId = String(options.spanId || attrs["span.id"] || "unknown");
178
175
  const provider = attrs["ai.model.provider"] || attrs["gen_ai.system"] || "";
179
176
  const modelId = attrs["ai.model.id"] || attrs["gen_ai.request.model"] || attrs["ai.response.model"] || "";
180
177
  const model = provider && modelId ? `${provider}/${modelId}` : provider || modelId || null;
181
- const skillNames = normalizeSkillNames(options.skillNames ?? options.skill_names);
182
- const toolCallCount = Number(options.toolCallCount ?? options.tool_call_count ?? 0) || 0;
183
- const toolResultCount = Number(options.toolResultCount ?? options.tool_result_count ?? 0) || 0;
184
- const tokenMetrics = normalizeTokenMetrics({
185
- input: attrs["ai.usage.inputTokens"] ?? attrs["ai.usage.promptTokens"] ?? attrs["gen_ai.usage.input_tokens"],
186
- output: attrs["ai.usage.outputTokens"] ?? attrs["ai.usage.completionTokens"] ?? attrs["gen_ai.usage.output_tokens"],
187
- total: attrs["ai.usage.totalTokens"],
188
- cacheRead: attrs["ai.usage.cachedInputTokens"] ?? attrs["ai.usage.inputTokenDetails.cacheReadTokens"],
189
- reasoning: attrs["ai.usage.reasoningTokens"] ?? attrs["ai.usage.outputTokenDetails.reasoningTokens"],
190
- });
191
-
192
- const out = {
178
+
179
+ return {
193
180
  "langfuse.observation.metadata.source": "opencode",
194
181
  "langfuse.observation.metadata.user_id": userId,
195
- "langfuse.observation.metadata.session_id": sessionId,
196
- "langfuse.observation.metadata.interaction_id": `opencode:${userId || "unknown"}:${sessionId}:${requestId}:${spanId}`,
197
182
  "langfuse.observation.metadata.metrics_schema_version": METRICS_SCHEMA_VERSION,
198
- "langfuse.observation.metadata.interaction_count": 1,
199
- "langfuse.observation.metadata.user_message_count": 1,
200
- "langfuse.observation.metadata.assistant_message_count": 1,
201
- "langfuse.observation.metadata.tool_call_count": toolCallCount,
202
- "langfuse.observation.metadata.tool_result_count": toolResultCount,
203
- "langfuse.observation.metadata.skill_use_count": skillNames.length,
204
- "langfuse.observation.metadata.token_metrics_available": tokenMetrics.token_metrics_available,
205
183
  "langfuse.observation.metadata.model": model,
206
184
  };
207
-
208
- if (skillNames.length) {
209
- out["langfuse.observation.metadata.skill_names"] = skillNames;
210
- out["langfuse.observation.metadata.skill_names_json"] = JSON.stringify(skillNames);
211
- }
212
-
213
- for (const key of ["input_tokens", "output_tokens", "total_tokens", "cache_read_tokens", "reasoning_tokens"]) {
214
- if (tokenMetrics[key] != null) out[`langfuse.observation.metadata.${key}`] = tokenMetrics[key];
215
- }
216
-
217
- return out;
218
185
  }
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import os from "node:os";
4
4
  import { spawn, spawnSync } from "node:child_process";
5
5
  import { parseJsonRelaxed, stripBom } from "./json-utils.mjs";
6
+ import { shouldSuppressAgentLogLine } from "./log-filter-utils.mjs";
6
7
 
7
8
  const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
8
9
  const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
@@ -114,7 +115,8 @@ function getPatchedLangfuseDistIndexJs() {
114
115
  // This replaces opencode-plugin-langfuse/dist/index.js to inject userId into spans.
115
116
  // It is intentionally self-contained (no extra build steps).
116
117
  return [
117
- 'import { LangfuseSpanProcessor } from "@langfuse/otel";',
118
+ 'import { LangfuseSpanProcessor } from "@langfuse/otel";',
119
+ 'import { configureGlobalLogger, LogLevel } from "@langfuse/core";',
118
120
  'import { promises as fs } from "node:fs";',
119
121
  'import os from "node:os";',
120
122
  'import path from "node:path";',
@@ -128,7 +130,7 @@ function getPatchedLangfuseDistIndexJs() {
128
130
  ' "config.json"',
129
131
  ");",
130
132
  "",
131
- "const readConfiguredUserId = async () => {",
133
+ "const readConfiguredUserId = async () => {",
132
134
  " try {",
133
135
  ' const content = await fs.readFile(USER_CONFIG_PATH, "utf8");',
134
136
  " // tolerate UTF-8 BOM",
@@ -144,8 +146,14 @@ function getPatchedLangfuseDistIndexJs() {
144
146
  " } catch {",
145
147
  " return undefined;",
146
148
  " }",
147
- "};",
148
- "",
149
+ "};",
150
+ "",
151
+ "const showWarnings = /^(1|true|yes|on)$/i.test(String(process.env.OH_LANGFUSE_SHOW_WARN || process.env.LANGFUSE_SHOW_WARN || ''));",
152
+ "if (!showWarnings) {",
153
+ " process.env.LANGFUSE_LOG_LEVEL = 'ERROR';",
154
+ " configureGlobalLogger({ level: LogLevel.ERROR });",
155
+ "}",
156
+ "",
149
157
  "const createUserIdSpanProcessor = (userId) => ({",
150
158
  " onStart: (span) => {",
151
159
  ' span.setAttribute("langfuse.user.id", userId);',
@@ -218,34 +226,10 @@ function getPatchedLangfuseDistIndexJs() {
218
226
  ' const hasModel = attrs["ai.model.id"] || attrs["ai.model.provider"] || attrs["gen_ai.request.model"];',
219
227
  ' const isTool = attrs["ai.toolCall.name"] || attrs["ai.toolCall.id"];',
220
228
  " if (!hasModel || isTool) return false;",
221
- ' if (typeof span.updateName === "function") span.updateName("AI Interaction");',
222
- ' const sessionId = String(attrs["ai.request.headers.x-opencode-session"] || attrs["session.id"] || "unknown");',
223
- ' const requestId = String(attrs["ai.request.headers.x-opencode-request"] || attrs["ai.response.id"] || attrs["ai.operationId"] || "unknown");',
224
- ' const spanId = span.spanContext?.().spanId || attrs["span.id"] || "unknown";',
225
229
  ' const provider = attrs["ai.model.provider"] || attrs["gen_ai.system"] || "";',
226
230
  ' const modelId = attrs["ai.model.id"] || attrs["gen_ai.request.model"] || attrs["ai.response.model"] || "";',
227
231
  ' const model = provider && modelId ? `${provider}/${modelId}` : provider || modelId || undefined;',
228
- ' const inputTokens = metricNumber(attrs["ai.usage.inputTokens"] ?? attrs["ai.usage.promptTokens"] ?? attrs["gen_ai.usage.input_tokens"]);',
229
- ' const outputTokens = metricNumber(attrs["ai.usage.outputTokens"] ?? attrs["ai.usage.completionTokens"] ?? attrs["gen_ai.usage.output_tokens"]);',
230
- ' const totalTokens = metricNumber(attrs["ai.usage.totalTokens"]) ?? (inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined);',
231
- ' const cacheReadTokens = metricNumber(attrs["ai.usage.cachedInputTokens"] ?? attrs["ai.usage.inputTokenDetails.cacheReadTokens"]);',
232
- ' const reasoningTokens = metricNumber(attrs["ai.usage.reasoningTokens"] ?? attrs["ai.usage.outputTokenDetails.reasoningTokens"]);',
233
- " const tokenAvailable = [inputTokens, outputTokens, totalTokens, cacheReadTokens, reasoningTokens].some((value) => value !== undefined);",
234
- ' writeMetric(span, "session_id", sessionId);',
235
- ' writeMetric(span, "interaction_id", `opencode:${userId || "unknown"}:${sessionId}:${requestId}:${spanId}`);',
236
- ' writeMetric(span, "interaction_count", 1);',
237
- ' writeMetric(span, "user_message_count", 1);',
238
- ' writeMetric(span, "assistant_message_count", 1);',
239
- ' writeMetric(span, "token_metrics_available", tokenAvailable);',
240
- ' writeMetric(span, "tool_call_count", 0);',
241
- ' writeMetric(span, "tool_result_count", 0);',
242
- ' writeMetric(span, "skill_use_count", 0);',
243
232
  ' writeMetric(span, "model", model);',
244
- ' writeMetric(span, "input_tokens", inputTokens);',
245
- ' writeMetric(span, "output_tokens", outputTokens);',
246
- ' writeMetric(span, "total_tokens", totalTokens);',
247
- ' writeMetric(span, "cache_read_tokens", cacheReadTokens);',
248
- ' writeMetric(span, "reasoning_tokens", reasoningTokens);',
249
233
  " return true;",
250
234
  "};",
251
235
  "",
@@ -384,11 +368,12 @@ function getPatchedLangfuseDistIndexJs() {
384
368
  "",
385
369
  " const userIdFromConfig = await readConfiguredUserId();",
386
370
  " const userIdEnv = process.env.LANGFUSE_USER_ID?.trim();",
387
- " const userId = userIdFromConfig || userIdEnv;",
388
- "",
389
- " const log = (level, message) => {",
390
- ' client.app.log({ body: { service: "langfuse-otel", level, message } });',
391
- " };",
371
+ " const userId = userIdFromConfig || userIdEnv;",
372
+ "",
373
+ " const log = (level, message) => {",
374
+ " if (level === 'warn' && !showWarnings) return;",
375
+ ' client.app.log({ body: { service: "langfuse-otel", level, message } });',
376
+ " };",
392
377
  "",
393
378
  " if (!publicKey || !secretKey) {",
394
379
  ' log("warn", "Missing LANGFUSE_PUBLIC_KEY or LANGFUSE_SECRET_KEY - tracing disabled");',
@@ -499,8 +484,8 @@ function getPatchedLangfuseDistIndexJs() {
499
484
  " const span = metricsTracer.startSpan('AI Interaction');",
500
485
  " const text = messageTextById.get(messageId) || '';",
501
486
  " const skillNames = [...new Set([...(skillNamesByMessageId.get(messageId) ?? []), ...(skillNamesBySessionId.get(sessionId) ?? [])])];",
502
- " const toolCallCount = new Set([...(toolCallIdsByMessageId.get(messageId) ?? []), ...(toolCallIdsBySessionId.get(sessionId) ?? [])]).size;",
503
- " const toolResultCount = new Set([...(toolResultIdsByMessageId.get(messageId) ?? []), ...(toolResultIdsBySessionId.get(sessionId) ?? [])]).size;",
487
+ " const toolCallCount = Math.max(new Set([...(toolCallIdsByMessageId.get(messageId) ?? []), ...(toolCallIdsBySessionId.get(sessionId) ?? [])]).size, skillNames.length);",
488
+ " const toolResultCount = Math.max(new Set([...(toolResultIdsByMessageId.get(messageId) ?? []), ...(toolResultIdsBySessionId.get(sessionId) ?? [])]).size, skillNames.length);",
504
489
  ' span.setAttribute("oh.langfuse.source", "opencode");',
505
490
  ' span.setAttribute("oh.langfuse.user_id", userId || "");',
506
491
  ' span.setAttribute("oh.langfuse.metrics_schema_version", "1.0");',
@@ -700,7 +685,11 @@ function runNpmInstallCapture(npmArgs) {
700
685
  child.stderr?.on("data", (chunk) => {
701
686
  const text = String(chunk);
702
687
  stderr += text;
703
- process.stderr.write(text);
688
+ const filtered = text
689
+ .split(/(\r?\n)/)
690
+ .filter((part) => part === "\n" || part === "\r\n" || !shouldSuppressAgentLogLine(part))
691
+ .join("");
692
+ if (filtered) process.stderr.write(filtered);
704
693
  });
705
694
  child.on("error", (error) => {
706
695
  clearInterval(timer);