@librechat/agents 3.3.9 → 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/agents/AgentContext.cjs +4 -0
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +21 -2
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- 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 +11 -1
- package/dist/cjs/run.cjs.map +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/agents/AgentContext.mjs +4 -0
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +21 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- 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 +11 -1
- package/dist/esm/run.mjs.map +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/agents/AgentContext.d.ts +2 -0
- package/dist/types/graphs/Graph.d.ts +5 -0
- 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/dist/types/run.d.ts +7 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +5 -0
- package/src/graphs/Graph.ts +39 -0
- 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/run.ts +15 -0
- package/src/specs/discovered-tools.test.ts +217 -0
- package/src/specs/langfuse-tool-output-tracing.test.ts +887 -0
- package/src/specs/preemptSeal.test.ts +374 -5
package/dist/types/run.d.ts
CHANGED
|
@@ -102,6 +102,13 @@ export declare class Run<_T extends t.BaseGraphState> {
|
|
|
102
102
|
private runPreStreamHooks;
|
|
103
103
|
static create<T extends t.BaseGraphState>(config: t.RunConfig): Promise<Run<T>>;
|
|
104
104
|
getRunMessages(): BaseMessage[] | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Returns a defensive snapshot of tools discovered by the current run.
|
|
107
|
+
* Pass an agent id for that context, or omit it for the ordered union across
|
|
108
|
+
* contexts. Interrupted state is available immediately for host persistence;
|
|
109
|
+
* completed runs retain their final snapshot through graph cleanup.
|
|
110
|
+
*/
|
|
111
|
+
getDiscoveredTools(agentId?: string): string[];
|
|
105
112
|
/**
|
|
106
113
|
* Returns the current calibration ratio (EMA of provider-vs-estimate token ratios).
|
|
107
114
|
* Hosts should persist this value and pass it back as `RunConfig.calibrationRatio`
|
package/package.json
CHANGED
|
@@ -1708,6 +1708,11 @@ export class AgentContext {
|
|
|
1708
1708
|
this.totalTokensFresh = false;
|
|
1709
1709
|
}
|
|
1710
1710
|
|
|
1711
|
+
/** Returns a snapshot of the deferred tools discovered in this context. */
|
|
1712
|
+
getDiscoveredTools(): string[] {
|
|
1713
|
+
return Array.from(this.discoveredToolNames);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1711
1716
|
/**
|
|
1712
1717
|
* Marks tools as discovered via tool search.
|
|
1713
1718
|
* Discovered tools will be included in the next model binding.
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -643,6 +643,10 @@ export abstract class Graph<
|
|
|
643
643
|
currentToolMap?: t.ToolMap;
|
|
644
644
|
}): CustomToolNode<T> | ToolNode<T>;
|
|
645
645
|
abstract getRunMessages(): BaseMessage[] | undefined;
|
|
646
|
+
/** Returns a snapshot of deferred tools discovered by this graph. */
|
|
647
|
+
getDiscoveredTools(_agentId?: string): string[] {
|
|
648
|
+
return [];
|
|
649
|
+
}
|
|
646
650
|
abstract getContentParts(): t.MessageContentComplex[] | undefined;
|
|
647
651
|
abstract generateStepId(stepKey: string): [string, number];
|
|
648
652
|
abstract getKeyList(
|
|
@@ -1006,6 +1010,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1006
1010
|
messages: BaseMessage[] = [];
|
|
1007
1011
|
/** Cached run messages preserved before clearHeavyState() so getRunMessages() works after cleanup. */
|
|
1008
1012
|
private cachedRunMessages?: BaseMessage[];
|
|
1013
|
+
/** Per-agent discovery snapshots preserved before contexts are reset on cleanup. */
|
|
1014
|
+
private cachedDiscoveredTools?: Map<string, string[]>;
|
|
1009
1015
|
/** Ids of AI turns the agent node returned THIS run; see isRunProducedMessage. */
|
|
1010
1016
|
protected runProducedAiMessageIds = new Set<string>();
|
|
1011
1017
|
/** Checkpoint scope whose messages match index-keyed tool snapshots. */
|
|
@@ -1137,6 +1143,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1137
1143
|
resetValues(keepContent?: boolean, checkpointScope?: string): void {
|
|
1138
1144
|
this.messages = [];
|
|
1139
1145
|
this.cachedRunMessages = undefined;
|
|
1146
|
+
this.cachedDiscoveredTools = undefined;
|
|
1140
1147
|
this.config = resetIfNotEmpty(this.config, undefined);
|
|
1141
1148
|
if (keepContent !== true) {
|
|
1142
1149
|
this.contentData = resetIfNotEmpty(this.contentData, []);
|
|
@@ -1203,6 +1210,12 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1203
1210
|
|
|
1204
1211
|
override clearHeavyState(): void {
|
|
1205
1212
|
this.cachedRunMessages = this.messages.slice(this.startIndex);
|
|
1213
|
+
this.cachedDiscoveredTools = new Map(
|
|
1214
|
+
Array.from(this.agentContexts, ([agentId, context]) => [
|
|
1215
|
+
agentId,
|
|
1216
|
+
context.getDiscoveredTools(),
|
|
1217
|
+
])
|
|
1218
|
+
);
|
|
1206
1219
|
super.clearHeavyState();
|
|
1207
1220
|
this.messages = [];
|
|
1208
1221
|
this.overrideModel = undefined;
|
|
@@ -1497,6 +1510,32 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1497
1510
|
return this.messages.slice(this.startIndex);
|
|
1498
1511
|
}
|
|
1499
1512
|
|
|
1513
|
+
override getDiscoveredTools(agentId?: string): string[] {
|
|
1514
|
+
if (agentId != null) {
|
|
1515
|
+
const current =
|
|
1516
|
+
this.agentContexts.get(agentId)?.getDiscoveredTools() ?? [];
|
|
1517
|
+
if (current.length > 0 || this.cachedDiscoveredTools == null) {
|
|
1518
|
+
return current;
|
|
1519
|
+
}
|
|
1520
|
+
return [...(this.cachedDiscoveredTools.get(agentId) ?? [])];
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
const discoveredTools = new Set<string>();
|
|
1524
|
+
for (const context of this.agentContexts.values()) {
|
|
1525
|
+
for (const toolName of context.getDiscoveredTools()) {
|
|
1526
|
+
discoveredTools.add(toolName);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
if (discoveredTools.size === 0 && this.cachedDiscoveredTools != null) {
|
|
1530
|
+
for (const snapshot of this.cachedDiscoveredTools.values()) {
|
|
1531
|
+
for (const toolName of snapshot) {
|
|
1532
|
+
discoveredTools.add(toolName);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
return Array.from(discoveredTools);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1500
1539
|
/**
|
|
1501
1540
|
* True when THIS RUN produced `message` — the provenance the handoff cue
|
|
1502
1541
|
* gate needs. Tracked as an id set rather than inferred from `startIndex`
|
|
@@ -25,6 +25,8 @@ import { resolveToolOutputTracingConfigForSpan } from '@/langfuseRuntimeScope';
|
|
|
25
25
|
export { LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT, resolveLangfuseConfig };
|
|
26
26
|
|
|
27
27
|
const LANGGRAPH_TOOL_NODE_PREFIX = 'tools=';
|
|
28
|
+
const SERVER_TOOL_RESULT_PREFIX = '{"serverToolResult":';
|
|
29
|
+
const SERVER_TOOL_RESULT_REPLAY_MARKER_KEY = 'librechatResponsesReplay';
|
|
28
30
|
|
|
29
31
|
const CHAT_ROLES = new Set([
|
|
30
32
|
'assistant',
|
|
@@ -44,11 +46,73 @@ type RedactionResult = {
|
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
type RedactionContext = {
|
|
49
|
+
generatedImageData: Set<string>;
|
|
50
|
+
generatedImageIds: Set<string>;
|
|
47
51
|
toolNamesByCallId: Map<string, string>;
|
|
48
52
|
};
|
|
49
53
|
|
|
50
54
|
const TOOL_OUTPUT_FIELD_KEYS = ['content', 'artifact'];
|
|
51
55
|
|
|
56
|
+
type ResponsesReplayOutputDescriptor = {
|
|
57
|
+
nestedOutputFields?: Readonly<Record<string, readonly string[]>>;
|
|
58
|
+
outputFields: readonly string[];
|
|
59
|
+
resolveToolNameFromCallId?: boolean;
|
|
60
|
+
toolName?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const RESPONSES_REPLAY_OUTPUT_DESCRIPTORS: Readonly<
|
|
64
|
+
Record<string, ResponsesReplayOutputDescriptor>
|
|
65
|
+
> = {
|
|
66
|
+
local_shell_call_output: {
|
|
67
|
+
outputFields: ['output'],
|
|
68
|
+
toolName: 'local_shell',
|
|
69
|
+
},
|
|
70
|
+
shell_call_output: { outputFields: ['output'], toolName: 'shell' },
|
|
71
|
+
apply_patch_call_output: {
|
|
72
|
+
outputFields: ['output'],
|
|
73
|
+
toolName: 'apply_patch',
|
|
74
|
+
},
|
|
75
|
+
program_output: { outputFields: ['result'], toolName: 'program' },
|
|
76
|
+
code_interpreter_call: {
|
|
77
|
+
outputFields: ['outputs'],
|
|
78
|
+
toolName: 'code_interpreter',
|
|
79
|
+
},
|
|
80
|
+
mcp_call: { outputFields: ['output', 'error'], toolName: 'mcp' },
|
|
81
|
+
mcp_list_tools: {
|
|
82
|
+
outputFields: ['tools', 'error'],
|
|
83
|
+
toolName: 'mcp_list_tools',
|
|
84
|
+
},
|
|
85
|
+
image_generation_call: {
|
|
86
|
+
outputFields: ['result'],
|
|
87
|
+
toolName: 'image_generation',
|
|
88
|
+
},
|
|
89
|
+
file_search_call: {
|
|
90
|
+
outputFields: ['results'],
|
|
91
|
+
toolName: 'file_search',
|
|
92
|
+
},
|
|
93
|
+
web_search_call: {
|
|
94
|
+
nestedOutputFields: { action: ['sources'] },
|
|
95
|
+
outputFields: ['results'],
|
|
96
|
+
toolName: 'web_search',
|
|
97
|
+
},
|
|
98
|
+
tool_search_output: {
|
|
99
|
+
outputFields: ['tools'],
|
|
100
|
+
toolName: 'tool_search',
|
|
101
|
+
},
|
|
102
|
+
function_call_output: {
|
|
103
|
+
outputFields: ['output'],
|
|
104
|
+
resolveToolNameFromCallId: true,
|
|
105
|
+
},
|
|
106
|
+
custom_tool_call_output: {
|
|
107
|
+
outputFields: ['output'],
|
|
108
|
+
resolveToolNameFromCallId: true,
|
|
109
|
+
},
|
|
110
|
+
computer_call_output: {
|
|
111
|
+
outputFields: ['output'],
|
|
112
|
+
toolName: 'computer_use',
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
52
116
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
53
117
|
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
54
118
|
}
|
|
@@ -119,6 +183,8 @@ function getSerializedToolCallId(
|
|
|
119
183
|
): string | undefined {
|
|
120
184
|
return (
|
|
121
185
|
getStringField(value, 'tool_call_id') ??
|
|
186
|
+
getStringField(value, 'toolCallId') ??
|
|
187
|
+
getStringField(value, 'call_id') ??
|
|
122
188
|
getNestedStringField(value, 'kwargs', 'tool_call_id') ??
|
|
123
189
|
getNestedStringField(value, 'additional_kwargs', 'tool_call_id') ??
|
|
124
190
|
getNestedStringField(value, 'data', 'tool_call_id') ??
|
|
@@ -180,6 +246,26 @@ function hasToolMessageIdentity(value: Record<string, unknown>): boolean {
|
|
|
180
246
|
);
|
|
181
247
|
}
|
|
182
248
|
|
|
249
|
+
function hasAssistantMessageIdentity(value: Record<string, unknown>): boolean {
|
|
250
|
+
const role = getStringField(value, 'role');
|
|
251
|
+
if (role?.toLowerCase() === 'assistant') {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
const type = getStringField(value, 'type') ?? getStringField(value, '_type');
|
|
255
|
+
if (type === 'ai' || type === 'assistant') {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
const id = value.id;
|
|
259
|
+
return (
|
|
260
|
+
Array.isArray(id) &&
|
|
261
|
+
id.some(
|
|
262
|
+
(part) =>
|
|
263
|
+
typeof part === 'string' &&
|
|
264
|
+
(part === 'AIMessage' || part === 'AIMessageChunk')
|
|
265
|
+
)
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
183
269
|
function redactToolContentFields(
|
|
184
270
|
value: Record<string, unknown>,
|
|
185
271
|
config: ResolvedLangfuseToolOutputTracingConfig
|
|
@@ -213,13 +299,250 @@ function redactToolContentFields(
|
|
|
213
299
|
return next;
|
|
214
300
|
}
|
|
215
301
|
|
|
216
|
-
function
|
|
302
|
+
function redactResponsesReplayOutput(
|
|
303
|
+
value: Record<string, unknown>,
|
|
304
|
+
config: ResolvedLangfuseToolOutputTracingConfig,
|
|
305
|
+
redactionContext: RedactionContext
|
|
306
|
+
): RedactionResult | undefined {
|
|
307
|
+
const type = getStringField(value, 'type');
|
|
308
|
+
if (
|
|
309
|
+
type == null ||
|
|
310
|
+
!Object.prototype.hasOwnProperty.call(
|
|
311
|
+
RESPONSES_REPLAY_OUTPUT_DESCRIPTORS,
|
|
312
|
+
type
|
|
313
|
+
)
|
|
314
|
+
) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
const descriptor =
|
|
318
|
+
RESPONSES_REPLAY_OUTPUT_DESCRIPTORS[
|
|
319
|
+
type as keyof typeof RESPONSES_REPLAY_OUTPUT_DESCRIPTORS
|
|
320
|
+
];
|
|
321
|
+
const hasDirectOutput = descriptor.outputFields.some(
|
|
322
|
+
(outputField) => outputField in value
|
|
323
|
+
);
|
|
324
|
+
const hasNestedOutput = Object.entries(
|
|
325
|
+
descriptor.nestedOutputFields ?? {}
|
|
326
|
+
).some(([objectField, outputFields]) => {
|
|
327
|
+
const nested = value[objectField];
|
|
328
|
+
return (
|
|
329
|
+
isRecord(nested) &&
|
|
330
|
+
outputFields.some((outputField) => outputField in nested)
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
if (!hasDirectOutput && !hasNestedOutput) {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
let toolName = descriptor.toolName;
|
|
337
|
+
if (descriptor.resolveToolNameFromCallId === true) {
|
|
338
|
+
toolName = getSerializedToolName(value, redactionContext);
|
|
339
|
+
} else if (type === 'mcp_call') {
|
|
340
|
+
toolName = getStringField(value, 'name') ?? descriptor.toolName;
|
|
341
|
+
}
|
|
342
|
+
if (!shouldRedactTool(toolName, config)) {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
const redacted = { ...value };
|
|
346
|
+
for (const outputField of descriptor.outputFields) {
|
|
347
|
+
if (outputField in redacted) {
|
|
348
|
+
redacted[outputField] = config.redactionText;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
for (const [objectField, outputFields] of Object.entries(
|
|
352
|
+
descriptor.nestedOutputFields ?? {}
|
|
353
|
+
)) {
|
|
354
|
+
const nested = redacted[objectField];
|
|
355
|
+
if (!isRecord(nested)) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const redactedNested = { ...nested };
|
|
359
|
+
for (const outputField of outputFields) {
|
|
360
|
+
if (outputField in redactedNested) {
|
|
361
|
+
redactedNested[outputField] = config.redactionText;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
redacted[objectField] = redactedNested;
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
value: redacted,
|
|
368
|
+
changed: true,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function redactStandardServerToolResult(
|
|
373
|
+
value: Record<string, unknown>,
|
|
374
|
+
config: ResolvedLangfuseToolOutputTracingConfig,
|
|
375
|
+
redactionContext: RedactionContext
|
|
376
|
+
): RedactionResult | undefined {
|
|
377
|
+
if (
|
|
378
|
+
getStringField(value, 'type') !== 'server_tool_call_result' ||
|
|
379
|
+
!('output' in value)
|
|
380
|
+
) {
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
const toolName =
|
|
384
|
+
getNestedStringField(value, 'extras', 'name') ??
|
|
385
|
+
getSerializedToolName(value, redactionContext);
|
|
386
|
+
if (!shouldRedactTool(toolName, config)) {
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
value: { ...value, output: config.redactionText },
|
|
391
|
+
changed: true,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function parseSerializedServerToolResultMarker(
|
|
396
|
+
text: string
|
|
397
|
+
): { toolName?: string } | undefined {
|
|
398
|
+
if (!text.startsWith(SERVER_TOOL_RESULT_PREFIX)) {
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
const parsed = JSON.parse(text) as unknown;
|
|
403
|
+
if (!isRecord(parsed) || !isRecord(parsed.serverToolResult)) {
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
const result = parsed.serverToolResult;
|
|
407
|
+
if (result[SERVER_TOOL_RESULT_REPLAY_MARKER_KEY] !== true) {
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
const status = getStringField(result, 'status');
|
|
411
|
+
if (
|
|
412
|
+
(status !== 'error' && status !== 'success') ||
|
|
413
|
+
!Object.prototype.hasOwnProperty.call(result, 'output')
|
|
414
|
+
) {
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
const toolName = getStringField(result, 'toolName');
|
|
418
|
+
return toolName != null ? { toolName } : {};
|
|
419
|
+
} catch {
|
|
420
|
+
const match =
|
|
421
|
+
/^\{"serverToolResult":\{"librechatResponsesReplay":true,(?:"toolName":("(?:\\.|[^"\\])*"),)?"status":"(?:error|success)","output":/.exec(
|
|
422
|
+
text.slice(0, 512)
|
|
423
|
+
);
|
|
424
|
+
if (match == null) {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
const encodedToolName = match.at(1);
|
|
428
|
+
if (encodedToolName == null) {
|
|
429
|
+
return {};
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
const toolName = JSON.parse(encodedToolName) as unknown;
|
|
433
|
+
return typeof toolName === 'string' ? { toolName } : {};
|
|
434
|
+
} catch {
|
|
435
|
+
return {};
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function redactMarkedServerToolResult(
|
|
441
|
+
value: Record<string, unknown>,
|
|
442
|
+
config: ResolvedLangfuseToolOutputTracingConfig,
|
|
443
|
+
allowSerializedMarker: boolean
|
|
444
|
+
): RedactionResult | undefined {
|
|
445
|
+
const type = getStringField(value, 'type');
|
|
446
|
+
if (type !== 'text' && type !== 'image') {
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
const text = type === 'text' ? getStringField(value, 'text') : undefined;
|
|
450
|
+
const extras = value.extras;
|
|
451
|
+
const marker = isRecord(extras)
|
|
452
|
+
? extras.librechatServerToolResult
|
|
453
|
+
: undefined;
|
|
454
|
+
const serializedMarker =
|
|
455
|
+
allowSerializedMarker && text != null
|
|
456
|
+
? parseSerializedServerToolResultMarker(text)
|
|
457
|
+
: undefined;
|
|
458
|
+
if (!isRecord(marker) && serializedMarker == null) {
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
const toolName = isRecord(marker)
|
|
462
|
+
? getStringField(marker, 'toolName')
|
|
463
|
+
: serializedMarker?.toolName;
|
|
464
|
+
if (!shouldRedactTool(toolName, config)) {
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
if (type === 'image') {
|
|
468
|
+
const redacted = { ...value };
|
|
469
|
+
let changed = false;
|
|
470
|
+
for (const outputField of ['data', 'url', 'fileId']) {
|
|
471
|
+
if (outputField in redacted) {
|
|
472
|
+
redacted[outputField] = config.redactionText;
|
|
473
|
+
changed = true;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return changed ? { value: redacted, changed: true } : undefined;
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
value: { ...value, text: config.redactionText },
|
|
480
|
+
changed: true,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function redactGeneratedImageBlock(
|
|
485
|
+
value: Record<string, unknown>,
|
|
486
|
+
config: ResolvedLangfuseToolOutputTracingConfig,
|
|
487
|
+
redactionContext: RedactionContext,
|
|
488
|
+
isAssistantContent: boolean
|
|
489
|
+
): RedactionResult | undefined {
|
|
490
|
+
if (
|
|
491
|
+
getStringField(value, 'type') !== 'image' ||
|
|
492
|
+
!shouldRedactTool('image_generation', config)
|
|
493
|
+
) {
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
const extras = value.extras;
|
|
497
|
+
const explicitMarker = isRecord(extras)
|
|
498
|
+
? extras.librechatServerToolResult
|
|
499
|
+
: undefined;
|
|
500
|
+
const explicitToolName = isRecord(explicitMarker)
|
|
501
|
+
? getStringField(explicitMarker, 'toolName')
|
|
502
|
+
: undefined;
|
|
503
|
+
if (
|
|
504
|
+
explicitToolName != null &&
|
|
505
|
+
normalizeToolName(explicitToolName) !== 'image_generation'
|
|
506
|
+
) {
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
const id = getStringField(value, 'id');
|
|
510
|
+
const data = getStringField(value, 'data');
|
|
511
|
+
const metadata = value.metadata;
|
|
512
|
+
const normalizedStatus = isRecord(metadata)
|
|
513
|
+
? getStringField(metadata, 'status')
|
|
514
|
+
: undefined;
|
|
515
|
+
const hasNormalizedGeneratedImageIdentity =
|
|
516
|
+
isAssistantContent &&
|
|
517
|
+
id?.startsWith('ig_') === true &&
|
|
518
|
+
isPresent(normalizedStatus);
|
|
519
|
+
const isGeneratedImage =
|
|
520
|
+
explicitToolName != null ||
|
|
521
|
+
hasNormalizedGeneratedImageIdentity ||
|
|
522
|
+
(id != null
|
|
523
|
+
? redactionContext.generatedImageIds.has(id)
|
|
524
|
+
: data != null && redactionContext.generatedImageData.has(data));
|
|
525
|
+
if (!isGeneratedImage) {
|
|
526
|
+
return undefined;
|
|
527
|
+
}
|
|
528
|
+
const redacted = { ...value };
|
|
529
|
+
let changed = false;
|
|
530
|
+
for (const outputField of ['data', 'url', 'fileId']) {
|
|
531
|
+
if (outputField in redacted) {
|
|
532
|
+
redacted[outputField] = config.redactionText;
|
|
533
|
+
changed = true;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return changed ? { value: redacted, changed: true } : undefined;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function collectRedactionContext(
|
|
217
540
|
value: unknown,
|
|
218
541
|
redactionContext: RedactionContext
|
|
219
542
|
): void {
|
|
220
543
|
if (Array.isArray(value)) {
|
|
221
544
|
for (const item of value) {
|
|
222
|
-
|
|
545
|
+
collectRedactionContext(item, redactionContext);
|
|
223
546
|
}
|
|
224
547
|
return;
|
|
225
548
|
}
|
|
@@ -234,21 +557,38 @@ function collectToolCallNames(
|
|
|
234
557
|
redactionContext.toolNamesByCallId.set(toolCallId, toolName);
|
|
235
558
|
}
|
|
236
559
|
|
|
560
|
+
if (getStringField(value, 'type') === 'image_generation_call') {
|
|
561
|
+
const id = getStringField(value, 'id');
|
|
562
|
+
const result = getStringField(value, 'result');
|
|
563
|
+
if (id != null) {
|
|
564
|
+
redactionContext.generatedImageIds.add(id);
|
|
565
|
+
}
|
|
566
|
+
if (result != null) {
|
|
567
|
+
redactionContext.generatedImageData.add(result);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
237
571
|
for (const child of Object.values(value)) {
|
|
238
|
-
|
|
572
|
+
collectRedactionContext(child, redactionContext);
|
|
239
573
|
}
|
|
240
574
|
}
|
|
241
575
|
|
|
242
576
|
function redactValue(
|
|
243
577
|
value: unknown,
|
|
244
578
|
config: ResolvedLangfuseToolOutputTracingConfig,
|
|
245
|
-
redactionContext: RedactionContext
|
|
579
|
+
redactionContext: RedactionContext,
|
|
580
|
+
allowSerializedServerToolResult = false
|
|
246
581
|
): RedactionResult {
|
|
247
582
|
if (Array.isArray(value)) {
|
|
248
583
|
let changed = false;
|
|
249
584
|
const next: unknown[] = [];
|
|
250
585
|
for (const item of value) {
|
|
251
|
-
const result = redactValue(
|
|
586
|
+
const result = redactValue(
|
|
587
|
+
item,
|
|
588
|
+
config,
|
|
589
|
+
redactionContext,
|
|
590
|
+
allowSerializedServerToolResult
|
|
591
|
+
);
|
|
252
592
|
if (result.changed) {
|
|
253
593
|
changed = true;
|
|
254
594
|
}
|
|
@@ -261,6 +601,43 @@ function redactValue(
|
|
|
261
601
|
return { value, changed: false };
|
|
262
602
|
}
|
|
263
603
|
|
|
604
|
+
const allowSerializedMarker =
|
|
605
|
+
allowSerializedServerToolResult || hasAssistantMessageIdentity(value);
|
|
606
|
+
|
|
607
|
+
const replayOutput = redactResponsesReplayOutput(
|
|
608
|
+
value,
|
|
609
|
+
config,
|
|
610
|
+
redactionContext
|
|
611
|
+
);
|
|
612
|
+
if (replayOutput != null) {
|
|
613
|
+
return replayOutput;
|
|
614
|
+
}
|
|
615
|
+
const standardServerToolResult = redactStandardServerToolResult(
|
|
616
|
+
value,
|
|
617
|
+
config,
|
|
618
|
+
redactionContext
|
|
619
|
+
);
|
|
620
|
+
if (standardServerToolResult != null) {
|
|
621
|
+
return standardServerToolResult;
|
|
622
|
+
}
|
|
623
|
+
const markedServerToolResult = redactMarkedServerToolResult(
|
|
624
|
+
value,
|
|
625
|
+
config,
|
|
626
|
+
allowSerializedMarker
|
|
627
|
+
);
|
|
628
|
+
if (markedServerToolResult != null) {
|
|
629
|
+
return markedServerToolResult;
|
|
630
|
+
}
|
|
631
|
+
const generatedImageBlock = redactGeneratedImageBlock(
|
|
632
|
+
value,
|
|
633
|
+
config,
|
|
634
|
+
redactionContext,
|
|
635
|
+
allowSerializedMarker
|
|
636
|
+
);
|
|
637
|
+
if (generatedImageBlock != null) {
|
|
638
|
+
return generatedImageBlock;
|
|
639
|
+
}
|
|
640
|
+
|
|
264
641
|
const toolName = getSerializedToolName(value, redactionContext);
|
|
265
642
|
if (hasToolMessageIdentity(value) && shouldRedactTool(toolName, config)) {
|
|
266
643
|
return {
|
|
@@ -272,7 +649,12 @@ function redactValue(
|
|
|
272
649
|
let changed = false;
|
|
273
650
|
const next: Record<string, unknown> = {};
|
|
274
651
|
for (const [key, child] of Object.entries(value)) {
|
|
275
|
-
const result = redactValue(
|
|
652
|
+
const result = redactValue(
|
|
653
|
+
child,
|
|
654
|
+
config,
|
|
655
|
+
redactionContext,
|
|
656
|
+
allowSerializedMarker
|
|
657
|
+
);
|
|
276
658
|
if (result.changed) {
|
|
277
659
|
changed = true;
|
|
278
660
|
}
|
|
@@ -287,10 +669,12 @@ function redactSerializedValue(
|
|
|
287
669
|
config: ResolvedLangfuseToolOutputTracingConfig
|
|
288
670
|
): RedactionResult {
|
|
289
671
|
const redactionContext: RedactionContext = {
|
|
672
|
+
generatedImageData: new Set(),
|
|
673
|
+
generatedImageIds: new Set(),
|
|
290
674
|
toolNamesByCallId: new Map(),
|
|
291
675
|
};
|
|
292
676
|
if (typeof value !== 'string') {
|
|
293
|
-
|
|
677
|
+
collectRedactionContext(value, redactionContext);
|
|
294
678
|
return redactValue(value, config, redactionContext);
|
|
295
679
|
}
|
|
296
680
|
|
|
@@ -301,13 +685,18 @@ function redactSerializedValue(
|
|
|
301
685
|
|
|
302
686
|
try {
|
|
303
687
|
const parsed = JSON.parse(value) as unknown;
|
|
304
|
-
|
|
688
|
+
collectRedactionContext(parsed, redactionContext);
|
|
305
689
|
const result = redactValue(parsed, config, redactionContext);
|
|
306
690
|
return result.changed
|
|
307
691
|
? { value: JSON.stringify(result.value), changed: true }
|
|
308
692
|
: { value, changed: false };
|
|
309
693
|
} catch {
|
|
310
|
-
|
|
694
|
+
// OpenTelemetry truncates string attributes before span processors run.
|
|
695
|
+
// Langfuse serializes structured message values to JSON first, so a
|
|
696
|
+
// truncated attribute can still contain tool output even though it is no
|
|
697
|
+
// longer parseable. Fail closed for JSON-shaped attributes whenever tool
|
|
698
|
+
// output redaction is active instead of exporting a partial secret.
|
|
699
|
+
return { value: config.redactionText, changed: true };
|
|
311
700
|
}
|
|
312
701
|
}
|
|
313
702
|
|
|
@@ -397,6 +786,17 @@ export function redactLangfuseSpanToolOutputs(
|
|
|
397
786
|
}
|
|
398
787
|
}
|
|
399
788
|
|
|
789
|
+
export function prepareLangfuseSpanForExport(
|
|
790
|
+
span: ReadableSpan,
|
|
791
|
+
config?: ResolvedLangfuseToolOutputTracingConfig
|
|
792
|
+
): void {
|
|
793
|
+
classifyLangfuseToolNodeSpan(span);
|
|
794
|
+
if (config != null) {
|
|
795
|
+
redactLangfuseSpanToolOutputs(span, config);
|
|
796
|
+
}
|
|
797
|
+
shapeLangfuseSpan(span);
|
|
798
|
+
}
|
|
799
|
+
|
|
400
800
|
class ToolOutputRedactingLangfuseSpanProcessor implements SpanProcessor {
|
|
401
801
|
private readonly processor: LangfuseSpanProcessor;
|
|
402
802
|
private readonly fallbackConfig?: ResolvedLangfuseToolOutputTracingConfig;
|
|
@@ -430,12 +830,8 @@ class ToolOutputRedactingLangfuseSpanProcessor implements SpanProcessor {
|
|
|
430
830
|
if (shouldDropLangfuseSpan(span.name)) {
|
|
431
831
|
return;
|
|
432
832
|
}
|
|
433
|
-
classifyLangfuseToolNodeSpan(span);
|
|
434
|
-
shapeLangfuseSpan(span);
|
|
435
833
|
const config = this.spanConfigs.get(span) ?? this.fallbackConfig;
|
|
436
|
-
|
|
437
|
-
redactLangfuseSpanToolOutputs(span, config);
|
|
438
|
-
}
|
|
834
|
+
prepareLangfuseSpanForExport(span, config);
|
|
439
835
|
this.processor.onEnd(span);
|
|
440
836
|
}
|
|
441
837
|
|