@librechat/agents 3.3.10 → 3.3.11
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 +2 -2
- package/dist/cjs/langfuseToolOutputTracing.cjs +228 -16
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +1 -1
- package/dist/cjs/llm/invoke.cjs +14 -7
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +188 -11
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +4 -3
- package/dist/cjs/messages/core.cjs +592 -27
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/run.cjs +1 -1
- package/dist/cjs/stream.cjs +2 -2
- package/dist/cjs/tools/ToolNode.cjs +1 -1
- package/dist/cjs/tools/search/tool.cjs +1 -1
- package/dist/cjs/utils/index.cjs +1 -1
- package/dist/esm/graphs/Graph.mjs +2 -2
- package/dist/esm/langfuseToolOutputTracing.mjs +228 -16
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +14 -7
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +190 -13
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +5 -5
- package/dist/esm/messages/core.mjs +592 -28
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/run.mjs +1 -1
- package/dist/esm/stream.mjs +2 -2
- package/dist/esm/tools/ToolNode.mjs +1 -1
- package/dist/esm/tools/search/tool.mjs +1 -1
- package/dist/esm/utils/index.mjs +1 -1
- package/dist/types/langfuseToolOutputTracing.d.ts +1 -0
- package/dist/types/llm/invoke.d.ts +1 -1
- package/dist/types/messages/core.d.ts +11 -6
- package/package.json +1 -1
- package/src/langfuseToolOutputTracing.ts +410 -14
- package/src/llm/custom-chat-models.smoke.test.ts +747 -0
- package/src/llm/invoke.test.ts +98 -0
- package/src/llm/invoke.ts +34 -23
- package/src/llm/openai/index.ts +334 -25
- package/src/llm/openai/llm.spec.ts +107 -6
- package/src/messages/core.ts +1290 -42
- package/src/messages/formatAgentMessages.test.ts +2623 -0
- package/src/specs/langfuse-tool-output-tracing.test.ts +887 -0
- package/src/specs/preemptSeal.test.ts +374 -5
|
@@ -8,6 +8,7 @@ import type { TPayload } from '@/types';
|
|
|
8
8
|
import {
|
|
9
9
|
LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
|
|
10
10
|
classifyLangfuseToolNodeSpan,
|
|
11
|
+
prepareLangfuseSpanForExport,
|
|
11
12
|
redactLangfuseSpanToolOutputs,
|
|
12
13
|
shouldTraceToolNodeForLangfuse,
|
|
13
14
|
} from '@/langfuseToolOutputTracing';
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
resolveToolOutputTracingConfig,
|
|
27
28
|
} from '@/langfuseConfig';
|
|
28
29
|
import { ensureOpenTelemetryContextManager } from '@/instrumentation';
|
|
30
|
+
import { projectToolStreamContentForProvider } from '@/messages/core';
|
|
29
31
|
import { formatAgentMessages } from '@/messages/format';
|
|
30
32
|
import { ContentTypes } from '@/common';
|
|
31
33
|
|
|
@@ -279,6 +281,891 @@ describe('Langfuse tool output tracing redaction', () => {
|
|
|
279
281
|
expect(redacted[1].content).toBe(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
280
282
|
});
|
|
281
283
|
|
|
284
|
+
it('redacts captured Responses server-tool outputs wherever they are serialized', () => {
|
|
285
|
+
const outputs = [
|
|
286
|
+
{
|
|
287
|
+
id: 'local_output',
|
|
288
|
+
type: 'local_shell_call_output',
|
|
289
|
+
status: 'completed',
|
|
290
|
+
output: 'local shell secret',
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
id: 'shell_output',
|
|
294
|
+
call_id: 'shell_call',
|
|
295
|
+
type: 'shell_call_output',
|
|
296
|
+
status: 'completed',
|
|
297
|
+
output: 'shell secret',
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
id: 'patch_output',
|
|
301
|
+
call_id: 'patch_call',
|
|
302
|
+
type: 'apply_patch_call_output',
|
|
303
|
+
status: 'completed',
|
|
304
|
+
output: 'patch secret',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
id: 'program_output',
|
|
308
|
+
call_id: 'program_call',
|
|
309
|
+
type: 'program_output',
|
|
310
|
+
status: 'completed',
|
|
311
|
+
result: 'program secret',
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
id: 'code_interpreter_call',
|
|
315
|
+
type: 'code_interpreter_call',
|
|
316
|
+
code: 'code input stays visible',
|
|
317
|
+
container_id: 'container_1',
|
|
318
|
+
status: 'completed',
|
|
319
|
+
outputs: [{ type: 'logs', logs: 'code interpreter secret' }],
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
id: 'mcp_call',
|
|
323
|
+
type: 'mcp_call',
|
|
324
|
+
name: 'private_mcp_tool',
|
|
325
|
+
arguments: '{"query":"mcp input stays visible"}',
|
|
326
|
+
server_label: 'mcp_server',
|
|
327
|
+
status: 'failed',
|
|
328
|
+
output: 'mcp output secret',
|
|
329
|
+
error: 'mcp error secret',
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: 'mcp_list',
|
|
333
|
+
type: 'mcp_list_tools',
|
|
334
|
+
server_label: 'mcp_server',
|
|
335
|
+
tools: [{ name: 'listed secret tool', input_schema: {} }],
|
|
336
|
+
error: 'mcp list error secret',
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
id: 'image_generation_call',
|
|
340
|
+
type: 'image_generation_call',
|
|
341
|
+
status: 'completed',
|
|
342
|
+
revised_prompt: 'image prompt stays visible',
|
|
343
|
+
result: 'image generation secret',
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
id: 'file_search_call',
|
|
347
|
+
type: 'file_search_call',
|
|
348
|
+
status: 'completed',
|
|
349
|
+
queries: ['file query stays visible'],
|
|
350
|
+
results: [{ text: 'file search secret' }],
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
id: 'web_search_call',
|
|
354
|
+
type: 'web_search_call',
|
|
355
|
+
status: 'completed',
|
|
356
|
+
action: {
|
|
357
|
+
type: 'search',
|
|
358
|
+
queries: ['web query stays visible'],
|
|
359
|
+
sources: [{ type: 'url', url: 'web source secret' }],
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
id: 'tool_search_output',
|
|
364
|
+
call_id: 'tool_search_call',
|
|
365
|
+
type: 'tool_search_output',
|
|
366
|
+
status: 'completed',
|
|
367
|
+
tools: [{ type: 'function', name: 'tool definition secret' }],
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
id: 'function_call',
|
|
371
|
+
call_id: 'function_call_id',
|
|
372
|
+
type: 'function_call',
|
|
373
|
+
name: 'private_function',
|
|
374
|
+
arguments: 'function input stays visible',
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
id: 'function_output',
|
|
378
|
+
call_id: 'function_call_id',
|
|
379
|
+
type: 'function_call_output',
|
|
380
|
+
output: 'function output secret',
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
id: 'custom_call',
|
|
384
|
+
call_id: 'custom_call_id',
|
|
385
|
+
type: 'custom_tool_call',
|
|
386
|
+
name: 'private_custom',
|
|
387
|
+
input: 'custom input stays visible',
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
id: 'custom_output',
|
|
391
|
+
call_id: 'custom_call_id',
|
|
392
|
+
type: 'custom_tool_call_output',
|
|
393
|
+
output: 'custom output secret',
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
id: 'computer_output',
|
|
397
|
+
call_id: 'computer_call_id',
|
|
398
|
+
type: 'computer_call_output',
|
|
399
|
+
acknowledged_safety_checks: ['computer input stays visible'],
|
|
400
|
+
output: 'computer output secret',
|
|
401
|
+
},
|
|
402
|
+
];
|
|
403
|
+
const serializedMessage = {
|
|
404
|
+
role: 'assistant',
|
|
405
|
+
content: outputs.map((value) => ({ type: 'non_standard', value })),
|
|
406
|
+
content_blocks: outputs.map((value) => ({
|
|
407
|
+
type: 'non_standard',
|
|
408
|
+
value,
|
|
409
|
+
})),
|
|
410
|
+
additional_kwargs: { tool_outputs: outputs },
|
|
411
|
+
};
|
|
412
|
+
const span = createSpan('gpt-5.6', {
|
|
413
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'generation',
|
|
414
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: JSON.stringify([
|
|
415
|
+
serializedMessage,
|
|
416
|
+
]),
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
redactLangfuseSpanToolOutputs(span, createConfig({ enabled: false }));
|
|
420
|
+
|
|
421
|
+
const redacted = span.attributes[
|
|
422
|
+
LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT
|
|
423
|
+
] as string;
|
|
424
|
+
for (const secret of [
|
|
425
|
+
'local shell secret',
|
|
426
|
+
'shell secret',
|
|
427
|
+
'patch secret',
|
|
428
|
+
'program secret',
|
|
429
|
+
'code interpreter secret',
|
|
430
|
+
'mcp output secret',
|
|
431
|
+
'mcp error secret',
|
|
432
|
+
'listed secret tool',
|
|
433
|
+
'mcp list error secret',
|
|
434
|
+
'image generation secret',
|
|
435
|
+
'file search secret',
|
|
436
|
+
'web source secret',
|
|
437
|
+
'tool definition secret',
|
|
438
|
+
'function output secret',
|
|
439
|
+
'custom output secret',
|
|
440
|
+
'computer output secret',
|
|
441
|
+
]) {
|
|
442
|
+
expect(redacted).not.toContain(secret);
|
|
443
|
+
}
|
|
444
|
+
expect(redacted).toContain(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
445
|
+
for (const input of [
|
|
446
|
+
'local_output',
|
|
447
|
+
'shell_call',
|
|
448
|
+
'patch_call',
|
|
449
|
+
'program_call',
|
|
450
|
+
'code input stays visible',
|
|
451
|
+
'mcp input stays visible',
|
|
452
|
+
'private_mcp_tool',
|
|
453
|
+
'mcp_server',
|
|
454
|
+
'image prompt stays visible',
|
|
455
|
+
'file query stays visible',
|
|
456
|
+
'web query stays visible',
|
|
457
|
+
'tool_search_call',
|
|
458
|
+
'function input stays visible',
|
|
459
|
+
'custom input stays visible',
|
|
460
|
+
'computer input stays visible',
|
|
461
|
+
]) {
|
|
462
|
+
expect(redacted).toContain(input);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('selectively redacts captured Responses outputs by canonical tool name', () => {
|
|
467
|
+
const outputs = [
|
|
468
|
+
{
|
|
469
|
+
type: 'local_shell_call_output',
|
|
470
|
+
output: 'public local output',
|
|
471
|
+
},
|
|
472
|
+
{ type: 'shell_call_output', output: 'private shell output' },
|
|
473
|
+
{ type: 'apply_patch_call_output', output: 'public patch output' },
|
|
474
|
+
{ type: 'program_output', result: 'private program output' },
|
|
475
|
+
{
|
|
476
|
+
type: 'code_interpreter_call',
|
|
477
|
+
code: 'public code input',
|
|
478
|
+
outputs: [{ type: 'logs', logs: 'private code output' }],
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
type: 'mcp_call',
|
|
482
|
+
name: 'private_mcp_tool',
|
|
483
|
+
arguments: 'public mcp input',
|
|
484
|
+
output: 'private named mcp output',
|
|
485
|
+
error: 'private named mcp error',
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
type: 'mcp_call',
|
|
489
|
+
name: 'public_mcp_tool',
|
|
490
|
+
output: 'public named mcp output',
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
type: 'mcp_call',
|
|
494
|
+
output: 'private fallback mcp output',
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
type: 'mcp_list_tools',
|
|
498
|
+
server_label: 'public server label',
|
|
499
|
+
tools: [{ name: 'private listed tool' }],
|
|
500
|
+
error: 'private list error',
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
type: 'image_generation_call',
|
|
504
|
+
revised_prompt: 'public image prompt',
|
|
505
|
+
result: 'private generated image',
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
type: 'file_search_call',
|
|
509
|
+
queries: ['public file query'],
|
|
510
|
+
results: [{ text: 'private file result' }],
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
type: 'web_search_call',
|
|
514
|
+
results: [{ title: 'private web result' }],
|
|
515
|
+
action: {
|
|
516
|
+
type: 'search',
|
|
517
|
+
queries: ['public web query'],
|
|
518
|
+
sources: [{ url: 'private web source' }],
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
type: 'tool_search_output',
|
|
523
|
+
call_id: 'public tool search call id',
|
|
524
|
+
tools: [{ name: 'private tool definition' }],
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
type: 'computer_call_output',
|
|
528
|
+
acknowledged_safety_checks: ['public computer input'],
|
|
529
|
+
output: 'private computer output',
|
|
530
|
+
},
|
|
531
|
+
];
|
|
532
|
+
const span = createSpan('gpt-5.6', {
|
|
533
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify(outputs),
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
redactLangfuseSpanToolOutputs(
|
|
537
|
+
span,
|
|
538
|
+
createConfig({
|
|
539
|
+
redactedToolNames: new Set([
|
|
540
|
+
'shell',
|
|
541
|
+
'program',
|
|
542
|
+
'code_interpreter',
|
|
543
|
+
'private_mcp_tool',
|
|
544
|
+
'mcp',
|
|
545
|
+
'mcp_list_tools',
|
|
546
|
+
'image_generation',
|
|
547
|
+
'file_search',
|
|
548
|
+
'web_search',
|
|
549
|
+
'tool_search',
|
|
550
|
+
'computer_use',
|
|
551
|
+
]),
|
|
552
|
+
})
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
const redacted = readJsonAttribute<Array<Record<string, unknown>>>(
|
|
556
|
+
span,
|
|
557
|
+
LangfuseOtelSpanAttributes.OBSERVATION_INPUT
|
|
558
|
+
);
|
|
559
|
+
expect(redacted[0].output).toBe('public local output');
|
|
560
|
+
expect(redacted[1].output).toBe(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
561
|
+
expect(redacted[2].output).toBe('public patch output');
|
|
562
|
+
expect(redacted[3].result).toBe(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
563
|
+
expect(redacted[4]).toMatchObject({
|
|
564
|
+
code: 'public code input',
|
|
565
|
+
outputs: LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
|
|
566
|
+
});
|
|
567
|
+
expect(redacted[5]).toMatchObject({
|
|
568
|
+
name: 'private_mcp_tool',
|
|
569
|
+
arguments: 'public mcp input',
|
|
570
|
+
output: LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
|
|
571
|
+
error: LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
|
|
572
|
+
});
|
|
573
|
+
expect(redacted[6].output).toBe('public named mcp output');
|
|
574
|
+
expect(redacted[7].output).toBe(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
575
|
+
expect(redacted[8]).toMatchObject({
|
|
576
|
+
server_label: 'public server label',
|
|
577
|
+
tools: LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
|
|
578
|
+
error: LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
|
|
579
|
+
});
|
|
580
|
+
expect(redacted[9]).toMatchObject({
|
|
581
|
+
revised_prompt: 'public image prompt',
|
|
582
|
+
result: LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
|
|
583
|
+
});
|
|
584
|
+
expect(redacted[10]).toMatchObject({
|
|
585
|
+
queries: ['public file query'],
|
|
586
|
+
results: LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
|
|
587
|
+
});
|
|
588
|
+
expect(redacted[11]).toMatchObject({
|
|
589
|
+
results: LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
|
|
590
|
+
action: {
|
|
591
|
+
type: 'search',
|
|
592
|
+
queries: ['public web query'],
|
|
593
|
+
sources: LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
expect(redacted[12]).toMatchObject({
|
|
597
|
+
call_id: 'public tool search call id',
|
|
598
|
+
tools: LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
|
|
599
|
+
});
|
|
600
|
+
expect(redacted[13]).toMatchObject({
|
|
601
|
+
acknowledged_safety_checks: ['public computer input'],
|
|
602
|
+
output: LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('maps Responses function and custom outputs to their call names', () => {
|
|
607
|
+
const outputs = [
|
|
608
|
+
{
|
|
609
|
+
type: 'function_call',
|
|
610
|
+
call_id: 'private_function_call',
|
|
611
|
+
name: 'execute_sql',
|
|
612
|
+
arguments: 'private function input stays visible',
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
type: 'function_call_output',
|
|
616
|
+
call_id: 'private_function_call',
|
|
617
|
+
output: 'private function output',
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
type: 'function_call',
|
|
621
|
+
call_id: 'public_function_call',
|
|
622
|
+
name: 'public_function',
|
|
623
|
+
arguments: 'public function input',
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
type: 'function_call_output',
|
|
627
|
+
call_id: 'public_function_call',
|
|
628
|
+
output: 'public function output',
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
type: 'custom_tool_call',
|
|
632
|
+
call_id: 'private_custom_call',
|
|
633
|
+
name: 'private_custom_tool',
|
|
634
|
+
input: 'private custom input stays visible',
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
type: 'custom_tool_call_output',
|
|
638
|
+
call_id: 'private_custom_call',
|
|
639
|
+
output: 'private custom output',
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
type: 'custom_tool_call_output',
|
|
643
|
+
call_id: 'unmatched_custom_call',
|
|
644
|
+
output: 'unmatched custom output stays visible',
|
|
645
|
+
},
|
|
646
|
+
];
|
|
647
|
+
const span = createSpan('gpt-5.6', {
|
|
648
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify(outputs),
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
redactLangfuseSpanToolOutputs(
|
|
652
|
+
span,
|
|
653
|
+
createConfig({
|
|
654
|
+
redactedToolNames: new Set(['execute_sql', 'private_custom_tool']),
|
|
655
|
+
})
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
const redacted = readJsonAttribute<Array<Record<string, unknown>>>(
|
|
659
|
+
span,
|
|
660
|
+
LangfuseOtelSpanAttributes.OBSERVATION_INPUT
|
|
661
|
+
);
|
|
662
|
+
expect(redacted[0].arguments).toBe('private function input stays visible');
|
|
663
|
+
expect(redacted[1].output).toBe(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
664
|
+
expect(redacted[3].output).toBe('public function output');
|
|
665
|
+
expect(redacted[4].input).toBe('private custom input stays visible');
|
|
666
|
+
expect(redacted[5].output).toBe(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
667
|
+
expect(redacted[6].output).toBe('unmatched custom output stays visible');
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('redacts standard server-tool results globally and by paired call name', () => {
|
|
671
|
+
const blocks = [
|
|
672
|
+
{
|
|
673
|
+
type: 'server_tool_call',
|
|
674
|
+
id: 'private_server_call',
|
|
675
|
+
name: 'file_search',
|
|
676
|
+
args: { query: 'private server input stays visible' },
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
type: 'server_tool_call_result',
|
|
680
|
+
toolCallId: 'private_server_call',
|
|
681
|
+
status: 'success',
|
|
682
|
+
output: { results: ['private standard server result'] },
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
type: 'server_tool_call',
|
|
686
|
+
id: 'public_server_call',
|
|
687
|
+
name: 'web_search',
|
|
688
|
+
args: { query: 'public server input' },
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
type: 'server_tool_call_result',
|
|
692
|
+
toolCallId: 'public_server_call',
|
|
693
|
+
status: 'success',
|
|
694
|
+
output: 'public standard server result',
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
type: 'server_tool_call_result',
|
|
698
|
+
toolCallId: 'unknown_server_call',
|
|
699
|
+
status: 'success',
|
|
700
|
+
output: 'unmatched standard server result',
|
|
701
|
+
},
|
|
702
|
+
];
|
|
703
|
+
const selectiveSpan = createSpan('gpt-5.6', {
|
|
704
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify(blocks),
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
redactLangfuseSpanToolOutputs(
|
|
708
|
+
selectiveSpan,
|
|
709
|
+
createConfig({ redactedToolNames: new Set(['file_search']) })
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
const selectivelyRedacted = readJsonAttribute<
|
|
713
|
+
Array<Record<string, unknown>>
|
|
714
|
+
>(selectiveSpan, LangfuseOtelSpanAttributes.OBSERVATION_INPUT);
|
|
715
|
+
expect(selectivelyRedacted[0]).toMatchObject({
|
|
716
|
+
args: { query: 'private server input stays visible' },
|
|
717
|
+
});
|
|
718
|
+
expect(selectivelyRedacted[1].output).toBe(
|
|
719
|
+
LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
|
|
720
|
+
);
|
|
721
|
+
expect(selectivelyRedacted[3].output).toBe('public standard server result');
|
|
722
|
+
expect(selectivelyRedacted[4].output).toBe(
|
|
723
|
+
'unmatched standard server result'
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
const globalSpan = createSpan('gpt-5.6', {
|
|
727
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify(blocks),
|
|
728
|
+
});
|
|
729
|
+
redactLangfuseSpanToolOutputs(globalSpan, createConfig({ enabled: false }));
|
|
730
|
+
const globallyRedacted = readJsonAttribute<Array<Record<string, unknown>>>(
|
|
731
|
+
globalSpan,
|
|
732
|
+
LangfuseOtelSpanAttributes.OBSERVATION_INPUT
|
|
733
|
+
);
|
|
734
|
+
expect(globallyRedacted[1].output).toBe(
|
|
735
|
+
LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
|
|
736
|
+
);
|
|
737
|
+
expect(globallyRedacted[3].output).toBe(
|
|
738
|
+
LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
|
|
739
|
+
);
|
|
740
|
+
expect(globallyRedacted[4].output).toBe(
|
|
741
|
+
LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
|
|
742
|
+
);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('redacts marked projected server-tool images without touching user images', () => {
|
|
746
|
+
const message = new AIMessage({
|
|
747
|
+
content: [{ type: 'text', text: 'Partial answer stays visible.' }],
|
|
748
|
+
additional_kwargs: {
|
|
749
|
+
tool_outputs: [
|
|
750
|
+
{
|
|
751
|
+
id: 'code_image_call',
|
|
752
|
+
type: 'code_interpreter_call',
|
|
753
|
+
code: 'public image-producing code',
|
|
754
|
+
status: 'completed',
|
|
755
|
+
outputs: [
|
|
756
|
+
{
|
|
757
|
+
type: 'image',
|
|
758
|
+
url: 'data:image/png;base64,private-code-image-data',
|
|
759
|
+
},
|
|
760
|
+
],
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
id: 'generated_image_call',
|
|
764
|
+
type: 'image_generation_call',
|
|
765
|
+
status: 'completed',
|
|
766
|
+
result: 'private-generated-image-data',
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
},
|
|
770
|
+
response_metadata: {
|
|
771
|
+
model_provider: 'openai',
|
|
772
|
+
preempted: true,
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
const [projected] = projectToolStreamContentForProvider(
|
|
776
|
+
[message],
|
|
777
|
+
'native'
|
|
778
|
+
);
|
|
779
|
+
const projectedJson = projected.toJSON();
|
|
780
|
+
const serializedProjection = JSON.stringify(projectedJson);
|
|
781
|
+
expect(serializedProjection).toContain('private-code-image-data');
|
|
782
|
+
expect(serializedProjection).toContain('private-generated-image-data');
|
|
783
|
+
expect(serializedProjection).toContain('librechatServerToolResult');
|
|
784
|
+
|
|
785
|
+
const userImage = {
|
|
786
|
+
type: 'image',
|
|
787
|
+
mimeType: 'image/png',
|
|
788
|
+
data: 'public-user-image-data',
|
|
789
|
+
};
|
|
790
|
+
const span = createSpan('gpt-5.6', {
|
|
791
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify([
|
|
792
|
+
projectedJson,
|
|
793
|
+
userImage,
|
|
794
|
+
]),
|
|
795
|
+
});
|
|
796
|
+
redactLangfuseSpanToolOutputs(
|
|
797
|
+
span,
|
|
798
|
+
createConfig({
|
|
799
|
+
redactedToolNames: new Set(['code_interpreter', 'image_generation']),
|
|
800
|
+
})
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
const redacted = span.attributes[
|
|
804
|
+
LangfuseOtelSpanAttributes.OBSERVATION_INPUT
|
|
805
|
+
] as string;
|
|
806
|
+
expect(redacted).toContain('Partial answer stays visible.');
|
|
807
|
+
expect(redacted).toContain('public-user-image-data');
|
|
808
|
+
expect(redacted).not.toContain('private-code-image-data');
|
|
809
|
+
expect(redacted).not.toContain('private-generated-image-data');
|
|
810
|
+
expect(redacted).toContain(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it.each([
|
|
814
|
+
['globally', createConfig({ enabled: false })],
|
|
815
|
+
[
|
|
816
|
+
'selectively',
|
|
817
|
+
createConfig({ redactedToolNames: new Set(['image_generation']) }),
|
|
818
|
+
],
|
|
819
|
+
] as const)(
|
|
820
|
+
'redacts serialized normalized generated images %s without touching user images',
|
|
821
|
+
(_scope, config) => {
|
|
822
|
+
const generatedById = {
|
|
823
|
+
id: 'ig_normal_response',
|
|
824
|
+
type: 'image_generation_call',
|
|
825
|
+
status: 'completed',
|
|
826
|
+
result: 'private-generated-image-by-id',
|
|
827
|
+
};
|
|
828
|
+
const generatedSecond = {
|
|
829
|
+
id: 'ig_normal_response_second',
|
|
830
|
+
type: 'image_generation_call',
|
|
831
|
+
status: 'completed',
|
|
832
|
+
result: 'private-generated-image-by-data',
|
|
833
|
+
};
|
|
834
|
+
const message = new AIMessage({
|
|
835
|
+
content: [
|
|
836
|
+
{
|
|
837
|
+
type: 'image',
|
|
838
|
+
mimeType: 'image/png',
|
|
839
|
+
data: generatedById.result,
|
|
840
|
+
id: generatedById.id,
|
|
841
|
+
metadata: { status: generatedById.status },
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
type: 'image',
|
|
845
|
+
mimeType: 'image/png',
|
|
846
|
+
data: generatedSecond.result,
|
|
847
|
+
id: generatedSecond.id,
|
|
848
|
+
metadata: { status: generatedSecond.status },
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
type: 'image',
|
|
852
|
+
mimeType: 'image/png',
|
|
853
|
+
id: generatedById.id,
|
|
854
|
+
url: 'private-generated-image-url',
|
|
855
|
+
fileId: 'private-generated-image-file-id',
|
|
856
|
+
metadata: { status: generatedById.status },
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
type: 'image',
|
|
860
|
+
mimeType: 'image/png',
|
|
861
|
+
data: 'public-assistant-application-image',
|
|
862
|
+
id: 'application-image',
|
|
863
|
+
metadata: { status: 'completed' },
|
|
864
|
+
},
|
|
865
|
+
],
|
|
866
|
+
additional_kwargs: {
|
|
867
|
+
tool_outputs: [generatedById, generatedSecond],
|
|
868
|
+
},
|
|
869
|
+
response_metadata: {
|
|
870
|
+
model_provider: 'openai',
|
|
871
|
+
output: [generatedById, generatedSecond],
|
|
872
|
+
},
|
|
873
|
+
});
|
|
874
|
+
const serializedMessage = serializeMessageForLangfuse(message);
|
|
875
|
+
const serializedUserMessage = serializeMessageForLangfuse(
|
|
876
|
+
new HumanMessage({
|
|
877
|
+
content: [
|
|
878
|
+
{
|
|
879
|
+
type: 'image',
|
|
880
|
+
mimeType: 'image/png',
|
|
881
|
+
data: 'public-user-image-data',
|
|
882
|
+
},
|
|
883
|
+
],
|
|
884
|
+
})
|
|
885
|
+
);
|
|
886
|
+
expect(JSON.stringify(serializedMessage)).not.toContain(
|
|
887
|
+
'image_generation_call'
|
|
888
|
+
);
|
|
889
|
+
const span = createSpan('gpt-5.6', {
|
|
890
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: JSON.stringify([
|
|
891
|
+
serializedMessage,
|
|
892
|
+
serializedUserMessage,
|
|
893
|
+
]),
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
redactLangfuseSpanToolOutputs(span, config);
|
|
897
|
+
|
|
898
|
+
const redacted = span.attributes[
|
|
899
|
+
LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT
|
|
900
|
+
] as string;
|
|
901
|
+
expect(redacted).not.toContain(generatedById.result);
|
|
902
|
+
expect(redacted).not.toContain(generatedSecond.result);
|
|
903
|
+
expect(redacted).not.toContain('private-generated-image-url');
|
|
904
|
+
expect(redacted).not.toContain('private-generated-image-file-id');
|
|
905
|
+
expect(redacted).toContain('public-assistant-application-image');
|
|
906
|
+
expect(redacted).toContain('public-user-image-data');
|
|
907
|
+
expect(redacted).toContain(generatedById.id);
|
|
908
|
+
expect(redacted).toContain(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
909
|
+
}
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
it('does not reclassify a marked code-interpreter image as image generation', () => {
|
|
913
|
+
const codeImage = 'data:image/png;base64,public-code-image';
|
|
914
|
+
const span = createSpan('gpt-5.6', {
|
|
915
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: JSON.stringify([
|
|
916
|
+
{
|
|
917
|
+
role: 'assistant',
|
|
918
|
+
content: [
|
|
919
|
+
{
|
|
920
|
+
type: 'image',
|
|
921
|
+
url: codeImage,
|
|
922
|
+
extras: {
|
|
923
|
+
librechatServerToolResult: {
|
|
924
|
+
toolName: 'code_interpreter',
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
],
|
|
929
|
+
},
|
|
930
|
+
]),
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
redactLangfuseSpanToolOutputs(
|
|
934
|
+
span,
|
|
935
|
+
createConfig({
|
|
936
|
+
redactedToolNames: new Set(['image_generation']),
|
|
937
|
+
})
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
expect(
|
|
941
|
+
span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]
|
|
942
|
+
).toContain(codeImage);
|
|
943
|
+
expect(
|
|
944
|
+
span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]
|
|
945
|
+
).not.toContain(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it('fails closed when OpenTelemetry truncates a JSON-shaped attribute', () => {
|
|
949
|
+
const secret = 'private truncated tool output';
|
|
950
|
+
const serialized = JSON.stringify([
|
|
951
|
+
{
|
|
952
|
+
role: 'assistant',
|
|
953
|
+
content: [
|
|
954
|
+
{
|
|
955
|
+
type: 'text',
|
|
956
|
+
text: JSON.stringify({
|
|
957
|
+
serverToolResult: {
|
|
958
|
+
toolName: 'shell',
|
|
959
|
+
status: 'success',
|
|
960
|
+
output: secret,
|
|
961
|
+
},
|
|
962
|
+
}),
|
|
963
|
+
},
|
|
964
|
+
],
|
|
965
|
+
},
|
|
966
|
+
]);
|
|
967
|
+
const span = createSpan('gpt-5.6', {
|
|
968
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: serialized.slice(0, -1),
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
redactLangfuseSpanToolOutputs(
|
|
972
|
+
span,
|
|
973
|
+
createConfig({ redactedToolNames: new Set(['shell']) })
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
expect(span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]).toBe(
|
|
977
|
+
LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
|
|
978
|
+
);
|
|
979
|
+
expect(JSON.stringify(span.attributes)).not.toContain(secret);
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
it('redacts marked bounded replay text without parsing its payload', () => {
|
|
983
|
+
const truncatedSecret =
|
|
984
|
+
'{"serverToolResult":{"librechatResponsesReplay":true,"status":"success","output":"secret head…secret tail"';
|
|
985
|
+
const span = createSpan('gpt-5.6', {
|
|
986
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify([
|
|
987
|
+
{
|
|
988
|
+
role: 'assistant',
|
|
989
|
+
content: [
|
|
990
|
+
{
|
|
991
|
+
type: 'text',
|
|
992
|
+
text: truncatedSecret,
|
|
993
|
+
},
|
|
994
|
+
],
|
|
995
|
+
},
|
|
996
|
+
]),
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
redactLangfuseSpanToolOutputs(span, createConfig({ enabled: false }));
|
|
1000
|
+
|
|
1001
|
+
const redacted = span.attributes[
|
|
1002
|
+
LangfuseOtelSpanAttributes.OBSERVATION_INPUT
|
|
1003
|
+
] as string;
|
|
1004
|
+
expect(redacted).not.toContain('secret head');
|
|
1005
|
+
expect(redacted).not.toContain('secret tail');
|
|
1006
|
+
expect(redacted).toContain(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
it('redacts replay output before root-span answer shaping', () => {
|
|
1010
|
+
const rootSecret = 'private root replay result';
|
|
1011
|
+
const span = createSpan('LangGraph', {
|
|
1012
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify([
|
|
1013
|
+
{ role: 'user', content: 'Continue.' },
|
|
1014
|
+
]),
|
|
1015
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: JSON.stringify([
|
|
1016
|
+
{
|
|
1017
|
+
role: 'assistant',
|
|
1018
|
+
content: [
|
|
1019
|
+
{ type: 'text', text: 'Partial answer. ' },
|
|
1020
|
+
{
|
|
1021
|
+
type: 'text',
|
|
1022
|
+
text: JSON.stringify({
|
|
1023
|
+
serverToolResult: {
|
|
1024
|
+
toolName: 'shell',
|
|
1025
|
+
status: 'success',
|
|
1026
|
+
output: rootSecret,
|
|
1027
|
+
},
|
|
1028
|
+
}),
|
|
1029
|
+
extras: {
|
|
1030
|
+
librechatServerToolResult: { toolName: 'shell' },
|
|
1031
|
+
},
|
|
1032
|
+
},
|
|
1033
|
+
],
|
|
1034
|
+
},
|
|
1035
|
+
]),
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
prepareLangfuseSpanForExport(span, createConfig({ enabled: false }));
|
|
1039
|
+
|
|
1040
|
+
for (const key of [
|
|
1041
|
+
LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT,
|
|
1042
|
+
LangfuseOtelSpanAttributes.TRACE_OUTPUT,
|
|
1043
|
+
]) {
|
|
1044
|
+
const output = span.attributes[key] as string;
|
|
1045
|
+
expect(output).toContain('Partial answer.');
|
|
1046
|
+
expect(output).toContain(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
1047
|
+
expect(output).not.toContain(rootSecret);
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
it('redacts a root tool output before deriving trace output', () => {
|
|
1052
|
+
const rootSecret = 'private root tool result';
|
|
1053
|
+
const span = createSpan('shell', {
|
|
1054
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_TYPE]: 'tool',
|
|
1055
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]: rootSecret,
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
prepareLangfuseSpanForExport(span, createConfig({ enabled: false }));
|
|
1059
|
+
|
|
1060
|
+
expect(span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT]).toBe(
|
|
1061
|
+
LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
|
|
1062
|
+
);
|
|
1063
|
+
expect(span.attributes[LangfuseOtelSpanAttributes.TRACE_OUTPUT]).toBe(
|
|
1064
|
+
LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
|
|
1065
|
+
);
|
|
1066
|
+
expect(JSON.stringify(span.attributes)).not.toContain(rootSecret);
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
it('preserves ordinary text that only resembles a server-tool result', () => {
|
|
1070
|
+
const ordinaryText =
|
|
1071
|
+
'{"serverToolResult":{"status":"success","output":"public literal text"}}';
|
|
1072
|
+
const span = createSpan('gpt-5.6', {
|
|
1073
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify([
|
|
1074
|
+
{
|
|
1075
|
+
role: 'assistant',
|
|
1076
|
+
content: [{ type: 'text', text: ordinaryText }],
|
|
1077
|
+
},
|
|
1078
|
+
]),
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
redactLangfuseSpanToolOutputs(span, createConfig({ enabled: false }));
|
|
1082
|
+
|
|
1083
|
+
expect(
|
|
1084
|
+
span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]
|
|
1085
|
+
).toContain('public literal text');
|
|
1086
|
+
expect(
|
|
1087
|
+
span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]
|
|
1088
|
+
).not.toContain(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
it('recognizes serialized fallback results only inside assistant messages', () => {
|
|
1092
|
+
const userLiteral =
|
|
1093
|
+
'{"serverToolResult":{"status":"success","output":"public user literal"}}';
|
|
1094
|
+
const assistantResult =
|
|
1095
|
+
'{"serverToolResult":{"librechatResponsesReplay":true,"status":"success","output":"private replay result"}}';
|
|
1096
|
+
const span = createSpan('gpt-5.6', {
|
|
1097
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify([
|
|
1098
|
+
{
|
|
1099
|
+
role: 'user',
|
|
1100
|
+
content: [{ type: 'text', text: userLiteral }],
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
role: 'assistant',
|
|
1104
|
+
content: [{ type: 'text', text: assistantResult }],
|
|
1105
|
+
},
|
|
1106
|
+
]),
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
redactLangfuseSpanToolOutputs(span, createConfig({ enabled: false }));
|
|
1110
|
+
|
|
1111
|
+
const redacted = span.attributes[
|
|
1112
|
+
LangfuseOtelSpanAttributes.OBSERVATION_INPUT
|
|
1113
|
+
] as string;
|
|
1114
|
+
expect(redacted).toContain('public user literal');
|
|
1115
|
+
expect(redacted).not.toContain('private replay result');
|
|
1116
|
+
expect(redacted).toContain(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
it('redacts neutralized replay results from resumed generation input', () => {
|
|
1120
|
+
const message = new AIMessage({
|
|
1121
|
+
content: [{ type: 'text', text: 'Partial answer.' }],
|
|
1122
|
+
additional_kwargs: {
|
|
1123
|
+
tool_outputs: [
|
|
1124
|
+
{
|
|
1125
|
+
id: 'shell_output_item',
|
|
1126
|
+
call_id: 'shell_call',
|
|
1127
|
+
type: 'shell_call_output',
|
|
1128
|
+
status: 'completed',
|
|
1129
|
+
output: 'resumed generation secret',
|
|
1130
|
+
},
|
|
1131
|
+
],
|
|
1132
|
+
},
|
|
1133
|
+
response_metadata: {
|
|
1134
|
+
model_provider: 'openai',
|
|
1135
|
+
preempted: true,
|
|
1136
|
+
},
|
|
1137
|
+
});
|
|
1138
|
+
const [projected] = projectToolStreamContentForProvider(
|
|
1139
|
+
[message],
|
|
1140
|
+
'fallback'
|
|
1141
|
+
);
|
|
1142
|
+
expect(projected.content).toEqual([
|
|
1143
|
+
{ type: 'text', text: 'Partial answer.' },
|
|
1144
|
+
expect.objectContaining({
|
|
1145
|
+
type: 'text',
|
|
1146
|
+
text: expect.stringContaining('resumed generation secret'),
|
|
1147
|
+
}),
|
|
1148
|
+
]);
|
|
1149
|
+
expect(JSON.stringify(projected.content)).not.toContain('extras');
|
|
1150
|
+
const span = createSpan('gpt-5.6', {
|
|
1151
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: JSON.stringify([
|
|
1152
|
+
projected.toJSON(),
|
|
1153
|
+
]),
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
redactLangfuseSpanToolOutputs(
|
|
1157
|
+
span,
|
|
1158
|
+
createConfig({ redactedToolNames: new Set(['shell']) })
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
const redacted = span.attributes[
|
|
1162
|
+
LangfuseOtelSpanAttributes.OBSERVATION_INPUT
|
|
1163
|
+
] as string;
|
|
1164
|
+
expect(redacted).toContain('Partial answer.');
|
|
1165
|
+
expect(redacted).not.toContain('resumed generation secret');
|
|
1166
|
+
expect(redacted).toContain(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
|
|
1167
|
+
});
|
|
1168
|
+
|
|
282
1169
|
it('redacts only configured tool names when output tracing stays enabled', () => {
|
|
283
1170
|
const messages = [
|
|
284
1171
|
{ role: 'execute_sql', content: 'private query result' },
|