@librechat/agents 3.2.43 → 3.2.45

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.
Files changed (76) hide show
  1. package/dist/cjs/graphs/Graph.cjs +17 -8
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/instrumentation.cjs +20 -21
  4. package/dist/cjs/instrumentation.cjs.map +1 -1
  5. package/dist/cjs/langfuse.cjs +65 -13
  6. package/dist/cjs/langfuse.cjs.map +1 -1
  7. package/dist/cjs/langfuseConfig.cjs +106 -0
  8. package/dist/cjs/langfuseConfig.cjs.map +1 -0
  9. package/dist/cjs/langfuseRuntimeContext.cjs +48 -0
  10. package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -0
  11. package/dist/cjs/langfuseRuntimeScope.cjs +57 -0
  12. package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -0
  13. package/dist/cjs/langfuseToolOutputTracing.cjs +14 -119
  14. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  15. package/dist/cjs/main.cjs +1 -0
  16. package/dist/cjs/messages/reducer.cjs +15 -2
  17. package/dist/cjs/messages/reducer.cjs.map +1 -1
  18. package/dist/cjs/run.cjs +25 -12
  19. package/dist/cjs/run.cjs.map +1 -1
  20. package/dist/cjs/tools/ToolNode.cjs +5 -2
  21. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  22. package/dist/cjs/utils/misc.cjs +17 -0
  23. package/dist/cjs/utils/misc.cjs.map +1 -1
  24. package/dist/esm/graphs/Graph.mjs +16 -7
  25. package/dist/esm/graphs/Graph.mjs.map +1 -1
  26. package/dist/esm/instrumentation.mjs +22 -22
  27. package/dist/esm/instrumentation.mjs.map +1 -1
  28. package/dist/esm/langfuse.mjs +65 -14
  29. package/dist/esm/langfuse.mjs.map +1 -1
  30. package/dist/esm/langfuseConfig.mjs +102 -0
  31. package/dist/esm/langfuseConfig.mjs.map +1 -0
  32. package/dist/esm/langfuseRuntimeContext.mjs +44 -0
  33. package/dist/esm/langfuseRuntimeContext.mjs.map +1 -0
  34. package/dist/esm/langfuseRuntimeScope.mjs +53 -0
  35. package/dist/esm/langfuseRuntimeScope.mjs.map +1 -0
  36. package/dist/esm/langfuseToolOutputTracing.mjs +13 -115
  37. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  38. package/dist/esm/main.mjs +2 -2
  39. package/dist/esm/messages/reducer.mjs +15 -2
  40. package/dist/esm/messages/reducer.mjs.map +1 -1
  41. package/dist/esm/run.mjs +21 -8
  42. package/dist/esm/run.mjs.map +1 -1
  43. package/dist/esm/tools/ToolNode.mjs +5 -2
  44. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  45. package/dist/esm/utils/misc.mjs +17 -1
  46. package/dist/esm/utils/misc.mjs.map +1 -1
  47. package/dist/types/instrumentation.d.ts +0 -1
  48. package/dist/types/langfuse.d.ts +5 -1
  49. package/dist/types/langfuseConfig.d.ts +7 -0
  50. package/dist/types/langfuseRuntimeContext.d.ts +26 -0
  51. package/dist/types/langfuseRuntimeScope.d.ts +17 -0
  52. package/dist/types/langfuseToolOutputTracing.d.ts +4 -11
  53. package/dist/types/types/graph.d.ts +6 -0
  54. package/dist/types/utils/misc.d.ts +1 -0
  55. package/package.json +1 -1
  56. package/src/graphs/Graph.ts +20 -13
  57. package/src/instrumentation.ts +33 -29
  58. package/src/langfuse.ts +114 -7
  59. package/src/langfuseConfig.ts +214 -0
  60. package/src/langfuseRuntimeContext.ts +93 -0
  61. package/src/langfuseRuntimeScope.ts +135 -0
  62. package/src/langfuseToolOutputTracing.ts +25 -263
  63. package/src/messages/reducer.spec.ts +32 -0
  64. package/src/messages/reducer.ts +23 -7
  65. package/src/run.ts +44 -34
  66. package/src/specs/deterministic-trace-id.test.ts +4 -11
  67. package/src/specs/langfuse-callbacks.test.ts +278 -22
  68. package/src/specs/langfuse-config.test.ts +10 -3
  69. package/src/specs/langfuse-instrumentation.test.ts +32 -12
  70. package/src/specs/langfuse-routing.integration.test.ts +543 -0
  71. package/src/specs/langfuse-runtime-context.test.ts +92 -0
  72. package/src/specs/langfuse-tool-output-tracing.test.ts +353 -5
  73. package/src/tools/ToolNode.ts +10 -5
  74. package/src/tools/__tests__/ToolNode.langfuse.test.ts +21 -11
  75. package/src/types/graph.ts +9 -0
  76. package/src/utils/misc.ts +18 -0
