@librechat/agents 3.2.44 → 3.2.46

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 (77) 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/format.cjs +1 -1
  17. package/dist/cjs/messages/format.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/format.mjs +1 -1
  40. package/dist/esm/messages/format.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/messages/format.d.ts +4 -0
  54. package/dist/types/types/graph.d.ts +6 -0
  55. package/dist/types/utils/misc.d.ts +1 -0
  56. package/package.json +1 -1
  57. package/src/graphs/Graph.ts +20 -13
  58. package/src/instrumentation.ts +33 -29
  59. package/src/langfuse.ts +114 -7
  60. package/src/langfuseConfig.ts +214 -0
  61. package/src/langfuseRuntimeContext.ts +93 -0
  62. package/src/langfuseRuntimeScope.ts +135 -0
  63. package/src/langfuseToolOutputTracing.ts +25 -263
  64. package/src/messages/format.ts +6 -1
  65. package/src/messages/formatAgentMessages.test.ts +76 -0
  66. package/src/run.ts +44 -34
  67. package/src/specs/deterministic-trace-id.test.ts +4 -11
  68. package/src/specs/langfuse-callbacks.test.ts +278 -22
  69. package/src/specs/langfuse-config.test.ts +10 -3
  70. package/src/specs/langfuse-instrumentation.test.ts +32 -12
  71. package/src/specs/langfuse-routing.integration.test.ts +543 -0
  72. package/src/specs/langfuse-runtime-context.test.ts +92 -0
  73. package/src/specs/langfuse-tool-output-tracing.test.ts +353 -5
  74. package/src/tools/ToolNode.ts +10 -5
  75. package/src/tools/__tests__/ToolNode.langfuse.test.ts +21 -11
  76. package/src/types/graph.ts +9 -0
  77. package/src/utils/misc.ts +18 -0
@@ -2,6 +2,22 @@
2
2
  function isPresent(value) {
3
3
  return value != null && value !== "";
4
4
  }
