@librechat/agents 3.3.1 → 3.3.3

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.
@@ -1,6 +1,7 @@
1
1
  const require_misc = require("./utils/misc.cjs");
2
2
  const require_langfuseRuntimeScope = require("./langfuseRuntimeScope.cjs");
3
3
  let _langchain_core_messages = require("@langchain/core/messages");
4
+ let _langchain_langgraph = require("@langchain/langgraph");
4
5
  let _langfuse_langchain = require("@langfuse/langchain");
5
6
  let _opentelemetry_api = require("@opentelemetry/api");
6
7
  let _langfuse_tracing = require("@langfuse/tracing");
@@ -88,6 +89,11 @@ var ScopedLangfuseCallbackHandler = class extends _langfuse_langchain.CallbackHa
88
89
  handleChainStart(...args) {
89
90
  return this.withRuntimeContext(() => super.handleChainStart(...args));
90
91
  }
92
+ handleChainError(...args) {
93
+ const [error, runId, parentRunId] = args;
94
+ if (error != null && parentRunId != null && (0, _langchain_langgraph.isParentCommand)(error)) return super.handleChainEnd({ controlFlow: "ParentCommand" }, runId, parentRunId);
95
+ return super.handleChainError(...args);
96
+ }
91
97
  handleAgentAction(...args) {
92
98
  return this.withRuntimeContext(() => super.handleAgentAction(...args));
93
99
  }
