mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-21 09:32:24 +00:00
📛 feat: Tag Langfuse Traces With Tenant ID (#13808)
* feat: tag Langfuse traces with tenant id * fix: propagate tenant id to agent Langfuse config
This commit is contained in:
parent
8628897c9c
commit
27b0782201
6 changed files with 96 additions and 28 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
user?: Record<string, unknown>;
|
||||
tenantId?: string;
|
||||
} = {}): Promise<Record<string, unknown>> {
|
||||
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<string, unknown>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
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<string, unknown>;
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, EventHandler>;
|
||||
requestBody: Record<string, unknown>;
|
||||
user: Record<string, unknown>;
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
}),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue