oh-langfuse 0.1.70 → 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.70",
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);",
@@ -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);