@librechat/agents 3.3.12 → 3.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/instrumentation.cjs +1 -0
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuseSpanRegistry.cjs +6 -3
- package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -1
- package/dist/esm/instrumentation.mjs +1 -0
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuseSpanRegistry.mjs +6 -3
- package/dist/esm/langfuseSpanRegistry.mjs.map +1 -1
- package/dist/types/types/graph.d.ts +5 -0
- package/package.json +1 -1
- package/src/instrumentation.ts +1 -0
- package/src/langfuseSpanRegistry.ts +9 -0
- package/src/specs/langfuse-instrumentation.test.ts +26 -0
- package/src/specs/langfuse-span-registry.test.ts +17 -0
- package/src/specs/summarization.test.ts +5 -2
- package/src/types/graph.ts +5 -0
|
@@ -48,6 +48,7 @@ function ensureOpenTelemetryContextManager() {
|
|
|
48
48
|
function getLangfuseProcessorCacheKey(destinationKey, langfuse) {
|
|
49
49
|
return JSON.stringify({
|
|
50
50
|
destinationKey,
|
|
51
|
+
mediaUploadEnabled: langfuse?.mediaUploadEnabled,
|
|
51
52
|
toolOutputTracing: langfuse?.toolOutputTracing
|
|
52
53
|
});
|
|
53
54
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instrumentation.cjs","names":["resolveTraceIdSeedForSpan","context","isPresent","traceIdFromSeed","ROOT_CONTEXT","AsyncLocalStorageContextManager","getLangfuseSpanProcessorParams","getLangfuseDestinationKey","createLangfuseSpanProcessor","resolveLangfuseConfigForSpan","createLibreChatTraceAttributes","BasicTracerProvider"],"sources":["../../src/instrumentation.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { setLangfuseTracerProvider } from '@langfuse/tracing';\nimport { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';\nimport { context, ROOT_CONTEXT, createContextKey } from '@opentelemetry/api';\nimport { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';\nimport type {\n IdGenerator,\n ReadableSpan,\n Span,\n SpanProcessor,\n} from '@opentelemetry/sdk-trace-base';\nimport type { LangfuseSpanProcessorParams } from '@langfuse/otel';\nimport type { Context } from '@opentelemetry/api';\nimport type * as t from '@/types';\nimport {\n getLangfuseDestinationKey,\n getLangfuseSpanProcessorParams,\n registerLangfuseManagedSpan,\n} from '@/langfuseSpanRegistry';\nimport {\n resolveLangfuseConfigForSpan,\n resolveTraceIdSeedForSpan,\n} from '@/langfuseRuntimeScope';\nimport { createLangfuseSpanProcessor } from '@/langfuseToolOutputTracing';\nimport { createLibreChatTraceAttributes } from '@/langfuse';\nimport { traceIdFromSeed } from '@/langfuseRuntimeContext';\nimport { isPresent } from '@/utils/misc';\n\n/**\n * Per-run seed for deterministic Langfuse trace ids. When a run opts in\n * (`LangfuseConfig.deterministicTraceId`), it executes its stream inside\n * `runWithTraceIdSeed(runId, ...)` from `./langfuseRuntimeContext`, and the\n * IdGenerator below derives the root trace id from that seed instead of a\n * random one. This lets external systems (e.g. a host app recording user\n * feedback after the fact) attach scores or observations to the trace by\n * regenerating the same id from the run/message id; no trace lookup required.\n * With no active seed it falls back to random ids, so default behavior is\n * unchanged.\n */\nclass SeededTraceIdGenerator implements IdGenerator {\n generateTraceId(): string {\n const seed = resolveTraceIdSeedForSpan(context.active());\n return isPresent(seed)\n ? traceIdFromSeed(seed)\n : randomBytes(16).toString('hex');\n }\n\n generateSpanId(): string {\n return randomBytes(8).toString('hex');\n }\n}\n\nlet langfuseTracerProvider: BasicTracerProvider | undefined;\nlet langfuseRoutingSpanProcessor: RoutingLangfuseSpanProcessor | undefined;\nconst contextManagerProbeKey = createContextKey(\n 'langfuse-context-manager-probe'\n);\n\nfunction hasActiveContextManager(): boolean {\n return context.with(\n ROOT_CONTEXT.setValue(contextManagerProbeKey, true),\n () => context.active().getValue(contextManagerProbeKey) === true\n );\n}\n\nexport function ensureOpenTelemetryContextManager(): void {\n if (hasActiveContextManager()) {\n return;\n }\n\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n if (!context.setGlobalContextManager(contextManager)) {\n contextManager.disable();\n }\n}\n\n/** Cache key for processor instances: the destination plus processor-level\n * policy (`toolOutputTracing` is baked into each processor's redaction\n * behavior), unlike the pure destination identity used for span parenting. */\nfunction getLangfuseProcessorCacheKey(\n destinationKey: string,\n langfuse?: t.LangfuseConfig\n): string {\n return JSON.stringify({\n destinationKey,\n toolOutputTracing: langfuse?.toolOutputTracing,\n });\n}\n\nclass RoutingLangfuseSpanProcessor implements SpanProcessor {\n // Processors live for the process lifetime. LibreChat tenant Langfuse\n // destinations are expected to be a bounded admin-managed set, and shutdown\n // drains every cached processor when the provider is disposed.\n private readonly processors = new Map<string, SpanProcessor>();\n private readonly spanProcessors = new WeakMap<object, SpanProcessor>();\n\n ensureProcessor(langfuse?: t.LangfuseConfig): SpanProcessor | undefined {\n const params = getLangfuseSpanProcessorParams(langfuse);\n if (params == null) {\n return undefined;\n }\n return this.ensureProcessorForKey(\n getLangfuseProcessorCacheKey(getLangfuseDestinationKey(params), langfuse),\n params,\n langfuse\n );\n }\n\n private ensureProcessorForKey(\n processorCacheKey: string,\n params: LangfuseSpanProcessorParams,\n langfuse?: t.LangfuseConfig\n ): SpanProcessor {\n const existing = this.processors.get(processorCacheKey);\n if (existing != null) {\n return existing;\n }\n\n const processor = createLangfuseSpanProcessor(params, langfuse);\n this.processors.set(processorCacheKey, processor);\n return processor;\n }\n\n onStart(span: Span, parentContext: Context): void {\n const langfuse = resolveLangfuseConfigForSpan(parentContext);\n const params = getLangfuseSpanProcessorParams(langfuse);\n if (params == null) {\n return;\n }\n\n const destinationKey = getLangfuseDestinationKey(params);\n const processor = this.ensureProcessorForKey(\n getLangfuseProcessorCacheKey(destinationKey, langfuse),\n params,\n langfuse\n );\n registerLangfuseManagedSpan(span, destinationKey);\n\n const librechatTraceAttributes = createLibreChatTraceAttributes(\n langfuse?.librechatTraceAttributes ?? {}\n );\n if (Object.keys(librechatTraceAttributes).length > 0) {\n span.setAttributes(librechatTraceAttributes);\n }\n\n this.spanProcessors.set(span, processor);\n processor.onStart(span, parentContext);\n }\n\n onEnd(span: ReadableSpan): void {\n this.spanProcessors.get(span)?.onEnd(span);\n }\n\n async forceFlush(): Promise<void> {\n await Promise.all(\n Array.from(this.processors.values(), (processor) =>\n processor.forceFlush()\n )\n );\n }\n\n async shutdown(): Promise<void> {\n await Promise.all(\n Array.from(this.processors.values(), (processor) => processor.shutdown())\n );\n }\n}\n\nexport function initializeLangfuseTracing(\n langfuse?: t.LangfuseConfig\n): BasicTracerProvider | undefined {\n const params = getLangfuseSpanProcessorParams(langfuse);\n if (params == null) {\n return undefined;\n }\n\n if (langfuseTracerProvider != null) {\n langfuseRoutingSpanProcessor?.ensureProcessor(langfuse);\n return langfuseTracerProvider;\n }\n\n ensureOpenTelemetryContextManager();\n langfuseRoutingSpanProcessor = new RoutingLangfuseSpanProcessor();\n langfuseRoutingSpanProcessor.ensureProcessor(langfuse);\n langfuseTracerProvider = new BasicTracerProvider({\n spanProcessors: [langfuseRoutingSpanProcessor],\n idGenerator: new SeededTraceIdGenerator(),\n });\n\n setLangfuseTracerProvider(langfuseTracerProvider);\n return langfuseTracerProvider;\n}\n\nexport function initializeLangfuseTracingFromEnv():\n | BasicTracerProvider\n | undefined {\n return initializeLangfuseTracing();\n}\n\ninitializeLangfuseTracingFromEnv();\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"instrumentation.cjs","names":["resolveTraceIdSeedForSpan","context","isPresent","traceIdFromSeed","ROOT_CONTEXT","AsyncLocalStorageContextManager","getLangfuseSpanProcessorParams","getLangfuseDestinationKey","createLangfuseSpanProcessor","resolveLangfuseConfigForSpan","createLibreChatTraceAttributes","BasicTracerProvider"],"sources":["../../src/instrumentation.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { setLangfuseTracerProvider } from '@langfuse/tracing';\nimport { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';\nimport { context, ROOT_CONTEXT, createContextKey } from '@opentelemetry/api';\nimport { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';\nimport type {\n IdGenerator,\n ReadableSpan,\n Span,\n SpanProcessor,\n} from '@opentelemetry/sdk-trace-base';\nimport type { LangfuseSpanProcessorParams } from '@langfuse/otel';\nimport type { Context } from '@opentelemetry/api';\nimport type * as t from '@/types';\nimport {\n getLangfuseDestinationKey,\n getLangfuseSpanProcessorParams,\n registerLangfuseManagedSpan,\n} from '@/langfuseSpanRegistry';\nimport {\n resolveLangfuseConfigForSpan,\n resolveTraceIdSeedForSpan,\n} from '@/langfuseRuntimeScope';\nimport { createLangfuseSpanProcessor } from '@/langfuseToolOutputTracing';\nimport { createLibreChatTraceAttributes } from '@/langfuse';\nimport { traceIdFromSeed } from '@/langfuseRuntimeContext';\nimport { isPresent } from '@/utils/misc';\n\n/**\n * Per-run seed for deterministic Langfuse trace ids. When a run opts in\n * (`LangfuseConfig.deterministicTraceId`), it executes its stream inside\n * `runWithTraceIdSeed(runId, ...)` from `./langfuseRuntimeContext`, and the\n * IdGenerator below derives the root trace id from that seed instead of a\n * random one. This lets external systems (e.g. a host app recording user\n * feedback after the fact) attach scores or observations to the trace by\n * regenerating the same id from the run/message id; no trace lookup required.\n * With no active seed it falls back to random ids, so default behavior is\n * unchanged.\n */\nclass SeededTraceIdGenerator implements IdGenerator {\n generateTraceId(): string {\n const seed = resolveTraceIdSeedForSpan(context.active());\n return isPresent(seed)\n ? traceIdFromSeed(seed)\n : randomBytes(16).toString('hex');\n }\n\n generateSpanId(): string {\n return randomBytes(8).toString('hex');\n }\n}\n\nlet langfuseTracerProvider: BasicTracerProvider | undefined;\nlet langfuseRoutingSpanProcessor: RoutingLangfuseSpanProcessor | undefined;\nconst contextManagerProbeKey = createContextKey(\n 'langfuse-context-manager-probe'\n);\n\nfunction hasActiveContextManager(): boolean {\n return context.with(\n ROOT_CONTEXT.setValue(contextManagerProbeKey, true),\n () => context.active().getValue(contextManagerProbeKey) === true\n );\n}\n\nexport function ensureOpenTelemetryContextManager(): void {\n if (hasActiveContextManager()) {\n return;\n }\n\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n if (!context.setGlobalContextManager(contextManager)) {\n contextManager.disable();\n }\n}\n\n/** Cache key for processor instances: the destination plus processor-level\n * policy (`toolOutputTracing` is baked into each processor's redaction\n * behavior), unlike the pure destination identity used for span parenting. */\nfunction getLangfuseProcessorCacheKey(\n destinationKey: string,\n langfuse?: t.LangfuseConfig\n): string {\n return JSON.stringify({\n destinationKey,\n mediaUploadEnabled: langfuse?.mediaUploadEnabled,\n toolOutputTracing: langfuse?.toolOutputTracing,\n });\n}\n\nclass RoutingLangfuseSpanProcessor implements SpanProcessor {\n // Processors live for the process lifetime. LibreChat tenant Langfuse\n // destinations are expected to be a bounded admin-managed set, and shutdown\n // drains every cached processor when the provider is disposed.\n private readonly processors = new Map<string, SpanProcessor>();\n private readonly spanProcessors = new WeakMap<object, SpanProcessor>();\n\n ensureProcessor(langfuse?: t.LangfuseConfig): SpanProcessor | undefined {\n const params = getLangfuseSpanProcessorParams(langfuse);\n if (params == null) {\n return undefined;\n }\n return this.ensureProcessorForKey(\n getLangfuseProcessorCacheKey(getLangfuseDestinationKey(params), langfuse),\n params,\n langfuse\n );\n }\n\n private ensureProcessorForKey(\n processorCacheKey: string,\n params: LangfuseSpanProcessorParams,\n langfuse?: t.LangfuseConfig\n ): SpanProcessor {\n const existing = this.processors.get(processorCacheKey);\n if (existing != null) {\n return existing;\n }\n\n const processor = createLangfuseSpanProcessor(params, langfuse);\n this.processors.set(processorCacheKey, processor);\n return processor;\n }\n\n onStart(span: Span, parentContext: Context): void {\n const langfuse = resolveLangfuseConfigForSpan(parentContext);\n const params = getLangfuseSpanProcessorParams(langfuse);\n if (params == null) {\n return;\n }\n\n const destinationKey = getLangfuseDestinationKey(params);\n const processor = this.ensureProcessorForKey(\n getLangfuseProcessorCacheKey(destinationKey, langfuse),\n params,\n langfuse\n );\n registerLangfuseManagedSpan(span, destinationKey);\n\n const librechatTraceAttributes = createLibreChatTraceAttributes(\n langfuse?.librechatTraceAttributes ?? {}\n );\n if (Object.keys(librechatTraceAttributes).length > 0) {\n span.setAttributes(librechatTraceAttributes);\n }\n\n this.spanProcessors.set(span, processor);\n processor.onStart(span, parentContext);\n }\n\n onEnd(span: ReadableSpan): void {\n this.spanProcessors.get(span)?.onEnd(span);\n }\n\n async forceFlush(): Promise<void> {\n await Promise.all(\n Array.from(this.processors.values(), (processor) =>\n processor.forceFlush()\n )\n );\n }\n\n async shutdown(): Promise<void> {\n await Promise.all(\n Array.from(this.processors.values(), (processor) => processor.shutdown())\n );\n }\n}\n\nexport function initializeLangfuseTracing(\n langfuse?: t.LangfuseConfig\n): BasicTracerProvider | undefined {\n const params = getLangfuseSpanProcessorParams(langfuse);\n if (params == null) {\n return undefined;\n }\n\n if (langfuseTracerProvider != null) {\n langfuseRoutingSpanProcessor?.ensureProcessor(langfuse);\n return langfuseTracerProvider;\n }\n\n ensureOpenTelemetryContextManager();\n langfuseRoutingSpanProcessor = new RoutingLangfuseSpanProcessor();\n langfuseRoutingSpanProcessor.ensureProcessor(langfuse);\n langfuseTracerProvider = new BasicTracerProvider({\n spanProcessors: [langfuseRoutingSpanProcessor],\n idGenerator: new SeededTraceIdGenerator(),\n });\n\n setLangfuseTracerProvider(langfuseTracerProvider);\n return langfuseTracerProvider;\n}\n\nexport function initializeLangfuseTracingFromEnv():\n | BasicTracerProvider\n | undefined {\n return initializeLangfuseTracing();\n}\n\ninitializeLangfuseTracingFromEnv();\n"],"mappings":"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"}
|
|
@@ -43,7 +43,8 @@ function getLangfuseSpanProcessorParams(langfuse) {
|
|
|
43
43
|
publicKey: langfuse.publicKey,
|
|
44
44
|
secretKey: langfuse.secretKey,
|
|
45
45
|
...require_misc.isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {},
|
|
46
|
-
...require_misc.isPresent(environment) ? { environment } : {}
|
|
46
|
+
...require_misc.isPresent(environment) ? { environment } : {},
|
|
47
|
+
...langfuse.mediaUploadEnabled != null ? { mediaUploadEnabled: langfuse.mediaUploadEnabled } : {}
|
|
47
48
|
};
|
|
48
49
|
if (require_langfuseConfig.hasLangfuseEnvConfig()) {
|
|
49
50
|
const baseUrl = langfuse?.baseUrl ?? process.env.LANGFUSE_BASE_URL ?? process.env.LANGFUSE_BASEURL;
|
|
@@ -51,14 +52,16 @@ function getLangfuseSpanProcessorParams(langfuse) {
|
|
|
51
52
|
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
|
52
53
|
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
|
53
54
|
...require_misc.isPresent(baseUrl) ? { baseUrl } : {},
|
|
54
|
-
...require_misc.isPresent(environment) ? { environment } : {}
|
|
55
|
+
...require_misc.isPresent(environment) ? { environment } : {},
|
|
56
|
+
...langfuse?.mediaUploadEnabled != null ? { mediaUploadEnabled: langfuse.mediaUploadEnabled } : {}
|
|
55
57
|
};
|
|
56
58
|
}
|
|
57
59
|
if (require_misc.isPresent(langfuse?.baseUrl) && require_langfuseConfig.hasLangfuseEnvCredentials()) return {
|
|
58
60
|
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
|
59
61
|
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
|
60
62
|
baseUrl: langfuse.baseUrl,
|
|
61
|
-
...require_misc.isPresent(environment) ? { environment } : {}
|
|
63
|
+
...require_misc.isPresent(environment) ? { environment } : {},
|
|
64
|
+
...langfuse.mediaUploadEnabled != null ? { mediaUploadEnabled: langfuse.mediaUploadEnabled } : {}
|
|
62
65
|
};
|
|
63
66
|
}
|
|
64
67
|
function hashCacheKeyValue(value) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"langfuseSpanRegistry.cjs","names":["hasLangfuseConfigCredentials","isPresent","hasLangfuseEnvConfig","hasLangfuseEnvCredentials"],"sources":["../../src/langfuseSpanRegistry.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { LangfuseSpanProcessorParams } from '@langfuse/otel';\nimport type { Span } from '@opentelemetry/api';\nimport type * as t from '@/types';\nimport {\n hasLangfuseConfigCredentials,\n hasLangfuseEnvCredentials,\n hasLangfuseEnvConfig,\n} from '@/langfuseConfig';\nimport { isPresent } from '@/utils/misc';\n\n/**\n * Spans created through the Langfuse tracer provider, keyed by the export\n * destination they were routed to. Callback handlers use this to distinguish\n * safe ambient parents from spans a root observation must not inherit:\n *\n * - Foreign spans (a host's own OpenTelemetry instrumentation, e.g. HTTP\n * server spans on the global provider) are never exported to Langfuse —\n * inheriting one orphans the trace root (its root/trace input-output\n * shaping is skipped), collapses concurrent runs inside one request\n * context into a single merged trace, and bypasses the seeded\n * deterministic trace id generator.\n * - Langfuse-managed spans bound to a *different* destination (another\n * tenant's project) would leave the new trace dangling in its own\n * destination while inheriting the other tenant's trace id.\n *\n * Only a managed span whose destination matches the starting run's resolved\n * destination is a safe parent — that is the sanctioned way for hosts to\n * group runs under their own Langfuse observations.\n */\nconst managedSpanDestinations = new WeakMap<Span, string>();\n\nexport function registerLangfuseManagedSpan(\n span: Span,\n destinationKey: string\n): void {\n managedSpanDestinations.set(span, destinationKey);\n}\n\nexport function getLangfuseManagedSpanDestination(\n span: Span\n): string | undefined {\n return managedSpanDestinations.get(span);\n}\n\nfunction resolveLangfuseEnvironment(\n langfuse?: t.LangfuseConfig\n): string | undefined {\n const candidates = [\n langfuse?.environment,\n process.env.LANGFUSE_TRACING_ENVIRONMENT,\n process.env.NODE_ENV,\n ];\n for (const candidate of candidates) {\n if (candidate != null && candidate.trim() !== '') {\n return candidate.trim();\n }\n }\n return undefined;\n}\n\nexport function getLangfuseSpanProcessorParams(\n langfuse?: t.LangfuseConfig\n): LangfuseSpanProcessorParams | undefined {\n if (langfuse?.enabled === false) {\n return undefined;\n }\n const environment = resolveLangfuseEnvironment(langfuse);\n if (hasLangfuseConfigCredentials(langfuse)) {\n return {\n publicKey: langfuse.publicKey,\n secretKey: langfuse.secretKey,\n ...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),\n ...(isPresent(environment) ? { environment } : {}),\n };\n }\n if (hasLangfuseEnvConfig()) {\n const baseUrl =\n langfuse?.baseUrl ??\n process.env.LANGFUSE_BASE_URL ??\n process.env.LANGFUSE_BASEURL;\n return {\n publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,\n secretKey: process.env.LANGFUSE_SECRET_KEY as string,\n ...(isPresent(baseUrl) ? { baseUrl } : {}),\n ...(isPresent(environment) ? { environment } : {}),\n };\n }\n if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) {\n return {\n publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,\n secretKey: process.env.LANGFUSE_SECRET_KEY as string,\n baseUrl: langfuse.baseUrl,\n ...(isPresent(environment) ? { environment } : {}),\n };\n }\n return undefined;\n}\n\nfunction hashCacheKeyValue(value: string | undefined): string | undefined {\n return isPresent(value)\n ? createHash('sha256').update(value, 'utf8').digest('hex')\n : undefined;\n}\n\n/**\n * Identity of an export destination (project credentials + endpoint +\n * environment) only. Processor-level policies like `toolOutputTracing` are\n * deliberately excluded: two spans exporting to the same project under\n * different redaction settings still share a destination and may parent one\n * another.\n */\nexport function getLangfuseDestinationKey(\n params: LangfuseSpanProcessorParams\n): string {\n return JSON.stringify({\n publicKey: params.publicKey,\n secretKeyHash: hashCacheKeyValue(params.secretKey),\n baseUrl: params.baseUrl,\n environment: params.environment,\n });\n}\n\n/** The export destination a run with this config resolves to, or `undefined`\n * when no Langfuse destination is configured. */\nexport function resolveLangfuseDestinationKey(\n langfuse?: t.LangfuseConfig\n): string | undefined {\n const params = getLangfuseSpanProcessorParams(langfuse);\n return params == null ? undefined : getLangfuseDestinationKey(params);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAM,0CAA0B,IAAI,QAAsB;AAE1D,SAAgB,4BACd,MACA,gBACM;CACN,wBAAwB,IAAI,MAAM,cAAc;AAClD;AAEA,SAAgB,kCACd,MACoB;CACpB,OAAO,wBAAwB,IAAI,IAAI;AACzC;AAEA,SAAS,2BACP,UACoB;CACpB,MAAM,aAAa;EACjB,UAAU;EACV,QAAQ,IAAI;EACZ,QAAQ,IAAI;CACd;CACA,KAAK,MAAM,aAAa,YACtB,IAAI,aAAa,QAAQ,UAAU,KAAK,MAAM,IAC5C,OAAO,UAAU,KAAK;AAI5B;AAEA,SAAgB,+BACd,UACyC;CACzC,IAAI,UAAU,YAAY,OACxB;CAEF,MAAM,cAAc,2BAA2B,QAAQ;CACvD,IAAIA,uBAAAA,6BAA6B,QAAQ,GACvC,OAAO;EACL,WAAW,SAAS;EACpB,WAAW,SAAS;EACpB,GAAIC,aAAAA,UAAU,SAAS,OAAO,IAAI,EAAE,SAAS,SAAS,QAAQ,IAAI,CAAC;EACnE,GAAIA,aAAAA,UAAU,WAAW,IAAI,EAAE,YAAY,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"langfuseSpanRegistry.cjs","names":["hasLangfuseConfigCredentials","isPresent","hasLangfuseEnvConfig","hasLangfuseEnvCredentials"],"sources":["../../src/langfuseSpanRegistry.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { LangfuseSpanProcessorParams } from '@langfuse/otel';\nimport type { Span } from '@opentelemetry/api';\nimport type * as t from '@/types';\nimport {\n hasLangfuseConfigCredentials,\n hasLangfuseEnvCredentials,\n hasLangfuseEnvConfig,\n} from '@/langfuseConfig';\nimport { isPresent } from '@/utils/misc';\n\n/**\n * Spans created through the Langfuse tracer provider, keyed by the export\n * destination they were routed to. Callback handlers use this to distinguish\n * safe ambient parents from spans a root observation must not inherit:\n *\n * - Foreign spans (a host's own OpenTelemetry instrumentation, e.g. HTTP\n * server spans on the global provider) are never exported to Langfuse —\n * inheriting one orphans the trace root (its root/trace input-output\n * shaping is skipped), collapses concurrent runs inside one request\n * context into a single merged trace, and bypasses the seeded\n * deterministic trace id generator.\n * - Langfuse-managed spans bound to a *different* destination (another\n * tenant's project) would leave the new trace dangling in its own\n * destination while inheriting the other tenant's trace id.\n *\n * Only a managed span whose destination matches the starting run's resolved\n * destination is a safe parent — that is the sanctioned way for hosts to\n * group runs under their own Langfuse observations.\n */\nconst managedSpanDestinations = new WeakMap<Span, string>();\n\nexport function registerLangfuseManagedSpan(\n span: Span,\n destinationKey: string\n): void {\n managedSpanDestinations.set(span, destinationKey);\n}\n\nexport function getLangfuseManagedSpanDestination(\n span: Span\n): string | undefined {\n return managedSpanDestinations.get(span);\n}\n\nfunction resolveLangfuseEnvironment(\n langfuse?: t.LangfuseConfig\n): string | undefined {\n const candidates = [\n langfuse?.environment,\n process.env.LANGFUSE_TRACING_ENVIRONMENT,\n process.env.NODE_ENV,\n ];\n for (const candidate of candidates) {\n if (candidate != null && candidate.trim() !== '') {\n return candidate.trim();\n }\n }\n return undefined;\n}\n\nexport function getLangfuseSpanProcessorParams(\n langfuse?: t.LangfuseConfig\n): LangfuseSpanProcessorParams | undefined {\n if (langfuse?.enabled === false) {\n return undefined;\n }\n const environment = resolveLangfuseEnvironment(langfuse);\n if (hasLangfuseConfigCredentials(langfuse)) {\n return {\n publicKey: langfuse.publicKey,\n secretKey: langfuse.secretKey,\n ...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),\n ...(isPresent(environment) ? { environment } : {}),\n ...(langfuse.mediaUploadEnabled != null\n ? { mediaUploadEnabled: langfuse.mediaUploadEnabled }\n : {}),\n };\n }\n if (hasLangfuseEnvConfig()) {\n const baseUrl =\n langfuse?.baseUrl ??\n process.env.LANGFUSE_BASE_URL ??\n process.env.LANGFUSE_BASEURL;\n return {\n publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,\n secretKey: process.env.LANGFUSE_SECRET_KEY as string,\n ...(isPresent(baseUrl) ? { baseUrl } : {}),\n ...(isPresent(environment) ? { environment } : {}),\n ...(langfuse?.mediaUploadEnabled != null\n ? { mediaUploadEnabled: langfuse.mediaUploadEnabled }\n : {}),\n };\n }\n if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) {\n return {\n publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,\n secretKey: process.env.LANGFUSE_SECRET_KEY as string,\n baseUrl: langfuse.baseUrl,\n ...(isPresent(environment) ? { environment } : {}),\n ...(langfuse.mediaUploadEnabled != null\n ? { mediaUploadEnabled: langfuse.mediaUploadEnabled }\n : {}),\n };\n }\n return undefined;\n}\n\nfunction hashCacheKeyValue(value: string | undefined): string | undefined {\n return isPresent(value)\n ? createHash('sha256').update(value, 'utf8').digest('hex')\n : undefined;\n}\n\n/**\n * Identity of an export destination (project credentials + endpoint +\n * environment) only. Processor-level policies like `toolOutputTracing` are\n * deliberately excluded: two spans exporting to the same project under\n * different redaction settings still share a destination and may parent one\n * another.\n */\nexport function getLangfuseDestinationKey(\n params: LangfuseSpanProcessorParams\n): string {\n return JSON.stringify({\n publicKey: params.publicKey,\n secretKeyHash: hashCacheKeyValue(params.secretKey),\n baseUrl: params.baseUrl,\n environment: params.environment,\n });\n}\n\n/** The export destination a run with this config resolves to, or `undefined`\n * when no Langfuse destination is configured. */\nexport function resolveLangfuseDestinationKey(\n langfuse?: t.LangfuseConfig\n): string | undefined {\n const params = getLangfuseSpanProcessorParams(langfuse);\n return params == null ? undefined : getLangfuseDestinationKey(params);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAM,0CAA0B,IAAI,QAAsB;AAE1D,SAAgB,4BACd,MACA,gBACM;CACN,wBAAwB,IAAI,MAAM,cAAc;AAClD;AAEA,SAAgB,kCACd,MACoB;CACpB,OAAO,wBAAwB,IAAI,IAAI;AACzC;AAEA,SAAS,2BACP,UACoB;CACpB,MAAM,aAAa;EACjB,UAAU;EACV,QAAQ,IAAI;EACZ,QAAQ,IAAI;CACd;CACA,KAAK,MAAM,aAAa,YACtB,IAAI,aAAa,QAAQ,UAAU,KAAK,MAAM,IAC5C,OAAO,UAAU,KAAK;AAI5B;AAEA,SAAgB,+BACd,UACyC;CACzC,IAAI,UAAU,YAAY,OACxB;CAEF,MAAM,cAAc,2BAA2B,QAAQ;CACvD,IAAIA,uBAAAA,6BAA6B,QAAQ,GACvC,OAAO;EACL,WAAW,SAAS;EACpB,WAAW,SAAS;EACpB,GAAIC,aAAAA,UAAU,SAAS,OAAO,IAAI,EAAE,SAAS,SAAS,QAAQ,IAAI,CAAC;EACnE,GAAIA,aAAAA,UAAU,WAAW,IAAI,EAAE,YAAY,IAAI,CAAC;EAChD,GAAI,SAAS,sBAAsB,OAC/B,EAAE,oBAAoB,SAAS,mBAAmB,IAClD,CAAC;CACP;CAEF,IAAIC,uBAAAA,qBAAqB,GAAG;EAC1B,MAAM,UACJ,UAAU,WACV,QAAQ,IAAI,qBACZ,QAAQ,IAAI;EACd,OAAO;GACL,WAAW,QAAQ,IAAI;GACvB,WAAW,QAAQ,IAAI;GACvB,GAAID,aAAAA,UAAU,OAAO,IAAI,EAAE,QAAQ,IAAI,CAAC;GACxC,GAAIA,aAAAA,UAAU,WAAW,IAAI,EAAE,YAAY,IAAI,CAAC;GAChD,GAAI,UAAU,sBAAsB,OAChC,EAAE,oBAAoB,SAAS,mBAAmB,IAClD,CAAC;EACP;CACF;CACA,IAAIA,aAAAA,UAAU,UAAU,OAAO,KAAKE,uBAAAA,0BAA0B,GAC5D,OAAO;EACL,WAAW,QAAQ,IAAI;EACvB,WAAW,QAAQ,IAAI;EACvB,SAAS,SAAS;EAClB,GAAIF,aAAAA,UAAU,WAAW,IAAI,EAAE,YAAY,IAAI,CAAC;EAChD,GAAI,SAAS,sBAAsB,OAC/B,EAAE,oBAAoB,SAAS,mBAAmB,IAClD,CAAC;CACP;AAGJ;AAEA,SAAS,kBAAkB,OAA+C;CACxE,OAAOA,aAAAA,UAAU,KAAK,KAAA,GAAA,YAAA,WAAA,CACP,QAAQ,CAAC,CAAC,OAAO,OAAO,MAAM,CAAC,CAAC,OAAO,KAAK,IACvD,KAAA;AACN;;;;;;;;AASA,SAAgB,0BACd,QACQ;CACR,OAAO,KAAK,UAAU;EACpB,WAAW,OAAO;EAClB,eAAe,kBAAkB,OAAO,SAAS;EACjD,SAAS,OAAO;EAChB,aAAa,OAAO;CACtB,CAAC;AACH;;;AAIA,SAAgB,8BACd,UACoB;CACpB,MAAM,SAAS,+BAA+B,QAAQ;CACtD,OAAO,UAAU,OAAO,KAAA,IAAY,0BAA0B,MAAM;AACtE"}
|
|
@@ -48,6 +48,7 @@ function ensureOpenTelemetryContextManager() {
|
|
|
48
48
|
function getLangfuseProcessorCacheKey(destinationKey, langfuse) {
|
|
49
49
|
return JSON.stringify({
|
|
50
50
|
destinationKey,
|
|
51
|
+
mediaUploadEnabled: langfuse?.mediaUploadEnabled,
|
|
51
52
|
toolOutputTracing: langfuse?.toolOutputTracing
|
|
52
53
|
});
|
|
53
54
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instrumentation.mjs","names":[],"sources":["../../src/instrumentation.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { setLangfuseTracerProvider } from '@langfuse/tracing';\nimport { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';\nimport { context, ROOT_CONTEXT, createContextKey } from '@opentelemetry/api';\nimport { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';\nimport type {\n IdGenerator,\n ReadableSpan,\n Span,\n SpanProcessor,\n} from '@opentelemetry/sdk-trace-base';\nimport type { LangfuseSpanProcessorParams } from '@langfuse/otel';\nimport type { Context } from '@opentelemetry/api';\nimport type * as t from '@/types';\nimport {\n getLangfuseDestinationKey,\n getLangfuseSpanProcessorParams,\n registerLangfuseManagedSpan,\n} from '@/langfuseSpanRegistry';\nimport {\n resolveLangfuseConfigForSpan,\n resolveTraceIdSeedForSpan,\n} from '@/langfuseRuntimeScope';\nimport { createLangfuseSpanProcessor } from '@/langfuseToolOutputTracing';\nimport { createLibreChatTraceAttributes } from '@/langfuse';\nimport { traceIdFromSeed } from '@/langfuseRuntimeContext';\nimport { isPresent } from '@/utils/misc';\n\n/**\n * Per-run seed for deterministic Langfuse trace ids. When a run opts in\n * (`LangfuseConfig.deterministicTraceId`), it executes its stream inside\n * `runWithTraceIdSeed(runId, ...)` from `./langfuseRuntimeContext`, and the\n * IdGenerator below derives the root trace id from that seed instead of a\n * random one. This lets external systems (e.g. a host app recording user\n * feedback after the fact) attach scores or observations to the trace by\n * regenerating the same id from the run/message id; no trace lookup required.\n * With no active seed it falls back to random ids, so default behavior is\n * unchanged.\n */\nclass SeededTraceIdGenerator implements IdGenerator {\n generateTraceId(): string {\n const seed = resolveTraceIdSeedForSpan(context.active());\n return isPresent(seed)\n ? traceIdFromSeed(seed)\n : randomBytes(16).toString('hex');\n }\n\n generateSpanId(): string {\n return randomBytes(8).toString('hex');\n }\n}\n\nlet langfuseTracerProvider: BasicTracerProvider | undefined;\nlet langfuseRoutingSpanProcessor: RoutingLangfuseSpanProcessor | undefined;\nconst contextManagerProbeKey = createContextKey(\n 'langfuse-context-manager-probe'\n);\n\nfunction hasActiveContextManager(): boolean {\n return context.with(\n ROOT_CONTEXT.setValue(contextManagerProbeKey, true),\n () => context.active().getValue(contextManagerProbeKey) === true\n );\n}\n\nexport function ensureOpenTelemetryContextManager(): void {\n if (hasActiveContextManager()) {\n return;\n }\n\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n if (!context.setGlobalContextManager(contextManager)) {\n contextManager.disable();\n }\n}\n\n/** Cache key for processor instances: the destination plus processor-level\n * policy (`toolOutputTracing` is baked into each processor's redaction\n * behavior), unlike the pure destination identity used for span parenting. */\nfunction getLangfuseProcessorCacheKey(\n destinationKey: string,\n langfuse?: t.LangfuseConfig\n): string {\n return JSON.stringify({\n destinationKey,\n toolOutputTracing: langfuse?.toolOutputTracing,\n });\n}\n\nclass RoutingLangfuseSpanProcessor implements SpanProcessor {\n // Processors live for the process lifetime. LibreChat tenant Langfuse\n // destinations are expected to be a bounded admin-managed set, and shutdown\n // drains every cached processor when the provider is disposed.\n private readonly processors = new Map<string, SpanProcessor>();\n private readonly spanProcessors = new WeakMap<object, SpanProcessor>();\n\n ensureProcessor(langfuse?: t.LangfuseConfig): SpanProcessor | undefined {\n const params = getLangfuseSpanProcessorParams(langfuse);\n if (params == null) {\n return undefined;\n }\n return this.ensureProcessorForKey(\n getLangfuseProcessorCacheKey(getLangfuseDestinationKey(params), langfuse),\n params,\n langfuse\n );\n }\n\n private ensureProcessorForKey(\n processorCacheKey: string,\n params: LangfuseSpanProcessorParams,\n langfuse?: t.LangfuseConfig\n ): SpanProcessor {\n const existing = this.processors.get(processorCacheKey);\n if (existing != null) {\n return existing;\n }\n\n const processor = createLangfuseSpanProcessor(params, langfuse);\n this.processors.set(processorCacheKey, processor);\n return processor;\n }\n\n onStart(span: Span, parentContext: Context): void {\n const langfuse = resolveLangfuseConfigForSpan(parentContext);\n const params = getLangfuseSpanProcessorParams(langfuse);\n if (params == null) {\n return;\n }\n\n const destinationKey = getLangfuseDestinationKey(params);\n const processor = this.ensureProcessorForKey(\n getLangfuseProcessorCacheKey(destinationKey, langfuse),\n params,\n langfuse\n );\n registerLangfuseManagedSpan(span, destinationKey);\n\n const librechatTraceAttributes = createLibreChatTraceAttributes(\n langfuse?.librechatTraceAttributes ?? {}\n );\n if (Object.keys(librechatTraceAttributes).length > 0) {\n span.setAttributes(librechatTraceAttributes);\n }\n\n this.spanProcessors.set(span, processor);\n processor.onStart(span, parentContext);\n }\n\n onEnd(span: ReadableSpan): void {\n this.spanProcessors.get(span)?.onEnd(span);\n }\n\n async forceFlush(): Promise<void> {\n await Promise.all(\n Array.from(this.processors.values(), (processor) =>\n processor.forceFlush()\n )\n );\n }\n\n async shutdown(): Promise<void> {\n await Promise.all(\n Array.from(this.processors.values(), (processor) => processor.shutdown())\n );\n }\n}\n\nexport function initializeLangfuseTracing(\n langfuse?: t.LangfuseConfig\n): BasicTracerProvider | undefined {\n const params = getLangfuseSpanProcessorParams(langfuse);\n if (params == null) {\n return undefined;\n }\n\n if (langfuseTracerProvider != null) {\n langfuseRoutingSpanProcessor?.ensureProcessor(langfuse);\n return langfuseTracerProvider;\n }\n\n ensureOpenTelemetryContextManager();\n langfuseRoutingSpanProcessor = new RoutingLangfuseSpanProcessor();\n langfuseRoutingSpanProcessor.ensureProcessor(langfuse);\n langfuseTracerProvider = new BasicTracerProvider({\n spanProcessors: [langfuseRoutingSpanProcessor],\n idGenerator: new SeededTraceIdGenerator(),\n });\n\n setLangfuseTracerProvider(langfuseTracerProvider);\n return langfuseTracerProvider;\n}\n\nexport function initializeLangfuseTracingFromEnv():\n | BasicTracerProvider\n | undefined {\n return initializeLangfuseTracing();\n}\n\ninitializeLangfuseTracingFromEnv();\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"instrumentation.mjs","names":[],"sources":["../../src/instrumentation.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { setLangfuseTracerProvider } from '@langfuse/tracing';\nimport { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';\nimport { context, ROOT_CONTEXT, createContextKey } from '@opentelemetry/api';\nimport { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';\nimport type {\n IdGenerator,\n ReadableSpan,\n Span,\n SpanProcessor,\n} from '@opentelemetry/sdk-trace-base';\nimport type { LangfuseSpanProcessorParams } from '@langfuse/otel';\nimport type { Context } from '@opentelemetry/api';\nimport type * as t from '@/types';\nimport {\n getLangfuseDestinationKey,\n getLangfuseSpanProcessorParams,\n registerLangfuseManagedSpan,\n} from '@/langfuseSpanRegistry';\nimport {\n resolveLangfuseConfigForSpan,\n resolveTraceIdSeedForSpan,\n} from '@/langfuseRuntimeScope';\nimport { createLangfuseSpanProcessor } from '@/langfuseToolOutputTracing';\nimport { createLibreChatTraceAttributes } from '@/langfuse';\nimport { traceIdFromSeed } from '@/langfuseRuntimeContext';\nimport { isPresent } from '@/utils/misc';\n\n/**\n * Per-run seed for deterministic Langfuse trace ids. When a run opts in\n * (`LangfuseConfig.deterministicTraceId`), it executes its stream inside\n * `runWithTraceIdSeed(runId, ...)` from `./langfuseRuntimeContext`, and the\n * IdGenerator below derives the root trace id from that seed instead of a\n * random one. This lets external systems (e.g. a host app recording user\n * feedback after the fact) attach scores or observations to the trace by\n * regenerating the same id from the run/message id; no trace lookup required.\n * With no active seed it falls back to random ids, so default behavior is\n * unchanged.\n */\nclass SeededTraceIdGenerator implements IdGenerator {\n generateTraceId(): string {\n const seed = resolveTraceIdSeedForSpan(context.active());\n return isPresent(seed)\n ? traceIdFromSeed(seed)\n : randomBytes(16).toString('hex');\n }\n\n generateSpanId(): string {\n return randomBytes(8).toString('hex');\n }\n}\n\nlet langfuseTracerProvider: BasicTracerProvider | undefined;\nlet langfuseRoutingSpanProcessor: RoutingLangfuseSpanProcessor | undefined;\nconst contextManagerProbeKey = createContextKey(\n 'langfuse-context-manager-probe'\n);\n\nfunction hasActiveContextManager(): boolean {\n return context.with(\n ROOT_CONTEXT.setValue(contextManagerProbeKey, true),\n () => context.active().getValue(contextManagerProbeKey) === true\n );\n}\n\nexport function ensureOpenTelemetryContextManager(): void {\n if (hasActiveContextManager()) {\n return;\n }\n\n const contextManager = new AsyncLocalStorageContextManager();\n contextManager.enable();\n if (!context.setGlobalContextManager(contextManager)) {\n contextManager.disable();\n }\n}\n\n/** Cache key for processor instances: the destination plus processor-level\n * policy (`toolOutputTracing` is baked into each processor's redaction\n * behavior), unlike the pure destination identity used for span parenting. */\nfunction getLangfuseProcessorCacheKey(\n destinationKey: string,\n langfuse?: t.LangfuseConfig\n): string {\n return JSON.stringify({\n destinationKey,\n mediaUploadEnabled: langfuse?.mediaUploadEnabled,\n toolOutputTracing: langfuse?.toolOutputTracing,\n });\n}\n\nclass RoutingLangfuseSpanProcessor implements SpanProcessor {\n // Processors live for the process lifetime. LibreChat tenant Langfuse\n // destinations are expected to be a bounded admin-managed set, and shutdown\n // drains every cached processor when the provider is disposed.\n private readonly processors = new Map<string, SpanProcessor>();\n private readonly spanProcessors = new WeakMap<object, SpanProcessor>();\n\n ensureProcessor(langfuse?: t.LangfuseConfig): SpanProcessor | undefined {\n const params = getLangfuseSpanProcessorParams(langfuse);\n if (params == null) {\n return undefined;\n }\n return this.ensureProcessorForKey(\n getLangfuseProcessorCacheKey(getLangfuseDestinationKey(params), langfuse),\n params,\n langfuse\n );\n }\n\n private ensureProcessorForKey(\n processorCacheKey: string,\n params: LangfuseSpanProcessorParams,\n langfuse?: t.LangfuseConfig\n ): SpanProcessor {\n const existing = this.processors.get(processorCacheKey);\n if (existing != null) {\n return existing;\n }\n\n const processor = createLangfuseSpanProcessor(params, langfuse);\n this.processors.set(processorCacheKey, processor);\n return processor;\n }\n\n onStart(span: Span, parentContext: Context): void {\n const langfuse = resolveLangfuseConfigForSpan(parentContext);\n const params = getLangfuseSpanProcessorParams(langfuse);\n if (params == null) {\n return;\n }\n\n const destinationKey = getLangfuseDestinationKey(params);\n const processor = this.ensureProcessorForKey(\n getLangfuseProcessorCacheKey(destinationKey, langfuse),\n params,\n langfuse\n );\n registerLangfuseManagedSpan(span, destinationKey);\n\n const librechatTraceAttributes = createLibreChatTraceAttributes(\n langfuse?.librechatTraceAttributes ?? {}\n );\n if (Object.keys(librechatTraceAttributes).length > 0) {\n span.setAttributes(librechatTraceAttributes);\n }\n\n this.spanProcessors.set(span, processor);\n processor.onStart(span, parentContext);\n }\n\n onEnd(span: ReadableSpan): void {\n this.spanProcessors.get(span)?.onEnd(span);\n }\n\n async forceFlush(): Promise<void> {\n await Promise.all(\n Array.from(this.processors.values(), (processor) =>\n processor.forceFlush()\n )\n );\n }\n\n async shutdown(): Promise<void> {\n await Promise.all(\n Array.from(this.processors.values(), (processor) => processor.shutdown())\n );\n }\n}\n\nexport function initializeLangfuseTracing(\n langfuse?: t.LangfuseConfig\n): BasicTracerProvider | undefined {\n const params = getLangfuseSpanProcessorParams(langfuse);\n if (params == null) {\n return undefined;\n }\n\n if (langfuseTracerProvider != null) {\n langfuseRoutingSpanProcessor?.ensureProcessor(langfuse);\n return langfuseTracerProvider;\n }\n\n ensureOpenTelemetryContextManager();\n langfuseRoutingSpanProcessor = new RoutingLangfuseSpanProcessor();\n langfuseRoutingSpanProcessor.ensureProcessor(langfuse);\n langfuseTracerProvider = new BasicTracerProvider({\n spanProcessors: [langfuseRoutingSpanProcessor],\n idGenerator: new SeededTraceIdGenerator(),\n });\n\n setLangfuseTracerProvider(langfuseTracerProvider);\n return langfuseTracerProvider;\n}\n\nexport function initializeLangfuseTracingFromEnv():\n | BasicTracerProvider\n | undefined {\n return initializeLangfuseTracing();\n}\n\ninitializeLangfuseTracingFromEnv();\n"],"mappings":"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"}
|
|
@@ -43,7 +43,8 @@ function getLangfuseSpanProcessorParams(langfuse) {
|
|
|
43
43
|
publicKey: langfuse.publicKey,
|
|
44
44
|
secretKey: langfuse.secretKey,
|
|
45
45
|
...isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {},
|
|
46
|
-
...isPresent(environment) ? { environment } : {}
|
|
46
|
+
...isPresent(environment) ? { environment } : {},
|
|
47
|
+
...langfuse.mediaUploadEnabled != null ? { mediaUploadEnabled: langfuse.mediaUploadEnabled } : {}
|
|
47
48
|
};
|
|
48
49
|
if (hasLangfuseEnvConfig()) {
|
|
49
50
|
const baseUrl = langfuse?.baseUrl ?? process.env.LANGFUSE_BASE_URL ?? process.env.LANGFUSE_BASEURL;
|
|
@@ -51,14 +52,16 @@ function getLangfuseSpanProcessorParams(langfuse) {
|
|
|
51
52
|
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
|
52
53
|
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
|
53
54
|
...isPresent(baseUrl) ? { baseUrl } : {},
|
|
54
|
-
...isPresent(environment) ? { environment } : {}
|
|
55
|
+
...isPresent(environment) ? { environment } : {},
|
|
56
|
+
...langfuse?.mediaUploadEnabled != null ? { mediaUploadEnabled: langfuse.mediaUploadEnabled } : {}
|
|
55
57
|
};
|
|
56
58
|
}
|
|
57
59
|
if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) return {
|
|
58
60
|
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
|
59
61
|
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
|
60
62
|
baseUrl: langfuse.baseUrl,
|
|
61
|
-
...isPresent(environment) ? { environment } : {}
|
|
63
|
+
...isPresent(environment) ? { environment } : {},
|
|
64
|
+
...langfuse.mediaUploadEnabled != null ? { mediaUploadEnabled: langfuse.mediaUploadEnabled } : {}
|
|
62
65
|
};
|
|
63
66
|
}
|
|
64
67
|
function hashCacheKeyValue(value) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"langfuseSpanRegistry.mjs","names":[],"sources":["../../src/langfuseSpanRegistry.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { LangfuseSpanProcessorParams } from '@langfuse/otel';\nimport type { Span } from '@opentelemetry/api';\nimport type * as t from '@/types';\nimport {\n hasLangfuseConfigCredentials,\n hasLangfuseEnvCredentials,\n hasLangfuseEnvConfig,\n} from '@/langfuseConfig';\nimport { isPresent } from '@/utils/misc';\n\n/**\n * Spans created through the Langfuse tracer provider, keyed by the export\n * destination they were routed to. Callback handlers use this to distinguish\n * safe ambient parents from spans a root observation must not inherit:\n *\n * - Foreign spans (a host's own OpenTelemetry instrumentation, e.g. HTTP\n * server spans on the global provider) are never exported to Langfuse —\n * inheriting one orphans the trace root (its root/trace input-output\n * shaping is skipped), collapses concurrent runs inside one request\n * context into a single merged trace, and bypasses the seeded\n * deterministic trace id generator.\n * - Langfuse-managed spans bound to a *different* destination (another\n * tenant's project) would leave the new trace dangling in its own\n * destination while inheriting the other tenant's trace id.\n *\n * Only a managed span whose destination matches the starting run's resolved\n * destination is a safe parent — that is the sanctioned way for hosts to\n * group runs under their own Langfuse observations.\n */\nconst managedSpanDestinations = new WeakMap<Span, string>();\n\nexport function registerLangfuseManagedSpan(\n span: Span,\n destinationKey: string\n): void {\n managedSpanDestinations.set(span, destinationKey);\n}\n\nexport function getLangfuseManagedSpanDestination(\n span: Span\n): string | undefined {\n return managedSpanDestinations.get(span);\n}\n\nfunction resolveLangfuseEnvironment(\n langfuse?: t.LangfuseConfig\n): string | undefined {\n const candidates = [\n langfuse?.environment,\n process.env.LANGFUSE_TRACING_ENVIRONMENT,\n process.env.NODE_ENV,\n ];\n for (const candidate of candidates) {\n if (candidate != null && candidate.trim() !== '') {\n return candidate.trim();\n }\n }\n return undefined;\n}\n\nexport function getLangfuseSpanProcessorParams(\n langfuse?: t.LangfuseConfig\n): LangfuseSpanProcessorParams | undefined {\n if (langfuse?.enabled === false) {\n return undefined;\n }\n const environment = resolveLangfuseEnvironment(langfuse);\n if (hasLangfuseConfigCredentials(langfuse)) {\n return {\n publicKey: langfuse.publicKey,\n secretKey: langfuse.secretKey,\n ...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),\n ...(isPresent(environment) ? { environment } : {}),\n };\n }\n if (hasLangfuseEnvConfig()) {\n const baseUrl =\n langfuse?.baseUrl ??\n process.env.LANGFUSE_BASE_URL ??\n process.env.LANGFUSE_BASEURL;\n return {\n publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,\n secretKey: process.env.LANGFUSE_SECRET_KEY as string,\n ...(isPresent(baseUrl) ? { baseUrl } : {}),\n ...(isPresent(environment) ? { environment } : {}),\n };\n }\n if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) {\n return {\n publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,\n secretKey: process.env.LANGFUSE_SECRET_KEY as string,\n baseUrl: langfuse.baseUrl,\n ...(isPresent(environment) ? { environment } : {}),\n };\n }\n return undefined;\n}\n\nfunction hashCacheKeyValue(value: string | undefined): string | undefined {\n return isPresent(value)\n ? createHash('sha256').update(value, 'utf8').digest('hex')\n : undefined;\n}\n\n/**\n * Identity of an export destination (project credentials + endpoint +\n * environment) only. Processor-level policies like `toolOutputTracing` are\n * deliberately excluded: two spans exporting to the same project under\n * different redaction settings still share a destination and may parent one\n * another.\n */\nexport function getLangfuseDestinationKey(\n params: LangfuseSpanProcessorParams\n): string {\n return JSON.stringify({\n publicKey: params.publicKey,\n secretKeyHash: hashCacheKeyValue(params.secretKey),\n baseUrl: params.baseUrl,\n environment: params.environment,\n });\n}\n\n/** The export destination a run with this config resolves to, or `undefined`\n * when no Langfuse destination is configured. */\nexport function resolveLangfuseDestinationKey(\n langfuse?: t.LangfuseConfig\n): string | undefined {\n const params = getLangfuseSpanProcessorParams(langfuse);\n return params == null ? undefined : getLangfuseDestinationKey(params);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAM,0CAA0B,IAAI,QAAsB;AAE1D,SAAgB,4BACd,MACA,gBACM;CACN,wBAAwB,IAAI,MAAM,cAAc;AAClD;AAEA,SAAgB,kCACd,MACoB;CACpB,OAAO,wBAAwB,IAAI,IAAI;AACzC;AAEA,SAAS,2BACP,UACoB;CACpB,MAAM,aAAa;EACjB,UAAU;EACV,QAAQ,IAAI;EACZ,QAAQ,IAAI;CACd;CACA,KAAK,MAAM,aAAa,YACtB,IAAI,aAAa,QAAQ,UAAU,KAAK,MAAM,IAC5C,OAAO,UAAU,KAAK;AAI5B;AAEA,SAAgB,+BACd,UACyC;CACzC,IAAI,UAAU,YAAY,OACxB;CAEF,MAAM,cAAc,2BAA2B,QAAQ;CACvD,IAAI,6BAA6B,QAAQ,GACvC,OAAO;EACL,WAAW,SAAS;EACpB,WAAW,SAAS;EACpB,GAAI,UAAU,SAAS,OAAO,IAAI,EAAE,SAAS,SAAS,QAAQ,IAAI,CAAC;EACnE,GAAI,UAAU,WAAW,IAAI,EAAE,YAAY,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"langfuseSpanRegistry.mjs","names":[],"sources":["../../src/langfuseSpanRegistry.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { LangfuseSpanProcessorParams } from '@langfuse/otel';\nimport type { Span } from '@opentelemetry/api';\nimport type * as t from '@/types';\nimport {\n hasLangfuseConfigCredentials,\n hasLangfuseEnvCredentials,\n hasLangfuseEnvConfig,\n} from '@/langfuseConfig';\nimport { isPresent } from '@/utils/misc';\n\n/**\n * Spans created through the Langfuse tracer provider, keyed by the export\n * destination they were routed to. Callback handlers use this to distinguish\n * safe ambient parents from spans a root observation must not inherit:\n *\n * - Foreign spans (a host's own OpenTelemetry instrumentation, e.g. HTTP\n * server spans on the global provider) are never exported to Langfuse —\n * inheriting one orphans the trace root (its root/trace input-output\n * shaping is skipped), collapses concurrent runs inside one request\n * context into a single merged trace, and bypasses the seeded\n * deterministic trace id generator.\n * - Langfuse-managed spans bound to a *different* destination (another\n * tenant's project) would leave the new trace dangling in its own\n * destination while inheriting the other tenant's trace id.\n *\n * Only a managed span whose destination matches the starting run's resolved\n * destination is a safe parent — that is the sanctioned way for hosts to\n * group runs under their own Langfuse observations.\n */\nconst managedSpanDestinations = new WeakMap<Span, string>();\n\nexport function registerLangfuseManagedSpan(\n span: Span,\n destinationKey: string\n): void {\n managedSpanDestinations.set(span, destinationKey);\n}\n\nexport function getLangfuseManagedSpanDestination(\n span: Span\n): string | undefined {\n return managedSpanDestinations.get(span);\n}\n\nfunction resolveLangfuseEnvironment(\n langfuse?: t.LangfuseConfig\n): string | undefined {\n const candidates = [\n langfuse?.environment,\n process.env.LANGFUSE_TRACING_ENVIRONMENT,\n process.env.NODE_ENV,\n ];\n for (const candidate of candidates) {\n if (candidate != null && candidate.trim() !== '') {\n return candidate.trim();\n }\n }\n return undefined;\n}\n\nexport function getLangfuseSpanProcessorParams(\n langfuse?: t.LangfuseConfig\n): LangfuseSpanProcessorParams | undefined {\n if (langfuse?.enabled === false) {\n return undefined;\n }\n const environment = resolveLangfuseEnvironment(langfuse);\n if (hasLangfuseConfigCredentials(langfuse)) {\n return {\n publicKey: langfuse.publicKey,\n secretKey: langfuse.secretKey,\n ...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),\n ...(isPresent(environment) ? { environment } : {}),\n ...(langfuse.mediaUploadEnabled != null\n ? { mediaUploadEnabled: langfuse.mediaUploadEnabled }\n : {}),\n };\n }\n if (hasLangfuseEnvConfig()) {\n const baseUrl =\n langfuse?.baseUrl ??\n process.env.LANGFUSE_BASE_URL ??\n process.env.LANGFUSE_BASEURL;\n return {\n publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,\n secretKey: process.env.LANGFUSE_SECRET_KEY as string,\n ...(isPresent(baseUrl) ? { baseUrl } : {}),\n ...(isPresent(environment) ? { environment } : {}),\n ...(langfuse?.mediaUploadEnabled != null\n ? { mediaUploadEnabled: langfuse.mediaUploadEnabled }\n : {}),\n };\n }\n if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) {\n return {\n publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,\n secretKey: process.env.LANGFUSE_SECRET_KEY as string,\n baseUrl: langfuse.baseUrl,\n ...(isPresent(environment) ? { environment } : {}),\n ...(langfuse.mediaUploadEnabled != null\n ? { mediaUploadEnabled: langfuse.mediaUploadEnabled }\n : {}),\n };\n }\n return undefined;\n}\n\nfunction hashCacheKeyValue(value: string | undefined): string | undefined {\n return isPresent(value)\n ? createHash('sha256').update(value, 'utf8').digest('hex')\n : undefined;\n}\n\n/**\n * Identity of an export destination (project credentials + endpoint +\n * environment) only. Processor-level policies like `toolOutputTracing` are\n * deliberately excluded: two spans exporting to the same project under\n * different redaction settings still share a destination and may parent one\n * another.\n */\nexport function getLangfuseDestinationKey(\n params: LangfuseSpanProcessorParams\n): string {\n return JSON.stringify({\n publicKey: params.publicKey,\n secretKeyHash: hashCacheKeyValue(params.secretKey),\n baseUrl: params.baseUrl,\n environment: params.environment,\n });\n}\n\n/** The export destination a run with this config resolves to, or `undefined`\n * when no Langfuse destination is configured. */\nexport function resolveLangfuseDestinationKey(\n langfuse?: t.LangfuseConfig\n): string | undefined {\n const params = getLangfuseSpanProcessorParams(langfuse);\n return params == null ? undefined : getLangfuseDestinationKey(params);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAM,0CAA0B,IAAI,QAAsB;AAE1D,SAAgB,4BACd,MACA,gBACM;CACN,wBAAwB,IAAI,MAAM,cAAc;AAClD;AAEA,SAAgB,kCACd,MACoB;CACpB,OAAO,wBAAwB,IAAI,IAAI;AACzC;AAEA,SAAS,2BACP,UACoB;CACpB,MAAM,aAAa;EACjB,UAAU;EACV,QAAQ,IAAI;EACZ,QAAQ,IAAI;CACd;CACA,KAAK,MAAM,aAAa,YACtB,IAAI,aAAa,QAAQ,UAAU,KAAK,MAAM,IAC5C,OAAO,UAAU,KAAK;AAI5B;AAEA,SAAgB,+BACd,UACyC;CACzC,IAAI,UAAU,YAAY,OACxB;CAEF,MAAM,cAAc,2BAA2B,QAAQ;CACvD,IAAI,6BAA6B,QAAQ,GACvC,OAAO;EACL,WAAW,SAAS;EACpB,WAAW,SAAS;EACpB,GAAI,UAAU,SAAS,OAAO,IAAI,EAAE,SAAS,SAAS,QAAQ,IAAI,CAAC;EACnE,GAAI,UAAU,WAAW,IAAI,EAAE,YAAY,IAAI,CAAC;EAChD,GAAI,SAAS,sBAAsB,OAC/B,EAAE,oBAAoB,SAAS,mBAAmB,IAClD,CAAC;CACP;CAEF,IAAI,qBAAqB,GAAG;EAC1B,MAAM,UACJ,UAAU,WACV,QAAQ,IAAI,qBACZ,QAAQ,IAAI;EACd,OAAO;GACL,WAAW,QAAQ,IAAI;GACvB,WAAW,QAAQ,IAAI;GACvB,GAAI,UAAU,OAAO,IAAI,EAAE,QAAQ,IAAI,CAAC;GACxC,GAAI,UAAU,WAAW,IAAI,EAAE,YAAY,IAAI,CAAC;GAChD,GAAI,UAAU,sBAAsB,OAChC,EAAE,oBAAoB,SAAS,mBAAmB,IAClD,CAAC;EACP;CACF;CACA,IAAI,UAAU,UAAU,OAAO,KAAK,0BAA0B,GAC5D,OAAO;EACL,WAAW,QAAQ,IAAI;EACvB,WAAW,QAAQ,IAAI;EACvB,SAAS,SAAS;EAClB,GAAI,UAAU,WAAW,IAAI,EAAE,YAAY,IAAI,CAAC;EAChD,GAAI,SAAS,sBAAsB,OAC/B,EAAE,oBAAoB,SAAS,mBAAmB,IAClD,CAAC;CACP;AAGJ;AAEA,SAAS,kBAAkB,OAA+C;CACxE,OAAO,UAAU,KAAK,IAClB,WAAW,QAAQ,CAAC,CAAC,OAAO,OAAO,MAAM,CAAC,CAAC,OAAO,KAAK,IACvD,KAAA;AACN;;;;;;;;AASA,SAAgB,0BACd,QACQ;CACR,OAAO,KAAK,UAAU;EACpB,WAAW,OAAO;EAClB,eAAe,kBAAkB,OAAO,SAAS;EACjD,SAAS,OAAO;EAChB,aAAa,OAAO;CACtB,CAAC;AACH;;;AAIA,SAAgB,8BACd,UACoB;CACpB,MAAM,SAAS,+BAA+B,QAAQ;CACtD,OAAO,UAAU,OAAO,KAAA,IAAY,0BAA0B,MAAM;AACtE"}
|
|
@@ -437,6 +437,11 @@ export interface LangfuseConfig {
|
|
|
437
437
|
* `default` environment.
|
|
438
438
|
*/
|
|
439
439
|
environment?: string;
|
|
440
|
+
/**
|
|
441
|
+
* Whether the Langfuse span processor should detect and upload base64 media
|
|
442
|
+
* payloads. Defaults to the Langfuse SDK behavior.
|
|
443
|
+
*/
|
|
444
|
+
mediaUploadEnabled?: boolean;
|
|
440
445
|
metadata?: Record<string, string | number | boolean | null | undefined>;
|
|
441
446
|
/**
|
|
442
447
|
* Internal OTLP span attributes to attach to Langfuse observations before
|
package/package.json
CHANGED
package/src/instrumentation.ts
CHANGED
|
@@ -72,6 +72,9 @@ export function getLangfuseSpanProcessorParams(
|
|
|
72
72
|
secretKey: langfuse.secretKey,
|
|
73
73
|
...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),
|
|
74
74
|
...(isPresent(environment) ? { environment } : {}),
|
|
75
|
+
...(langfuse.mediaUploadEnabled != null
|
|
76
|
+
? { mediaUploadEnabled: langfuse.mediaUploadEnabled }
|
|
77
|
+
: {}),
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
80
|
if (hasLangfuseEnvConfig()) {
|
|
@@ -84,6 +87,9 @@ export function getLangfuseSpanProcessorParams(
|
|
|
84
87
|
secretKey: process.env.LANGFUSE_SECRET_KEY as string,
|
|
85
88
|
...(isPresent(baseUrl) ? { baseUrl } : {}),
|
|
86
89
|
...(isPresent(environment) ? { environment } : {}),
|
|
90
|
+
...(langfuse?.mediaUploadEnabled != null
|
|
91
|
+
? { mediaUploadEnabled: langfuse.mediaUploadEnabled }
|
|
92
|
+
: {}),
|
|
87
93
|
};
|
|
88
94
|
}
|
|
89
95
|
if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) {
|
|
@@ -92,6 +98,9 @@ export function getLangfuseSpanProcessorParams(
|
|
|
92
98
|
secretKey: process.env.LANGFUSE_SECRET_KEY as string,
|
|
93
99
|
baseUrl: langfuse.baseUrl,
|
|
94
100
|
...(isPresent(environment) ? { environment } : {}),
|
|
101
|
+
...(langfuse.mediaUploadEnabled != null
|
|
102
|
+
? { mediaUploadEnabled: langfuse.mediaUploadEnabled }
|
|
103
|
+
: {}),
|
|
95
104
|
};
|
|
96
105
|
}
|
|
97
106
|
return undefined;
|
|
@@ -320,6 +320,32 @@ describe('Langfuse instrumentation', () => {
|
|
|
320
320
|
});
|
|
321
321
|
});
|
|
322
322
|
|
|
323
|
+
it('does not reuse processors across different media upload policies', async () => {
|
|
324
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
325
|
+
initializeLangfuseTracing({
|
|
326
|
+
publicKey: 'pk-media',
|
|
327
|
+
secretKey: 'sk-media',
|
|
328
|
+
baseUrl: 'https://langfuse.media',
|
|
329
|
+
mediaUploadEnabled: false,
|
|
330
|
+
});
|
|
331
|
+
initializeLangfuseTracing({
|
|
332
|
+
publicKey: 'pk-media',
|
|
333
|
+
secretKey: 'sk-media',
|
|
334
|
+
baseUrl: 'https://langfuse.media',
|
|
335
|
+
mediaUploadEnabled: true,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
expect(mockLangfuseSpanProcessor).toHaveBeenCalledTimes(2);
|
|
339
|
+
expect(mockLangfuseSpanProcessor).toHaveBeenNthCalledWith(
|
|
340
|
+
1,
|
|
341
|
+
expect.objectContaining({ mediaUploadEnabled: false })
|
|
342
|
+
);
|
|
343
|
+
expect(mockLangfuseSpanProcessor).toHaveBeenNthCalledWith(
|
|
344
|
+
2,
|
|
345
|
+
expect.objectContaining({ mediaUploadEnabled: true })
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
323
349
|
it('reuses the isolated provider after initialization', async () => {
|
|
324
350
|
process.env.LANGFUSE_SECRET_KEY = 'sk-test';
|
|
325
351
|
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Span } from '@opentelemetry/api';
|
|
2
2
|
import type * as t from '@/types';
|
|
3
3
|
import {
|
|
4
|
+
getLangfuseSpanProcessorParams,
|
|
4
5
|
getLangfuseManagedSpanDestination,
|
|
5
6
|
registerLangfuseManagedSpan,
|
|
6
7
|
resolveLangfuseDestinationKey,
|
|
@@ -41,7 +42,23 @@ describe('Langfuse span registry', () => {
|
|
|
41
42
|
const redacting = resolveLangfuseDestinationKey(
|
|
42
43
|
tenantConfig({ toolOutputTracing: { enabled: false } })
|
|
43
44
|
);
|
|
45
|
+
const mediaDisabled = resolveLangfuseDestinationKey(
|
|
46
|
+
tenantConfig({ mediaUploadEnabled: false })
|
|
47
|
+
);
|
|
44
48
|
expect(redacting).toBe(base);
|
|
49
|
+
expect(mediaDisabled).toBe(base);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('passes media upload policy to the Langfuse span processor params', () => {
|
|
53
|
+
expect(
|
|
54
|
+
getLangfuseSpanProcessorParams(
|
|
55
|
+
tenantConfig({ mediaUploadEnabled: false })
|
|
56
|
+
)
|
|
57
|
+
).toEqual(
|
|
58
|
+
expect.objectContaining({
|
|
59
|
+
mediaUploadEnabled: false,
|
|
60
|
+
})
|
|
61
|
+
);
|
|
45
62
|
});
|
|
46
63
|
|
|
47
64
|
it('separates destinations by credentials, endpoint, and environment', () => {
|
|
@@ -1162,7 +1162,7 @@ const hasBedrock = hasEveryEnv(requiredBedrockEnv);
|
|
|
1162
1162
|
|
|
1163
1163
|
const hasOpenAI = hasEnv('OPENAI_API_KEY');
|
|
1164
1164
|
(hasOpenAI ? describe : describe.skip)('OpenAI Summarization E2E', () => {
|
|
1165
|
-
jest.setTimeout(
|
|
1165
|
+
jest.setTimeout(240_000);
|
|
1166
1166
|
|
|
1167
1167
|
const agentProvider = Providers.OPENAI;
|
|
1168
1168
|
const streamConfig = {
|
|
@@ -1190,6 +1190,9 @@ const hasOpenAI = hasEnv('OPENAI_API_KEY');
|
|
|
1190
1190
|
agentProvider,
|
|
1191
1191
|
summarizationProvider: Providers.OPENAI,
|
|
1192
1192
|
summarizationModel: 'gpt-4.1-mini',
|
|
1193
|
+
llmConfigOverride: {
|
|
1194
|
+
model: 'gpt-4.1-mini',
|
|
1195
|
+
},
|
|
1193
1196
|
maxContextTokens: maxTokens,
|
|
1194
1197
|
instructions:
|
|
1195
1198
|
'You are a helpful math tutor. Use the calculator tool for ALL computations. Keep responses concise.',
|
|
@@ -1320,7 +1323,7 @@ const hasOpenAI = hasEnv('OPENAI_API_KEY');
|
|
|
1320
1323
|
` OpenAI summary: "${getSummaryText(completePayload.summary).substring(0, 200)}…"`
|
|
1321
1324
|
);
|
|
1322
1325
|
console.log(` Final messages: ${conversationHistory.length}`);
|
|
1323
|
-
},
|
|
1326
|
+
}, 240_000);
|
|
1324
1327
|
});
|
|
1325
1328
|
|
|
1326
1329
|
// ---------------------------------------------------------------------------
|
package/src/types/graph.ts
CHANGED
|
@@ -571,6 +571,11 @@ export interface LangfuseConfig {
|
|
|
571
571
|
* `default` environment.
|
|
572
572
|
*/
|
|
573
573
|
environment?: string;
|
|
574
|
+
/**
|
|
575
|
+
* Whether the Langfuse span processor should detect and upload base64 media
|
|
576
|
+
* payloads. Defaults to the Langfuse SDK behavior.
|
|
577
|
+
*/
|
|
578
|
+
mediaUploadEnabled?: boolean;
|
|
574
579
|
metadata?: Record<string, string | number | boolean | null | undefined>;
|
|
575
580
|
/**
|
|
576
581
|
* Internal OTLP span attributes to attach to Langfuse observations before
|