@librechat/agents 3.3.7 → 3.3.9
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/graphs/Graph.cjs +47 -13
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +56 -6
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/hitl/askUserQuestion.cjs +3 -2
- package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +18 -48
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +174 -29
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/langfuseConfig.cjs +12 -0
- package/dist/cjs/langfuseConfig.cjs.map +1 -1
- package/dist/cjs/langfuseRuntimeContext.cjs +23 -2
- package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
- package/dist/cjs/langfuseRuntimeScope.cjs +39 -8
- package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
- package/dist/cjs/langfuseSpanRegistry.cjs +95 -0
- package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -0
- package/dist/cjs/langfuseTraceShaping.cjs +121 -4
- package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +39 -15
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +25 -5
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +3 -3
- package/dist/cjs/llm/invoke.cjs +5 -5
- package/dist/cjs/llm/openai/index.cjs +1 -1
- package/dist/cjs/main.cjs +10 -10
- package/dist/cjs/messages/format.cjs +124 -15
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/injected.cjs +10 -1
- package/dist/cjs/messages/injected.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +13 -1
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/prompts/activityLabel.cjs +51 -11
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
- package/dist/cjs/run.cjs +54 -24
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/messageSerialization.cjs +6 -0
- package/dist/cjs/session/messageSerialization.cjs.map +1 -1
- package/dist/cjs/stream.cjs +21 -10
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +60 -0
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +253 -24
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +1 -1
- package/dist/cjs/tools/search/tool.cjs +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
- package/dist/cjs/utils/index.cjs +2 -2
- package/dist/esm/graphs/Graph.mjs +48 -14
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +56 -6
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/hitl/askUserQuestion.mjs +3 -2
- package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +18 -48
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +176 -28
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/langfuseConfig.mjs +10 -1
- package/dist/esm/langfuseConfig.mjs.map +1 -1
- package/dist/esm/langfuseRuntimeContext.mjs +21 -3
- package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
- package/dist/esm/langfuseRuntimeScope.mjs +39 -10
- package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
- package/dist/esm/langfuseSpanRegistry.mjs +91 -0
- package/dist/esm/langfuseSpanRegistry.mjs.map +1 -0
- package/dist/esm/langfuseTraceShaping.mjs +121 -4
- package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +39 -15
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +25 -5
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +2 -2
- package/dist/esm/llm/invoke.mjs +5 -5
- package/dist/esm/llm/openai/index.mjs +1 -1
- package/dist/esm/main.mjs +8 -8
- package/dist/esm/messages/format.mjs +124 -15
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/injected.mjs +10 -1
- package/dist/esm/messages/injected.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +13 -1
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/prompts/activityLabel.mjs +51 -11
- package/dist/esm/prompts/activityLabel.mjs.map +1 -1
- package/dist/esm/run.mjs +54 -24
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/messageSerialization.mjs +6 -0
- package/dist/esm/session/messageSerialization.mjs.map +1 -1
- package/dist/esm/stream.mjs +21 -10
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +60 -0
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +254 -25
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +1 -1
- package/dist/esm/tools/search/tool.mjs +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
- package/dist/esm/utils/index.mjs +2 -2
- package/dist/types/graphs/Graph.d.ts +19 -0
- package/dist/types/hitl/askUserQuestion.d.ts +11 -1
- package/dist/types/langfuse.d.ts +16 -8
- package/dist/types/langfuseConfig.d.ts +6 -0
- package/dist/types/langfuseRuntimeContext.d.ts +27 -1
- package/dist/types/langfuseRuntimeScope.d.ts +17 -2
- package/dist/types/langfuseSpanRegistry.d.ts +17 -0
- package/dist/types/langfuseTraceShaping.d.ts +2 -1
- package/dist/types/llm/anthropic/utils/message_inputs.d.ts +1 -0
- package/dist/types/messages/format.d.ts +9 -8
- package/dist/types/prompts/activityLabel.d.ts +8 -1
- package/dist/types/run.d.ts +1 -1
- package/dist/types/session/types.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +7 -1
- package/dist/types/types/activityLabel.d.ts +8 -0
- package/dist/types/types/hitl.d.ts +8 -0
- package/dist/types/types/stream.d.ts +19 -0
- package/dist/types/types/tools.d.ts +30 -0
- package/package.json +7 -4
- package/src/__tests__/stream.eagerArgsDivergence.test.ts +753 -0
- package/src/graphs/Graph.ts +69 -20
- package/src/graphs/MultiAgentGraph.ts +74 -6
- package/src/graphs/__tests__/composition.smoke.test.ts +4 -0
- package/src/hitl/askUserQuestion.ts +14 -1
- package/src/instrumentation.ts +35 -77
- package/src/langfuse.ts +320 -43
- package/src/langfuseConfig.ts +24 -0
- package/src/langfuseRuntimeContext.ts +43 -1
- package/src/langfuseRuntimeScope.ts +94 -21
- package/src/langfuseSpanRegistry.ts +131 -0
- package/src/langfuseTraceShaping.ts +194 -7
- package/src/llm/anthropic/utils/message_inputs.ts +70 -19
- package/src/llm/anthropic/utils/streaming-tool-input.test.ts +186 -11
- package/src/llm/bedrock/utils/message_inputs.test.ts +120 -4
- package/src/llm/bedrock/utils/message_inputs.ts +32 -7
- package/src/messages/format.ts +222 -50
- package/src/messages/formatAgentMessages.test.ts +308 -6
- package/src/messages/injected.test.ts +18 -1
- package/src/messages/injected.ts +8 -1
- package/src/messages/prune.ts +12 -1
- package/src/prompts/activityLabel.ts +67 -2
- package/src/run.ts +86 -46
- package/src/scripts/activity-labels/captured.json +56 -0
- package/src/scripts/activity-labels/checks.cjs +205 -0
- package/src/scripts/activity-labels/corpus.cjs +473 -0
- package/src/scripts/activity-labels/report.cjs +203 -0
- package/src/scripts/activity-labels/rescore.cjs +102 -0
- package/src/scripts/activity-labels/run.ts +705 -0
- package/src/scripts/activity-labels/variants.ts +71 -0
- package/src/session/messageSerialization.ts +12 -1
- package/src/session/types.ts +1 -0
- package/src/specs/activity-label-prompt.test.ts +109 -0
- package/src/specs/agent-handoffs.test.ts +306 -0
- package/src/specs/langfuse-callbacks.test.ts +456 -0
- package/src/specs/langfuse-routing.integration.test.ts +138 -1
- package/src/specs/langfuse-span-registry.test.ts +70 -0
- package/src/specs/langfuse-trace-shaping.test.ts +294 -0
- package/src/specs/prune.test.ts +38 -1
- package/src/stream.ts +70 -6
- package/src/summarization/__tests__/node.test.ts +188 -0
- package/src/summarization/node.ts +72 -0
- package/src/tools/ToolNode.ts +400 -9
- package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +757 -0
- package/src/tools/__tests__/hitl.test.ts +58 -0
- package/src/types/activityLabel.ts +8 -0
- package/src/types/hitl.ts +8 -0
- package/src/types/stream.ts +20 -0
- package/src/types/tools.ts +35 -1
|
@@ -385,6 +385,462 @@ describe('Langfuse callback composition', () => {
|
|
|
385
385
|
);
|
|
386
386
|
});
|
|
387
387
|
|
|
388
|
+
it('keeps callback spans on their own run when a foreign run scope is active', async () => {
|
|
389
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
390
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
391
|
+
const { withLangfuseRuntimeScope } = await import('@/langfuseRuntimeScope');
|
|
392
|
+
const tenantA = {
|
|
393
|
+
publicKey: 'pk-tenant-a',
|
|
394
|
+
secretKey: 'sk-tenant-a',
|
|
395
|
+
baseUrl: 'https://langfuse.tenant-a',
|
|
396
|
+
deterministicTraceId: true,
|
|
397
|
+
};
|
|
398
|
+
const tenantB = {
|
|
399
|
+
publicKey: 'pk-tenant-b',
|
|
400
|
+
secretKey: 'sk-tenant-b',
|
|
401
|
+
baseUrl: 'https://langfuse.tenant-b',
|
|
402
|
+
deterministicTraceId: true,
|
|
403
|
+
};
|
|
404
|
+
initializeLangfuseTracing(tenantA);
|
|
405
|
+
initializeLangfuseTracing(tenantB);
|
|
406
|
+
const handlerB = createLangfuseHandler({
|
|
407
|
+
langfuse: tenantB,
|
|
408
|
+
traceIdSeed: 'run-b',
|
|
409
|
+
runId: 'run-b',
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// LangChain's background callback queue (`consumeCallback`) executes
|
|
413
|
+
// non-awaited callbacks inside whichever concurrent run's async context
|
|
414
|
+
// the queue drain happens to be on — here, tenant-A's. A scope stamped
|
|
415
|
+
// with a different run must never reroute tenant-B's spans.
|
|
416
|
+
await withLangfuseRuntimeScope(
|
|
417
|
+
{ langfuse: tenantA, traceIdSeed: 'run-a', runId: 'run-a' },
|
|
418
|
+
() =>
|
|
419
|
+
handlerB?.handleChainStart(
|
|
420
|
+
{ lc: 1, type: 'not_implemented', id: ['ForeignScopedChain'] },
|
|
421
|
+
{ input: 'foreign scoped' },
|
|
422
|
+
'lc-foreign-run'
|
|
423
|
+
)
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
expect(mockProcessorStarts).toContainEqual(
|
|
427
|
+
expect.objectContaining({
|
|
428
|
+
params: expect.objectContaining({
|
|
429
|
+
publicKey: 'pk-tenant-b',
|
|
430
|
+
secretKey: 'sk-tenant-b',
|
|
431
|
+
baseUrl: 'https://langfuse.tenant-b',
|
|
432
|
+
}),
|
|
433
|
+
traceId: traceIdFromSeed('run-b'),
|
|
434
|
+
})
|
|
435
|
+
);
|
|
436
|
+
expect(
|
|
437
|
+
mockProcessorStarts.filter(
|
|
438
|
+
(record) =>
|
|
439
|
+
(record.params as { publicKey?: string }).publicKey ===
|
|
440
|
+
'pk-tenant-a' || record.traceId === traceIdFromSeed('run-a')
|
|
441
|
+
)
|
|
442
|
+
).toHaveLength(0);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('adopts an agent overlay scope stamped with the handler run', async () => {
|
|
446
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
447
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
448
|
+
const { withLangfuseRuntimeScope } = await import('@/langfuseRuntimeScope');
|
|
449
|
+
const runLangfuse = {
|
|
450
|
+
publicKey: 'pk-run',
|
|
451
|
+
secretKey: 'sk-run',
|
|
452
|
+
baseUrl: 'https://langfuse.run',
|
|
453
|
+
deterministicTraceId: true,
|
|
454
|
+
};
|
|
455
|
+
const agentLangfuse = {
|
|
456
|
+
publicKey: 'pk-agent',
|
|
457
|
+
secretKey: 'sk-agent',
|
|
458
|
+
baseUrl: 'https://langfuse.agent',
|
|
459
|
+
deterministicTraceId: true,
|
|
460
|
+
};
|
|
461
|
+
initializeLangfuseTracing(runLangfuse);
|
|
462
|
+
initializeLangfuseTracing(agentLangfuse);
|
|
463
|
+
const streamHandler = createLangfuseHandler({
|
|
464
|
+
langfuse: runLangfuse,
|
|
465
|
+
traceIdSeed: 'run-seed',
|
|
466
|
+
runId: 'run-1',
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
await withLangfuseRuntimeScope(
|
|
470
|
+
{ langfuse: agentLangfuse, traceIdSeed: 'agent-seed', runId: 'run-1' },
|
|
471
|
+
() =>
|
|
472
|
+
streamHandler?.handleChainStart(
|
|
473
|
+
{ lc: 1, type: 'not_implemented', id: ['SameRunOverlayChain'] },
|
|
474
|
+
{ input: 'same run overlay' },
|
|
475
|
+
'lc-overlay-run'
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
expect(mockProcessorStarts).toContainEqual(
|
|
480
|
+
expect.objectContaining({
|
|
481
|
+
params: expect.objectContaining({
|
|
482
|
+
publicKey: 'pk-agent',
|
|
483
|
+
secretKey: 'sk-agent',
|
|
484
|
+
baseUrl: 'https://langfuse.agent',
|
|
485
|
+
}),
|
|
486
|
+
traceId: traceIdFromSeed('agent-seed'),
|
|
487
|
+
})
|
|
488
|
+
);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('scopes agent overlays to the callback agent in parallel fan-out', async () => {
|
|
492
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
493
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
494
|
+
const { withLangfuseRuntimeScope } = await import('@/langfuseRuntimeScope');
|
|
495
|
+
const runLangfuse = {
|
|
496
|
+
publicKey: 'pk-run',
|
|
497
|
+
secretKey: 'sk-run',
|
|
498
|
+
baseUrl: 'https://langfuse.run',
|
|
499
|
+
deterministicTraceId: true,
|
|
500
|
+
};
|
|
501
|
+
const agentBLangfuse = {
|
|
502
|
+
publicKey: 'pk-agent-b',
|
|
503
|
+
secretKey: 'sk-agent-b',
|
|
504
|
+
baseUrl: 'https://langfuse.agent-b',
|
|
505
|
+
deterministicTraceId: true,
|
|
506
|
+
};
|
|
507
|
+
initializeLangfuseTracing(runLangfuse);
|
|
508
|
+
initializeLangfuseTracing(agentBLangfuse);
|
|
509
|
+
const streamHandler = createLangfuseHandler({
|
|
510
|
+
langfuse: runLangfuse,
|
|
511
|
+
traceIdSeed: 'run-seed',
|
|
512
|
+
runId: 'run-1',
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// Agent B's overlay scope is ambient (the background callback queue can
|
|
516
|
+
// interleave concurrent fan-out agents), but this callback reports agent
|
|
517
|
+
// A via inherited langgraph metadata — a sibling's overlay must not
|
|
518
|
+
// capture it.
|
|
519
|
+
await withLangfuseRuntimeScope(
|
|
520
|
+
{
|
|
521
|
+
langfuse: agentBLangfuse,
|
|
522
|
+
traceIdSeed: 'agent-b-seed',
|
|
523
|
+
runId: 'run-1',
|
|
524
|
+
agentId: 'agent-b',
|
|
525
|
+
},
|
|
526
|
+
() =>
|
|
527
|
+
streamHandler?.handleChainStart(
|
|
528
|
+
{ lc: 1, type: 'not_implemented', id: ['SiblingAgentChain'] },
|
|
529
|
+
{ input: 'sibling agent' },
|
|
530
|
+
'lc-sibling-run',
|
|
531
|
+
undefined,
|
|
532
|
+
undefined,
|
|
533
|
+
{ langgraph_node: 'agent=agent-a' }
|
|
534
|
+
)
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
expect(mockProcessorStarts).toContainEqual(
|
|
538
|
+
expect.objectContaining({
|
|
539
|
+
params: expect.objectContaining({ publicKey: 'pk-run' }),
|
|
540
|
+
traceId: traceIdFromSeed('run-seed'),
|
|
541
|
+
})
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
// The SAME agent's overlay scope is still adopted.
|
|
545
|
+
await withLangfuseRuntimeScope(
|
|
546
|
+
{
|
|
547
|
+
langfuse: agentBLangfuse,
|
|
548
|
+
traceIdSeed: 'agent-b-seed',
|
|
549
|
+
runId: 'run-1',
|
|
550
|
+
agentId: 'agent-b',
|
|
551
|
+
},
|
|
552
|
+
() =>
|
|
553
|
+
streamHandler?.handleChainStart(
|
|
554
|
+
{ lc: 1, type: 'not_implemented', id: ['OwnAgentChain'] },
|
|
555
|
+
{ input: 'own agent' },
|
|
556
|
+
'lc-own-agent-run',
|
|
557
|
+
undefined,
|
|
558
|
+
undefined,
|
|
559
|
+
{ langgraph_node: 'agent=agent-b' }
|
|
560
|
+
)
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
expect(mockProcessorStarts).toContainEqual(
|
|
564
|
+
expect.objectContaining({
|
|
565
|
+
params: expect.objectContaining({ publicKey: 'pk-agent-b' }),
|
|
566
|
+
traceId: traceIdFromSeed('agent-b-seed'),
|
|
567
|
+
})
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
// Explicit `agentId` metadata (stamped by the graph's model path and
|
|
571
|
+
// ToolNode) beats node-name parsing: agent `research`'s inner node is
|
|
572
|
+
// named `agent=research`, indistinguishable by name from a sibling
|
|
573
|
+
// literally named `agent=research` — the explicit identity disambiguates.
|
|
574
|
+
await withLangfuseRuntimeScope(
|
|
575
|
+
{
|
|
576
|
+
langfuse: agentBLangfuse,
|
|
577
|
+
traceIdSeed: 'prefixed-sibling-seed',
|
|
578
|
+
runId: 'run-1',
|
|
579
|
+
agentId: 'agent=research',
|
|
580
|
+
},
|
|
581
|
+
() =>
|
|
582
|
+
streamHandler?.handleChainStart(
|
|
583
|
+
{ lc: 1, type: 'not_implemented', id: ['AmbiguousNodeChain'] },
|
|
584
|
+
{ input: 'ambiguous node' },
|
|
585
|
+
'lc-ambiguous-node-run',
|
|
586
|
+
undefined,
|
|
587
|
+
undefined,
|
|
588
|
+
{ langgraph_node: 'agent=research', agentId: 'research' }
|
|
589
|
+
)
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
expect(mockProcessorStarts).toContainEqual(
|
|
593
|
+
expect.objectContaining({
|
|
594
|
+
params: expect.objectContaining({ publicKey: 'pk-run' }),
|
|
595
|
+
traceId: traceIdFromSeed('run-seed'),
|
|
596
|
+
})
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
// An agent id that itself begins with an internal node prefix is carried
|
|
600
|
+
// VERBATIM by its outer workflow node — still recognized as its own scope.
|
|
601
|
+
await withLangfuseRuntimeScope(
|
|
602
|
+
{
|
|
603
|
+
langfuse: agentBLangfuse,
|
|
604
|
+
traceIdSeed: 'prefixed-agent-seed',
|
|
605
|
+
runId: 'run-1',
|
|
606
|
+
agentId: 'agent=research',
|
|
607
|
+
},
|
|
608
|
+
() =>
|
|
609
|
+
streamHandler?.handleChainStart(
|
|
610
|
+
{ lc: 1, type: 'not_implemented', id: ['PrefixedAgentChain'] },
|
|
611
|
+
{ input: 'prefixed agent' },
|
|
612
|
+
'lc-prefixed-agent-run',
|
|
613
|
+
undefined,
|
|
614
|
+
undefined,
|
|
615
|
+
{ langgraph_node: 'agent=research' }
|
|
616
|
+
)
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
expect(mockProcessorStarts).toContainEqual(
|
|
620
|
+
expect.objectContaining({
|
|
621
|
+
params: expect.objectContaining({ publicKey: 'pk-agent-b' }),
|
|
622
|
+
traceId: traceIdFromSeed('prefixed-agent-seed'),
|
|
623
|
+
})
|
|
624
|
+
);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('rejects a sibling agent overlay for tool callbacks', async () => {
|
|
628
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
629
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
630
|
+
const { withLangfuseRuntimeScope } = await import('@/langfuseRuntimeScope');
|
|
631
|
+
const runLangfuse = {
|
|
632
|
+
publicKey: 'pk-run',
|
|
633
|
+
secretKey: 'sk-run',
|
|
634
|
+
baseUrl: 'https://langfuse.run',
|
|
635
|
+
deterministicTraceId: true,
|
|
636
|
+
};
|
|
637
|
+
const agentBLangfuse = {
|
|
638
|
+
publicKey: 'pk-agent-b',
|
|
639
|
+
secretKey: 'sk-agent-b',
|
|
640
|
+
baseUrl: 'https://langfuse.agent-b',
|
|
641
|
+
deterministicTraceId: true,
|
|
642
|
+
};
|
|
643
|
+
initializeLangfuseTracing(runLangfuse);
|
|
644
|
+
initializeLangfuseTracing(agentBLangfuse);
|
|
645
|
+
const streamHandler = createLangfuseHandler({
|
|
646
|
+
langfuse: runLangfuse,
|
|
647
|
+
traceIdSeed: 'run-seed',
|
|
648
|
+
runId: 'run-1',
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// Tool supersteps stamp their scope with the executing agent; a tool
|
|
652
|
+
// callback reporting a different agent via `tools=<id>` metadata must
|
|
653
|
+
// not adopt the sibling's overlay.
|
|
654
|
+
await withLangfuseRuntimeScope(
|
|
655
|
+
{
|
|
656
|
+
langfuse: agentBLangfuse,
|
|
657
|
+
traceIdSeed: 'agent-b-seed',
|
|
658
|
+
runId: 'run-1',
|
|
659
|
+
agentId: 'agent-b',
|
|
660
|
+
},
|
|
661
|
+
() =>
|
|
662
|
+
streamHandler?.handleToolStart(
|
|
663
|
+
{ lc: 1, type: 'not_implemented', id: ['SiblingTool'] },
|
|
664
|
+
'sibling tool input',
|
|
665
|
+
'lc-sibling-tool-run',
|
|
666
|
+
undefined,
|
|
667
|
+
undefined,
|
|
668
|
+
{ langgraph_node: 'tools=agent-a' }
|
|
669
|
+
)
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
expect(mockProcessorStarts).toContainEqual(
|
|
673
|
+
expect.objectContaining({
|
|
674
|
+
params: expect.objectContaining({ publicKey: 'pk-run' }),
|
|
675
|
+
traceId: traceIdFromSeed('run-seed'),
|
|
676
|
+
})
|
|
677
|
+
);
|
|
678
|
+
expect(
|
|
679
|
+
mockProcessorStarts.filter(
|
|
680
|
+
(record) =>
|
|
681
|
+
(record.params as { publicKey?: string }).publicKey === 'pk-agent-b'
|
|
682
|
+
)
|
|
683
|
+
).toHaveLength(0);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('replaces foreign propagated identity attributes with its own', async () => {
|
|
687
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
688
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
689
|
+
const { withLangfuseRuntimeScope } = await import('@/langfuseRuntimeScope');
|
|
690
|
+
const { propagateAttributes } = await import('@langfuse/tracing');
|
|
691
|
+
const { getPropagatedAttributesFromContext } = await import(
|
|
692
|
+
'@langfuse/core'
|
|
693
|
+
);
|
|
694
|
+
const tenantA = {
|
|
695
|
+
publicKey: 'pk-tenant-a',
|
|
696
|
+
secretKey: 'sk-tenant-a',
|
|
697
|
+
baseUrl: 'https://langfuse.tenant-a',
|
|
698
|
+
};
|
|
699
|
+
const tenantB = {
|
|
700
|
+
publicKey: 'pk-tenant-b',
|
|
701
|
+
secretKey: 'sk-tenant-b',
|
|
702
|
+
baseUrl: 'https://langfuse.tenant-b',
|
|
703
|
+
};
|
|
704
|
+
initializeLangfuseTracing(tenantA);
|
|
705
|
+
initializeLangfuseTracing(tenantB);
|
|
706
|
+
const handlerB = createLangfuseHandler({
|
|
707
|
+
langfuse: tenantB,
|
|
708
|
+
userId: 'user-b',
|
|
709
|
+
runId: 'run-b',
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
let observedAttributes: Record<string, unknown> | undefined;
|
|
713
|
+
mockStartActiveSpan.mockImplementationOnce(
|
|
714
|
+
(_name, _options, activeContext, callback) => {
|
|
715
|
+
observedAttributes = getPropagatedAttributesFromContext(
|
|
716
|
+
otelContext.active()
|
|
717
|
+
) as Record<string, unknown>;
|
|
718
|
+
return callback(
|
|
719
|
+
createMockSpan(otelTrace.getSpanContext(activeContext)?.traceId)
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
// Run A's propagated identity (user, session) is ambient; B's spans must
|
|
725
|
+
// carry B's identity — and NOT retain A's session where B has none.
|
|
726
|
+
await propagateAttributes(
|
|
727
|
+
{ userId: 'user-a', sessionId: 'session-a' },
|
|
728
|
+
async () =>
|
|
729
|
+
withLangfuseRuntimeScope({ langfuse: tenantA, runId: 'run-a' }, () =>
|
|
730
|
+
handlerB?.handleChainStart(
|
|
731
|
+
{ lc: 1, type: 'not_implemented', id: ['IdentityChain'] },
|
|
732
|
+
{ input: 'identity' },
|
|
733
|
+
'lc-identity-run'
|
|
734
|
+
)
|
|
735
|
+
)
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
expect(observedAttributes?.['user.id']).toBe('user-b');
|
|
739
|
+
expect(observedAttributes?.['session.id']).toBeUndefined();
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it('clears a foreign scope for env-credential runs instead of inheriting it', async () => {
|
|
743
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
744
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
745
|
+
const { withLangfuseRuntimeScope } = await import('@/langfuseRuntimeScope');
|
|
746
|
+
process.env.LANGFUSE_PUBLIC_KEY = 'pk-env';
|
|
747
|
+
process.env.LANGFUSE_SECRET_KEY = 'sk-env';
|
|
748
|
+
const tenantA = {
|
|
749
|
+
publicKey: 'pk-tenant-a',
|
|
750
|
+
secretKey: 'sk-tenant-a',
|
|
751
|
+
baseUrl: 'https://langfuse.tenant-a',
|
|
752
|
+
deterministicTraceId: true,
|
|
753
|
+
};
|
|
754
|
+
initializeLangfuseTracing(tenantA);
|
|
755
|
+
initializeLangfuseTracing();
|
|
756
|
+
// Run B has no explicit config or seed of its own — it relies on env
|
|
757
|
+
// credentials. Rejecting a foreign scope must CLEAR the foreign values
|
|
758
|
+
// rather than let merge inheritance re-adopt them through `undefined`.
|
|
759
|
+
const handlerB = createLangfuseHandler({ runId: 'run-b' });
|
|
760
|
+
|
|
761
|
+
await withLangfuseRuntimeScope(
|
|
762
|
+
{ langfuse: tenantA, traceIdSeed: 'run-a', runId: 'run-a' },
|
|
763
|
+
() =>
|
|
764
|
+
handlerB?.handleChainStart(
|
|
765
|
+
{ lc: 1, type: 'not_implemented', id: ['EnvCredentialChain'] },
|
|
766
|
+
{ input: 'env credentials' },
|
|
767
|
+
'lc-env-run'
|
|
768
|
+
)
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
expect(mockProcessorStarts).toContainEqual(
|
|
772
|
+
expect.objectContaining({
|
|
773
|
+
params: expect.objectContaining({
|
|
774
|
+
publicKey: 'pk-env',
|
|
775
|
+
secretKey: 'sk-env',
|
|
776
|
+
}),
|
|
777
|
+
})
|
|
778
|
+
);
|
|
779
|
+
expect(
|
|
780
|
+
mockProcessorStarts.filter(
|
|
781
|
+
(record) =>
|
|
782
|
+
(record.params as { publicKey?: string }).publicKey ===
|
|
783
|
+
'pk-tenant-a' || record.traceId === traceIdFromSeed('run-a')
|
|
784
|
+
)
|
|
785
|
+
).toHaveLength(0);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it('applies its own tool-output policy inside a foreign run scope', async () => {
|
|
789
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
790
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
791
|
+
const { withLangfuseRuntimeScope } = await import('@/langfuseRuntimeScope');
|
|
792
|
+
const { resolveToolOutputTracingConfig } = await import('@/langfuseConfig');
|
|
793
|
+
const { getLangfuseRuntimeToolOutputTracingConfig } = await import(
|
|
794
|
+
'@/langfuseRuntimeContext'
|
|
795
|
+
);
|
|
796
|
+
const tenantA = {
|
|
797
|
+
publicKey: 'pk-tenant-a',
|
|
798
|
+
secretKey: 'sk-tenant-a',
|
|
799
|
+
baseUrl: 'https://langfuse.tenant-a',
|
|
800
|
+
toolOutputTracing: { enabled: true },
|
|
801
|
+
};
|
|
802
|
+
const tenantB = {
|
|
803
|
+
publicKey: 'pk-tenant-b',
|
|
804
|
+
secretKey: 'sk-tenant-b',
|
|
805
|
+
baseUrl: 'https://langfuse.tenant-b',
|
|
806
|
+
toolOutputTracing: { enabled: false },
|
|
807
|
+
};
|
|
808
|
+
initializeLangfuseTracing(tenantA);
|
|
809
|
+
initializeLangfuseTracing(tenantB);
|
|
810
|
+
const handlerB = createLangfuseHandler({
|
|
811
|
+
langfuse: tenantB,
|
|
812
|
+
runId: 'run-b',
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
let observedPolicy: { enabled: boolean } | undefined;
|
|
816
|
+
mockStartActiveSpan.mockImplementationOnce(
|
|
817
|
+
(_name, _options, activeContext, callback) => {
|
|
818
|
+
observedPolicy = getLangfuseRuntimeToolOutputTracingConfig();
|
|
819
|
+
return callback(
|
|
820
|
+
createMockSpan(otelTrace.getSpanContext(activeContext)?.traceId)
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
// Tenant A permits tool output; tenant B redacts. B's callback running
|
|
826
|
+
// inside A's scope must not inherit A's permissive policy.
|
|
827
|
+
await withLangfuseRuntimeScope(
|
|
828
|
+
{
|
|
829
|
+
langfuse: tenantA,
|
|
830
|
+
runId: 'run-a',
|
|
831
|
+
toolOutputTracing: resolveToolOutputTracingConfig(tenantA),
|
|
832
|
+
},
|
|
833
|
+
() =>
|
|
834
|
+
handlerB?.handleToolStart(
|
|
835
|
+
{ lc: 1, type: 'not_implemented', id: ['SensitiveTool'] },
|
|
836
|
+
'sensitive input',
|
|
837
|
+
'lc-tool-run'
|
|
838
|
+
)
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
expect(observedPolicy?.enabled).toBe(false);
|
|
842
|
+
});
|
|
843
|
+
|
|
388
844
|
it('attaches configured trace attributes to Langfuse callback spans', async () => {
|
|
389
845
|
const { createLangfuseHandler } = await import('@/langfuse');
|
|
390
846
|
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
@@ -256,7 +256,9 @@ function expectChildSpanParentName({
|
|
|
256
256
|
const children = starts.filter((record) => record.name === childName);
|
|
257
257
|
expect(children).not.toHaveLength(0);
|
|
258
258
|
for (const child of children) {
|
|
259
|
-
const parent = starts.find(
|
|
259
|
+
const parent = starts.find(
|
|
260
|
+
(record) => record.spanId === child.parentSpanId
|
|
261
|
+
);
|
|
260
262
|
expect(parent?.name.startsWith(parentNamePrefix)).toBe(true);
|
|
261
263
|
}
|
|
262
264
|
}
|
|
@@ -560,6 +562,141 @@ describe('Langfuse per-run routing integration', () => {
|
|
|
560
562
|
}
|
|
561
563
|
});
|
|
562
564
|
|
|
565
|
+
it('detaches root observations from foreign ambient spans', async () => {
|
|
566
|
+
const tenantId = 'tenant-ambient';
|
|
567
|
+
const foreignTraceId = 'f0e1d2c3b4a5968778695a4b3c2d1e0f';
|
|
568
|
+
const foreignSpanId = 'a1b2c3d4e5f60718';
|
|
569
|
+
initializeLangfuseTracing(tenantLangfuse(tenantId));
|
|
570
|
+
|
|
571
|
+
// Simulates a host running agent code inside its own OpenTelemetry span
|
|
572
|
+
// (HTTP server auto-instrumentation on the global provider): the span is
|
|
573
|
+
// never exported to Langfuse, so inheriting it would orphan the trace
|
|
574
|
+
// root, merge concurrent runs in one request into a single trace, and
|
|
575
|
+
// bypass deterministic trace ids.
|
|
576
|
+
const foreignSpan = otelTrace.wrapSpanContext({
|
|
577
|
+
traceId: foreignTraceId,
|
|
578
|
+
spanId: foreignSpanId,
|
|
579
|
+
traceFlags: 1,
|
|
580
|
+
});
|
|
581
|
+
await otelContext.with(
|
|
582
|
+
otelTrace.setSpan(otelContext.active(), foreignSpan),
|
|
583
|
+
() => runTenantFlow(tenantId)
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
const starts = startsForTenant(tenantId);
|
|
587
|
+
expect(starts).not.toHaveLength(0);
|
|
588
|
+
expect(
|
|
589
|
+
starts.filter(
|
|
590
|
+
(record) =>
|
|
591
|
+
record.traceId === foreignTraceId ||
|
|
592
|
+
record.parentSpanId === foreignSpanId
|
|
593
|
+
)
|
|
594
|
+
).toHaveLength(0);
|
|
595
|
+
|
|
596
|
+
const agentRoot = starts.find(
|
|
597
|
+
(record) => record.name === `LibreChat Agent: Parent ${tenantId}`
|
|
598
|
+
);
|
|
599
|
+
expect(agentRoot?.traceId).toBe(traceIdFromSeed(`routing-${tenantId}`));
|
|
600
|
+
expect(agentRoot?.parentSpanId).toBeUndefined();
|
|
601
|
+
|
|
602
|
+
const titleRoot = starts.find(
|
|
603
|
+
(record) => record.name === `LibreChat Title: Parent ${tenantId}`
|
|
604
|
+
);
|
|
605
|
+
expect(titleRoot?.traceId).toBe(
|
|
606
|
+
traceIdFromSeed(`title-routing-${tenantId}`)
|
|
607
|
+
);
|
|
608
|
+
expect(titleRoot?.parentSpanId).toBeUndefined();
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('keeps root observations nested under Langfuse-managed ambient spans', async () => {
|
|
612
|
+
const tenantId = 'tenant-managed';
|
|
613
|
+
const langfuse = tenantLangfuse(tenantId);
|
|
614
|
+
initializeLangfuseTracing(langfuse);
|
|
615
|
+
|
|
616
|
+
let hostSpan: MockSpan | undefined;
|
|
617
|
+
await withLangfuseRuntimeScope({ langfuse }, async () => {
|
|
618
|
+
hostSpan = createMockSpan('host-group');
|
|
619
|
+
await otelContext.with(
|
|
620
|
+
otelTrace.setSpan(otelContext.active(), hostSpan as never),
|
|
621
|
+
() => runTenantFlow(tenantId)
|
|
622
|
+
);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const hostSpanContext = hostSpan?.spanContext() as {
|
|
626
|
+
traceId: string;
|
|
627
|
+
spanId: string;
|
|
628
|
+
};
|
|
629
|
+
const agentRoot = startsForTenant(tenantId).find(
|
|
630
|
+
(record) => record.name === `LibreChat Agent: Parent ${tenantId}`
|
|
631
|
+
);
|
|
632
|
+
expect(agentRoot?.traceId).toBe(hostSpanContext.traceId);
|
|
633
|
+
expect(agentRoot?.parentSpanId).toBe(hostSpanContext.spanId);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('detaches root observations from managed ambient spans of another destination', async () => {
|
|
637
|
+
const hostTenantId = 'tenant-managed-a';
|
|
638
|
+
const runTenantId = 'tenant-managed-b';
|
|
639
|
+
const hostLangfuse = tenantLangfuse(hostTenantId);
|
|
640
|
+
initializeLangfuseTracing(hostLangfuse);
|
|
641
|
+
initializeLangfuseTracing(tenantLangfuse(runTenantId));
|
|
642
|
+
|
|
643
|
+
let hostSpan: MockSpan | undefined;
|
|
644
|
+
await withLangfuseRuntimeScope({ langfuse: hostLangfuse }, async () => {
|
|
645
|
+
hostSpan = createMockSpan('host-group');
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// A managed span is only a safe parent for runs exporting to the SAME
|
|
649
|
+
// destination; nesting tenant-B under tenant-A's span would leave B's
|
|
650
|
+
// trace dangling in B's project with A's trace id.
|
|
651
|
+
await otelContext.with(
|
|
652
|
+
otelTrace.setSpan(otelContext.active(), hostSpan as never),
|
|
653
|
+
() => runTenantFlow(runTenantId)
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
const hostSpanContext = hostSpan?.spanContext() as {
|
|
657
|
+
traceId: string;
|
|
658
|
+
spanId: string;
|
|
659
|
+
};
|
|
660
|
+
const agentRoot = startsForTenant(runTenantId).find(
|
|
661
|
+
(record) => record.name === `LibreChat Agent: Parent ${runTenantId}`
|
|
662
|
+
);
|
|
663
|
+
expect(agentRoot?.traceId).toBe(traceIdFromSeed(`routing-${runTenantId}`));
|
|
664
|
+
expect(agentRoot?.traceId).not.toBe(hostSpanContext.traceId);
|
|
665
|
+
expect(agentRoot?.parentSpanId).toBeUndefined();
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it('generates a scope stamp for directly-constructed graphs without a run id', async () => {
|
|
669
|
+
const { StandardGraph } = await import('@/graphs/Graph');
|
|
670
|
+
const buildGraph = (): { langfuseScopeRunId: string } =>
|
|
671
|
+
new StandardGraph({
|
|
672
|
+
agents: [createAgent('stampless')],
|
|
673
|
+
langfuse: tenantLangfuse('stampless'),
|
|
674
|
+
}) as unknown as { langfuseScopeRunId: string };
|
|
675
|
+
|
|
676
|
+
const graphA = buildGraph();
|
|
677
|
+
const graphB = buildGraph();
|
|
678
|
+
expect(graphA.langfuseScopeRunId).toEqual(expect.stringMatching(/^graph:/));
|
|
679
|
+
expect(graphB.langfuseScopeRunId).toEqual(expect.stringMatching(/^graph:/));
|
|
680
|
+
expect(graphA.langfuseScopeRunId).not.toBe(graphB.langfuseScopeRunId);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('stamps concurrent executions of the same public run id distinctly', async () => {
|
|
684
|
+
const { StandardGraph } = await import('@/graphs/Graph');
|
|
685
|
+
const buildGraph = (): { langfuseScopeRunId: string } =>
|
|
686
|
+
new StandardGraph({
|
|
687
|
+
runId: 'duplicate-run',
|
|
688
|
+
agents: [createAgent('duplicate')],
|
|
689
|
+
langfuse: tenantLangfuse('duplicate'),
|
|
690
|
+
}) as unknown as { langfuseScopeRunId: string };
|
|
691
|
+
|
|
692
|
+
const first = buildGraph();
|
|
693
|
+
const second = buildGraph();
|
|
694
|
+
expect(first.langfuseScopeRunId).toEqual(
|
|
695
|
+
expect.stringMatching(/^duplicate-run:/)
|
|
696
|
+
);
|
|
697
|
+
expect(first.langfuseScopeRunId).not.toBe(second.langfuseScopeRunId);
|
|
698
|
+
});
|
|
699
|
+
|
|
563
700
|
it('routes spans from captured OTel context after ALS scope exits', () => {
|
|
564
701
|
const langfuse = tenantLangfuse('tenant-otel');
|
|
565
702
|
initializeLangfuseTracing(langfuse);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Span } from '@opentelemetry/api';
|
|
2
|
+
import type * as t from '@/types';
|
|
3
|
+
import {
|
|
4
|
+
getLangfuseManagedSpanDestination,
|
|
5
|
+
registerLangfuseManagedSpan,
|
|
6
|
+
resolveLangfuseDestinationKey,
|
|
7
|
+
} from '@/langfuseSpanRegistry';
|
|
8
|
+
|
|
9
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
10
|
+
|
|
11
|
+
function tenantConfig(
|
|
12
|
+
overrides: Partial<t.LangfuseConfig> = {}
|
|
13
|
+
): t.LangfuseConfig {
|
|
14
|
+
return {
|
|
15
|
+
enabled: true,
|
|
16
|
+
publicKey: 'pk-registry',
|
|
17
|
+
secretKey: 'sk-registry',
|
|
18
|
+
baseUrl: 'https://langfuse.registry',
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('Langfuse span registry', () => {
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
process.env = { ...ORIGINAL_ENV };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('tracks managed spans by destination', () => {
|
|
29
|
+
const span = { name: 'managed' } as unknown as Span;
|
|
30
|
+
const key = resolveLangfuseDestinationKey(tenantConfig());
|
|
31
|
+
expect(key).toBeDefined();
|
|
32
|
+
registerLangfuseManagedSpan(span, key as string);
|
|
33
|
+
expect(getLangfuseManagedSpanDestination(span)).toBe(key);
|
|
34
|
+
|
|
35
|
+
const untracked = { name: 'foreign' } as unknown as Span;
|
|
36
|
+
expect(getLangfuseManagedSpanDestination(untracked)).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('treats processor policy as irrelevant to destination identity', () => {
|
|
40
|
+
const base = resolveLangfuseDestinationKey(tenantConfig());
|
|
41
|
+
const redacting = resolveLangfuseDestinationKey(
|
|
42
|
+
tenantConfig({ toolOutputTracing: { enabled: false } })
|
|
43
|
+
);
|
|
44
|
+
expect(redacting).toBe(base);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('separates destinations by credentials, endpoint, and environment', () => {
|
|
48
|
+
const base = resolveLangfuseDestinationKey(tenantConfig());
|
|
49
|
+
expect(
|
|
50
|
+
resolveLangfuseDestinationKey(tenantConfig({ publicKey: 'pk-other' }))
|
|
51
|
+
).not.toBe(base);
|
|
52
|
+
expect(
|
|
53
|
+
resolveLangfuseDestinationKey(
|
|
54
|
+
tenantConfig({ baseUrl: 'https://langfuse.other' })
|
|
55
|
+
)
|
|
56
|
+
).not.toBe(base);
|
|
57
|
+
expect(
|
|
58
|
+
resolveLangfuseDestinationKey(tenantConfig({ environment: 'staging' }))
|
|
59
|
+
).not.toBe(base);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('resolves no destination without credentials', () => {
|
|
63
|
+
delete process.env.LANGFUSE_PUBLIC_KEY;
|
|
64
|
+
delete process.env.LANGFUSE_SECRET_KEY;
|
|
65
|
+
expect(resolveLangfuseDestinationKey(undefined)).toBeUndefined();
|
|
66
|
+
expect(
|
|
67
|
+
resolveLangfuseDestinationKey(tenantConfig({ enabled: false }))
|
|
68
|
+
).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
});
|