@librechat/agents 3.2.44 → 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.
- package/dist/cjs/graphs/Graph.cjs +17 -8
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +20 -21
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +65 -13
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/langfuseConfig.cjs +106 -0
- package/dist/cjs/langfuseConfig.cjs.map +1 -0
- package/dist/cjs/langfuseRuntimeContext.cjs +48 -0
- package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -0
- package/dist/cjs/langfuseRuntimeScope.cjs +57 -0
- package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -0
- package/dist/cjs/langfuseToolOutputTracing.cjs +14 -119
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/main.cjs +1 -0
- package/dist/cjs/run.cjs +25 -12
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +5 -2
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/utils/misc.cjs +17 -0
- package/dist/cjs/utils/misc.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +16 -7
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +22 -22
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +65 -14
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/langfuseConfig.mjs +102 -0
- package/dist/esm/langfuseConfig.mjs.map +1 -0
- package/dist/esm/langfuseRuntimeContext.mjs +44 -0
- package/dist/esm/langfuseRuntimeContext.mjs.map +1 -0
- package/dist/esm/langfuseRuntimeScope.mjs +53 -0
- package/dist/esm/langfuseRuntimeScope.mjs.map +1 -0
- package/dist/esm/langfuseToolOutputTracing.mjs +13 -115
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -2
- package/dist/esm/run.mjs +21 -8
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +5 -2
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/utils/misc.mjs +17 -1
- package/dist/esm/utils/misc.mjs.map +1 -1
- package/dist/types/instrumentation.d.ts +0 -1
- package/dist/types/langfuse.d.ts +5 -1
- package/dist/types/langfuseConfig.d.ts +7 -0
- package/dist/types/langfuseRuntimeContext.d.ts +26 -0
- package/dist/types/langfuseRuntimeScope.d.ts +17 -0
- package/dist/types/langfuseToolOutputTracing.d.ts +4 -11
- package/dist/types/types/graph.d.ts +6 -0
- package/dist/types/utils/misc.d.ts +1 -0
- package/package.json +1 -1
- package/src/graphs/Graph.ts +20 -13
- package/src/instrumentation.ts +33 -29
- package/src/langfuse.ts +114 -7
- package/src/langfuseConfig.ts +214 -0
- package/src/langfuseRuntimeContext.ts +93 -0
- package/src/langfuseRuntimeScope.ts +135 -0
- package/src/langfuseToolOutputTracing.ts +25 -263
- package/src/run.ts +44 -34
- package/src/specs/deterministic-trace-id.test.ts +4 -11
- package/src/specs/langfuse-callbacks.test.ts +278 -22
- package/src/specs/langfuse-config.test.ts +10 -3
- package/src/specs/langfuse-instrumentation.test.ts +32 -12
- package/src/specs/langfuse-routing.integration.test.ts +543 -0
- package/src/specs/langfuse-runtime-context.test.ts +92 -0
- package/src/specs/langfuse-tool-output-tracing.test.ts +353 -5
- package/src/tools/ToolNode.ts +10 -5
- package/src/tools/__tests__/ToolNode.langfuse.test.ts +21 -11
- package/src/types/graph.ts +9 -0
- 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-
|
|
610
|
-
secretKey: 'sk-
|
|
611
|
-
baseUrl: 'https://langfuse.
|
|
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
|
});
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -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
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
|
2
|
-
(
|
|
3
|
-
action()
|
|
1
|
+
const mockWithLangfuseRuntimeScope = jest.fn(
|
|
2
|
+
(_scope: unknown, action: () => unknown) => action()
|
|
4
3
|
);
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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(
|
|
48
|
+
expect(mockResolveLangfuseRuntimeScope).toHaveBeenCalledWith({
|
|
42
49
|
runLangfuse,
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
langfuseOverlay: agentLangfuse,
|
|
51
|
+
});
|
|
52
|
+
expect(mockWithLangfuseRuntimeScope).toHaveBeenCalledWith(
|
|
53
|
+
mockResolvedScope,
|
|
54
|
+
expect.any(Function)
|
|
45
55
|
);
|
|
46
56
|
});
|
|
47
57
|
});
|
package/src/types/graph.ts
CHANGED
|
@@ -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
|