oh-langfuse 0.1.69 → 0.1.71

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.69",
3
+ "version": "0.1.71",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
@@ -413,16 +413,42 @@ function getPatchedLangfuseDistIndexJs() {
413
413
  " toolCallId: callId || '',",
414
414
  " };",
415
415
  "};",
416
- "const tokenMetricsFromPart = (part) => {",
417
- " const tokens = part?.tokens ?? part?.usage ?? {};",
418
- " return {",
419
- " input: metricNumber(tokens.input ?? tokens.input_tokens ?? tokens.inputTokens),",
416
+ "const tokenMetricsFromPart = (part) => {",
417
+ " const tokens = part?.tokens ?? part?.usage ?? {};",
418
+ " return {",
419
+ " input: metricNumber(tokens.input ?? tokens.input_tokens ?? tokens.inputTokens),",
420
420
  " output: metricNumber(tokens.output ?? tokens.output_tokens ?? tokens.outputTokens),",
421
421
  " total: metricNumber(tokens.total ?? tokens.total_tokens ?? tokens.totalTokens),",
422
422
  " cacheRead: metricNumber(tokens.cache?.read ?? tokens.cacheRead ?? tokens.cache_read_tokens ?? tokens.cachedInputTokens),",
423
- " reasoning: metricNumber(tokens.reasoning ?? tokens.reasoning_tokens ?? tokens.reasoningTokens),",
424
- " };",
425
- "};",
423
+ " reasoning: metricNumber(tokens.reasoning ?? tokens.reasoning_tokens ?? tokens.reasoningTokens),",
424
+ " };",
425
+ "};",
426
+ "const OBSERVATION_TEXT_LIMIT = 20000;",
427
+ "const limitObservationText = (text) => {",
428
+ " const normalized = typeof text === 'string' ? text.trim() : '';",
429
+ " if (!normalized) return '';",
430
+ " return normalized.length > OBSERVATION_TEXT_LIMIT ? `${normalized.slice(0, OBSERVATION_TEXT_LIMIT)}\\n...[truncated]` : normalized;",
431
+ "};",
432
+ "const textFromContent = (value, depth = 0) => {",
433
+ " if (depth > 6 || value === null || value === undefined) return '';",
434
+ " if (typeof value === 'string') return value.trim();",
435
+ " if (Array.isArray(value)) return value.map((item) => textFromContent(item, depth + 1)).filter(Boolean).join('\\n').trim();",
436
+ " if (typeof value !== 'object') return '';",
437
+ " for (const key of ['text', 'content', 'prompt', 'message', 'input', 'output', 'parts']) {",
438
+ " const text = textFromContent(value[key], depth + 1);",
439
+ " if (text) return text;",
440
+ " }",
441
+ " return '';",
442
+ "};",
443
+ "const promptFromArgv = () => {",
444
+ " const args = process.argv.slice(2);",
445
+ " if (!args.includes('run')) return '';",
446
+ " for (let i = args.length - 1; i >= 0; i -= 1) {",
447
+ " const arg = args[i];",
448
+ " if (typeof arg === 'string' && arg.trim() && !arg.startsWith('-')) return arg.trim();",
449
+ " }",
450
+ " return '';",
451
+ "};",
426
452
  "",
427
453
  "const normalizeSkillNames = (names) => {",
428
454
  " if (!Array.isArray(names)) return [];",
@@ -643,11 +669,14 @@ function getPatchedLangfuseDistIndexJs() {
643
669
  " const sdkStartPromise = Promise.resolve().then(() => sdk.start()).catch((err) => {",
644
670
  ' log("warn", `OTEL SDK start failed: ${err?.message ?? err}`);',
645
671
  " });",
646
- " const getMetricsTracer = () => trace.getTracer('oh-langfuse-opencode-metrics');",
647
- " const knownSkillNames = await collectKnownSkillNames();",
648
- " const startupSkillUsages = detectOpencodeSkillUsages(process.argv.join('\\n'), knownSkillNames);",
649
- " const messageTextById = new Map();",
650
- " const skillUsagesByMessageId = new Map();",
672
+ " const getMetricsTracer = () => trace.getTracer('oh-langfuse-opencode-metrics');",
673
+ " const knownSkillNames = await collectKnownSkillNames();",
674
+ " const startupSkillUsages = detectOpencodeSkillUsages(process.argv.join('\\n'), knownSkillNames);",
675
+ " const startupPromptText = limitObservationText(promptFromArgv());",
676
+ " const messageTextById = new Map();",
677
+ " const messageInputById = new Map();",
678
+ " const lastUserTextBySessionId = new Map();",
679
+ " const skillUsagesByMessageId = new Map();",
651
680
  " const skillUsagesBySessionId = new Map();",
652
681
  " const startupSkillSessionIds = new Set();",
653
682
  " const toolCallIdsByMessageId = new Map();",
@@ -717,9 +746,17 @@ function getPatchedLangfuseDistIndexJs() {
717
746
  " const recordInteractionMetric = async (event) => {",
718
747
  " const payload = eventPayload(event);",
719
748
  " const part = eventPart(event);",
720
- " const partType = part?.type ?? '';",
721
- " const sessionId = pickEventString(part?.sessionID, part?.sessionId, payload?.sessionID, payload?.sessionId, payload?.session?.id, event?.sessionID, event?.sessionId);",
722
- " const messageId = pickEventString(part?.messageID, part?.messageId, payload?.messageID, payload?.messageId, payload?.message?.id, event?.messageID, event?.messageId);",
749
+ " const partType = part?.type ?? '';",
750
+ " const sessionId = pickEventString(part?.sessionID, part?.sessionId, payload?.sessionID, payload?.sessionId, payload?.session?.id, event?.sessionID, event?.sessionId);",
751
+ " const messageId = pickEventString(part?.messageID, part?.messageId, payload?.messageID, payload?.messageId, payload?.message?.id, event?.messageID, event?.messageId);",
752
+ " const role = pickEventString(part?.role, payload?.role, payload?.message?.role, event?.role).toLowerCase();",
753
+ " const eventText = textFromContent(part) || textFromContent(payload?.message) || textFromContent(payload);",
754
+ " if (eventText && role === 'user') {",
755
+ " const inputText = limitObservationText(eventText);",
756
+ " if (messageId) messageInputById.set(messageId, inputText);",
757
+ " if (sessionId) lastUserTextBySessionId.set(sessionId, inputText);",
758
+ " return;",
759
+ " }",
723
760
  " if (sessionId && startupSkillUsages.length && !startupSkillSessionIds.has(sessionId)) {",
724
761
  " startupSkillSessionIds.add(sessionId);",
725
762
  " rememberSkillUsages(skillUsagesBySessionId, sessionId, startupSkillUsages);",
@@ -733,11 +770,11 @@ function getPatchedLangfuseDistIndexJs() {
733
770
  " rememberToolActivity(toolCallIdsBySessionId, sessionId, toolActivity, 'toolCallCount');",
734
771
  " rememberToolActivity(toolResultIdsByMessageId, messageId, toolActivity, 'toolResultCount');",
735
772
  " rememberToolActivity(toolResultIdsBySessionId, sessionId, toolActivity, 'toolResultCount');",
736
- " if (partType === 'text' && messageId && typeof part.text === 'string') {",
737
- " messageTextById.set(messageId, part.text);",
738
- " const textSkillUsages = detectOpencodeSkillUsages(part.text, knownSkillNames);",
739
- " rememberSkillUsages(skillUsagesByMessageId, messageId, textSkillUsages);",
740
- " rememberSkillUsages(skillUsagesBySessionId, sessionId, textSkillUsages);",
773
+ " if (eventText && messageId && (role === 'assistant' || partType === 'text')) {",
774
+ " messageTextById.set(messageId, limitObservationText(eventText));",
775
+ " const textSkillUsages = detectOpencodeSkillUsages(eventText, knownSkillNames);",
776
+ " rememberSkillUsages(skillUsagesByMessageId, messageId, textSkillUsages);",
777
+ " rememberSkillUsages(skillUsagesBySessionId, sessionId, textSkillUsages);",
741
778
  " return;",
742
779
  " }",
743
780
  " if (partType !== 'step-finish' || !messageId || emittedMessageIds.has(messageId)) return;",
@@ -745,10 +782,11 @@ function getPatchedLangfuseDistIndexJs() {
745
782
  " const tokenMetrics = tokenMetricsFromPart(part);",
746
783
  " const total = tokenMetrics.total ?? (tokenMetrics.input !== undefined && tokenMetrics.output !== undefined ? tokenMetrics.input + tokenMetrics.output : undefined);",
747
784
  " const tokenAvailable = [tokenMetrics.input, tokenMetrics.output, total, tokenMetrics.cacheRead, tokenMetrics.reasoning].some((value) => value !== undefined);",
748
- " await sdkStartPromise;",
749
- " const span = getMetricsTracer().startSpan('OpenCode Agent Turn');",
750
- " const text = messageTextById.get(messageId) || '';",
751
- " const skillUsages = dedupeSkillUsages([...(skillUsagesByMessageId.get(messageId) ?? []), ...(skillUsagesBySessionId.get(sessionId) ?? [])]);",
785
+ " await sdkStartPromise;",
786
+ " const span = getMetricsTracer().startSpan('OpenCode Agent Turn');",
787
+ " const text = messageTextById.get(messageId) || '';",
788
+ " const inputText = messageInputById.get(messageId) || lastUserTextBySessionId.get(sessionId) || startupPromptText || '';",
789
+ " const skillUsages = dedupeSkillUsages([...(skillUsagesByMessageId.get(messageId) ?? []), ...(skillUsagesBySessionId.get(sessionId) ?? [])]);",
752
790
  " const interactionId = `opencode:${userId || \"unknown\"}:${sessionId || \"unknown\"}:${messageId}`;",
753
791
  " const skillUseEvents = buildSkillUseEvents(interactionId, skillUsages);",
754
792
  " const skillNames = uniqueSkillNames(skillUsages);",
@@ -781,14 +819,22 @@ function getPatchedLangfuseDistIndexJs() {
781
819
  ' if (skillInvocationModes.length) span.setAttribute("langfuse.observation.metadata.skill_invocation_modes", skillInvocationModes);',
782
820
  ' if (skillAgentPaths.length) span.setAttribute("langfuse.observation.metadata.skill_agent_paths", skillAgentPaths);',
783
821
  ' if (tokenMetrics.input !== undefined) span.setAttribute("langfuse.observation.metadata.input_tokens", tokenMetrics.input);',
784
- ' if (tokenMetrics.output !== undefined) span.setAttribute("langfuse.observation.metadata.output_tokens", tokenMetrics.output);',
785
- ' if (total !== undefined) span.setAttribute("langfuse.observation.metadata.total_tokens", total);',
786
- ' if (tokenMetrics.cacheRead !== undefined) span.setAttribute("langfuse.observation.metadata.cache_read_tokens", tokenMetrics.cacheRead);',
787
- ' if (tokenMetrics.reasoning !== undefined) span.setAttribute("langfuse.observation.metadata.reasoning_tokens", tokenMetrics.reasoning);',
788
- ' if (text) span.setAttribute("langfuse.observation.metadata.output_text_preview", text.slice(0, 512));',
789
- ' writeRepoContextMetrics(span, collectRepoContext(process.cwd(), "process"));',
790
- " span.end();",
791
- " messageTextById.delete(messageId);",
822
+ ' if (tokenMetrics.output !== undefined) span.setAttribute("langfuse.observation.metadata.output_tokens", tokenMetrics.output);',
823
+ ' if (total !== undefined) span.setAttribute("langfuse.observation.metadata.total_tokens", total);',
824
+ ' if (tokenMetrics.cacheRead !== undefined) span.setAttribute("langfuse.observation.metadata.cache_read_tokens", tokenMetrics.cacheRead);',
825
+ ' if (tokenMetrics.reasoning !== undefined) span.setAttribute("langfuse.observation.metadata.reasoning_tokens", tokenMetrics.reasoning);',
826
+ ' if (inputText) span.setAttribute("langfuse.observation.input", inputText);',
827
+ ' if (inputText) span.setAttribute("langfuse.trace.input", inputText);',
828
+ ' if (inputText) span.setAttribute("input.value", inputText);',
829
+ ' if (text) span.setAttribute("langfuse.observation.output", text);',
830
+ ' if (text) span.setAttribute("langfuse.trace.output", text);',
831
+ ' if (text) span.setAttribute("output.value", text);',
832
+ ' if (inputText) span.setAttribute("langfuse.observation.metadata.input_text_preview", inputText.slice(0, 512));',
833
+ ' if (text) span.setAttribute("langfuse.observation.metadata.output_text_preview", text.slice(0, 512));',
834
+ ' writeRepoContextMetrics(span, collectRepoContext(process.cwd(), "process"));',
835
+ " span.end();",
836
+ " messageTextById.delete(messageId);",
837
+ " messageInputById.delete(messageId);",
792
838
  " skillUsagesByMessageId.delete(messageId);",
793
839
  " skillUsagesBySessionId.delete(sessionId);",
794
840
  " toolCallIdsByMessageId.delete(messageId);",
@@ -977,6 +1023,26 @@ function writeOpencodeCommandShim(opencodeDir, { publicKey, secretKey, baseUrl,
977
1023
  cmd.push(' call "%APPDATA%\\npm\\opencode.cmd" %*');
978
1024
  cmd.push(" exit /b %ERRORLEVEL%");
979
1025
  cmd.push(")");
1026
+ cmd.push('if exist "%APPDATA%\\npm\\node_modules\\opencode-ai\\bin\\opencode.exe" (');
1027
+ cmd.push(' call "%APPDATA%\\npm\\node_modules\\opencode-ai\\bin\\opencode.exe" %*');
1028
+ cmd.push(" exit /b %ERRORLEVEL%");
1029
+ cmd.push(")");
1030
+ cmd.push('if exist "%APPDATA%\\npm\\node_modules\\opencode-ai\\node_modules\\opencode-windows-x64\\bin\\opencode.exe" (');
1031
+ cmd.push(' call "%APPDATA%\\npm\\node_modules\\opencode-ai\\node_modules\\opencode-windows-x64\\bin\\opencode.exe" %*');
1032
+ cmd.push(" exit /b %ERRORLEVEL%");
1033
+ cmd.push(")");
1034
+ cmd.push('if exist "%APPDATA%\\npm\\node_modules\\opencode-ai\\node_modules\\opencode-windows-x64-baseline\\bin\\opencode.exe" (');
1035
+ cmd.push(' call "%APPDATA%\\npm\\node_modules\\opencode-ai\\node_modules\\opencode-windows-x64-baseline\\bin\\opencode.exe" %*');
1036
+ cmd.push(" exit /b %ERRORLEVEL%");
1037
+ cmd.push(")");
1038
+ cmd.push('if exist "%APPDATA%\\npm\\node_modules\\opencode-windows-x64\\bin\\opencode.exe" (');
1039
+ cmd.push(' call "%APPDATA%\\npm\\node_modules\\opencode-windows-x64\\bin\\opencode.exe" %*');
1040
+ cmd.push(" exit /b %ERRORLEVEL%");
1041
+ cmd.push(")");
1042
+ cmd.push('if exist "%APPDATA%\\npm\\node_modules\\opencode-windows-x64-baseline\\bin\\opencode.exe" (');
1043
+ cmd.push(' call "%APPDATA%\\npm\\node_modules\\opencode-windows-x64-baseline\\bin\\opencode.exe" %*');
1044
+ cmd.push(" exit /b %ERRORLEVEL%");
1045
+ cmd.push(")");
980
1046
  cmd.push('for /f "delims=" %%O in (\'where.exe opencode 2^>nul\') do (');
981
1047
  cmd.push(' if /I not "%%~fO"=="%~f0" (');
982
1048
  cmd.push(' call "%%O" %*');
@@ -984,7 +1050,7 @@ function writeOpencodeCommandShim(opencodeDir, { publicKey, secretKey, baseUrl,
984
1050
  cmd.push(" )");
985
1051
  cmd.push(")");
986
1052
  cmd.push("echo [ERROR] OpenCode CLI not found. Install OpenCode CLI first. 1>&2");
987
- cmd.push("echo Try: npm install -g opencode-windows-x64 1>&2");
1053
+ cmd.push("echo Try: npm install -g opencode-ai@latest 1>&2");
988
1054
  cmd.push("echo Then run: npx oh-langfuse@latest update opencode 1>&2");
989
1055
  cmd.push("exit /b 127");
990
1056
  fs.writeFileSync(shim, cmd.join("\r\n") + "\r\n", "utf8");
@@ -1021,14 +1087,12 @@ function writeOpencodeCommandShim(opencodeDir, { publicKey, secretKey, baseUrl,
1021
1087
  "fi",
1022
1088
  'if [ -n "$OH_LANGFUSE_BROKEN_OPENCODE" ]; then',
1023
1089
  ' echo "[ERROR] OpenCode CLI was found but failed to start: $OH_LANGFUSE_BROKEN_OPENCODE" >&2',
1024
- ' echo "Try: npm install -g opencode-linux-x64 --registry=https://registry.npmjs.org/" >&2',
1025
- ' echo "If that package fails on your host, try opencode-linux-x64-baseline or opencode-linux-x64-musl." >&2',
1090
+ ' echo "Try: npm install -g opencode-ai@latest --registry=https://registry.npmjs.org/" >&2',
1026
1091
  ' echo "Then run: npx --registry=https://registry.npmjs.org/ -y oh-langfuse@latest update opencode --npmRegistry=https://registry.npmjs.org/" >&2',
1027
1092
  " exit 127",
1028
1093
  "fi",
1029
1094
  'echo "[ERROR] OpenCode CLI not found. Install OpenCode CLI first." >&2',
1030
- 'echo "Try: npm install -g opencode-linux-x64" >&2',
1031
- 'echo "If that package fails on your host, try opencode-linux-x64-baseline or opencode-linux-x64-musl." >&2',
1095
+ 'echo "Try: npm install -g opencode-ai@latest" >&2',
1032
1096
  'echo "Then run: npx oh-langfuse@latest update opencode" >&2',
1033
1097
  "exit 127",
1034
1098
  ""
@@ -354,14 +354,29 @@ function metadataValue(item, key) {
354
354
  return undefined;
355
355
  }
356
356
 
357
- function directMetadataValue(item, key) {
358
- const metadata = item?.metadata || {};
359
- return metadata[key];
360
- }
361
-
362
- function hasMetadataKey(item, key) {
363
- return metadataValue(item, key) !== undefined;
364
- }
357
+ function directMetadataValue(item, key) {
358
+ const metadata = item?.metadata || {};
359
+ return metadata[key];
360
+ }
361
+
362
+ function observationIOValue(item, key) {
363
+ const metadata = item?.metadata || {};
364
+ const attrs = metadata.attributes || {};
365
+ for (const value of [
366
+ item?.[key],
367
+ metadata[key],
368
+ attrs[`langfuse.observation.${key}`],
369
+ attrs[`langfuse.trace.${key}`],
370
+ attrs[`${key}.value`],
371
+ ]) {
372
+ if (value !== undefined && value !== null && String(value).trim() !== "") return value;
373
+ }
374
+ return undefined;
375
+ }
376
+
377
+ function hasMetadataKey(item, key) {
378
+ return metadataValue(item, key) !== undefined;
379
+ }
365
380
 
366
381
  function metricInteractionId(item, target) {
367
382
  return target === "opencode"
@@ -507,13 +522,20 @@ async function verifyMetricObservations(config, found, { since, target, marker =
507
522
  throw new Error(`Metric verification failed for ${target}: Agent Turn is missing repo context ${key}.`);
508
523
  }
509
524
  }
510
- if (target === "opencode") {
511
- for (const key of ["interaction_id", "interaction_count", "token_metrics_available", "tool_call_count", "skill_use_count", "input_tokens", "output_tokens", "total_tokens"]) {
512
- if (metadataValue(item, key) === undefined) {
513
- throw new Error(`Metric verification failed for ${target}: effective metadata is missing ${key}.`);
514
- }
515
- }
516
- }
525
+ if (target === "opencode") {
526
+ for (const key of ["interaction_id", "interaction_count", "token_metrics_available", "tool_call_count", "skill_use_count", "input_tokens", "output_tokens", "total_tokens"]) {
527
+ if (metadataValue(item, key) === undefined) {
528
+ throw new Error(`Metric verification failed for ${target}: effective metadata is missing ${key}.`);
529
+ }
530
+ }
531
+ if (item?.name === expectedName) {
532
+ for (const key of ["input", "output"]) {
533
+ if (observationIOValue(item, key) === undefined) {
534
+ throw new Error(`Metric verification failed for ${target}: ${expectedName} is missing ${key}.`);
535
+ }
536
+ }
537
+ }
538
+ }
517
539
  const tokenAvailable = metadataValue(item, "token_metrics_available");
518
540
  for (const tokenKey of ["input_tokens", "output_tokens", "total_tokens"]) {
519
541
  const value = metadataValue(item, tokenKey);
@@ -26,10 +26,15 @@ export function resolveOpencodeCli(preferred) {
26
26
  if (process.env.LOCALAPPDATA) {
27
27
  candidates.push(path.join(process.env.LOCALAPPDATA, "Programs", "opencode", "opencode.exe"));
28
28
  }
29
- if (process.env.APPDATA) {
30
- candidates.push(path.join(process.env.APPDATA, "npm", "opencode.cmd"));
31
- candidates.push(path.join(process.env.APPDATA, "npm", "opencode"));
32
- }
29
+ if (process.env.APPDATA) {
30
+ candidates.push(path.join(process.env.APPDATA, "npm", "opencode.cmd"));
31
+ candidates.push(path.join(process.env.APPDATA, "npm", "opencode"));
32
+ candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-ai", "bin", "opencode.exe"));
33
+ candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-ai", "node_modules", "opencode-windows-x64", "bin", "opencode.exe"));
34
+ candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-ai", "node_modules", "opencode-windows-x64-baseline", "bin", "opencode.exe"));
35
+ candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-windows-x64", "bin", "opencode.exe"));
36
+ candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-windows-x64-baseline", "bin", "opencode.exe"));
37
+ }
33
38
  candidates.push(path.join(home, "scoop", "shims", "opencode.exe"));
34
39
  } else {
35
40
  candidates.push(path.join(home, ".opencode", "bin", "opencode"));