From 27b0782201142583aa89d3c680f9f4ea4ef83b04 Mon Sep 17 00:00:00 2001 From: Ravi Kumar L Date: Thu, 18 Jun 2026 02:27:55 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9B=20feat:=20Tag=20Langfuse=20Traces?= =?UTF-8?q?=20With=20Tenant=20ID=20(#13808)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: tag Langfuse traces with tenant id * fix: propagate tenant id to agent Langfuse config --- api/server/controllers/agents/client.js | 1 + api/server/controllers/agents/openai.js | 1 + api/server/controllers/agents/responses.js | 2 + .../__tests__/run-summarization.test.ts | 101 +++++++++++++----- packages/api/src/agents/openai/service.ts | 4 +- packages/api/src/agents/run.ts | 15 ++- 6 files changed, 96 insertions(+), 28 deletions(-) diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 94d17fa3f5..0182ade3ae 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -1327,6 +1327,7 @@ class AgentClient extends BaseClient { customHandlers: this.options.eventHandlers, requestBody: config.configurable.requestBody, user: createSafeUser(this.options.req?.user), + tenantId: this.options.req?.user?.tenantId, summarizationConfig: appConfig?.summarization, appConfig, tokenCounter, diff --git a/api/server/controllers/agents/openai.js b/api/server/controllers/agents/openai.js index 3c00b1fce2..9000c24708 100644 --- a/api/server/controllers/agents/openai.js +++ b/api/server/controllers/agents/openai.js @@ -743,6 +743,7 @@ const OpenAIChatCompletionController = async (req, res) => { conversationId, }, user: { id: userId }, + tenantId: req.user?.tenantId, /** Bills subagent child-run model calls (reported outside the * streamEvents loop) into the same collectedUsage array. */ subagentUsageSink: createSubagentUsageSink(collectedUsage), diff --git a/api/server/controllers/agents/responses.js b/api/server/controllers/agents/responses.js index abd292fd55..212b245a49 100644 --- a/api/server/controllers/agents/responses.js +++ b/api/server/controllers/agents/responses.js @@ -750,6 +750,7 @@ const createResponse = async (req, res) => { conversationId, }, user: { id: userId }, + tenantId: req.user?.tenantId, /** Bills subagent child-run model calls (reported outside the * streamEvents loop) into the same collectedUsage array. */ subagentUsageSink: createSubagentUsageSink(collectedUsage), @@ -927,6 +928,7 @@ const createResponse = async (req, res) => { conversationId, }, user: { id: userId }, + tenantId: req.user?.tenantId, /** Bills subagent child-run model calls (reported outside the * streamEvents loop) into the same collectedUsage array. */ subagentUsageSink: createSubagentUsageSink(collectedUsage), diff --git a/packages/api/src/agents/__tests__/run-summarization.test.ts b/packages/api/src/agents/__tests__/run-summarization.test.ts index ba31a0cdac..868010b1e8 100644 --- a/packages/api/src/agents/__tests__/run-summarization.test.ts +++ b/packages/api/src/agents/__tests__/run-summarization.test.ts @@ -1,12 +1,12 @@ import { logger } from '@librechat/data-schemas'; -import type { AppConfig } from '@librechat/data-schemas'; -import type { SummarizationConfig, TEndpoint } from 'librechat-data-provider'; import { EModelEndpoint, FileSources, MAX_SUBAGENT_DEPTH, MAX_SUBAGENT_RUN_CONFIGS, } from 'librechat-data-provider'; +import type { SummarizationConfig, TEndpoint } from 'librechat-data-provider'; +import type { AppConfig } from '@librechat/data-schemas'; import { createRun } from '~/agents/run'; // Mock winston logger — `format` must be callable so @librechat/data-schemas @@ -1103,39 +1103,88 @@ describe('subagentConfigs', () => { }); }); +/** + * Captures the top-level `Run.create` config (not just agentInputs) so tests + * can assert presence/absence of run-level options. + */ +async function callAndCaptureRunConfig({ + overrides, + user, + tenantId, +}: { + overrides?: Record; + user?: Record; + tenantId?: string; +} = {}): Promise> { + const agents = [makeAgent(overrides)]; + const signal = new AbortController().signal; + + await createRun({ + agents: agents as never, + signal, + streaming: true, + streamUsage: true, + user: user as never, + tenantId, + }); + + const createMock = Run.create as jest.Mock; + expect(createMock).toHaveBeenCalledTimes(1); + return createMock.mock.calls[0][0] as Record; +} + +// --------------------------------------------------------------------------- +// Suite: Langfuse run config +// --------------------------------------------------------------------------- +describe('Langfuse run config', () => { + it('passes deterministic Langfuse trace config without tenant metadata by default', async () => { + const callArgs = await callAndCaptureRunConfig(); + expect(callArgs.langfuse).toEqual({ deterministicTraceId: true }); + }); + + it('adds the explicit request tenant id to Langfuse trace metadata and tags', async () => { + const callArgs = await callAndCaptureRunConfig({ + user: { + id: 'user-1', + }, + tenantId: 'tenant-1', + }); + expect(callArgs.langfuse).toEqual({ + deterministicTraceId: true, + metadata: { 'librechat.tenant.id': 'tenant-1' }, + tags: ['tenant:tenant-1'], + }); + }); + + it('falls back to a full user tenant id for direct createRun callers', async () => { + const callArgs = await callAndCaptureRunConfig({ + user: { + tenantId: 'tenant-2', + }, + }); + expect(callArgs.langfuse).toEqual({ + deterministicTraceId: true, + metadata: { 'librechat.tenant.id': 'tenant-2' }, + tags: ['tenant:tenant-2'], + }); + }); +}); + // --------------------------------------------------------------------------- // Suite: toolOutputReferences gating // --------------------------------------------------------------------------- describe('toolOutputReferences gating', () => { - /** - * Captures the top-level `Run.create` config (not just agentInputs) so the - * test can assert presence/absence of the `toolOutputReferences` key. - */ - async function callAndCaptureRunConfig( - overrides?: Record, - ): Promise> { - const agents = [makeAgent(overrides)]; - const signal = new AbortController().signal; - - await createRun({ - agents: agents as never, - signal, - streaming: true, - streamUsage: true, - }); - - const createMock = Run.create as jest.Mock; - expect(createMock).toHaveBeenCalledTimes(1); - return createMock.mock.calls[0][0] as Record; - } - it('passes toolOutputReferences when agent has codeEnvAvailable=true', async () => { - const callArgs = await callAndCaptureRunConfig({ codeEnvAvailable: true }); + const callArgs = await callAndCaptureRunConfig({ + overrides: { codeEnvAvailable: true }, + }); expect(callArgs.toolOutputReferences).toEqual({ enabled: true }); }); it('omits toolOutputReferences when codeEnvAvailable is false', async () => { - const callArgs = await callAndCaptureRunConfig({ codeEnvAvailable: false }); + const callArgs = await callAndCaptureRunConfig({ + overrides: { codeEnvAvailable: false }, + }); expect(callArgs).not.toHaveProperty('toolOutputReferences'); }); diff --git a/packages/api/src/agents/openai/service.ts b/packages/api/src/agents/openai/service.ts index dd8688d051..22b06226dd 100644 --- a/packages/api/src/agents/openai/service.ts +++ b/packages/api/src/agents/openai/service.ts @@ -31,6 +31,7 @@ import type { ToolCall, } from './types'; import type { OpenAIStreamHandlerConfig, EventHandler } from './handlers'; +import type { ToolExecuteOptions } from '../handlers'; import { createOpenAIContentAggregator, createOpenAIStreamTracker, @@ -40,7 +41,6 @@ import { writeSSE, } from './handlers'; import { createSafeUser } from '~/utils'; -import type { ToolExecuteOptions } from '../handlers'; /** * Dependencies for the chat completion service @@ -175,6 +175,7 @@ type CreateRunFn = (params: { customHandlers: Record; requestBody: Record; user: Record; + tenantId?: string; tokenCounter?: (message: unknown) => number; }) => Promise<{ Graph?: unknown; @@ -524,6 +525,7 @@ export async function createAgentChatCompletion( conversationId, }, user: safeUser, + tenantId: typeof reqUser?.tenantId === 'string' ? reqUser.tenantId : undefined, }); if (run) { diff --git a/packages/api/src/agents/run.ts b/packages/api/src/agents/run.ts index 833213a86c..0ecfe2d50a 100644 --- a/packages/api/src/agents/run.ts +++ b/packages/api/src/agents/run.ts @@ -794,6 +794,17 @@ function buildSubagentConfigs( return configs; } +function buildLangfuseConfig(tenantIdInput?: unknown) { + const tenantId = typeof tenantIdInput === 'string' ? tenantIdInput.trim() : ''; + return { + deterministicTraceId: true, + ...(tenantId !== '' && { + metadata: { 'librechat.tenant.id': tenantId }, + tags: [`tenant:${tenantId}`], + }), + }; +} + /** * Creates a new Run instance with custom handlers and configuration. * @@ -816,6 +827,7 @@ export async function createRun({ messages, requestBody, user, + tenantId, tokenCounter, customHandlers, indexTokenCountMap, @@ -835,6 +847,7 @@ export async function createRun({ streamUsage?: boolean; requestBody?: t.RequestBody; user?: IUser; + tenantId?: string; /** Message history for extracting previously discovered tools */ messages?: BaseMessage[]; summarizationConfig?: SummarizationConfig; @@ -1097,7 +1110,7 @@ export async function createRun({ // feedback can be scored against the trace without a lookup (see the // feedback route in api/server/routes/messages.js). No-op unless Langfuse // tracing is enabled. Requires @librechat/agents >= 3.2.21. - langfuse: { deterministicTraceId: true }, + langfuse: buildLangfuseConfig(tenantId ?? user?.tenantId), ...(enableToolOutputReferences && { toolOutputReferences: { enabled: true }, }),