@@ -1 +1 @@
1
- {"version":3,"file":"langfuse.cjs","names":["AIMessageChunk","AIMessage","CallbackHandler","otelContext","resolveLangfuseConfigForSpan","withLangfuseRuntimeScope","resolveTraceIdSeedForSpan","isPresent","parseBooleanEnv"],"sources":["../../src/langfuse.ts"],"sourcesContent":["import { CallbackHandler } from '@langfuse/langchain';\nimport { context as otelContext } from '@opentelemetry/api';\nimport { AIMessage, AIMessageChunk } from '@langchain/core/messages';\nimport {\n getLangfuseTracerProvider,\n propagateAttributes,\n} from '@langfuse/tracing';\nimport type {\n AIMessageChunkFields,\n AIMessageFields,\n UsageMetadata,\n} from '@langchain/core/messages';\nimport type {\n ChatGeneration,\n Generation,\n LLMResult,\n} from '@langchain/core/outputs';\nimport type { PropagateAttributesParams } from '@langfuse/tracing';\nimport type * as t from '@/types';\nimport {\n resolveLangfuseConfigForSpan,\n resolveTraceIdSeedForSpan,\n withLangfuseRuntimeScope,\n} from '@/langfuseRuntimeScope';\nimport { isPresent, parseBooleanEnv } from '@/utils/misc';\n\nconst TRACE_METADATA_MAX_LENGTH = 200;\nconst LANGFUSE_FORCE_FLUSH_ON_DISPOSE = 'LANGFUSE_FORCE_FLUSH_ON_DISPOSE';\n\nexport type LangfuseTraceMetadata = Record<string, string>;\nexport type LangfuseTraceAttributes = Record<string, string | number | boolean>;\ntype LangfuseMetadata = NonNullable<t.LangfuseConfig['metadata']>;\ntype LangfuseConfigTraceAttributes = NonNullable<\n t.LangfuseConfig['librechatTraceAttributes']\n>;\n\ntype LangfuseHandlerParams = {\n userId?: string;\n sessionId?: string;\n traceMetadata?: LangfuseTraceMetadata;\n tags?: string[];\n traceIdSeed?: string;\n};\n\ntype AgentLangfuseHandlerParams = LangfuseHandlerParams & {\n langfuse?: t.LangfuseConfig;\n};\n\ntype LangfuseAttributeParams = AgentLangfuseHandlerParams & {\n traceName?: string;\n};\n\ntype FlushableTracerProvider = {\n forceFlush?: () => Promise<void> | void;\n};\n\ntype BedrockResponseUsage = {\n inputTokens?: number;\n cacheReadInputTokens?: number;\n cacheWriteInputTokens?: number;\n};\n\ntype BedrockResponseMetadata = {\n metadata?: {\n usage?: BedrockResponseUsage;\n };\n};\n\nfunction getLangfuseBedrockUsage(\n message: AIMessage | AIMessageChunk\n): UsageMetadata | undefined {\n const usageMetadata = message.usage_metadata;\n const bedrockUsage = (message.response_metadata as BedrockResponseMetadata)\n .metadata?.usage;\n if (\n usageMetadata == null ||\n bedrockUsage == null ||\n usageMetadata.input_tokens !== bedrockUsage.inputTokens\n ) {\n return usageMetadata;\n }\n\n const cacheRead = bedrockUsage.cacheReadInputTokens ?? 0;\n const cacheCreation = bedrockUsage.cacheWriteInputTokens ?? 0;\n if (cacheRead === 0 && cacheCreation === 0) {\n return usageMetadata;\n }\n\n return {\n ...usageMetadata,\n input_tokens: usageMetadata.input_tokens + cacheRead + cacheCreation,\n };\n}\n\nfunction cloneMessageWithUsage(\n message: AIMessage | AIMessageChunk,\n usageMetadata: UsageMetadata\n): AIMessage | AIMessageChunk {\n const fields: AIMessageFields = {\n content: message.content,\n additional_kwargs: message.additional_kwargs,\n response_metadata: message.response_metadata,\n id: message.id,\n name: message.name,\n tool_calls: message.tool_calls,\n invalid_tool_calls: message.invalid_tool_calls,\n usage_metadata: usageMetadata,\n };\n\n if (message instanceof AIMessageChunk) {\n const chunkFields: AIMessageChunkFields = {\n ...fields,\n tool_call_chunks: message.tool_call_chunks,\n };\n return new AIMessageChunk(chunkFields);\n }\n\n return new AIMessage(fields);\n}\n\nfunction normalizeGenerationForLangfuse(generation: Generation): Generation {\n if (!('message' in generation)) {\n return generation;\n }\n\n const message = (generation as ChatGeneration).message;\n if (!(message instanceof AIMessage || message instanceof AIMessageChunk)) {\n return generation;\n }\n\n const usageMetadata = getLangfuseBedrockUsage(message);\n if (usageMetadata == null || usageMetadata === message.usage_metadata) {\n return generation;\n }\n\n const chatGeneration: ChatGeneration = {\n ...(generation as ChatGeneration),\n message: cloneMessageWithUsage(message, usageMetadata),\n };\n return chatGeneration;\n}\n\nfunction normalizeBedrockUsageForLangfuse(output: LLMResult): LLMResult {\n if (output.generations.length === 0) {\n return output;\n }\n\n const listIndex = output.generations.length - 1;\n const generationList = output.generations[listIndex];\n if (generationList.length === 0) {\n return output;\n }\n\n const generationIndex = generationList.length - 1;\n const generation = generationList[generationIndex];\n const normalized = normalizeGenerationForLangfuse(generation);\n if (normalized === generation) {\n return output;\n }\n\n const generations = [...output.generations];\n generations[listIndex] = [...generationList];\n generations[listIndex][generationIndex] = normalized;\n return { ...output, generations };\n}\n\nclass ScopedLangfuseCallbackHandler extends CallbackHandler {\n private readonly langfuse?: t.LangfuseConfig;\n private readonly traceIdSeed?: string;\n\n constructor(params?: AgentLangfuseHandlerParams) {\n const { langfuse, traceIdSeed, ...handlerParams } = params ?? {};\n super(handlerParams);\n this.langfuse = langfuse;\n this.traceIdSeed = traceIdSeed;\n }\n\n private getDeterministicTraceSeed(): string | undefined {\n return this.langfuse?.deterministicTraceId === true\n ? this.traceIdSeed\n : undefined;\n }\n\n private withRuntimeContext<T>(action: () => T): T {\n const activeContext = otelContext.active();\n const langfuse =\n resolveLangfuseConfigForSpan(activeContext) ?? this.langfuse;\n const seed = this.getDeterministicTraceSeed();\n return withLangfuseRuntimeScope(\n {\n langfuse,\n traceIdSeed: resolveTraceIdSeedForSpan(activeContext) ?? seed,\n },\n action\n );\n }\n\n // LangChain may invoke callback handlers outside the caller's OTEL context.\n // Re-enter tenant scope only for callbacks that start Langfuse observations;\n // end/error/token callbacks use spans already bound to a processor at start.\n override handleChainStart(\n ...args: Parameters<CallbackHandler['handleChainStart']>\n ): ReturnType<CallbackHandler['handleChainStart']> {\n return this.withRuntimeContext(() => super.handleChainStart(...args));\n }\n\n override handleAgentAction(\n ...args: Parameters<CallbackHandler['handleAgentAction']>\n ): ReturnType<CallbackHandler['handleAgentAction']> {\n return this.withRuntimeContext(() => super.handleAgentAction(...args));\n }\n\n override handleGenerationStart(\n ...args: Parameters<CallbackHandler['handleGenerationStart']>\n ): ReturnType<CallbackHandler['handleGenerationStart']> {\n return this.withRuntimeContext(() => super.handleGenerationStart(...args));\n }\n\n override handleChatModelStart(\n ...args: Parameters<CallbackHandler['handleChatModelStart']>\n ): ReturnType<CallbackHandler['handleChatModelStart']> {\n return this.withRuntimeContext(() => super.handleChatModelStart(...args));\n }\n\n override handleLLMStart(\n ...args: Parameters<CallbackHandler['handleLLMStart']>\n ): ReturnType<CallbackHandler['handleLLMStart']> {\n return this.withRuntimeContext(() => super.handleLLMStart(...args));\n }\n\n override handleLLMEnd(\n output: LLMResult,\n runId: string,\n parentRunId?: string\n ): Promise<void> {\n return super.handleLLMEnd(\n normalizeBedrockUsageForLangfuse(output),\n runId,\n parentRunId\n );\n }\n\n override handleToolStart(\n ...args: Parameters<CallbackHandler['handleToolStart']>\n ): ReturnType<CallbackHandler['handleToolStart']> {\n return this.withRuntimeContext(() => super.handleToolStart(...args));\n }\n\n override handleRetrieverStart(\n ...args: Parameters<CallbackHandler['handleRetrieverStart']>\n ): ReturnType<CallbackHandler['handleRetrieverStart']> {\n return this.withRuntimeContext(() => super.handleRetrieverStart(...args));\n }\n}\n\nfunction hasLangfuseTracingConfig(langfuse?: t.LangfuseConfig): boolean {\n return (\n langfuse?.toolNodeTracing != null || langfuse?.toolOutputTracing != null\n );\n}\n\nfunction hasLangfuseTraceAttributes(langfuse?: t.LangfuseConfig): boolean {\n return (\n Object.keys(createTraceMetadata(langfuse?.metadata ?? {})).length > 0 ||\n Object.keys(\n createLibreChatTraceAttributes(langfuse?.librechatTraceAttributes ?? {})\n ).length > 0 ||\n (mergeLangfuseTags(undefined, langfuse?.tags)?.length ?? 0) > 0\n );\n}\n\nexport function hasLangfuseConfigCredentials(\n langfuse?: t.LangfuseConfig\n): langfuse is t.LangfuseConfig & {\n publicKey: string;\n secretKey: string;\n} {\n return (\n langfuse != null &&\n isPresent(langfuse.publicKey) &&\n isPresent(langfuse.secretKey)\n );\n}\n\nfunction hasLangfuseConfigBaseUrl(langfuse?: t.LangfuseConfig): boolean {\n return isPresent(langfuse?.baseUrl);\n}\n\nexport function isExplicitLangfuseConfig(langfuse?: t.LangfuseConfig): boolean {\n return (\n langfuse?.enabled != null ||\n isPresent(langfuse?.publicKey) ||\n isPresent(langfuse?.secretKey) ||\n isPresent(langfuse?.baseUrl) ||\n hasLangfuseTraceAttributes(langfuse) ||\n hasLangfuseTracingConfig(langfuse)\n );\n}\n\nfunction createTraceMetadata(\n metadata: Record<string, unknown>\n): LangfuseTraceMetadata {\n const traceMetadata: LangfuseTraceMetadata = {};\n for (const [key, value] of Object.entries(metadata)) {\n if (value == null) {\n continue;\n }\n const stringValue = typeof value === 'string' ? value : String(value);\n if (\n stringValue.trim() === '' ||\n stringValue.length > TRACE_METADATA_MAX_LENGTH\n ) {\n continue;\n }\n traceMetadata[key] = stringValue;\n }\n return traceMetadata;\n}\n\nexport function createLibreChatTraceAttributes(\n attributes: LangfuseConfigTraceAttributes\n): LangfuseTraceAttributes {\n const librechatTraceAttributes: LangfuseTraceAttributes = {};\n for (const [key, value] of Object.entries(attributes)) {\n if (value == null || key.trim() === '') {\n continue;\n }\n if (typeof value === 'string') {\n if (value.trim() === '' || value.length > TRACE_METADATA_MAX_LENGTH) {\n continue;\n }\n librechatTraceAttributes[key] = value;\n continue;\n }\n librechatTraceAttributes[key] = value;\n }\n return librechatTraceAttributes;\n}\n\nexport function createLangfuseTraceMetadata({\n messageId,\n parentMessageId,\n agentId,\n agentName,\n}: {\n messageId?: unknown;\n parentMessageId?: unknown;\n agentId?: unknown;\n agentName?: unknown;\n}): LangfuseTraceMetadata {\n return createTraceMetadata({\n messageId,\n parentMessageId,\n agentId,\n agentName,\n });\n}\n\nfunction mergeLangfuseTraceMetadata(\n traceMetadata?: LangfuseTraceMetadata,\n metadata?: LangfuseMetadata\n): LangfuseTraceMetadata | undefined {\n const merged = createTraceMetadata({\n ...(metadata ?? {}),\n ...(traceMetadata ?? {}),\n });\n return Object.keys(merged).length > 0 ? merged : undefined;\n}\n\nfunction mergeLangfuseTags(\n tags?: string[],\n configTags?: string[]\n): string[] | undefined {\n const merged = [...(tags ?? []), ...(configTags ?? [])].filter(\n (tag) => tag.trim() !== ''\n );\n return merged.length > 0 ? [...new Set(merged)] : undefined;\n}\n\nexport function getLangfuseTraceName(\n traceMetadata?: LangfuseTraceMetadata,\n fallback: string = 'LibreChat Agent'\n): string {\n const agentName = traceMetadata?.agentName;\n return isPresent(agentName) ? `${fallback}: ${agentName}` : fallback;\n}\n\nexport function hasLangfuseEnvConfig(): boolean {\n return hasLangfuseEnvCredentials();\n}\n\nexport function hasLangfuseEnvCredentials(): boolean {\n return (\n isPresent(process.env.LANGFUSE_SECRET_KEY) &&\n isPresent(process.env.LANGFUSE_PUBLIC_KEY)\n );\n}\n\nexport function shouldCreateLangfuseHandler(\n langfuse?: t.LangfuseConfig\n): boolean {\n if (langfuse?.enabled === false) {\n return false;\n }\n return (\n hasLangfuseEnvConfig() ||\n hasLangfuseConfigCredentials(langfuse) ||\n (hasLangfuseConfigBaseUrl(langfuse) && hasLangfuseEnvCredentials())\n );\n}\n\nexport function createLegacyLangfuseHandler(\n params: LangfuseHandlerParams\n): CallbackHandler {\n return new ScopedLangfuseCallbackHandler(params);\n}\n\nexport function createLangfuseHandler({\n langfuse,\n userId,\n sessionId,\n traceMetadata,\n tags,\n traceIdSeed,\n}: AgentLangfuseHandlerParams): CallbackHandler | undefined {\n if (!shouldCreateLangfuseHandler(langfuse)) {\n return undefined;\n }\n return new ScopedLangfuseCallbackHandler({\n userId,\n sessionId,\n traceMetadata: mergeLangfuseTraceMetadata(\n traceMetadata,\n langfuse?.metadata\n ),\n tags: mergeLangfuseTags(tags, langfuse?.tags),\n langfuse,\n traceIdSeed,\n });\n}\n\nfunction createPropagateAttributeParams({\n langfuse,\n userId,\n sessionId,\n traceMetadata,\n traceName,\n tags,\n}: LangfuseAttributeParams): PropagateAttributesParams {\n return {\n userId,\n sessionId,\n traceName,\n tags: mergeLangfuseTags(tags, langfuse?.tags),\n metadata: mergeLangfuseTraceMetadata(traceMetadata, langfuse?.metadata),\n };\n}\n\nexport function withLangfuseAttributes<T>(\n params: LangfuseAttributeParams,\n action: () => T\n): T {\n if (!shouldCreateLangfuseHandler(params.langfuse)) {\n return action();\n }\n return propagateAttributes(createPropagateAttributeParams(params), action);\n}\n\nexport function hasExplicitLangfuseConfig(\n contexts: Iterable<{ langfuse?: t.LangfuseConfig }>\n): boolean {\n for (const context of contexts) {\n if (isExplicitLangfuseConfig(context.langfuse)) {\n return true;\n }\n }\n return false;\n}\n\nexport function isLangfuseCallbackHandler(value: unknown): boolean {\n return value instanceof CallbackHandler;\n}\n\nexport async function disposeLangfuseHandler(value: unknown): Promise<void> {\n if (\n value == null ||\n parseBooleanEnv(process.env[LANGFUSE_FORCE_FLUSH_ON_DISPOSE]) !== true\n ) {\n return;\n }\n const provider = getLangfuseTracerProvider() as FlushableTracerProvider;\n await provider.forceFlush?.();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"langfuse.cjs","names":["AIMessageChunk","AIMessage","CallbackHandler","otelContext","resolveLangfuseConfigForSpan","withLangfuseRuntimeScope","resolveTraceIdSeedForSpan","isPresent","parseBooleanEnv"],"sources":["../../src/langfuse.ts"],"sourcesContent":["import { CallbackHandler } from '@langfuse/langchain';\nimport { isParentCommand } from '@langchain/langgraph';\nimport { context as otelContext } from '@opentelemetry/api';\nimport { AIMessage, AIMessageChunk } from '@langchain/core/messages';\nimport {\n getLangfuseTracerProvider,\n propagateAttributes,\n} from '@langfuse/tracing';\nimport type {\n AIMessageChunkFields,\n AIMessageFields,\n UsageMetadata,\n} from '@langchain/core/messages';\nimport type {\n ChatGeneration,\n Generation,\n LLMResult,\n} from '@langchain/core/outputs';\nimport type { PropagateAttributesParams } from '@langfuse/tracing';\nimport type * as t from '@/types';\nimport {\n resolveLangfuseConfigForSpan,\n resolveTraceIdSeedForSpan,\n withLangfuseRuntimeScope,\n} from '@/langfuseRuntimeScope';\nimport { isPresent, parseBooleanEnv } from '@/utils/misc';\n\nconst TRACE_METADATA_MAX_LENGTH = 200;\nconst LANGFUSE_FORCE_FLUSH_ON_DISPOSE = 'LANGFUSE_FORCE_FLUSH_ON_DISPOSE';\n\nexport type LangfuseTraceMetadata = Record<string, string>;\nexport type LangfuseTraceAttributes = Record<string, string | number | boolean>;\ntype LangfuseMetadata = NonNullable<t.LangfuseConfig['metadata']>;\ntype LangfuseConfigTraceAttributes = NonNullable<\n t.LangfuseConfig['librechatTraceAttributes']\n>;\n\ntype LangfuseHandlerParams = {\n userId?: string;\n sessionId?: string;\n traceMetadata?: LangfuseTraceMetadata;\n tags?: string[];\n traceIdSeed?: string;\n};\n\ntype AgentLangfuseHandlerParams = LangfuseHandlerParams & {\n langfuse?: t.LangfuseConfig;\n};\n\ntype LangfuseAttributeParams = AgentLangfuseHandlerParams & {\n traceName?: string;\n};\n\ntype FlushableTracerProvider = {\n forceFlush?: () => Promise<void> | void;\n};\n\ntype BedrockResponseUsage = {\n inputTokens?: number;\n cacheReadInputTokens?: number;\n cacheWriteInputTokens?: number;\n};\n\ntype BedrockResponseMetadata = {\n metadata?: {\n usage?: BedrockResponseUsage;\n };\n};\n\nfunction getLangfuseBedrockUsage(\n message: AIMessage | AIMessageChunk\n): UsageMetadata | undefined {\n const usageMetadata = message.usage_metadata;\n const bedrockUsage = (message.response_metadata as BedrockResponseMetadata)\n .metadata?.usage;\n if (\n usageMetadata == null ||\n bedrockUsage == null ||\n usageMetadata.input_tokens !== bedrockUsage.inputTokens\n ) {\n return usageMetadata;\n }\n\n const cacheRead = bedrockUsage.cacheReadInputTokens ?? 0;\n const cacheCreation = bedrockUsage.cacheWriteInputTokens ?? 0;\n if (cacheRead === 0 && cacheCreation === 0) {\n return usageMetadata;\n }\n\n return {\n ...usageMetadata,\n input_tokens: usageMetadata.input_tokens + cacheRead + cacheCreation,\n };\n}\n\nfunction cloneMessageWithUsage(\n message: AIMessage | AIMessageChunk,\n usageMetadata: UsageMetadata\n): AIMessage | AIMessageChunk {\n const fields: AIMessageFields = {\n content: message.content,\n additional_kwargs: message.additional_kwargs,\n response_metadata: message.response_metadata,\n id: message.id,\n name: message.name,\n tool_calls: message.tool_calls,\n invalid_tool_calls: message.invalid_tool_calls,\n usage_metadata: usageMetadata,\n };\n\n if (message instanceof AIMessageChunk) {\n const chunkFields: AIMessageChunkFields = {\n ...fields,\n tool_call_chunks: message.tool_call_chunks,\n };\n return new AIMessageChunk(chunkFields);\n }\n\n return new AIMessage(fields);\n}\n\nfunction normalizeGenerationForLangfuse(generation: Generation): Generation {\n if (!('message' in generation)) {\n return generation;\n }\n\n const message = (generation as ChatGeneration).message;\n if (!(message instanceof AIMessage || message instanceof AIMessageChunk)) {\n return generation;\n }\n\n const usageMetadata = getLangfuseBedrockUsage(message);\n if (usageMetadata == null || usageMetadata === message.usage_metadata) {\n return generation;\n }\n\n const chatGeneration: ChatGeneration = {\n ...(generation as ChatGeneration),\n message: cloneMessageWithUsage(message, usageMetadata),\n };\n return chatGeneration;\n}\n\nfunction normalizeBedrockUsageForLangfuse(output: LLMResult): LLMResult {\n if (output.generations.length === 0) {\n return output;\n }\n\n const listIndex = output.generations.length - 1;\n const generationList = output.generations[listIndex];\n if (generationList.length === 0) {\n return output;\n }\n\n const generationIndex = generationList.length - 1;\n const generation = generationList[generationIndex];\n const normalized = normalizeGenerationForLangfuse(generation);\n if (normalized === generation) {\n return output;\n }\n\n const generations = [...output.generations];\n generations[listIndex] = [...generationList];\n generations[listIndex][generationIndex] = normalized;\n return { ...output, generations };\n}\n\nclass ScopedLangfuseCallbackHandler extends CallbackHandler {\n private readonly langfuse?: t.LangfuseConfig;\n private readonly traceIdSeed?: string;\n\n constructor(params?: AgentLangfuseHandlerParams) {\n const { langfuse, traceIdSeed, ...handlerParams } = params ?? {};\n super(handlerParams);\n this.langfuse = langfuse;\n this.traceIdSeed = traceIdSeed;\n }\n\n private getDeterministicTraceSeed(): string | undefined {\n return this.langfuse?.deterministicTraceId === true\n ? this.traceIdSeed\n : undefined;\n }\n\n private withRuntimeContext<T>(action: () => T): T {\n const activeContext = otelContext.active();\n const langfuse =\n resolveLangfuseConfigForSpan(activeContext) ?? this.langfuse;\n const seed = this.getDeterministicTraceSeed();\n return withLangfuseRuntimeScope(\n {\n langfuse,\n traceIdSeed: resolveTraceIdSeedForSpan(activeContext) ?? seed,\n },\n action\n );\n }\n\n // LangChain may invoke callback handlers outside the caller's OTEL context.\n // Re-enter tenant scope only for callbacks that start Langfuse observations;\n // end/error/token callbacks use spans already bound to a processor at start.\n override handleChainStart(\n ...args: Parameters<CallbackHandler['handleChainStart']>\n ): ReturnType<CallbackHandler['handleChainStart']> {\n return this.withRuntimeContext(() => super.handleChainStart(...args));\n }\n\n override handleChainError(\n ...args: Parameters<CallbackHandler['handleChainError']>\n ): ReturnType<CallbackHandler['handleChainError']> {\n const [error, runId, parentRunId] = args;\n if (error != null && parentRunId != null && isParentCommand(error)) {\n return super.handleChainEnd(\n { controlFlow: 'ParentCommand' },\n runId,\n parentRunId\n );\n }\n return super.handleChainError(...args);\n }\n\n override handleAgentAction(\n ...args: Parameters<CallbackHandler['handleAgentAction']>\n ): ReturnType<CallbackHandler['handleAgentAction']> {\n return this.withRuntimeContext(() => super.handleAgentAction(...args));\n }\n\n override handleGenerationStart(\n ...args: Parameters<CallbackHandler['handleGenerationStart']>\n ): ReturnType<CallbackHandler['handleGenerationStart']> {\n return this.withRuntimeContext(() => super.handleGenerationStart(...args));\n }\n\n override handleChatModelStart(\n ...args: Parameters<CallbackHandler['handleChatModelStart']>\n ): ReturnType<CallbackHandler['handleChatModelStart']> {\n return this.withRuntimeContext(() => super.handleChatModelStart(...args));\n }\n\n override handleLLMStart(\n ...args: Parameters<CallbackHandler['handleLLMStart']>\n ): ReturnType<CallbackHandler['handleLLMStart']> {\n return this.withRuntimeContext(() => super.handleLLMStart(...args));\n }\n\n override handleLLMEnd(\n output: LLMResult,\n runId: string,\n parentRunId?: string\n ): Promise<void> {\n return super.handleLLMEnd(\n normalizeBedrockUsageForLangfuse(output),\n runId,\n parentRunId\n );\n }\n\n override handleToolStart(\n ...args: Parameters<CallbackHandler['handleToolStart']>\n ): ReturnType<CallbackHandler['handleToolStart']> {\n return this.withRuntimeContext(() => super.handleToolStart(...args));\n }\n\n override handleRetrieverStart(\n ...args: Parameters<CallbackHandler['handleRetrieverStart']>\n ): ReturnType<CallbackHandler['handleRetrieverStart']> {\n return this.withRuntimeContext(() => super.handleRetrieverStart(...args));\n }\n}\n\nfunction hasLangfuseTracingConfig(langfuse?: t.LangfuseConfig): boolean {\n return (\n langfuse?.toolNodeTracing != null || langfuse?.toolOutputTracing != null\n );\n}\n\nfunction hasLangfuseTraceAttributes(langfuse?: t.LangfuseConfig): boolean {\n return (\n Object.keys(createTraceMetadata(langfuse?.metadata ?? {})).length > 0 ||\n Object.keys(\n createLibreChatTraceAttributes(langfuse?.librechatTraceAttributes ?? {})\n ).length > 0 ||\n (mergeLangfuseTags(undefined, langfuse?.tags)?.length ?? 0) > 0\n );\n}\n\nexport function hasLangfuseConfigCredentials(\n langfuse?: t.LangfuseConfig\n): langfuse is t.LangfuseConfig & {\n publicKey: string;\n secretKey: string;\n} {\n return (\n langfuse != null &&\n isPresent(langfuse.publicKey) &&\n isPresent(langfuse.secretKey)\n );\n}\n\nfunction hasLangfuseConfigBaseUrl(langfuse?: t.LangfuseConfig): boolean {\n return isPresent(langfuse?.baseUrl);\n}\n\nexport function isExplicitLangfuseConfig(langfuse?: t.LangfuseConfig): boolean {\n return (\n langfuse?.enabled != null ||\n isPresent(langfuse?.publicKey) ||\n isPresent(langfuse?.secretKey) ||\n isPresent(langfuse?.baseUrl) ||\n hasLangfuseTraceAttributes(langfuse) ||\n hasLangfuseTracingConfig(langfuse)\n );\n}\n\nfunction createTraceMetadata(\n metadata: Record<string, unknown>\n): LangfuseTraceMetadata {\n const traceMetadata: LangfuseTraceMetadata = {};\n for (const [key, value] of Object.entries(metadata)) {\n if (value == null) {\n continue;\n }\n const stringValue = typeof value === 'string' ? value : String(value);\n if (\n stringValue.trim() === '' ||\n stringValue.length > TRACE_METADATA_MAX_LENGTH\n ) {\n continue;\n }\n traceMetadata[key] = stringValue;\n }\n return traceMetadata;\n}\n\nexport function createLibreChatTraceAttributes(\n attributes: LangfuseConfigTraceAttributes\n): LangfuseTraceAttributes {\n const librechatTraceAttributes: LangfuseTraceAttributes = {};\n for (const [key, value] of Object.entries(attributes)) {\n if (value == null || key.trim() === '') {\n continue;\n }\n if (typeof value === 'string') {\n if (value.trim() === '' || value.length > TRACE_METADATA_MAX_LENGTH) {\n continue;\n }\n librechatTraceAttributes[key] = value;\n continue;\n }\n librechatTraceAttributes[key] = value;\n }\n return librechatTraceAttributes;\n}\n\nexport function createLangfuseTraceMetadata({\n messageId,\n parentMessageId,\n agentId,\n agentName,\n}: {\n messageId?: unknown;\n parentMessageId?: unknown;\n agentId?: unknown;\n agentName?: unknown;\n}): LangfuseTraceMetadata {\n return createTraceMetadata({\n messageId,\n parentMessageId,\n agentId,\n agentName,\n });\n}\n\nfunction mergeLangfuseTraceMetadata(\n traceMetadata?: LangfuseTraceMetadata,\n metadata?: LangfuseMetadata\n): LangfuseTraceMetadata | undefined {\n const merged = createTraceMetadata({\n ...(metadata ?? {}),\n ...(traceMetadata ?? {}),\n });\n return Object.keys(merged).length > 0 ? merged : undefined;\n}\n\nfunction mergeLangfuseTags(\n tags?: string[],\n configTags?: string[]\n): string[] | undefined {\n const merged = [...(tags ?? []), ...(configTags ?? [])].filter(\n (tag) => tag.trim() !== ''\n );\n return merged.length > 0 ? [...new Set(merged)] : undefined;\n}\n\nexport function getLangfuseTraceName(\n traceMetadata?: LangfuseTraceMetadata,\n fallback: string = 'LibreChat Agent'\n): string {\n const agentName = traceMetadata?.agentName;\n return isPresent(agentName) ? `${fallback}: ${agentName}` : fallback;\n}\n\nexport function hasLangfuseEnvConfig(): boolean {\n return hasLangfuseEnvCredentials();\n}\n\nexport function hasLangfuseEnvCredentials(): boolean {\n return (\n isPresent(process.env.LANGFUSE_SECRET_KEY) &&\n isPresent(process.env.LANGFUSE_PUBLIC_KEY)\n );\n}\n\nexport function shouldCreateLangfuseHandler(\n langfuse?: t.LangfuseConfig\n): boolean {\n if (langfuse?.enabled === false) {\n return false;\n }\n return (\n hasLangfuseEnvConfig() ||\n hasLangfuseConfigCredentials(langfuse) ||\n (hasLangfuseConfigBaseUrl(langfuse) && hasLangfuseEnvCredentials())\n );\n}\n\nexport function createLegacyLangfuseHandler(\n params: LangfuseHandlerParams\n): CallbackHandler {\n return new ScopedLangfuseCallbackHandler(params);\n}\n\nexport function createLangfuseHandler({\n langfuse,\n userId,\n sessionId,\n traceMetadata,\n tags,\n traceIdSeed,\n}: AgentLangfuseHandlerParams): CallbackHandler | undefined {\n if (!shouldCreateLangfuseHandler(langfuse)) {\n return undefined;\n }\n return new ScopedLangfuseCallbackHandler({\n userId,\n sessionId,\n traceMetadata: mergeLangfuseTraceMetadata(\n traceMetadata,\n langfuse?.metadata\n ),\n tags: mergeLangfuseTags(tags, langfuse?.tags),\n langfuse,\n traceIdSeed,\n });\n}\n\nfunction createPropagateAttributeParams({\n langfuse,\n userId,\n sessionId,\n traceMetadata,\n traceName,\n tags,\n}: LangfuseAttributeParams): PropagateAttributesParams {\n return {\n userId,\n sessionId,\n traceName,\n tags: mergeLangfuseTags(tags, langfuse?.tags),\n metadata: mergeLangfuseTraceMetadata(traceMetadata, langfuse?.metadata),\n };\n}\n\nexport function withLangfuseAttributes<T>(\n params: LangfuseAttributeParams,\n action: () => T\n): T {\n if (!shouldCreateLangfuseHandler(params.langfuse)) {\n return action();\n }\n return propagateAttributes(createPropagateAttributeParams(params), action);\n}\n\nexport function hasExplicitLangfuseConfig(\n contexts: Iterable<{ langfuse?: t.LangfuseConfig }>\n): boolean {\n for (const context of contexts) {\n if (isExplicitLangfuseConfig(context.langfuse)) {\n return true;\n }\n }\n return false;\n}\n\nexport function isLangfuseCallbackHandler(value: unknown): boolean {\n return value instanceof CallbackHandler;\n}\n\nexport async function disposeLangfuseHandler(value: unknown): Promise<void> {\n if (\n value == null ||\n parseBooleanEnv(process.env[LANGFUSE_FORCE_FLUSH_ON_DISPOSE]) !== true\n ) {\n return;\n }\n const provider = getLangfuseTracerProvider() as FlushableTracerProvider;\n await provider.forceFlush?.();\n}\n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { isPresent, parseBooleanEnv } from "./utils/misc.mjs";
2
2
  import { resolveLangfuseConfigForSpan, resolveTraceIdSeedForSpan, withLangfuseRuntimeScope } from "./langfuseRuntimeScope.mjs";
3
3
  import { AIMessage, AIMessageChunk } from "@langchain/core/messages";
4
+ import { isParentCommand } from "@langchain/langgraph";
4
5
  import { CallbackHandler } from "@langfuse/langchain";
5
6
  import { context } from "@opentelemetry/api";
6
7
  import { getLangfuseTracerProvider, propagateAttributes } from "@langfuse/tracing";
@@ -88,6 +89,11 @@ var ScopedLangfuseCallbackHandler = class extends CallbackHandler {
88
89
  handleChainStart(...args) {
89
90
  return this.withRuntimeContext(() => super.handleChainStart(...args));
90
91
  }
92
+ handleChainError(...args) {
93
+ const [error, runId, parentRunId] = args;
94
+ if (error != null && parentRunId != null && isParentCommand(error)) return super.handleChainEnd({ controlFlow: "ParentCommand" }, runId, parentRunId);
95
+ return super.handleChainError(...args);
96
+ }
91
97
  handleAgentAction(...args) {
92
98
  return this.withRuntimeContext(() => super.handleAgentAction(...args));
93
99
  }
@@ -1 +1 @@
1
- {"version":3,"file":"langfuse.mjs","names":["otelContext"],"sources":["../../src/langfuse.ts"],"sourcesContent":["import { CallbackHandler } from '@langfuse/langchain';\nimport { context as otelContext } from '@opentelemetry/api';\nimport { AIMessage, AIMessageChunk } from '@langchain/core/messages';\nimport {\n getLangfuseTracerProvider,\n propagateAttributes,\n} from '@langfuse/tracing';\nimport type {\n AIMessageChunkFields,\n AIMessageFields,\n UsageMetadata,\n} from '@langchain/core/messages';\nimport type {\n ChatGeneration,\n Generation,\n LLMResult,\n} from '@langchain/core/outputs';\nimport type { PropagateAttributesParams } from '@langfuse/tracing';\nimport type * as t from '@/types';\nimport {\n resolveLangfuseConfigForSpan,\n resolveTraceIdSeedForSpan,\n withLangfuseRuntimeScope,\n} from '@/langfuseRuntimeScope';\nimport { isPresent, parseBooleanEnv } from '@/utils/misc';\n\nconst TRACE_METADATA_MAX_LENGTH = 200;\nconst LANGFUSE_FORCE_FLUSH_ON_DISPOSE = 'LANGFUSE_FORCE_FLUSH_ON_DISPOSE';\n\nexport type LangfuseTraceMetadata = Record<string, string>;\nexport type LangfuseTraceAttributes = Record<string, string | number | boolean>;\ntype LangfuseMetadata = NonNullable<t.LangfuseConfig['metadata']>;\ntype LangfuseConfigTraceAttributes = NonNullable<\n t.LangfuseConfig['librechatTraceAttributes']\n>;\n\ntype LangfuseHandlerParams = {\n userId?: string;\n sessionId?: string;\n traceMetadata?: LangfuseTraceMetadata;\n tags?: string[];\n traceIdSeed?: string;\n};\n\ntype AgentLangfuseHandlerParams = LangfuseHandlerParams & {\n langfuse?: t.LangfuseConfig;\n};\n\ntype LangfuseAttributeParams = AgentLangfuseHandlerParams & {\n traceName?: string;\n};\n\ntype FlushableTracerProvider = {\n forceFlush?: () => Promise<void> | void;\n};\n\ntype BedrockResponseUsage = {\n inputTokens?: number;\n cacheReadInputTokens?: number;\n cacheWriteInputTokens?: number;\n};\n\ntype BedrockResponseMetadata = {\n metadata?: {\n usage?: BedrockResponseUsage;\n };\n};\n\nfunction getLangfuseBedrockUsage(\n message: AIMessage | AIMessageChunk\n): UsageMetadata | undefined {\n const usageMetadata = message.usage_metadata;\n const bedrockUsage = (message.response_metadata as BedrockResponseMetadata)\n .metadata?.usage;\n if (\n usageMetadata == null ||\n bedrockUsage == null ||\n usageMetadata.input_tokens !== bedrockUsage.inputTokens\n ) {\n return usageMetadata;\n }\n\n const cacheRead = bedrockUsage.cacheReadInputTokens ?? 0;\n const cacheCreation = bedrockUsage.cacheWriteInputTokens ?? 0;\n if (cacheRead === 0 && cacheCreation === 0) {\n return usageMetadata;\n }\n\n return {\n ...usageMetadata,\n input_tokens: usageMetadata.input_tokens + cacheRead + cacheCreation,\n };\n}\n\nfunction cloneMessageWithUsage(\n message: AIMessage | AIMessageChunk,\n usageMetadata: UsageMetadata\n): AIMessage | AIMessageChunk {\n const fields: AIMessageFields = {\n content: message.content,\n additional_kwargs: message.additional_kwargs,\n response_metadata: message.response_metadata,\n id: message.id,\n name: message.name,\n tool_calls: message.tool_calls,\n invalid_tool_calls: message.invalid_tool_calls,\n usage_metadata: usageMetadata,\n };\n\n if (message instanceof AIMessageChunk) {\n const chunkFields: AIMessageChunkFields = {\n ...fields,\n tool_call_chunks: message.tool_call_chunks,\n };\n return new AIMessageChunk(chunkFields);\n }\n\n return new AIMessage(fields);\n}\n\nfunction normalizeGenerationForLangfuse(generation: Generation): Generation {\n if (!('message' in generation)) {\n return generation;\n }\n\n const message = (generation as ChatGeneration).message;\n if (!(message instanceof AIMessage || message instanceof AIMessageChunk)) {\n return generation;\n }\n\n const usageMetadata = getLangfuseBedrockUsage(message);\n if (usageMetadata == null || usageMetadata === message.usage_metadata) {\n return generation;\n }\n\n const chatGeneration: ChatGeneration = {\n ...(generation as ChatGeneration),\n message: cloneMessageWithUsage(message, usageMetadata),\n };\n return chatGeneration;\n}\n\nfunction normalizeBedrockUsageForLangfuse(output: LLMResult): LLMResult {\n if (output.generations.length === 0) {\n return output;\n }\n\n const listIndex = output.generations.length - 1;\n const generationList = output.generations[listIndex];\n if (generationList.length === 0) {\n return output;\n }\n\n const generationIndex = generationList.length - 1;\n const generation = generationList[generationIndex];\n const normalized = normalizeGenerationForLangfuse(generation);\n if (normalized === generation) {\n return output;\n }\n\n const generations = [...output.generations];\n generations[listIndex] = [...generationList];\n generations[listIndex][generationIndex] = normalized;\n return { ...output, generations };\n}\n\nclass ScopedLangfuseCallbackHandler extends CallbackHandler {\n private readonly langfuse?: t.LangfuseConfig;\n private readonly traceIdSeed?: string;\n\n constructor(params?: AgentLangfuseHandlerParams) {\n const { langfuse, traceIdSeed, ...handlerParams } = params ?? {};\n super(handlerParams);\n this.langfuse = langfuse;\n this.traceIdSeed = traceIdSeed;\n }\n\n private getDeterministicTraceSeed(): string | undefined {\n return this.langfuse?.deterministicTraceId === true\n ? this.traceIdSeed\n : undefined;\n }\n\n private withRuntimeContext<T>(action: () => T): T {\n const activeContext = otelContext.active();\n const langfuse =\n resolveLangfuseConfigForSpan(activeContext) ?? this.langfuse;\n const seed = this.getDeterministicTraceSeed();\n return withLangfuseRuntimeScope(\n {\n langfuse,\n traceIdSeed: resolveTraceIdSeedForSpan(activeContext) ?? seed,\n },\n action\n );\n }\n\n // LangChain may invoke callback handlers outside the caller's OTEL context.\n // Re-enter tenant scope only for callbacks that start Langfuse observations;\n // end/error/token callbacks use spans already bound to a processor at start.\n override handleChainStart(\n ...args: Parameters<CallbackHandler['handleChainStart']>\n ): ReturnType<CallbackHandler['handleChainStart']> {\n return this.withRuntimeContext(() => super.handleChainStart(...args));\n }\n\n override handleAgentAction(\n ...args: Parameters<CallbackHandler['handleAgentAction']>\n ): ReturnType<CallbackHandler['handleAgentAction']> {\n return this.withRuntimeContext(() => super.handleAgentAction(...args));\n }\n\n override handleGenerationStart(\n ...args: Parameters<CallbackHandler['handleGenerationStart']>\n ): ReturnType<CallbackHandler['handleGenerationStart']> {\n return this.withRuntimeContext(() => super.handleGenerationStart(...args));\n }\n\n override handleChatModelStart(\n ...args: Parameters<CallbackHandler['handleChatModelStart']>\n ): ReturnType<CallbackHandler['handleChatModelStart']> {\n return this.withRuntimeContext(() => super.handleChatModelStart(...args));\n }\n\n override handleLLMStart(\n ...args: Parameters<CallbackHandler['handleLLMStart']>\n ): ReturnType<CallbackHandler['handleLLMStart']> {\n return this.withRuntimeContext(() => super.handleLLMStart(...args));\n }\n\n override handleLLMEnd(\n output: LLMResult,\n runId: string,\n parentRunId?: string\n ): Promise<void> {\n return super.handleLLMEnd(\n normalizeBedrockUsageForLangfuse(output),\n runId,\n parentRunId\n );\n }\n\n override handleToolStart(\n ...args: Parameters<CallbackHandler['handleToolStart']>\n ): ReturnType<CallbackHandler['handleToolStart']> {\n return this.withRuntimeContext(() => super.handleToolStart(...args));\n }\n\n override handleRetrieverStart(\n ...args: Parameters<CallbackHandler['handleRetrieverStart']>\n ): ReturnType<CallbackHandler['handleRetrieverStart']> {\n return this.withRuntimeContext(() => super.handleRetrieverStart(...args));\n }\n}\n\nfunction hasLangfuseTracingConfig(langfuse?: t.LangfuseConfig): boolean {\n return (\n langfuse?.toolNodeTracing != null || langfuse?.toolOutputTracing != null\n );\n}\n\nfunction hasLangfuseTraceAttributes(langfuse?: t.LangfuseConfig): boolean {\n return (\n Object.keys(createTraceMetadata(langfuse?.metadata ?? {})).length > 0 ||\n Object.keys(\n createLibreChatTraceAttributes(langfuse?.librechatTraceAttributes ?? {})\n ).length > 0 ||\n (mergeLangfuseTags(undefined, langfuse?.tags)?.length ?? 0) > 0\n );\n}\n\nexport function hasLangfuseConfigCredentials(\n langfuse?: t.LangfuseConfig\n): langfuse is t.LangfuseConfig & {\n publicKey: string;\n secretKey: string;\n} {\n return (\n langfuse != null &&\n isPresent(langfuse.publicKey) &&\n isPresent(langfuse.secretKey)\n );\n}\n\nfunction hasLangfuseConfigBaseUrl(langfuse?: t.LangfuseConfig): boolean {\n return isPresent(langfuse?.baseUrl);\n}\n\nexport function isExplicitLangfuseConfig(langfuse?: t.LangfuseConfig): boolean {\n return (\n langfuse?.enabled != null ||\n isPresent(langfuse?.publicKey) ||\n isPresent(langfuse?.secretKey) ||\n isPresent(langfuse?.baseUrl) ||\n hasLangfuseTraceAttributes(langfuse) ||\n hasLangfuseTracingConfig(langfuse)\n );\n}\n\nfunction createTraceMetadata(\n metadata: Record<string, unknown>\n): LangfuseTraceMetadata {\n const traceMetadata: LangfuseTraceMetadata = {};\n for (const [key, value] of Object.entries(metadata)) {\n if (value == null) {\n continue;\n }\n const stringValue = typeof value === 'string' ? value : String(value);\n if (\n stringValue.trim() === '' ||\n stringValue.length > TRACE_METADATA_MAX_LENGTH\n ) {\n continue;\n }\n traceMetadata[key] = stringValue;\n }\n return traceMetadata;\n}\n\nexport function createLibreChatTraceAttributes(\n attributes: LangfuseConfigTraceAttributes\n): LangfuseTraceAttributes {\n const librechatTraceAttributes: LangfuseTraceAttributes = {};\n for (const [key, value] of Object.entries(attributes)) {\n if (value == null || key.trim() === '') {\n continue;\n }\n if (typeof value === 'string') {\n if (value.trim() === '' || value.length > TRACE_METADATA_MAX_LENGTH) {\n continue;\n }\n librechatTraceAttributes[key] = value;\n continue;\n }\n librechatTraceAttributes[key] = value;\n }\n return librechatTraceAttributes;\n}\n\nexport function createLangfuseTraceMetadata({\n messageId,\n parentMessageId,\n agentId,\n agentName,\n}: {\n messageId?: unknown;\n parentMessageId?: unknown;\n agentId?: unknown;\n agentName?: unknown;\n}): LangfuseTraceMetadata {\n return createTraceMetadata({\n messageId,\n parentMessageId,\n agentId,\n agentName,\n });\n}\n\nfunction mergeLangfuseTraceMetadata(\n traceMetadata?: LangfuseTraceMetadata,\n metadata?: LangfuseMetadata\n): LangfuseTraceMetadata | undefined {\n const merged = createTraceMetadata({\n ...(metadata ?? {}),\n ...(traceMetadata ?? {}),\n });\n return Object.keys(merged).length > 0 ? merged : undefined;\n}\n\nfunction mergeLangfuseTags(\n tags?: string[],\n configTags?: string[]\n): string[] | undefined {\n const merged = [...(tags ?? []), ...(configTags ?? [])].filter(\n (tag) => tag.trim() !== ''\n );\n return merged.length > 0 ? [...new Set(merged)] : undefined;\n}\n\nexport function getLangfuseTraceName(\n traceMetadata?: LangfuseTraceMetadata,\n fallback: string = 'LibreChat Agent'\n): string {\n const agentName = traceMetadata?.agentName;\n return isPresent(agentName) ? `${fallback}: ${agentName}` : fallback;\n}\n\nexport function hasLangfuseEnvConfig(): boolean {\n return hasLangfuseEnvCredentials();\n}\n\nexport function hasLangfuseEnvCredentials(): boolean {\n return (\n isPresent(process.env.LANGFUSE_SECRET_KEY) &&\n isPresent(process.env.LANGFUSE_PUBLIC_KEY)\n );\n}\n\nexport function shouldCreateLangfuseHandler(\n langfuse?: t.LangfuseConfig\n): boolean {\n if (langfuse?.enabled === false) {\n return false;\n }\n return (\n hasLangfuseEnvConfig() ||\n hasLangfuseConfigCredentials(langfuse) ||\n (hasLangfuseConfigBaseUrl(langfuse) && hasLangfuseEnvCredentials())\n );\n}\n\nexport function createLegacyLangfuseHandler(\n params: LangfuseHandlerParams\n): CallbackHandler {\n return new ScopedLangfuseCallbackHandler(params);\n}\n\nexport function createLangfuseHandler({\n langfuse,\n userId,\n sessionId,\n traceMetadata,\n tags,\n traceIdSeed,\n}: AgentLangfuseHandlerParams): CallbackHandler | undefined {\n if (!shouldCreateLangfuseHandler(langfuse)) {\n return undefined;\n }\n return new ScopedLangfuseCallbackHandler({\n userId,\n sessionId,\n traceMetadata: mergeLangfuseTraceMetadata(\n traceMetadata,\n langfuse?.metadata\n ),\n tags: mergeLangfuseTags(tags, langfuse?.tags),\n langfuse,\n traceIdSeed,\n });\n}\n\nfunction createPropagateAttributeParams({\n langfuse,\n userId,\n sessionId,\n traceMetadata,\n traceName,\n tags,\n}: LangfuseAttributeParams): PropagateAttributesParams {\n return {\n userId,\n sessionId,\n traceName,\n tags: mergeLangfuseTags(tags, langfuse?.tags),\n metadata: mergeLangfuseTraceMetadata(traceMetadata, langfuse?.metadata),\n };\n}\n\nexport function withLangfuseAttributes<T>(\n params: LangfuseAttributeParams,\n action: () => T\n): T {\n if (!shouldCreateLangfuseHandler(params.langfuse)) {\n return action();\n }\n return propagateAttributes(createPropagateAttributeParams(params), action);\n}\n\nexport function hasExplicitLangfuseConfig(\n contexts: Iterable<{ langfuse?: t.LangfuseConfig }>\n): boolean {\n for (const context of contexts) {\n if (isExplicitLangfuseConfig(context.langfuse)) {\n return true;\n }\n }\n return false;\n}\n\nexport function isLangfuseCallbackHandler(value: unknown): boolean {\n return value instanceof CallbackHandler;\n}\n\nexport async function disposeLangfuseHandler(value: unknown): Promise<void> {\n if (\n value == null ||\n parseBooleanEnv(process.env[LANGFUSE_FORCE_FLUSH_ON_DISPOSE]) !== true\n ) {\n return;\n }\n const provider = getLangfuseTracerProvider() as FlushableTracerProvider;\n await provider.forceFlush?.();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"langfuse.mjs","names":["otelContext"],"sources":["../../src/langfuse.ts"],"sourcesContent":["import { CallbackHandler } from '@langfuse/langchain';\nimport { isParentCommand } from '@langchain/langgraph';\nimport { context as otelContext } from '@opentelemetry/api';\nimport { AIMessage, AIMessageChunk } from '@langchain/core/messages';\nimport {\n getLangfuseTracerProvider,\n propagateAttributes,\n} from '@langfuse/tracing';\nimport type {\n AIMessageChunkFields,\n AIMessageFields,\n UsageMetadata,\n} from '@langchain/core/messages';\nimport type {\n ChatGeneration,\n Generation,\n LLMResult,\n} from '@langchain/core/outputs';\nimport type { PropagateAttributesParams } from '@langfuse/tracing';\nimport type * as t from '@/types';\nimport {\n resolveLangfuseConfigForSpan,\n resolveTraceIdSeedForSpan,\n withLangfuseRuntimeScope,\n} from '@/langfuseRuntimeScope';\nimport { isPresent, parseBooleanEnv } from '@/utils/misc';\n\nconst TRACE_METADATA_MAX_LENGTH = 200;\nconst LANGFUSE_FORCE_FLUSH_ON_DISPOSE = 'LANGFUSE_FORCE_FLUSH_ON_DISPOSE';\n\nexport type LangfuseTraceMetadata = Record<string, string>;\nexport type LangfuseTraceAttributes = Record<string, string | number | boolean>;\ntype LangfuseMetadata = NonNullable<t.LangfuseConfig['metadata']>;\ntype LangfuseConfigTraceAttributes = NonNullable<\n t.LangfuseConfig['librechatTraceAttributes']\n>;\n\ntype LangfuseHandlerParams = {\n userId?: string;\n sessionId?: string;\n traceMetadata?: LangfuseTraceMetadata;\n tags?: string[];\n traceIdSeed?: string;\n};\n\ntype AgentLangfuseHandlerParams = LangfuseHandlerParams & {\n langfuse?: t.LangfuseConfig;\n};\n\ntype LangfuseAttributeParams = AgentLangfuseHandlerParams & {\n traceName?: string;\n};\n\ntype FlushableTracerProvider = {\n forceFlush?: () => Promise<void> | void;\n};\n\ntype BedrockResponseUsage = {\n inputTokens?: number;\n cacheReadInputTokens?: number;\n cacheWriteInputTokens?: number;\n};\n\ntype BedrockResponseMetadata = {\n metadata?: {\n usage?: BedrockResponseUsage;\n };\n};\n\nfunction getLangfuseBedrockUsage(\n message: AIMessage | AIMessageChunk\n): UsageMetadata | undefined {\n const usageMetadata = message.usage_metadata;\n const bedrockUsage = (message.response_metadata as BedrockResponseMetadata)\n .metadata?.usage;\n if (\n usageMetadata == null ||\n bedrockUsage == null ||\n usageMetadata.input_tokens !== bedrockUsage.inputTokens\n ) {\n return usageMetadata;\n }\n\n const cacheRead = bedrockUsage.cacheReadInputTokens ?? 0;\n const cacheCreation = bedrockUsage.cacheWriteInputTokens ?? 0;\n if (cacheRead === 0 && cacheCreation === 0) {\n return usageMetadata;\n }\n\n return {\n ...usageMetadata,\n input_tokens: usageMetadata.input_tokens + cacheRead + cacheCreation,\n };\n}\n\nfunction cloneMessageWithUsage(\n message: AIMessage | AIMessageChunk,\n usageMetadata: UsageMetadata\n): AIMessage | AIMessageChunk {\n const fields: AIMessageFields = {\n content: message.content,\n additional_kwargs: message.additional_kwargs,\n response_metadata: message.response_metadata,\n id: message.id,\n name: message.name,\n tool_calls: message.tool_calls,\n invalid_tool_calls: message.invalid_tool_calls,\n usage_metadata: usageMetadata,\n };\n\n if (message instanceof AIMessageChunk) {\n const chunkFields: AIMessageChunkFields = {\n ...fields,\n tool_call_chunks: message.tool_call_chunks,\n };\n return new AIMessageChunk(chunkFields);\n }\n\n return new AIMessage(fields);\n}\n\nfunction normalizeGenerationForLangfuse(generation: Generation): Generation {\n if (!('message' in generation)) {\n return generation;\n }\n\n const message = (generation as ChatGeneration).message;\n if (!(message instanceof AIMessage || message instanceof AIMessageChunk)) {\n return generation;\n }\n\n const usageMetadata = getLangfuseBedrockUsage(message);\n if (usageMetadata == null || usageMetadata === message.usage_metadata) {\n return generation;\n }\n\n const chatGeneration: ChatGeneration = {\n ...(generation as ChatGeneration),\n message: cloneMessageWithUsage(message, usageMetadata),\n };\n return chatGeneration;\n}\n\nfunction normalizeBedrockUsageForLangfuse(output: LLMResult): LLMResult {\n if (output.generations.length === 0) {\n return output;\n }\n\n const listIndex = output.generations.length - 1;\n const generationList = output.generations[listIndex];\n if (generationList.length === 0) {\n return output;\n }\n\n const generationIndex = generationList.length - 1;\n const generation = generationList[generationIndex];\n const normalized = normalizeGenerationForLangfuse(generation);\n if (normalized === generation) {\n return output;\n }\n\n const generations = [...output.generations];\n generations[listIndex] = [...generationList];\n generations[listIndex][generationIndex] = normalized;\n return { ...output, generations };\n}\n\nclass ScopedLangfuseCallbackHandler extends CallbackHandler {\n private readonly langfuse?: t.LangfuseConfig;\n private readonly traceIdSeed?: string;\n\n constructor(params?: AgentLangfuseHandlerParams) {\n const { langfuse, traceIdSeed, ...handlerParams } = params ?? {};\n super(handlerParams);\n this.langfuse = langfuse;\n this.traceIdSeed = traceIdSeed;\n }\n\n private getDeterministicTraceSeed(): string | undefined {\n return this.langfuse?.deterministicTraceId === true\n ? this.traceIdSeed\n : undefined;\n }\n\n private withRuntimeContext<T>(action: () => T): T {\n const activeContext = otelContext.active();\n const langfuse =\n resolveLangfuseConfigForSpan(activeContext) ?? this.langfuse;\n const seed = this.getDeterministicTraceSeed();\n return withLangfuseRuntimeScope(\n {\n langfuse,\n traceIdSeed: resolveTraceIdSeedForSpan(activeContext) ?? seed,\n },\n action\n );\n }\n\n // LangChain may invoke callback handlers outside the caller's OTEL context.\n // Re-enter tenant scope only for callbacks that start Langfuse observations;\n // end/error/token callbacks use spans already bound to a processor at start.\n override handleChainStart(\n ...args: Parameters<CallbackHandler['handleChainStart']>\n ): ReturnType<CallbackHandler['handleChainStart']> {\n return this.withRuntimeContext(() => super.handleChainStart(...args));\n }\n\n override handleChainError(\n ...args: Parameters<CallbackHandler['handleChainError']>\n ): ReturnType<CallbackHandler['handleChainError']> {\n const [error, runId, parentRunId] = args;\n if (error != null && parentRunId != null && isParentCommand(error)) {\n return super.handleChainEnd(\n { controlFlow: 'ParentCommand' },\n runId,\n parentRunId\n );\n }\n return super.handleChainError(...args);\n }\n\n override handleAgentAction(\n ...args: Parameters<CallbackHandler['handleAgentAction']>\n ): ReturnType<CallbackHandler['handleAgentAction']> {\n return this.withRuntimeContext(() => super.handleAgentAction(...args));\n }\n\n override handleGenerationStart(\n ...args: Parameters<CallbackHandler['handleGenerationStart']>\n ): ReturnType<CallbackHandler['handleGenerationStart']> {\n return this.withRuntimeContext(() => super.handleGenerationStart(...args));\n }\n\n override handleChatModelStart(\n ...args: Parameters<CallbackHandler['handleChatModelStart']>\n ): ReturnType<CallbackHandler['handleChatModelStart']> {\n return this.withRuntimeContext(() => super.handleChatModelStart(...args));\n }\n\n override handleLLMStart(\n ...args: Parameters<CallbackHandler['handleLLMStart']>\n ): ReturnType<CallbackHandler['handleLLMStart']> {\n return this.withRuntimeContext(() => super.handleLLMStart(...args));\n }\n\n override handleLLMEnd(\n output: LLMResult,\n runId: string,\n parentRunId?: string\n ): Promise<void> {\n return super.handleLLMEnd(\n normalizeBedrockUsageForLangfuse(output),\n runId,\n parentRunId\n );\n }\n\n override handleToolStart(\n ...args: Parameters<CallbackHandler['handleToolStart']>\n ): ReturnType<CallbackHandler['handleToolStart']> {\n return this.withRuntimeContext(() => super.handleToolStart(...args));\n }\n\n override handleRetrieverStart(\n ...args: Parameters<CallbackHandler['handleRetrieverStart']>\n ): ReturnType<CallbackHandler['handleRetrieverStart']> {\n return this.withRuntimeContext(() => super.handleRetrieverStart(...args));\n }\n}\n\nfunction hasLangfuseTracingConfig(langfuse?: t.LangfuseConfig): boolean {\n return (\n langfuse?.toolNodeTracing != null || langfuse?.toolOutputTracing != null\n );\n}\n\nfunction hasLangfuseTraceAttributes(langfuse?: t.LangfuseConfig): boolean {\n return (\n Object.keys(createTraceMetadata(langfuse?.metadata ?? {})).length > 0 ||\n Object.keys(\n createLibreChatTraceAttributes(langfuse?.librechatTraceAttributes ?? {})\n ).length > 0 ||\n (mergeLangfuseTags(undefined, langfuse?.tags)?.length ?? 0) > 0\n );\n}\n\nexport function hasLangfuseConfigCredentials(\n langfuse?: t.LangfuseConfig\n): langfuse is t.LangfuseConfig & {\n publicKey: string;\n secretKey: string;\n} {\n return (\n langfuse != null &&\n isPresent(langfuse.publicKey) &&\n isPresent(langfuse.secretKey)\n );\n}\n\nfunction hasLangfuseConfigBaseUrl(langfuse?: t.LangfuseConfig): boolean {\n return isPresent(langfuse?.baseUrl);\n}\n\nexport function isExplicitLangfuseConfig(langfuse?: t.LangfuseConfig): boolean {\n return (\n langfuse?.enabled != null ||\n isPresent(langfuse?.publicKey) ||\n isPresent(langfuse?.secretKey) ||\n isPresent(langfuse?.baseUrl) ||\n hasLangfuseTraceAttributes(langfuse) ||\n hasLangfuseTracingConfig(langfuse)\n );\n}\n\nfunction createTraceMetadata(\n metadata: Record<string, unknown>\n): LangfuseTraceMetadata {\n const traceMetadata: LangfuseTraceMetadata = {};\n for (const [key, value] of Object.entries(metadata)) {\n if (value == null) {\n continue;\n }\n const stringValue = typeof value === 'string' ? value : String(value);\n if (\n stringValue.trim() === '' ||\n stringValue.length > TRACE_METADATA_MAX_LENGTH\n ) {\n continue;\n }\n traceMetadata[key] = stringValue;\n }\n return traceMetadata;\n}\n\nexport function createLibreChatTraceAttributes(\n attributes: LangfuseConfigTraceAttributes\n): LangfuseTraceAttributes {\n const librechatTraceAttributes: LangfuseTraceAttributes = {};\n for (const [key, value] of Object.entries(attributes)) {\n if (value == null || key.trim() === '') {\n continue;\n }\n if (typeof value === 'string') {\n if (value.trim() === '' || value.length > TRACE_METADATA_MAX_LENGTH) {\n continue;\n }\n librechatTraceAttributes[key] = value;\n continue;\n }\n librechatTraceAttributes[key] = value;\n }\n return librechatTraceAttributes;\n}\n\nexport function createLangfuseTraceMetadata({\n messageId,\n parentMessageId,\n agentId,\n agentName,\n}: {\n messageId?: unknown;\n parentMessageId?: unknown;\n agentId?: unknown;\n agentName?: unknown;\n}): LangfuseTraceMetadata {\n return createTraceMetadata({\n messageId,\n parentMessageId,\n agentId,\n agentName,\n });\n}\n\nfunction mergeLangfuseTraceMetadata(\n traceMetadata?: LangfuseTraceMetadata,\n metadata?: LangfuseMetadata\n): LangfuseTraceMetadata | undefined {\n const merged = createTraceMetadata({\n ...(metadata ?? {}),\n ...(traceMetadata ?? {}),\n });\n return Object.keys(merged).length > 0 ? merged : undefined;\n}\n\nfunction mergeLangfuseTags(\n tags?: string[],\n configTags?: string[]\n): string[] | undefined {\n const merged = [...(tags ?? []), ...(configTags ?? [])].filter(\n (tag) => tag.trim() !== ''\n );\n return merged.length > 0 ? [...new Set(merged)] : undefined;\n}\n\nexport function getLangfuseTraceName(\n traceMetadata?: LangfuseTraceMetadata,\n fallback: string = 'LibreChat Agent'\n): string {\n const agentName = traceMetadata?.agentName;\n return isPresent(agentName) ? `${fallback}: ${agentName}` : fallback;\n}\n\nexport function hasLangfuseEnvConfig(): boolean {\n return hasLangfuseEnvCredentials();\n}\n\nexport function hasLangfuseEnvCredentials(): boolean {\n return (\n isPresent(process.env.LANGFUSE_SECRET_KEY) &&\n isPresent(process.env.LANGFUSE_PUBLIC_KEY)\n );\n}\n\nexport function shouldCreateLangfuseHandler(\n langfuse?: t.LangfuseConfig\n): boolean {\n if (langfuse?.enabled === false) {\n return false;\n }\n return (\n hasLangfuseEnvConfig() ||\n hasLangfuseConfigCredentials(langfuse) ||\n (hasLangfuseConfigBaseUrl(langfuse) && hasLangfuseEnvCredentials())\n );\n}\n\nexport function createLegacyLangfuseHandler(\n params: LangfuseHandlerParams\n): CallbackHandler {\n return new ScopedLangfuseCallbackHandler(params);\n}\n\nexport function createLangfuseHandler({\n langfuse,\n userId,\n sessionId,\n traceMetadata,\n tags,\n traceIdSeed,\n}: AgentLangfuseHandlerParams): CallbackHandler | undefined {\n if (!shouldCreateLangfuseHandler(langfuse)) {\n return undefined;\n }\n return new ScopedLangfuseCallbackHandler({\n userId,\n sessionId,\n traceMetadata: mergeLangfuseTraceMetadata(\n traceMetadata,\n langfuse?.metadata\n ),\n tags: mergeLangfuseTags(tags, langfuse?.tags),\n langfuse,\n traceIdSeed,\n });\n}\n\nfunction createPropagateAttributeParams({\n langfuse,\n userId,\n sessionId,\n traceMetadata,\n traceName,\n tags,\n}: LangfuseAttributeParams): PropagateAttributesParams {\n return {\n userId,\n sessionId,\n traceName,\n tags: mergeLangfuseTags(tags, langfuse?.tags),\n metadata: mergeLangfuseTraceMetadata(traceMetadata, langfuse?.metadata),\n };\n}\n\nexport function withLangfuseAttributes<T>(\n params: LangfuseAttributeParams,\n action: () => T\n): T {\n if (!shouldCreateLangfuseHandler(params.langfuse)) {\n return action();\n }\n return propagateAttributes(createPropagateAttributeParams(params), action);\n}\n\nexport function hasExplicitLangfuseConfig(\n contexts: Iterable<{ langfuse?: t.LangfuseConfig }>\n): boolean {\n for (const context of contexts) {\n if (isExplicitLangfuseConfig(context.langfuse)) {\n return true;\n }\n }\n return false;\n}\n\nexport function isLangfuseCallbackHandler(value: unknown): boolean {\n return value instanceof CallbackHandler;\n}\n\nexport async function disposeLangfuseHandler(value: unknown): Promise<void> {\n if (\n value == null ||\n parseBooleanEnv(process.env[LANGFUSE_FORCE_FLUSH_ON_DISPOSE]) !== true\n ) {\n return;\n }\n const provider = getLangfuseTracerProvider() as FlushableTracerProvider;\n await provider.forceFlush?.();\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.3.1",
3
+ "version": "3.3.3",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -198,7 +198,8 @@
198
198
  "reinstall": "npm run clean && npm ci && rm -rf ./dist && npm run build",
199
199
  "re": "bun run clean && bun install && rm -rf ./dist && turbo build",
200
200
  "lint": "eslint \"{,!(node_modules|venv)/**/}*.{js,jsx,ts,tsx}\" --fix",
201
- "format": "prettier --write ."
201
+ "format": "prettier --write .",
202
+ "audit": "npm audit --omit=dev"
202
203
  },
203
204
  "overrides": {
204
205
  "@langchain/openai": "1.5.5",
@@ -208,14 +209,16 @@
208
209
  "uuid": "$uuid",
209
210
  "ajv": "6.14.0",
210
211
  "@opentelemetry/core": "^2.9.0",
211
- "js-yaml": "^4.3.0"
212
+ "js-yaml": "^4.3.0",
213
+ "minimatch": "^10.2.2",
214
+ "test-exclude": "^8.0.0"
212
215
  },
213
216
  "dependencies": {
214
- "@anthropic-ai/sdk": "^0.103.0",
217
+ "@anthropic-ai/sdk": "^0.115.0",
215
218
  "@aws-sdk/client-bedrock-runtime": "^3.1075.0",
216
- "@langchain/anthropic": "^1.5.1",
219
+ "@langchain/anthropic": "1.5.2",
217
220
  "@langchain/aws": "^1.4.2",
218
- "@langchain/core": "^1.2.2",
221
+ "@langchain/core": "^1.2.3",
219
222
  "@langchain/deepseek": "^1.1.3",
220
223
  "@langchain/google-common": "2.2.0",
221
224
  "@langchain/google-gauth": "2.2.0",
@@ -260,18 +263,19 @@
260
263
  "devDependencies": {
261
264
  "@anthropic-ai/sandbox-runtime": "^0.0.67",
262
265
  "@anthropic-ai/vertex-sdk": "^0.12.0",
263
- "@eslint/compat": "^1.2.7",
266
+ "@eslint/js": "^10.0.1",
264
267
  "@langchain/langgraph-checkpoint-mongodb": "^1.4.0",
265
268
  "@swc/core": "^1.6.13",
266
269
  "@types/jest": "^30.0.0",
267
270
  "@types/node": "^24.13.1",
268
271
  "@types/node-fetch": "^2.6.13",
269
272
  "@types/yargs-parser": "^21.0.3",
270
- "@typescript-eslint/eslint-plugin": "^8.24.0",
271
- "@typescript-eslint/parser": "^8.24.0",
272
- "eslint": "^9.39.5",
273
- "eslint-import-resolver-typescript": "^3.7.0",
274
- "eslint-plugin-import": "^2.31.0",
273
+ "@typescript-eslint/eslint-plugin": "^8.65.0",
274
+ "@typescript-eslint/parser": "^8.65.0",
275
+ "eslint": "^10.8.0",
276
+ "eslint-import-resolver-typescript": "^4.4.5",
277
+ "eslint-plugin-import-x": "^4.17.1",
278
+ "globals": "^17.8.0",
275
279
  "husky": "^9.1.7",
276
280
  "jest": "^30.2.0",
277
281
  "jest-util": "^30.2.0",
package/src/langfuse.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CallbackHandler } from '@langfuse/langchain';
2
+ import { isParentCommand } from '@langchain/langgraph';
2
3
  import { context as otelContext } from '@opentelemetry/api';
3
4
  import { AIMessage, AIMessageChunk } from '@langchain/core/messages';
4
5
  import {
@@ -204,6 +205,20 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
204
205
  return this.withRuntimeContext(() => super.handleChainStart(...args));
205
206
  }
206
207
 
208
+ override handleChainError(
209
+ ...args: Parameters<CallbackHandler['handleChainError']>
210
+ ): ReturnType<CallbackHandler['handleChainError']> {
211
+ const [error, runId, parentRunId] = args;
212
+ if (error != null && parentRunId != null && isParentCommand(error)) {
213
+ return super.handleChainEnd(
214
+ { controlFlow: 'ParentCommand' },
215
+ runId,
216
+ parentRunId
217
+ );
218
+ }
219
+ return super.handleChainError(...args);
220
+ }
221
+
207
222
  override handleAgentAction(
208
223
  ...args: Parameters<CallbackHandler['handleAgentAction']>
209
224
  ): ReturnType<CallbackHandler['handleAgentAction']> {
@@ -976,6 +976,7 @@ test('Anthropic message_delta usage preserves cumulative input totals', () => {
976
976
  output_tokens: 375,
977
977
  cache_creation_input_tokens: 11,
978
978
  cache_read_input_tokens: 13,
979
+ fallback_credit: null,
979
980
  server_tool_use: null,
980
981
  iterations: null,
981
982
  output_tokens_details: null,
@@ -1018,6 +1019,7 @@ test('Anthropic stream usage does not double-count cumulative input tokens', asy
1018
1019
  cache_creation: null,
1019
1020
  cache_creation_input_tokens: 20,
1020
1021
  cache_read_input_tokens: 30,
1022
+ fallback_credit: null,
1021
1023
  inference_geo: null,
1022
1024
  input_tokens: 243,
1023
1025
  iterations: null,
@@ -1043,6 +1045,7 @@ test('Anthropic stream usage does not double-count cumulative input tokens', asy
1043
1045
  output_tokens: 375,
1044
1046
  cache_creation_input_tokens: 20,
1045
1047
  cache_read_input_tokens: 30,
1048
+ fallback_credit: null,
1046
1049
  server_tool_use: null,
1047
1050
  iterations: null,
1048
1051
  output_tokens_details: null,
@@ -1089,6 +1092,7 @@ test('Anthropic stream usage accepts input first reported at message_delta', asy
1089
1092
  cache_creation: null,
1090
1093
  cache_creation_input_tokens: 0,
1091
1094
  cache_read_input_tokens: 0,
1095
+ fallback_credit: null,
1092
1096
  inference_geo: null,
1093
1097
  input_tokens: 0,
1094
1098
  iterations: null,
@@ -1114,6 +1118,7 @@ test('Anthropic stream usage accepts input first reported at message_delta', asy
1114
1118
  output_tokens: 42,
1115
1119
  cache_creation_input_tokens: 500,
1116
1120
  cache_read_input_tokens: 200,
1121
+ fallback_credit: null,
1117
1122
  server_tool_use: null,
1118
1123
  iterations: null,
1119
1124
  output_tokens_details: null,
@@ -1160,6 +1165,7 @@ test('Anthropic stream usage handles multiple cumulative message_delta events',
1160
1165
  cache_creation: null,
1161
1166
  cache_creation_input_tokens: 0,
1162
1167
  cache_read_input_tokens: 0,
1168
+ fallback_credit: null,
1163
1169
  inference_geo: null,
1164
1170
  input_tokens: 243,
1165
1171
  iterations: null,
@@ -1185,6 +1191,7 @@ test('Anthropic stream usage handles multiple cumulative message_delta events',
1185
1191
  output_tokens: 100,
1186
1192
  cache_creation_input_tokens: 0,
1187
1193
  cache_read_input_tokens: 0,
1194
+ fallback_credit: null,
1188
1195
  server_tool_use: null,
1189
1196
  iterations: null,
1190
1197
  output_tokens_details: null,
@@ -1204,6 +1211,7 @@ test('Anthropic stream usage handles multiple cumulative message_delta events',
1204
1211
  output_tokens: 375,
1205
1212
  cache_creation_input_tokens: 0,
1206
1213
  cache_read_input_tokens: 0,
1214
+ fallback_credit: null,
1207
1215
  server_tool_use: null,
1208
1216
  iterations: null,
1209
1217
  output_tokens_details: null,
@@ -1,11 +1,13 @@
1
1
  import { HumanMessage } from '@langchain/core/messages';
2
+ import { Command, ParentCommand } from '@langchain/langgraph';
2
3
  import { LangfuseOtelSpanAttributes } from '@langfuse/tracing';
3
4
  import { CallbackManager } from '@langchain/core/callbacks/manager';
4
5
  import { context as otelContext, trace as otelTrace } from '@opentelemetry/api';
6
+ import type { ToolCall } from '@langchain/core/messages/tool';
5
7
  import type * as t from '@/types';
6
8
  import { handleConverseStreamMetadata } from '@/llm/bedrock/utils/message_outputs';
7
9
  import { traceIdFromSeed } from '@/langfuseRuntimeContext';
8
- import { Providers } from '@/common';
10
+ import { Constants, Providers } from '@/common';
9
11
  import { Run } from '@/run';
10
12
 
11
13
  const mockProcessorStarts: Array<{
@@ -389,6 +391,206 @@ describe('Langfuse callback composition', () => {
389
391
  });
390
392
  });
391
393
 
394
+ it('ends nested ParentCommand control flow without reporting an error', async () => {
395
+ const { createLangfuseHandler } = await import('@/langfuse');
396
+ const { initializeLangfuseTracing } = await import('@/instrumentation');
397
+ const langfuse = {
398
+ publicKey: 'pk-test',
399
+ secretKey: 'sk-test',
400
+ };
401
+ initializeLangfuseTracing(langfuse);
402
+ const handler = createLangfuseHandler({ langfuse });
403
+ const runId = 'test-langfuse-parent-command';
404
+
405
+ await handler?.handleChainStart(
406
+ { lc: 1, type: 'not_implemented', id: ['HandoffChain'] },
407
+ { input: 'handoff' },
408
+ runId,
409
+ 'parent-run'
410
+ );
411
+ await handler?.handleChainError(
412
+ new ParentCommand(
413
+ new Command({ graph: 'source:graph', goto: 'destination' })
414
+ ),
415
+ runId,
416
+ 'parent-run'
417
+ );
418
+
419
+ expect(mockSpanAttributeSets).not.toContainEqual(
420
+ expect.objectContaining({
421
+ [LangfuseOtelSpanAttributes.OBSERVATION_LEVEL]: 'ERROR',
422
+ })
423
+ );
424
+ expect(mockSpanAttributeSets).toContainEqual(
425
+ expect.objectContaining({
426
+ [LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: JSON.stringify({
427
+ controlFlow: 'ParentCommand',
428
+ }),
429
+ })
430
+ );
431
+ });
432
+
433
+ it('reports a ParentCommand that escapes the root graph as an error', async () => {
434
+ const { createLangfuseHandler } = await import('@/langfuse');
435
+ const { initializeLangfuseTracing } = await import('@/instrumentation');
436
+ const langfuse = {
437
+ publicKey: 'pk-root-parent-command',
438
+ secretKey: 'sk-root-parent-command',
439
+ };
440
+ initializeLangfuseTracing(langfuse);
441
+ const handler = createLangfuseHandler({ langfuse });
442
+ const runId = 'test-langfuse-root-parent-command';
443
+
444
+ await handler?.handleChainStart(
445
+ { lc: 1, type: 'not_implemented', id: ['RootChain'] },
446
+ { input: 'root handoff' },
447
+ runId
448
+ );
449
+ await handler?.handleChainError(
450
+ new ParentCommand(
451
+ new Command({ graph: Command.PARENT, goto: 'destination' })
452
+ ),
453
+ runId
454
+ );
455
+
456
+ expect(mockSpanAttributeSets).toContainEqual(
457
+ expect.objectContaining({
458
+ [LangfuseOtelSpanAttributes.OBSERVATION_LEVEL]: 'ERROR',
459
+ [LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE]:
460
+ 'ParentCommand',
461
+ })
462
+ );
463
+ });
464
+
465
+ it('continues reporting genuine nested chain failures as errors', async () => {
466
+ const { createLangfuseHandler } = await import('@/langfuse');
467
+ const { initializeLangfuseTracing } = await import('@/instrumentation');
468
+ const langfuse = {
469
+ publicKey: 'pk-genuine-error',
470
+ secretKey: 'sk-genuine-error',
471
+ };
472
+ initializeLangfuseTracing(langfuse);
473
+ const handler = createLangfuseHandler({ langfuse });
474
+ const runId = 'test-langfuse-genuine-error';
475
+
476
+ await handler?.handleChainStart(
477
+ { lc: 1, type: 'not_implemented', id: ['FailingChain'] },
478
+ { input: 'fail' },
479
+ runId,
480
+ 'parent-run'
481
+ );
482
+ await handler?.handleChainError(new Error('boom'), runId, 'parent-run');
483
+
484
+ expect(mockSpanAttributeSets).toContainEqual(
485
+ expect.objectContaining({
486
+ [LangfuseOtelSpanAttributes.OBSERVATION_LEVEL]: 'ERROR',
487
+ [LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE]: 'Error: boom',
488
+ })
489
+ );
490
+ });
491
+
492
+ it('delegates null chain errors without throwing during classification', async () => {
493
+ const { createLangfuseHandler } = await import('@/langfuse');
494
+ const { initializeLangfuseTracing } = await import('@/instrumentation');
495
+ const langfuse = {
496
+ publicKey: 'pk-null-error',
497
+ secretKey: 'sk-null-error',
498
+ };
499
+ initializeLangfuseTracing(langfuse);
500
+ const handler = createLangfuseHandler({ langfuse });
501
+ const runId = 'test-langfuse-null-error';
502
+
503
+ await handler?.handleChainStart(
504
+ { lc: 1, type: 'not_implemented', id: ['NullErrorChain'] },
505
+ { input: 'fail' },
506
+ runId,
507
+ 'parent-run'
508
+ );
509
+
510
+ await expect(
511
+ handler?.handleChainError(null, runId, 'parent-run')
512
+ ).resolves.toBeUndefined();
513
+ });
514
+
515
+ it('traces a completed two-agent handoff without false error spans', async () => {
516
+ const langfuse = {
517
+ publicKey: 'pk-handoff',
518
+ secretKey: 'sk-handoff',
519
+ };
520
+ const run = await Run.create<t.IState>({
521
+ runId: 'test-langfuse-completed-handoff',
522
+ graphConfig: {
523
+ type: 'multi-agent',
524
+ agents: [
525
+ {
526
+ agentId: 'agent_source',
527
+ name: 'Source Agent',
528
+ provider: Providers.OPENAI,
529
+ clientOptions: { model: 'gpt-4' },
530
+ instructions: 'Transfer to the destination agent.',
531
+ tools: [],
532
+ },
533
+ {
534
+ agentId: 'agent_destination',
535
+ name: 'Destination Agent',
536
+ provider: Providers.OPENAI,
537
+ clientOptions: { model: 'gpt-4' },
538
+ instructions: 'Complete the request.',
539
+ tools: [],
540
+ },
541
+ ],
542
+ edges: [
543
+ {
544
+ from: 'agent_source',
545
+ to: 'agent_destination',
546
+ edgeType: 'handoff',
547
+ },
548
+ ],
549
+ },
550
+ langfuse,
551
+ skipCleanup: true,
552
+ });
553
+ run.Graph?.overrideTestModel(
554
+ ['Transferring to destination', 'Destination complete'],
555
+ 10,
556
+ [
557
+ {
558
+ id: 'handoff-call',
559
+ name: `${Constants.LC_TRANSFER_TO_}agent_destination`,
560
+ args: {},
561
+ } as ToolCall,
562
+ ]
563
+ );
564
+
565
+ await run.processStream(
566
+ { messages: [new HumanMessage('Please hand this off')] },
567
+ {
568
+ configurable: {
569
+ thread_id: 'test-langfuse-completed-handoff-thread',
570
+ },
571
+ version: 'v2',
572
+ }
573
+ );
574
+
575
+ expect(
576
+ run
577
+ .getRunMessages()
578
+ ?.some((message) => message.content === 'Destination complete')
579
+ ).toBe(true);
580
+ expect(mockSpanAttributeSets).toContainEqual(
581
+ expect.objectContaining({
582
+ [LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: JSON.stringify({
583
+ controlFlow: 'ParentCommand',
584
+ }),
585
+ })
586
+ );
587
+ expect(mockSpanAttributeSets).not.toContainEqual(
588
+ expect.objectContaining({
589
+ [LangfuseOtelSpanAttributes.OBSERVATION_LEVEL]: 'ERROR',
590
+ })
591
+ );
592
+ });
593
+
392
594
  it('exports Bedrock prompt-cache usage buckets to Langfuse', async () => {
393
595
  const { createLangfuseHandler } = await import('@/langfuse');
394
596
  const { initializeLangfuseTracing } = await import('@/instrumentation');