From fcf2a8918969e44b369a45c2718a4ea62a231f82 Mon Sep 17 00:00:00 2001 From: Ravi Kumar L Date: Mon, 22 Jun 2026 10:57:34 +0200 Subject: [PATCH] fix: align langfuse fanout env parsing --- .../__tests__/run-summarization.test.ts | 32 ++++++++++++++++++ packages/api/src/langfuse/config.ts | 6 ++-- packages/api/src/langfuse/destinations.ts | 7 ++-- packages/api/src/langfuse/feedback.spec.ts | 33 +++++++++++++++++++ packages/api/src/langfuse/utils.ts | 8 ----- 5 files changed, 72 insertions(+), 14 deletions(-) diff --git a/packages/api/src/agents/__tests__/run-summarization.test.ts b/packages/api/src/agents/__tests__/run-summarization.test.ts index 2b174fb01f..7c63b2b40b 100644 --- a/packages/api/src/agents/__tests__/run-summarization.test.ts +++ b/packages/api/src/agents/__tests__/run-summarization.test.ts @@ -1383,6 +1383,38 @@ describe('Langfuse run config', () => { }); }); + it('only disables tenant fanout export when the emergency toggle is true', async () => { + process.env.LANGFUSE_PUBLIC_KEY = 'pk-central'; + process.env.LANGFUSE_SECRET_KEY = 'sk-central'; + process.env.LANGFUSE_FANOUT_ENABLED = 'true'; + process.env.LANGFUSE_FANOUT_COLLECTOR_URL = 'http://collector-from-env:4318'; + process.env.LANGFUSE_FANOUT_TENANT_EXPORT_DISABLED = '1'; + + const callArgs = await callAndCaptureRunConfig({ + tenantId: 'tenant-1', + appConfig: { + langfuse: { + publicKey: 'pk-tenant-1', + secretKey: 'sk-tenant-1', + baseUrl: 'https://cloud.langfuse.com', + }, + } as AppConfig, + }); + + expect(callArgs.langfuse).toEqual({ + deterministicTraceId: true, + baseUrl: 'http://collector-from-env:4318', + metadata: { 'librechat.tenant.id': 'tenant-1' }, + publicKey: 'pk-tenant-1', + secretKey: 'sk-tenant-1', + tags: ['tenant:tenant-1'], + librechatTraceAttributes: { + 'librechat.langfuse.tenant_export.enabled': 'true', + 'librechat.langfuse.destination': 'eu', + }, + }); + }); + it('lets tenant fanout.enabled=false override deployment fanout env', async () => { process.env.LANGFUSE_FANOUT_ENABLED = 'true'; process.env.LANGFUSE_FANOUT_COLLECTOR_URL = 'http://collector-from-env:4318'; diff --git a/packages/api/src/langfuse/config.ts b/packages/api/src/langfuse/config.ts index 412221be6d..f989aefdd5 100644 --- a/packages/api/src/langfuse/config.ts +++ b/packages/api/src/langfuse/config.ts @@ -2,7 +2,7 @@ import type { AppConfig } from '@librechat/data-schemas'; import type { RunConfig } from '@librechat/agents'; import { resolveLangfuseTenantDestination } from './tenantDestinations'; import { normalizeString } from '~/utils/text'; -import { isTrueEnv } from './utils'; +import { isEnabled } from '~/utils'; type LangfuseRunConfig = NonNullable; type LangfuseRunConfigWithTraceAttributes = LangfuseRunConfig & { @@ -12,7 +12,7 @@ const TENANT_EXPORT_ATTRIBUTE = 'librechat.langfuse.tenant_export.enabled'; const TENANT_DESTINATION_ATTRIBUTE = 'librechat.langfuse.destination'; function isTenantExportEnabled(): boolean { - return !isTrueEnv(process.env.LANGFUSE_FANOUT_TENANT_EXPORT_DISABLED); + return !isEnabled(process.env.LANGFUSE_FANOUT_TENANT_EXPORT_DISABLED); } function mergeTraceMetadata( @@ -70,7 +70,7 @@ export function buildLangfuseConfig({ const fanout = config?.fanout; const fanoutEnabled = fanout?.enabled !== false && - (fanout?.enabled === true || process.env.LANGFUSE_FANOUT_ENABLED === 'true'); + (fanout?.enabled === true || isEnabled(process.env.LANGFUSE_FANOUT_ENABLED)); const fanoutCollectorUrl = normalizeString(fanout?.collectorUrl) ?? normalizeString(process.env.LANGFUSE_FANOUT_COLLECTOR_URL); diff --git a/packages/api/src/langfuse/destinations.ts b/packages/api/src/langfuse/destinations.ts index a5e4e32ff8..a9e400d251 100644 --- a/packages/api/src/langfuse/destinations.ts +++ b/packages/api/src/langfuse/destinations.ts @@ -1,7 +1,8 @@ import type { AppConfig } from '@librechat/data-schemas'; import { resolveLangfuseTenantDestination } from './tenantDestinations'; -import { isFalseEnv, isTrueEnv, toBasicAuthorization } from './utils'; +import { toBasicAuthorization } from './utils'; import { normalizeString } from '~/utils/text'; +import { isEnabled } from '~/utils'; const DEFAULT_BASE_URL = 'https://cloud.langfuse.com'; @@ -21,7 +22,7 @@ function isSampleRateEnabled(value?: string): boolean { function isTracingEnabled(): boolean { return ( - !isFalseEnv(process.env.LANGFUSE_TRACING_ENABLED) && + process.env.LANGFUSE_TRACING_ENABLED?.trim().toLowerCase() !== 'false' && isSampleRateEnabled(process.env.LANGFUSE_SAMPLE_RATE) ); } @@ -66,7 +67,7 @@ function getTenantScoreDestination(appConfig?: AppConfig): LangfuseScoreDestinat if (!isTracingEnabled()) { return undefined; } - if (isTrueEnv(process.env.LANGFUSE_FANOUT_TENANT_EXPORT_DISABLED)) { + if (isEnabled(process.env.LANGFUSE_FANOUT_TENANT_EXPORT_DISABLED)) { return undefined; } diff --git a/packages/api/src/langfuse/feedback.spec.ts b/packages/api/src/langfuse/feedback.spec.ts index 35e26591d6..11d980f91b 100644 --- a/packages/api/src/langfuse/feedback.spec.ts +++ b/packages/api/src/langfuse/feedback.spec.ts @@ -396,6 +396,39 @@ describe('Langfuse feedback scores', () => { ); }); + it('only disables tenant scores when the emergency toggle is true', async () => { + process.env.LANGFUSE_FANOUT_CENTRAL_AUTH_HEADER = 'Basic central-auth'; + process.env.LANGFUSE_FANOUT_CENTRAL_BASE_URL = 'http://central-langfuse:3000'; + process.env.LANGFUSE_FANOUT_TENANT_EXPORT_DISABLED = '1'; + const { sendFeedbackScore } = await loadFeedback(); + + await sendFeedbackScore({ + traceId: 'trace-id', + feedback: { rating: 'thumbsUp' }, + appConfig: appConfigWithLangfuse({ + publicKey: 'tenant-public-key', + secretKey: 'tenant-secret-key', + baseUrl: 'https://cloud.langfuse.com', + }), + }); + + expect(getFetchMock()).toHaveBeenCalledTimes(2); + expect(getFetchMock()).toHaveBeenCalledWith( + 'http://central-langfuse:3000/api/public/scores', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ Authorization: 'Basic central-auth' }), + }), + ); + expect(getFetchMock()).toHaveBeenCalledWith( + 'https://cloud.langfuse.com/api/public/scores', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ Authorization: getTenantAuthorization() }), + }), + ); + }); + it('deduplicates matching central and tenant score destinations', async () => { process.env.LANGFUSE_FANOUT_CENTRAL_AUTH_HEADER = getTenantAuthorization(); process.env.LANGFUSE_FANOUT_CENTRAL_BASE_URL = 'http://shared-langfuse:3000'; diff --git a/packages/api/src/langfuse/utils.ts b/packages/api/src/langfuse/utils.ts index e8131d0522..ac966a7d6b 100644 --- a/packages/api/src/langfuse/utils.ts +++ b/packages/api/src/langfuse/utils.ts @@ -1,11 +1,3 @@ -export function isFalseEnv(value?: string): boolean { - return value != null && ['0', 'false', 'no', 'off'].includes(value.trim().toLowerCase()); -} - -export function isTrueEnv(value?: string): boolean { - return value != null && ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase()); -} - export function toBasicAuthorization(publicKey: string, secretKey: string): string { return `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString('base64')}`; }