5
+ function parseBooleanEnv(value) {
6
+ if (value == null) return;
7
+ const normalized = value.trim().toLowerCase();
8
+ if ([
9
+ "1",
10
+ "true",
11
+ "yes",
12
+ "on"
13
+ ].includes(normalized)) return true;
14
+ if ([
15
+ "0",
16
+ "false",
17
+ "no",
18
+ "off"
19
+ ].includes(normalized)) return false;
20
+ }
5
21
  /**
6
22
  * Unescapes a c-escaped string
7
23
  * @param str The string to unescape
@@ -34,6 +50,6 @@ function unescapeObject(obj, key) {
34
50
  return obj;
35
51
  }
36
52
  //#endregion
37
- export { isPresent, unescapeObject };
53
+ export { isPresent, parseBooleanEnv, unescapeObject };
38
54
 
39
55
  //# sourceMappingURL=misc.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"misc.mjs","names":[],"sources":["../../../src/utils/misc.ts"],"sourcesContent":["export function isPresent(value: string | null | undefined): value is string {\n return value != null && value !== '';\n}\n\n/**\n * Unescapes a c-escaped string\n * @param str The string to unescape\n * @returns The unescaped string\n */\nconst unescapeString = (string: string): string =>\n string.replace(/\\\\(.)/g, (_, char) => {\n switch (char) {\n case 'n':\n return '\\n';\n case 't':\n return '\\t';\n case 'r':\n return '\\r';\n case '\"':\n return '\"';\n case '\\'':\n return '\\'';\n case '\\\\':\n return '\\\\';\n default:\n return char;\n }\n });\n\n/**\n * Recursively unescapes all string values in an object\n * @param obj The object to unescape\n * @returns The unescaped object\n */\nexport function unescapeObject(obj: unknown, key?: string): unknown {\n if (typeof obj === 'string') {\n let unescaped = unescapeString(obj);\n if (key === 'filePath' && unescaped.match(/^\"(.+)\"$/)) {\n unescaped = unescaped.substring(1, unescaped.length - 1);\n }\n return unescaped;\n }\n if (Array.isArray(obj)) {\n return obj.map((value) =>\n unescapeObject(value, key === 'contextPaths' ? 'filePath' : '')\n );\n }\n if (typeof obj === 'object' && obj !== null) {\n return Object.fromEntries(\n Object.entries(obj).map(([key, value]) => [\n key,\n unescapeObject(value, key),\n ])\n );\n }\n return obj;\n}\n"],"mappings":";AAAA,SAAgB,UAAU,OAAmD;CAC3E,OAAO,SAAS,QAAQ,UAAU;AACpC;;;;;;AAOA,MAAM,kBAAkB,WACtB,OAAO,QAAQ,WAAW,GAAG,SAAS;CACpC,QAAQ,MAAR;EACA,KAAK,KACH,OAAO;EACT,KAAK,KACH,OAAO;EACT,KAAK,KACH,OAAO;EACT,KAAK,MACH,OAAO;EACT,KAAK,KACH,OAAO;EACT,KAAK,MACH,OAAO;EACT,SACE,OAAO;CACT;AACF,CAAC;;;;;;AAOH,SAAgB,eAAe,KAAc,KAAuB;CAClE,IAAI,OAAO,QAAQ,UAAU;EAC3B,IAAI,YAAY,eAAe,GAAG;EAClC,IAAI,QAAQ,cAAc,UAAU,MAAM,UAAU,GAClD,YAAY,UAAU,UAAU,GAAG,UAAU,SAAS,CAAC;EAEzD,OAAO;CACT;CACA,IAAI,MAAM,QAAQ,GAAG,GACnB,OAAO,IAAI,KAAK,UACd,eAAe,OAAO,QAAQ,iBAAiB,aAAa,EAAE,CAChE;CAEF,IAAI,OAAO,QAAQ,YAAY,QAAQ,MACrC,OAAO,OAAO,YACZ,OAAO,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CACxC,KACA,eAAe,OAAO,GAAG,CAC3B,CAAC,CACH;CAEF,OAAO;AACT"}
1
+ {"version":3,"file":"misc.mjs","names":[],"sources":["../../../src/utils/misc.ts"],"sourcesContent":["export function isPresent(value: string | null | undefined): value is string {\n return value != null && value !== '';\n}\n\nexport function parseBooleanEnv(\n value: string | undefined\n): boolean | undefined {\n if (value == null) {\n return undefined;\n }\n\n const normalized = value.trim().toLowerCase();\n if (['1', 'true', 'yes', 'on'].includes(normalized)) {\n return true;\n }\n if (['0', 'false', 'no', 'off'].includes(normalized)) {\n return false;\n }\n\n return undefined;\n}\n\n/**\n * Unescapes a c-escaped string\n * @param str The string to unescape\n * @returns The unescaped string\n */\nconst unescapeString = (string: string): string =>\n string.replace(/\\\\(.)/g, (_, char) => {\n switch (char) {\n case 'n':\n return '\\n';\n case 't':\n return '\\t';\n case 'r':\n return '\\r';\n case '\"':\n return '\"';\n case '\\'':\n return '\\'';\n case '\\\\':\n return '\\\\';\n default:\n return char;\n }\n });\n\n/**\n * Recursively unescapes all string values in an object\n * @param obj The object to unescape\n * @returns The unescaped object\n */\nexport function unescapeObject(obj: unknown, key?: string): unknown {\n if (typeof obj === 'string') {\n let unescaped = unescapeString(obj);\n if (key === 'filePath' && unescaped.match(/^\"(.+)\"$/)) {\n unescaped = unescaped.substring(1, unescaped.length - 1);\n }\n return unescaped;\n }\n if (Array.isArray(obj)) {\n return obj.map((value) =>\n unescapeObject(value, key === 'contextPaths' ? 'filePath' : '')\n );\n }\n if (typeof obj === 'object' && obj !== null) {\n return Object.fromEntries(\n Object.entries(obj).map(([key, value]) => [\n key,\n unescapeObject(value, key),\n ])\n );\n }\n return obj;\n}\n"],"mappings":";AAAA,SAAgB,UAAU,OAAmD;CAC3E,OAAO,SAAS,QAAQ,UAAU;AACpC;AAEA,SAAgB,gBACd,OACqB;CACrB,IAAI,SAAS,MACX;CAGF,MAAM,aAAa,MAAM,KAAK,CAAC,CAAC,YAAY;CAC5C,IAAI;EAAC;EAAK;EAAQ;EAAO;CAAI,CAAC,CAAC,SAAS,UAAU,GAChD,OAAO;CAET,IAAI;EAAC;EAAK;EAAS;EAAM;CAAK,CAAC,CAAC,SAAS,UAAU,GACjD,OAAO;AAIX;;;;;;AAOA,MAAM,kBAAkB,WACtB,OAAO,QAAQ,WAAW,GAAG,SAAS;CACpC,QAAQ,MAAR;EACA,KAAK,KACH,OAAO;EACT,KAAK,KACH,OAAO;EACT,KAAK,KACH,OAAO;EACT,KAAK,MACH,OAAO;EACT,KAAK,KACH,OAAO;EACT,KAAK,MACH,OAAO;EACT,SACE,OAAO;CACT;AACF,CAAC;;;;;;AAOH,SAAgB,eAAe,KAAc,KAAuB;CAClE,IAAI,OAAO,QAAQ,UAAU;EAC3B,IAAI,YAAY,eAAe,GAAG;EAClC,IAAI,QAAQ,cAAc,UAAU,MAAM,UAAU,GAClD,YAAY,UAAU,UAAU,GAAG,UAAU,SAAS,CAAC;EAEzD,OAAO;CACT;CACA,IAAI,MAAM,QAAQ,GAAG,GACnB,OAAO,IAAI,KAAK,UACd,eAAe,OAAO,QAAQ,iBAAiB,aAAa,EAAE,CAChE;CAEF,IAAI,OAAO,QAAQ,YAAY,QAAQ,MACrC,OAAO,OAAO,YACZ,OAAO,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CACxC,KACA,eAAe,OAAO,GAAG,CAC3B,CAAC,CACH;CAEF,OAAO;AACT"}
@@ -1,6 +1,5 @@
1
1
  import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
2
2
  import type * as t from '@/types';
3
- export declare function runWithTraceIdSeed<T>(seed: string | undefined, fn: () => T): T;
4
3
  export declare function ensureOpenTelemetryContextManager(): void;
5
4
  export declare function initializeLangfuseTracing(langfuse?: t.LangfuseConfig): BasicTracerProvider | undefined;
6
5
  export declare function initializeLangfuseTracingFromEnv(): BasicTracerProvider | undefined;
@@ -1,11 +1,14 @@
1
1
  import { CallbackHandler } from '@langfuse/langchain';
2
2
  import type * as t from '@/types';
3
3
  export type LangfuseTraceMetadata = Record<string, string>;
4
+ export type LangfuseTraceAttributes = Record<string, string | number | boolean>;
5
+ type LangfuseConfigTraceAttributes = NonNullable<t.LangfuseConfig['librechatTraceAttributes']>;
4
6
  type LangfuseHandlerParams = {
5
7
  userId?: string;
6
8
  sessionId?: string;
7
9
  traceMetadata?: LangfuseTraceMetadata;
8
10
  tags?: string[];
11
+ traceIdSeed?: string;
9
12
  };
10
13
  type AgentLangfuseHandlerParams = LangfuseHandlerParams & {
11
14
  langfuse?: t.LangfuseConfig;
@@ -18,6 +21,7 @@ export declare function hasLangfuseConfigCredentials(langfuse?: t.LangfuseConfig
18
21
  secretKey: string;
19
22
  };
20
23
  export declare function isExplicitLangfuseConfig(langfuse?: t.LangfuseConfig): boolean;
24
+ export declare function createLibreChatTraceAttributes(attributes: LangfuseConfigTraceAttributes): LangfuseTraceAttributes;
21
25
  export declare function createLangfuseTraceMetadata({ messageId, parentMessageId, agentId, agentName, }: {
22
26
  messageId?: unknown;
23
27
  parentMessageId?: unknown;
@@ -29,7 +33,7 @@ export declare function hasLangfuseEnvConfig(): boolean;
29
33
  export declare function hasLangfuseEnvCredentials(): boolean;
30
34
  export declare function shouldCreateLangfuseHandler(langfuse?: t.LangfuseConfig): boolean;
31
35
  export declare function createLegacyLangfuseHandler(params: LangfuseHandlerParams): CallbackHandler;
32
- export declare function createLangfuseHandler({ langfuse, userId, sessionId, traceMetadata, tags, }: AgentLangfuseHandlerParams): CallbackHandler | undefined;
36
+ export declare function createLangfuseHandler({ langfuse, userId, sessionId, traceMetadata, tags, traceIdSeed, }: AgentLangfuseHandlerParams): CallbackHandler | undefined;
33
37
  export declare function withLangfuseAttributes<T>(params: LangfuseAttributeParams, action: () => T): T;
34
38
  export declare function hasExplicitLangfuseConfig(contexts: Iterable<{
35
39
  langfuse?: t.LangfuseConfig;
@@ -0,0 +1,7 @@
1
+ import type { ResolvedLangfuseToolOutputTracingConfig } from '@/langfuseRuntimeContext';
2
+ import type * as t from '@/types';
3
+ export declare const LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT = "[tool output redacted]";
4
+ export declare function normalizeToolName(name: string): string;
5
+ export declare function hasToolOutputTracingConfig(runLangfuse?: t.LangfuseConfig, agentLangfuse?: t.LangfuseConfig): boolean;
6
+ export declare function resolveToolOutputTracingConfig(runLangfuse?: t.LangfuseConfig, agentLangfuse?: t.LangfuseConfig): ResolvedLangfuseToolOutputTracingConfig;
7
+ export declare function resolveLangfuseConfig(runLangfuse?: t.LangfuseConfig, agentLangfuse?: t.LangfuseConfig): t.LangfuseConfig | undefined;
@@ -0,0 +1,26 @@
1
+ import type * as t from '@/types';
2
+ export type ResolvedLangfuseToolOutputTracingConfig = {
3
+ enabled: boolean;
4
+ redactedToolNames: Set<string>;
5
+ redactedToolNameMatchMode: 'exact' | 'partial';
6
+ redactionText: string;
7
+ };
8
+ export type LangfuseRuntimeContext = {
9
+ langfuse?: t.LangfuseConfig;
10
+ traceIdSeed?: string;
11
+ toolOutputTracing?: ResolvedLangfuseToolOutputTracingConfig;
12
+ };
13
+ export declare function hasLangfuseRuntimeContextValue(context: LangfuseRuntimeContext): boolean;
14
+ /** sha256(seed) -> first 32 hex chars; matches `@langfuse/tracing` `createTraceId`. */
15
+ export declare function traceIdFromSeed(seed: string): string;
16
+ export declare function getLangfuseRuntimeContext(): LangfuseRuntimeContext | undefined;
17
+ export declare function getLangfuseRuntimeConfig(): t.LangfuseConfig | undefined;
18
+ export declare function getTraceIdSeed(): string | undefined;
19
+ export declare function getLangfuseRuntimeToolOutputTracingConfig(): ResolvedLangfuseToolOutputTracingConfig | undefined;
20
+ /**
21
+ * Runs `fn` with a merged Langfuse runtime context. Undefined fields inherit
22
+ * from the parent scope; callers intentionally cannot clear parent values by
23
+ * passing `undefined`.
24
+ */
25
+ export declare function runWithLangfuseRuntimeContext<T>(context: LangfuseRuntimeContext, fn: () => T): T;
26
+ export declare function runWithTraceIdSeed<T>(seed: string | undefined, fn: () => T): T;
@@ -0,0 +1,17 @@
1
+ import type { Context } from '@opentelemetry/api';
2
+ import type { LangfuseRuntimeContext, ResolvedLangfuseToolOutputTracingConfig } from '@/langfuseRuntimeContext';
3
+ import type * as t from '@/types';
4
+ export type LangfuseRuntimeScope = LangfuseRuntimeContext;
5
+ export type ResolveLangfuseRuntimeScopeParams = {
6
+ runLangfuse?: t.LangfuseConfig;
7
+ langfuseOverlay?: t.LangfuseConfig;
8
+ traceIdSeed?: string;
9
+ };
10
+ export declare function getOtelLangfuseConfig(activeContext: Context): t.LangfuseConfig | undefined;
11
+ export declare function getOtelTraceIdSeed(activeContext: Context): string | undefined;
12
+ export declare function getOtelToolOutputTracingConfig(activeContext: Context): ResolvedLangfuseToolOutputTracingConfig | undefined;
13
+ export declare function resolveLangfuseConfigForSpan(activeContext: Context): t.LangfuseConfig | undefined;
14
+ export declare function resolveTraceIdSeedForSpan(activeContext: Context): string | undefined;
15
+ export declare function resolveToolOutputTracingConfigForSpan(activeContext: Context): ResolvedLangfuseToolOutputTracingConfig | undefined;
16
+ export declare function withLangfuseRuntimeScope<T>(scope: LangfuseRuntimeScope, action: () => T): T;
17
+ export declare function resolveLangfuseRuntimeScope({ runLangfuse, langfuseOverlay, traceIdSeed, }: ResolveLangfuseRuntimeScopeParams): LangfuseRuntimeScope;
@@ -1,20 +1,13 @@
1
1
  import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base';
2
2
  import type { LangfuseSpanProcessorParams } from '@langfuse/otel';
3
- import type { Context } from '@opentelemetry/api';
3
+ import type { ResolvedLangfuseToolOutputTracingConfig } from '@/langfuseRuntimeContext';
4
4
  import type * as t from '@/types';
5
- export declare const LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT = "[tool output redacted]";
6
- export type ResolvedLangfuseToolOutputTracingConfig = {
7
- enabled: boolean;
8
- redactedToolNames: Set<string>;
9
- redactedToolNameMatchMode: 'exact' | 'partial';
10
- redactionText: string;
11
- };
5
+ import { LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT, resolveLangfuseConfig } from '@/langfuseConfig';
6
+ export { LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT, resolveLangfuseConfig };
7
+ export declare function classifyLangfuseToolNodeSpan(span: ReadableSpan): void;
12
8
  export declare function redactLangfuseSpanToolOutputs(span: ReadableSpan, config: ResolvedLangfuseToolOutputTracingConfig): void;
13
- export declare function getContextLangfuseConfig(activeContext: Context): t.LangfuseConfig | undefined;
14
9
  export declare function createLangfuseSpanProcessor(params?: LangfuseSpanProcessorParams, runLangfuse?: t.LangfuseConfig, agentLangfuse?: t.LangfuseConfig): SpanProcessor;
15
- export declare function withLangfuseToolOutputTracingConfig<T>(runLangfuse: t.LangfuseConfig | undefined, action: () => T, agentLangfuse?: t.LangfuseConfig): T;
16
10
  export declare function shouldTraceToolNodeForLangfuse({ runLangfuse, agentLangfuse, }: {
17
11
  runLangfuse?: t.LangfuseConfig;
18
12
  agentLangfuse?: t.LangfuseConfig;
19
13
  }): boolean;
20
- export declare function resolveLangfuseConfig(runLangfuse?: t.LangfuseConfig, agentLangfuse?: t.LangfuseConfig): t.LangfuseConfig | undefined;
@@ -91,6 +91,10 @@ interface LangChainMessage {
91
91
  export declare const formatFromLangChain: (message: LangChainMessage) => Record<string, any>;
92
92
  interface FormatAgentMessagesOptions {
93
93
  provider?: Providers;
94
+ /** Reconstruct hidden `reasoning_content` from `THINK` parts onto prior
95
+ * tool-call messages. Explicit opt-in for OpenAI-compatible endpoints that
96
+ * replay reasoning across turns; defaults to on for DeepSeek thinking-mode. */
97
+ preserveReasoningContent?: boolean;
94
98
  /** Skill names already primed fresh this turn (manual/always-apply). Their
95
99
  * historical `skill` tool_calls are not reconstructed into a HumanMessage,
96
100
  * so the same SKILL.md body is not injected twice in one request. */
@@ -408,6 +408,12 @@ export interface LangfuseConfig {
408
408
  secretKey?: string;
409
409
  baseUrl?: string;
410
410
  metadata?: Record<string, string | number | boolean | null | undefined>;
411
+ /**
412
+ * Internal OTLP span attributes to attach to Langfuse observations before
413
+ * export. Intended for collector-side routing/filtering; strip these in the
414
+ * collector before forwarding spans to Langfuse.
415
+ */
416
+ librechatTraceAttributes?: Record<string, string | number | boolean | null | undefined>;
411
417
  tags?: string[];
412
418
  toolNodeTracing?: LangfuseToolNodeTracingConfig;
413
419
  toolOutputTracing?: LangfuseToolOutputTracingConfig;
@@ -1,4 +1,5 @@
1
1
  export declare function isPresent(value: string | null | undefined): value is string;
2
+ export declare function parseBooleanEnv(value: string | undefined): boolean | undefined;
2
3
  /**
3
4
  * Recursively unescapes all string values in an object
4
5
  * @param obj The object to unescape
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.2.44",
3
+ "version": "3.2.46",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -34,11 +34,6 @@ import {
34
34
  makeIsDeferred,
35
35
  partitionAndMarkAnthropicToolCache,
36
36
  } from '@/messages';
37
- import {
38
- resolveLangfuseConfig,
39
- shouldTraceToolNodeForLangfuse,
40
- withLangfuseToolOutputTracingConfig,
41
- } from '@/langfuseToolOutputTracing';
42
37
  import {
43
38
  createLangfuseHandler,
44
39
  createLangfuseTraceMetadata,
@@ -54,6 +49,10 @@ import {
54
49
  joinKeys,
55
50
  sleep,
56
51
  } from '@/utils';
52
+ import {
53
+ resolveLangfuseRuntimeScope,
54
+ withLangfuseRuntimeScope,
55
+ } from '@/langfuseRuntimeScope';
57
56
  import {
58
57
  GraphNodeKeys,
59
58
  ContentTypes,
@@ -67,6 +66,7 @@ import {
67
66
  type CallbackEntry,
68
67
  } from '@/utils/callbacks';
69
68
  import { partitionAndMarkOpenRouterToolCache } from '@/llm/openrouter/toolCache';
69
+ import { shouldTraceToolNodeForLangfuse } from '@/langfuseToolOutputTracing';
70
70
  import { ToolNode as CustomToolNode, toolsCondition } from '@/tools/ToolNode';
71
71
  import { createLocalCodingToolBundle } from '@/tools/local/LocalCodingTools';
72
72
  import { SubagentExecutor, resolveSubagentConfigs } from '@/tools/subagent';
@@ -81,6 +81,7 @@ import { shouldTriggerSummarization } from '@/summarization';
81
81
  import { resolveLocalToolsForBinding } from '@/tools/local';
82
82
  import { createSummarizeNode } from '@/summarization/node';
83
83
  import { messagesStateReducer } from '@/messages/reducer';
84
+ import { resolveLangfuseConfig } from '@/langfuseConfig';
84
85
  import { createSchemaOnlyTools } from '@/tools/schema';
85
86
  import { AgentContext } from '@/agents/AgentContext';
86
87
  import { createFakeStreamingLLM } from '@/llm/fake';
@@ -2079,6 +2080,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2079
2080
  sessionId: config.configurable?.thread_id as string | undefined,
2080
2081
  traceMetadata,
2081
2082
  tags: ['librechat', 'agent'],
2083
+ traceIdSeed:
2084
+ langfuse?.deterministicTraceId === true ? this.runId : undefined,
2082
2085
  });
2083
2086
  if (langfuseHandler != null) {
2084
2087
  invokeConfig = {
@@ -2092,8 +2095,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2092
2095
  const metadata = config.metadata as Record<string, unknown>;
2093
2096
 
2094
2097
  try {
2095
- result = await withLangfuseToolOutputTracingConfig(
2096
- this.langfuse,
2098
+ result = await withLangfuseRuntimeScope(
2099
+ resolveLangfuseRuntimeScope({
2100
+ runLangfuse: this.langfuse,
2101
+ langfuseOverlay: agentContext.langfuse,
2102
+ }),
2097
2103
  () =>
2098
2104
  attemptInvoke(
2099
2105
  {
@@ -2103,16 +2109,18 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2103
2109
  context: this,
2104
2110
  },
2105
2111
  invokeConfig
2106
- ),
2107
- agentContext.langfuse
2112
+ )
2108
2113
  );
2109
2114
  } catch (primaryError) {
2110
2115
  clearCurrentDeltaStepMarkers({
2111
2116
  graph: this,
2112
2117
  metadata,
2113
2118
  });
2114
- result = await withLangfuseToolOutputTracingConfig(
2115
- this.langfuse,
2119
+ result = await withLangfuseRuntimeScope(
2120
+ resolveLangfuseRuntimeScope({
2121
+ runLangfuse: this.langfuse,
2122
+ langfuseOverlay: agentContext.langfuse,
2123
+ }),
2116
2124
  () =>
2117
2125
  tryFallbackProviders({
2118
2126
  fallbacks,
@@ -2121,8 +2129,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2121
2129
  config: invokeConfig,
2122
2130
  primaryError,
2123
2131
  context: this,
2124
- }),
2125
- agentContext.langfuse
2132
+ })
2126
2133
  );
2127
2134
  } finally {
2128
2135
  await disposeLangfuseHandler(langfuseHandler);
@@ -1,4 +1,3 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks';
2
1
  import { createHash, randomBytes } from 'node:crypto';
3
2
  import { setLangfuseTracerProvider } from '@langfuse/tracing';
4
3
  import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
@@ -14,43 +13,33 @@ import type { LangfuseSpanProcessorParams } from '@langfuse/otel';
14
13
  import type { Context } from '@opentelemetry/api';
15
14
  import type * as t from '@/types';
16
15
  import {
16
+ createLibreChatTraceAttributes,
17
17
  hasLangfuseConfigCredentials,
18
18
  hasLangfuseEnvCredentials,
19
19
  hasLangfuseEnvConfig,
20
20
  } from '@/langfuse';
21
21
  import {
22
- createLangfuseSpanProcessor,
23
- getContextLangfuseConfig,
24
- } from '@/langfuseToolOutputTracing';
22
+ resolveLangfuseConfigForSpan,
23
+ resolveTraceIdSeedForSpan,
24
+ } from '@/langfuseRuntimeScope';
25
+ import { createLangfuseSpanProcessor } from '@/langfuseToolOutputTracing';
26
+ import { traceIdFromSeed } from '@/langfuseRuntimeContext';
25
27
  import { isPresent } from '@/utils/misc';
26
28
 
27
29
  /**
28
30
  * Per-run seed for deterministic Langfuse trace ids. When a run opts in
29
31
  * (`LangfuseConfig.deterministicTraceId`), it executes its stream inside
30
- * `runWithTraceIdSeed(runId, )` and the IdGenerator below derives the root
31
- * trace id from that seed instead of a random one. This lets external systems
32
- * (e.g. a host app recording user feedback after the fact) attach scores or
33
- * observations to the trace by regenerating the same id from the run/message
34
- * id no trace lookup required. With no active seed it falls back to random
35
- * ids, so default behavior is unchanged.
32
+ * `runWithTraceIdSeed(runId, ...)` from `./langfuseRuntimeContext`, and the
33
+ * IdGenerator below derives the root trace id from that seed instead of a
34
+ * random one. This lets external systems (e.g. a host app recording user
35
+ * feedback after the fact) attach scores or observations to the trace by
36
+ * regenerating the same id from the run/message id; no trace lookup required.
37
+ * With no active seed it falls back to random ids, so default behavior is
38
+ * unchanged.
36
39
  */
37
- const traceIdSeedStore = new AsyncLocalStorage<string>();
38
-
39
- export function runWithTraceIdSeed<T>(
40
- seed: string | undefined,
41
- fn: () => T
42
- ): T {
43
- return isPresent(seed) ? traceIdSeedStore.run(seed, fn) : fn();
44
- }
45
-
46
- /** sha256(seed) → first 32 hex chars; matches `@langfuse/tracing` `createTraceId`. */
47
- function traceIdFromSeed(seed: string): string {
48
- return createHash('sha256').update(seed, 'utf8').digest('hex').slice(0, 32);
49
- }
50
-
51
40
  class SeededTraceIdGenerator implements IdGenerator {
52
41
  generateTraceId(): string {
53
- const seed = traceIdSeedStore.getStore();
42
+ const seed = resolveTraceIdSeedForSpan(context.active());
54
43
  return isPresent(seed)
55
44
  ? traceIdFromSeed(seed)
56
45
  : randomBytes(16).toString('hex');
@@ -120,13 +109,19 @@ function getLangfuseSpanProcessorParams(
120
109
  return undefined;
121
110
  }
122
111
 
112
+ function hashCacheKeyValue(value: string | undefined): string | undefined {
113
+ return isPresent(value)
114
+ ? createHash('sha256').update(value, 'utf8').digest('hex')
115
+ : undefined;
116
+ }
117
+
123
118
  function getLangfuseTracerProviderKey(
124
119
  params: LangfuseSpanProcessorParams,
125
120
  langfuse?: t.LangfuseConfig
126
121
  ): string {
127
122
  return JSON.stringify({
128
123
  publicKey: params.publicKey,
129
- secretKey: params.secretKey,
124
+ secretKeyHash: hashCacheKeyValue(params.secretKey),
130
125
  baseUrl: params.baseUrl,
131
126
  environment: params.environment,
132
127
  toolOutputTracing: langfuse?.toolOutputTracing,
@@ -134,6 +129,9 @@ function getLangfuseTracerProviderKey(
134
129
  }
135
130
 
136
131
  class RoutingLangfuseSpanProcessor implements SpanProcessor {
132
+ // Processors live for the process lifetime. LibreChat tenant Langfuse
133
+ // destinations are expected to be a bounded admin-managed set, and shutdown
134
+ // drains every cached processor when the provider is disposed.
137
135
  private readonly processors = new Map<string, SpanProcessor>();
138
136
  private readonly spanProcessors = new WeakMap<object, SpanProcessor>();
139
137
 
@@ -155,13 +153,19 @@ class RoutingLangfuseSpanProcessor implements SpanProcessor {
155
153
  }
156
154
 
157
155
  onStart(span: Span, parentContext: Context): void {
158
- const processor = this.ensureProcessor(
159
- getContextLangfuseConfig(parentContext)
160
- );
156
+ const langfuse = resolveLangfuseConfigForSpan(parentContext);
157
+ const processor = this.ensureProcessor(langfuse);
161
158
  if (processor == null) {
162
159
  return;
163
160
  }
164
161
 
162
+ const librechatTraceAttributes = createLibreChatTraceAttributes(
163
+ langfuse?.librechatTraceAttributes ?? {}
164
+ );
165
+ if (Object.keys(librechatTraceAttributes).length > 0) {
166
+ span.setAttributes(librechatTraceAttributes);
167
+ }
168
+
165
169
  this.spanProcessors.set(span, processor);
166
170
  processor.onStart(span, parentContext);
167
171
  }
package/src/langfuse.ts CHANGED
@@ -1,23 +1,34 @@
1
1
  import { CallbackHandler } from '@langfuse/langchain';
2
+ import { context as otelContext } from '@opentelemetry/api';
2
3
  import {
3
4
  getLangfuseTracerProvider,
4
5
  propagateAttributes,
5
6
  } from '@langfuse/tracing';
6
7
  import type { PropagateAttributesParams } from '@langfuse/tracing';
7
8
  import type * as t from '@/types';
8
- import { isPresent } from '@/utils/misc';
9
+ import {
10
+ resolveLangfuseConfigForSpan,
11
+ resolveTraceIdSeedForSpan,
12
+ withLangfuseRuntimeScope,
13
+ } from '@/langfuseRuntimeScope';
14
+ import { isPresent, parseBooleanEnv } from '@/utils/misc';
9
15
 
10
16
  const TRACE_METADATA_MAX_LENGTH = 200;
11
17
  const LANGFUSE_FORCE_FLUSH_ON_DISPOSE = 'LANGFUSE_FORCE_FLUSH_ON_DISPOSE';
12
18
 
13
19
  export type LangfuseTraceMetadata = Record<string, string>;
20
+ export type LangfuseTraceAttributes = Record<string, string | number | boolean>;
14
21
  type LangfuseMetadata = NonNullable<t.LangfuseConfig['metadata']>;
22
+ type LangfuseConfigTraceAttributes = NonNullable<
23
+ t.LangfuseConfig['librechatTraceAttributes']
24
+ >;
15
25
 
16
26
  type LangfuseHandlerParams = {
17
27
  userId?: string;
18
28
  sessionId?: string;
19
29
  traceMetadata?: LangfuseTraceMetadata;
20
30
  tags?: string[];
31
+ traceIdSeed?: string;
21
32
  };
22
33
 
23
34
  type AgentLangfuseHandlerParams = LangfuseHandlerParams & {
@@ -32,11 +43,81 @@ type FlushableTracerProvider = {
32
43
  forceFlush?: () => Promise<void> | void;
33
44
  };
34
45
 
35
- function parseBooleanEnv(value?: string): boolean {
36
- if (value == null) {
37
- return false;
46
+ class ScopedLangfuseCallbackHandler extends CallbackHandler {
47
+ private readonly langfuse?: t.LangfuseConfig;
48
+ private readonly traceIdSeed?: string;
49
+
50
+ constructor(params?: AgentLangfuseHandlerParams) {
51
+ const { langfuse, traceIdSeed, ...handlerParams } = params ?? {};
52
+ super(handlerParams);
53
+ this.langfuse = langfuse;
54
+ this.traceIdSeed = traceIdSeed;
55
+ }
56
+
57
+ private getDeterministicTraceSeed(): string | undefined {
58
+ return this.langfuse?.deterministicTraceId === true
59
+ ? this.traceIdSeed
60
+ : undefined;
61
+ }
62
+
63
+ private withRuntimeContext<T>(action: () => T): T {
64
+ const activeContext = otelContext.active();
65
+ const langfuse =
66
+ resolveLangfuseConfigForSpan(activeContext) ?? this.langfuse;
67
+ const seed = this.getDeterministicTraceSeed();
68
+ return withLangfuseRuntimeScope(
69
+ {
70
+ langfuse,
71
+ traceIdSeed: resolveTraceIdSeedForSpan(activeContext) ?? seed,
72
+ },
73
+ action
74
+ );
75
+ }
76
+
77
+ // LangChain may invoke callback handlers outside the caller's OTEL context.
78
+ // Re-enter tenant scope only for callbacks that start Langfuse observations;
79
+ // end/error/token callbacks use spans already bound to a processor at start.
80
+ override handleChainStart(
81
+ ...args: Parameters<CallbackHandler['handleChainStart']>
82
+ ): ReturnType<CallbackHandler['handleChainStart']> {
83
+ return this.withRuntimeContext(() => super.handleChainStart(...args));
84
+ }
85
+
86
+ override handleAgentAction(
87
+ ...args: Parameters<CallbackHandler['handleAgentAction']>
88
+ ): ReturnType<CallbackHandler['handleAgentAction']> {
89
+ return this.withRuntimeContext(() => super.handleAgentAction(...args));
90
+ }
91
+
92
+ override handleGenerationStart(
93
+ ...args: Parameters<CallbackHandler['handleGenerationStart']>
94
+ ): ReturnType<CallbackHandler['handleGenerationStart']> {
95
+ return this.withRuntimeContext(() => super.handleGenerationStart(...args));
96
+ }
97
+
98
+ override handleChatModelStart(
99
+ ...args: Parameters<CallbackHandler['handleChatModelStart']>
100
+ ): ReturnType<CallbackHandler['handleChatModelStart']> {
101
+ return this.withRuntimeContext(() => super.handleChatModelStart(...args));
102
+ }
103
+
104
+ override handleLLMStart(
105
+ ...args: Parameters<CallbackHandler['handleLLMStart']>
106
+ ): ReturnType<CallbackHandler['handleLLMStart']> {
107
+ return this.withRuntimeContext(() => super.handleLLMStart(...args));
108
+ }
109
+
110
+ override handleToolStart(
111
+ ...args: Parameters<CallbackHandler['handleToolStart']>
112
+ ): ReturnType<CallbackHandler['handleToolStart']> {
113
+ return this.withRuntimeContext(() => super.handleToolStart(...args));
114
+ }
115
+
116
+ override handleRetrieverStart(
117
+ ...args: Parameters<CallbackHandler['handleRetrieverStart']>
118
+ ): ReturnType<CallbackHandler['handleRetrieverStart']> {
119
+ return this.withRuntimeContext(() => super.handleRetrieverStart(...args));
38
120
  }
39
- return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
40
121
  }
41
122
 
42
123
  function hasLangfuseTracingConfig(langfuse?: t.LangfuseConfig): boolean {
@@ -48,6 +129,9 @@ function hasLangfuseTracingConfig(langfuse?: t.LangfuseConfig): boolean {
48
129
  function hasLangfuseTraceAttributes(langfuse?: t.LangfuseConfig): boolean {
49
130
  return (
50
131
  Object.keys(createTraceMetadata(langfuse?.metadata ?? {})).length > 0 ||
132
+ Object.keys(
133
+ createLibreChatTraceAttributes(langfuse?.librechatTraceAttributes ?? {})
134
+ ).length > 0 ||
51
135
  (mergeLangfuseTags(undefined, langfuse?.tags)?.length ?? 0) > 0
52
136
  );
53
137
  }
@@ -100,6 +184,26 @@ function createTraceMetadata(
100
184
  return traceMetadata;
101
185
  }
102
186
 
187
+ export function createLibreChatTraceAttributes(
188
+ attributes: LangfuseConfigTraceAttributes
189
+ ): LangfuseTraceAttributes {
190
+ const librechatTraceAttributes: LangfuseTraceAttributes = {};
191
+ for (const [key, value] of Object.entries(attributes)) {
192
+ if (value == null || key.trim() === '') {
193
+ continue;
194
+ }
195
+ if (typeof value === 'string') {
196
+ if (value.trim() === '' || value.length > TRACE_METADATA_MAX_LENGTH) {
197
+ continue;
198
+ }
199
+ librechatTraceAttributes[key] = value;
200
+ continue;
201
+ }
202
+ librechatTraceAttributes[key] = value;
203
+ }
204
+ return librechatTraceAttributes;
205
+ }
206
+
103
207
  export function createLangfuseTraceMetadata({
104
208
  messageId,
105
209
  parentMessageId,
@@ -175,7 +279,7 @@ export function shouldCreateLangfuseHandler(
175
279
  export function createLegacyLangfuseHandler(
176
280
  params: LangfuseHandlerParams
177
281
  ): CallbackHandler {
178
- return new CallbackHandler(params);
282
+ return new ScopedLangfuseCallbackHandler(params);
179
283
  }
180
284
 
181
285
  export function createLangfuseHandler({
@@ -184,11 +288,12 @@ export function createLangfuseHandler({
184
288
  sessionId,
185
289
  traceMetadata,
186
290
  tags,
291
+ traceIdSeed,
187
292
  }: AgentLangfuseHandlerParams): CallbackHandler | undefined {
188
293
  if (!shouldCreateLangfuseHandler(langfuse)) {
189
294
  return undefined;
190
295
  }
191
- return new CallbackHandler({
296
+ return new ScopedLangfuseCallbackHandler({
192
297
  userId,
193
298
  sessionId,
194
299
  traceMetadata: mergeLangfuseTraceMetadata(
@@ -196,6 +301,8 @@ export function createLangfuseHandler({
196
301
  langfuse?.metadata
197
302
  ),
198
303
  tags: mergeLangfuseTags(tags, langfuse?.tags),
304
+ langfuse,
305
+ traceIdSeed,
199
306
  });
200
307
  }
201
308