@@ -1,15 +1,31 @@
1
+ import { context } from '@opentelemetry/api';
1
2
  import { LangfuseOtelSpanAttributes } from '@langfuse/tracing';
2
3
  import { AIMessage, ToolMessage, HumanMessage } from '@langchain/core/messages';
3
4
  import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
4
5
  import type { BaseMessage } from '@langchain/core/messages';
6
+ import type { Context } from '@opentelemetry/api';
5
7
  import type { TPayload } from '@/types';
6
8
  import {
7
9
  LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
10
+ classifyLangfuseToolNodeSpan,
8
11
  redactLangfuseSpanToolOutputs,
9
- resolveLangfuseConfig,
10
12
  shouldTraceToolNodeForLangfuse,
11
- type ResolvedLangfuseToolOutputTracingConfig,
12
13
  } from '@/langfuseToolOutputTracing';
14
+ import {
15
+ resolveLangfuseConfigForSpan,
16
+ resolveLangfuseRuntimeScope,
17
+ resolveToolOutputTracingConfigForSpan,
18
+ withLangfuseRuntimeScope,
19
+ } from '@/langfuseRuntimeScope';
20
+ import {
21
+ runWithLangfuseRuntimeContext,
22
+ type ResolvedLangfuseToolOutputTracingConfig,
23
+ } from '@/langfuseRuntimeContext';
24
+ import {
25
+ resolveLangfuseConfig,
26
+ resolveToolOutputTracingConfig,
27
+ } from '@/langfuseConfig';
28
+ import { ensureOpenTelemetryContextManager } from '@/instrumentation';
13
29
  import { formatAgentMessages } from '@/messages/format';
14
30
  import { ContentTypes } from '@/common';
15
31
 
@@ -194,6 +210,20 @@ describe('Langfuse tool output tracing redaction', () => {
194
210
  );
195
211
  });
196
212
 
213
+ it('classifies LangGraph tool-node spans without requiring redaction config', () => {
214
+ const span = createSpan('tool_batch', {
215
+ [LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'span',
216
+ [`${LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.langgraph_node`]:
217
+ 'tools=agent_1',
218
+ });
219
+
220
+ classifyLangfuseToolNodeSpan(span);
221
+
222
+ expect(span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]).toBe(
223
+ 'tool'
224
+ );
225
+ });
226
+
197
227
  it('does not reclassify non-tool LangGraph spans', () => {
198
228
  const span = createSpan('agent=agent_1', {
199
229
  [LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'span',
@@ -587,7 +617,12 @@ describe('Langfuse tool output tracing redaction', () => {
587
617
  secretKey: 'sk-run',
588
618
  baseUrl: 'https://langfuse.test',
589
619
  metadata: { tenantId: 'tenant-run' },
620
+ librechatTraceAttributes: {
621
+ 'librechat.langfuse.destination': 'eu',
622
+ 'librechat.langfuse.tenant_export.enabled': true,
623
+ },
590
624
  tags: ['tenant:tenant-run', 'shared'],
625
+ deterministicTraceId: true,
591
626
  toolNodeTracing: { enabled: true },
592
627
  toolOutputTracing: {
593
628
  enabled: true,
@@ -595,7 +630,13 @@ describe('Langfuse tool output tracing redaction', () => {
595
630
  },
596
631
  },
597
632
  {
633
+ publicKey: 'pk-agent',
634
+ secretKey: 'sk-agent',
635
+ baseUrl: 'https://langfuse.agent',
598
636
  metadata: { agentId: 'agent-1' },
637
+ librechatTraceAttributes: {
638
+ 'librechat.langfuse.public_key': 'pk-agent',
639
+ },
599
640
  tags: ['shared', 'agent:agent-1'],
600
641
  toolOutputTracing: {
601
642
  enabled: false,
@@ -606,11 +647,17 @@ describe('Langfuse tool output tracing redaction', () => {
606
647
 
607
648
  expect(resolved).toMatchObject({
608
649
  enabled: true,
609
- publicKey: 'pk-run',
610
- secretKey: 'sk-run',
611
- baseUrl: 'https://langfuse.test',
650
+ publicKey: 'pk-agent',
651
+ secretKey: 'sk-agent',
652
+ baseUrl: 'https://langfuse.agent',
612
653
  metadata: { tenantId: 'tenant-run', agentId: 'agent-1' },
654
+ librechatTraceAttributes: {
655
+ 'librechat.langfuse.destination': 'eu',
656
+ 'librechat.langfuse.tenant_export.enabled': true,
657
+ 'librechat.langfuse.public_key': 'pk-agent',
658
+ },
613
659
  tags: ['tenant:tenant-run', 'shared', 'agent:agent-1'],
660
+ deterministicTraceId: true,
614
661
  toolNodeTracing: { enabled: true },
615
662
  toolOutputTracing: {
616
663
  enabled: false,
@@ -619,4 +666,305 @@ describe('Langfuse tool output tracing redaction', () => {
619
666
  },
620
667
  });
621
668
  });
669
+
670
+ it('inherits deterministic trace ids when tenant config only supplies connection settings', () => {
671
+ const resolved = resolveLangfuseConfig(
672
+ {
673
+ deterministicTraceId: true,
674
+ },
675
+ {
676
+ publicKey: 'pk-tenant',
677
+ secretKey: 'sk-tenant',
678
+ baseUrl: 'https://langfuse.tenant',
679
+ }
680
+ );
681
+
682
+ expect(resolved).toMatchObject({
683
+ publicKey: 'pk-tenant',
684
+ secretKey: 'sk-tenant',
685
+ baseUrl: 'https://langfuse.tenant',
686
+ deterministicTraceId: true,
687
+ });
688
+ });
689
+
690
+ it('inherits application-level redaction when tenant config does not explicitly opt out', () => {
691
+ process.env.LANGFUSE_REDACT_TOOL_OUTPUTS = 'true';
692
+ process.env.LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT = '[app redacted]';
693
+
694
+ const config = resolveToolOutputTracingConfig(
695
+ {
696
+ publicKey: 'pk-tenant',
697
+ secretKey: 'sk-tenant',
698
+ baseUrl: 'https://langfuse.tenant',
699
+ toolOutputTracing: {
700
+ redactionText: '[tenant redacted]',
701
+ },
702
+ },
703
+ undefined
704
+ );
705
+
706
+ expect(config).toMatchObject({
707
+ enabled: false,
708
+ redactionText: '[tenant redacted]',
709
+ });
710
+ });
711
+
712
+ it('keeps application redacted tool names when tenant adds its own names', () => {
713
+ process.env.LANGFUSE_REDACT_TOOL_OUTPUT_NAMES = 'run_sql';
714
+
715
+ const config = resolveToolOutputTracingConfig(
716
+ {
717
+ toolOutputTracing: {
718
+ redactedToolNames: ['execute_sql'],
719
+ },
720
+ },
721
+ {
722
+ toolOutputTracing: {
723
+ redactedToolNames: ['web_search'],
724
+ },
725
+ }
726
+ );
727
+
728
+ expect([...config.redactedToolNames].sort()).toEqual([
729
+ 'execute_sql',
730
+ 'run_sql',
731
+ 'web_search',
732
+ ]);
733
+ });
734
+
735
+ it('keeps application partial redaction matching when tenant adds exact redaction config', () => {
736
+ process.env.LANGFUSE_REDACT_TOOL_OUTPUT_NAME_MATCH_MODE = 'partial';
737
+
738
+ const config = resolveToolOutputTracingConfig(
739
+ {
740
+ toolOutputTracing: {
741
+ redactedToolNames: ['execute'],
742
+ },
743
+ },
744
+ {
745
+ toolOutputTracing: {
746
+ redactedToolNameMatchMode: 'exact',
747
+ redactedToolNames: ['web_search'],
748
+ },
749
+ }
750
+ );
751
+
752
+ expect(config.redactedToolNameMatchMode).toBe('partial');
753
+
754
+ const span = createSpan('execute_sql', {
755
+ [LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'tool',
756
+ [LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: 'secret rows',
757
+ });
758
+
759
+ redactLangfuseSpanToolOutputs(span, config);
760
+
761
+ expect(span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]).toBe(
762
+ LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
763
+ );
764
+ });
765
+
766
+ it('lets tenant explicitly opt out of application-level redact-all outputs', () => {
767
+ process.env.LANGFUSE_REDACT_TOOL_OUTPUTS = 'true';
768
+
769
+ const config = resolveToolOutputTracingConfig(
770
+ {
771
+ publicKey: 'pk-tenant',
772
+ secretKey: 'sk-tenant',
773
+ toolOutputTracing: {
774
+ enabled: true,
775
+ },
776
+ },
777
+ undefined
778
+ );
779
+
780
+ expect(config.enabled).toBe(true);
781
+ expect(config.redactedToolNames.size).toBe(0);
782
+ });
783
+
784
+ it('applies application-level redaction through tenant runtime scope unless tenant opts out', () => {
785
+ ensureOpenTelemetryContextManager();
786
+ process.env.LANGFUSE_REDACT_TOOL_OUTPUTS = 'true';
787
+ process.env.LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT = '[app redacted]';
788
+ let capturedContext: Context | undefined;
789
+
790
+ withLangfuseRuntimeScope(
791
+ resolveLangfuseRuntimeScope({
792
+ runLangfuse: {
793
+ publicKey: 'pk-tenant',
794
+ secretKey: 'sk-tenant',
795
+ baseUrl: 'https://langfuse.tenant',
796
+ },
797
+ }),
798
+ () => {
799
+ capturedContext = context.active();
800
+ }
801
+ );
802
+
803
+ expect(capturedContext).toBeDefined();
804
+ const config = resolveToolOutputTracingConfigForSpan(capturedContext!);
805
+ expect(config).toMatchObject({
806
+ enabled: false,
807
+ redactionText: '[app redacted]',
808
+ });
809
+
810
+ const span = createSpan('execute_sql', {
811
+ [LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'tool',
812
+ [LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: 'tenant secret rows',
813
+ });
814
+
815
+ redactLangfuseSpanToolOutputs(span, config!);
816
+
817
+ expect(span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]).toBe(
818
+ '[app redacted]'
819
+ );
820
+ });
821
+
822
+ it('keeps OTEL context fallback for spans outside callback runtime scope', () => {
823
+ ensureOpenTelemetryContextManager();
824
+ const langfuse = {
825
+ publicKey: 'pk-context',
826
+ secretKey: 'sk-context',
827
+ baseUrl: 'https://langfuse.context',
828
+ };
829
+ let capturedContext: Context | undefined;
830
+
831
+ withLangfuseRuntimeScope({ langfuse }, () => {
832
+ capturedContext = context.active();
833
+ });
834
+
835
+ expect(capturedContext).toBeDefined();
836
+ expect(resolveLangfuseConfigForSpan(capturedContext!)).toBe(langfuse);
837
+ });
838
+
839
+ it('keeps OTEL tool-output fallback for spans outside callback runtime scope', () => {
840
+ ensureOpenTelemetryContextManager();
841
+ let capturedContext: Context | undefined;
842
+
843
+ withLangfuseRuntimeScope(
844
+ { toolOutputTracing: createConfig({ enabled: false }) },
845
+ () => {
846
+ capturedContext = context.active();
847
+ }
848
+ );
849
+
850
+ expect(capturedContext).toBeDefined();
851
+ expect(
852
+ resolveToolOutputTracingConfigForSpan(capturedContext!)
853
+ ).toMatchObject({
854
+ enabled: false,
855
+ });
856
+ });
857
+
858
+ it('honors env-only tool-output redaction in runtime scope', () => {
859
+ ensureOpenTelemetryContextManager();
860
+ process.env.LANGFUSE_REDACT_TOOL_OUTPUTS = 'true';
861
+ let capturedContext: Context | undefined;
862
+
863
+ withLangfuseRuntimeScope(resolveLangfuseRuntimeScope({}), () => {
864
+ capturedContext = context.active();
865
+ });
866
+
867
+ expect(capturedContext).toBeDefined();
868
+ const config = resolveToolOutputTracingConfigForSpan(capturedContext!);
869
+ expect(config).toMatchObject({
870
+ enabled: false,
871
+ redactedToolNameMatchMode: 'exact',
872
+ redactionText: LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
873
+ });
874
+ expect(config?.redactedToolNames.size).toBe(0);
875
+ });
876
+
877
+ it('applies agent tool-output redaction override through runtime scope', () => {
878
+ ensureOpenTelemetryContextManager();
879
+ let capturedContext: Context | undefined;
880
+
881
+ withLangfuseRuntimeScope(
882
+ resolveLangfuseRuntimeScope({
883
+ runLangfuse: {
884
+ toolOutputTracing: {
885
+ enabled: true,
886
+ redactionText: '[agent redacted]',
887
+ },
888
+ },
889
+ langfuseOverlay: {
890
+ toolOutputTracing: {
891
+ enabled: false,
892
+ },
893
+ },
894
+ }),
895
+ () => {
896
+ capturedContext = context.active();
897
+ }
898
+ );
899
+
900
+ expect(capturedContext).toBeDefined();
901
+ const config = resolveToolOutputTracingConfigForSpan(capturedContext!);
902
+ expect(config).toMatchObject({
903
+ enabled: false,
904
+ redactionText: '[agent redacted]',
905
+ });
906
+
907
+ const span = createSpan('execute_sql', {
908
+ [LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'tool',
909
+ [LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: 'secret rows',
910
+ });
911
+
912
+ redactLangfuseSpanToolOutputs(span, config!);
913
+
914
+ expect(span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]).toBe(
915
+ '[agent redacted]'
916
+ );
917
+ });
918
+
919
+ it('prefers ALS runtime tenant config over OTEL fallback config', () => {
920
+ ensureOpenTelemetryContextManager();
921
+ const otelLangfuse = {
922
+ publicKey: 'pk-otel',
923
+ secretKey: 'sk-otel',
924
+ baseUrl: 'https://langfuse.otel',
925
+ };
926
+ const runtimeLangfuse = {
927
+ publicKey: 'pk-runtime',
928
+ secretKey: 'sk-runtime',
929
+ baseUrl: 'https://langfuse.runtime',
930
+ };
931
+ let capturedContext: Context | undefined;
932
+
933
+ withLangfuseRuntimeScope({ langfuse: otelLangfuse }, () => {
934
+ capturedContext = context.active();
935
+ });
936
+
937
+ runWithLangfuseRuntimeContext({ langfuse: runtimeLangfuse }, () => {
938
+ expect(resolveLangfuseConfigForSpan(capturedContext!)).toBe(
939
+ runtimeLangfuse
940
+ );
941
+ });
942
+ });
943
+
944
+ it('prefers ALS runtime tool-output config over OTEL fallback config', () => {
945
+ ensureOpenTelemetryContextManager();
946
+ const runtimeToolOutputTracing = {
947
+ enabled: false,
948
+ redactedToolNames: new Set(['runtime_tool']),
949
+ redactedToolNameMatchMode: 'exact' as const,
950
+ redactionText: '[runtime]',
951
+ };
952
+ let capturedContext: Context | undefined;
953
+
954
+ withLangfuseRuntimeScope(
955
+ { toolOutputTracing: createConfig({ enabled: true }) },
956
+ () => {
957
+ capturedContext = context.active();
958
+ }
959
+ );
960
+
961
+ runWithLangfuseRuntimeContext(
962
+ { toolOutputTracing: runtimeToolOutputTracing },
963
+ () => {
964
+ expect(resolveToolOutputTracingConfigForSpan(capturedContext!)).toBe(
965
+ runtimeToolOutputTracing
966
+ );
967
+ }
968
+ );
969
+ });
622
970
  });
@@ -33,6 +33,10 @@ import type {
33
33
  PostToolBatchEntry,
34
34
  } from '@/hooks';
35
35
  import type * as t from '@/types';
36
+ import {
37
+ resolveLangfuseRuntimeScope,
38
+ withLangfuseRuntimeScope,
39
+ } from '@/langfuseRuntimeScope';
36
40
  import {
37
41
  buildReferenceKey,
38
42
  ToolOutputReferenceRegistry,
@@ -49,7 +53,6 @@ import {
49
53
  resolveLocalToolRegistry,
50
54
  resolveLocalExecutionTools,
51
55
  } from '@/tools/local';
52
- import { withLangfuseToolOutputTracingConfig } from '@/langfuseToolOutputTracing';
53
56
  import { stripCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';
54
57
  import { Constants, GraphEvents, CODE_EXECUTION_TOOLS } from '@/common';
55
58
  import { toLangChainContent } from '@/messages/langchain';
@@ -577,10 +580,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
577
580
  options?: Partial<RunnableConfig>
578
581
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
579
582
  ): Promise<any> {
580
- return withLangfuseToolOutputTracingConfig(
581
- this.runLangfuse,
582
- () => super.invoke(input, options),
583
- this.agentLangfuse
583
+ return withLangfuseRuntimeScope(
584
+ resolveLangfuseRuntimeScope({
585
+ runLangfuse: this.runLangfuse,
586
+ langfuseOverlay: this.agentLangfuse,
587
+ }),
588
+ () => super.invoke(input, options)
584
589
  );
585
590
  }
586
591
 
@@ -1,18 +1,25 @@
1
- const mockWithLangfuseToolOutputTracingConfig = jest.fn(
2
- (_runLangfuse: unknown, action: () => unknown, _agentLangfuse: unknown) =>
3
- action()
1
+ const mockWithLangfuseRuntimeScope = jest.fn(
2
+ (_scope: unknown, action: () => unknown) => action()
4
3
  );
5
-
6
- jest.mock('@/langfuseToolOutputTracing', () => ({
7
- ...jest.requireActual('@/langfuseToolOutputTracing'),
8
- withLangfuseToolOutputTracingConfig: mockWithLangfuseToolOutputTracingConfig,
4
+ const mockResolvedScope = {
5
+ langfuse: {
6
+ toolOutputTracing: { enabled: false },
7
+ },
8
+ };
9
+ const mockResolveLangfuseRuntimeScope = jest.fn(() => mockResolvedScope);
10
+
11
+ jest.mock('@/langfuseRuntimeScope', () => ({
12
+ ...jest.requireActual('@/langfuseRuntimeScope'),
13
+ resolveLangfuseRuntimeScope: mockResolveLangfuseRuntimeScope,
14
+ withLangfuseRuntimeScope: mockWithLangfuseRuntimeScope,
9
15
  }));
10
16
 
11
17
  import { ToolNode } from '../ToolNode';
12
18
 
13
19
  describe('ToolNode Langfuse redaction context', () => {
14
20
  beforeEach(() => {
15
- mockWithLangfuseToolOutputTracingConfig.mockClear();
21
+ mockWithLangfuseRuntimeScope.mockClear();
22
+ mockResolveLangfuseRuntimeScope.mockClear();
16
23
  });
17
24
 
18
25
  it('uses a stable default run name for tracing', () => {
@@ -38,10 +45,13 @@ describe('ToolNode Langfuse redaction context', () => {
38
45
  'ToolNode only accepts AIMessages'
39
46
  );
40
47
 
41
- expect(mockWithLangfuseToolOutputTracingConfig).toHaveBeenCalledWith(
48
+ expect(mockResolveLangfuseRuntimeScope).toHaveBeenCalledWith({
42
49
  runLangfuse,
43
- expect.any(Function),
44
- agentLangfuse
50
+ langfuseOverlay: agentLangfuse,
51
+ });
52
+ expect(mockWithLangfuseRuntimeScope).toHaveBeenCalledWith(
53
+ mockResolvedScope,
54
+ expect.any(Function)
45
55
  );
46
56
  });
47
57
  });
@@ -537,6 +537,15 @@ export interface LangfuseConfig {
537
537
  secretKey?: string;
538
538
  baseUrl?: string;
539
539
  metadata?: Record<string, string | number | boolean | null | undefined>;
540
+ /**
541
+ * Internal OTLP span attributes to attach to Langfuse observations before
542
+ * export. Intended for collector-side routing/filtering; strip these in the
543
+ * collector before forwarding spans to Langfuse.
544
+ */
545
+ librechatTraceAttributes?: Record<
546
+ string,
547
+ string | number | boolean | null | undefined
548
+ >;
540
549
  tags?: string[];
541
550
  toolNodeTracing?: LangfuseToolNodeTracingConfig;
542
551
  toolOutputTracing?: LangfuseToolOutputTracingConfig;
package/src/utils/misc.ts CHANGED
@@ -2,6 +2,24 @@ export function isPresent(value: string | null | undefined): value is string {
2
2
  return value != null && value !== '';
3
3
  }
4
4
 
5
+ export function parseBooleanEnv(
6
+ value: string | undefined
7
+ ): boolean | undefined {
8
+ if (value == null) {
9
+ return undefined;
10
+ }
11
+
12
+ const normalized = value.trim().toLowerCase();
13
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) {
14
+ return true;
15
+ }
16
+ if (['0', 'false', 'no', 'off'].includes(normalized)) {
17
+ return false;
18
+ }
19
+
20
+ return undefined;
21
+ }
22
+
5
23
  /**
6
24
  * Unescapes a c-escaped string
7
25
  * @param str The string to unescape