@librechat/agents 3.1.89 → 3.1.91

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.
Files changed (145) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +9 -5
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +53 -14
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/hooks/executeHooks.cjs +14 -7
  6. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  7. package/dist/cjs/langfuse.cjs +234 -0
  8. package/dist/cjs/langfuse.cjs.map +1 -0
  9. package/dist/cjs/llm/anthropic/index.cjs +8 -2
  10. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  11. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +34 -0
  12. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  13. package/dist/cjs/main.cjs +34 -0
  14. package/dist/cjs/main.cjs.map +1 -1
  15. package/dist/cjs/run.cjs +44 -27
  16. package/dist/cjs/run.cjs.map +1 -1
  17. package/dist/cjs/stream.cjs +10 -3
  18. package/dist/cjs/stream.cjs.map +1 -1
  19. package/dist/cjs/tools/BashExecutor.cjs +10 -9
  20. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  21. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +12 -8
  22. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  23. package/dist/cjs/tools/CodeExecutor.cjs +35 -11
  24. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  25. package/dist/cjs/tools/CodeSessionFileSummary.cjs +63 -0
  26. package/dist/cjs/tools/CodeSessionFileSummary.cjs.map +1 -0
  27. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +16 -12
  28. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  29. package/dist/cjs/tools/ToolNode.cjs +8 -5
  30. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  31. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs +380 -0
  32. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -0
  33. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +997 -0
  34. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -0
  35. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +575 -0
  36. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -0
  37. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs +165 -0
  38. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -0
  39. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +17 -5
  40. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  41. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +110 -6
  42. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
  43. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +319 -29
  44. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  45. package/dist/esm/agents/AgentContext.mjs +9 -5
  46. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  47. package/dist/esm/graphs/Graph.mjs +53 -14
  48. package/dist/esm/graphs/Graph.mjs.map +1 -1
  49. package/dist/esm/hooks/executeHooks.mjs +14 -7
  50. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  51. package/dist/esm/langfuse.mjs +226 -0
  52. package/dist/esm/langfuse.mjs.map +1 -0
  53. package/dist/esm/llm/anthropic/index.mjs +9 -3
  54. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  55. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +33 -1
  56. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  57. package/dist/esm/main.mjs +7 -2
  58. package/dist/esm/main.mjs.map +1 -1
  59. package/dist/esm/run.mjs +44 -27
  60. package/dist/esm/run.mjs.map +1 -1
  61. package/dist/esm/stream.mjs +10 -3
  62. package/dist/esm/stream.mjs.map +1 -1
  63. package/dist/esm/tools/BashExecutor.mjs +11 -10
  64. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  65. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +13 -9
  66. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  67. package/dist/esm/tools/CodeExecutor.mjs +29 -12
  68. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  69. package/dist/esm/tools/CodeSessionFileSummary.mjs +60 -0
  70. package/dist/esm/tools/CodeSessionFileSummary.mjs.map +1 -0
  71. package/dist/esm/tools/ProgrammaticToolCalling.mjs +17 -13
  72. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  73. package/dist/esm/tools/ToolNode.mjs +8 -5
  74. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  75. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs +378 -0
  76. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -0
  77. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +994 -0
  78. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -0
  79. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +566 -0
  80. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -0
  81. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs +155 -0
  82. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -0
  83. package/dist/esm/tools/local/LocalExecutionEngine.mjs +17 -6
  84. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  85. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +111 -7
  86. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
  87. package/dist/esm/tools/subagent/SubagentExecutor.mjs +320 -31
  88. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  89. package/dist/types/agents/AgentContext.d.ts +4 -1
  90. package/dist/types/graphs/Graph.d.ts +6 -5
  91. package/dist/types/index.d.ts +1 -0
  92. package/dist/types/langfuse.d.ts +48 -0
  93. package/dist/types/llm/anthropic/index.d.ts +3 -1
  94. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +4 -0
  95. package/dist/types/tools/BashExecutor.d.ts +3 -3
  96. package/dist/types/tools/CodeExecutor.d.ts +10 -3
  97. package/dist/types/tools/CodeSessionFileSummary.d.ts +3 -0
  98. package/dist/types/tools/ProgrammaticToolCalling.d.ts +4 -4
  99. package/dist/types/tools/cloudflare/CloudflareBridgeRuntime.d.ts +23 -0
  100. package/dist/types/tools/cloudflare/CloudflareProgrammaticToolCalling.d.ts +4 -0
  101. package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +21 -0
  102. package/dist/types/tools/cloudflare/CloudflareSandboxTools.d.ts +22 -0
  103. package/dist/types/tools/cloudflare/index.d.ts +4 -0
  104. package/dist/types/tools/local/LocalExecutionEngine.d.ts +1 -0
  105. package/dist/types/tools/subagent/SubagentExecutor.d.ts +8 -5
  106. package/dist/types/types/graph.d.ts +8 -0
  107. package/dist/types/types/tools.d.ts +120 -5
  108. package/package.json +4 -4
  109. package/src/__tests__/stream.eagerEventExecution.test.ts +66 -0
  110. package/src/agents/AgentContext.ts +13 -3
  111. package/src/graphs/Graph.ts +60 -16
  112. package/src/hooks/__tests__/executeHooks.test.ts +38 -0
  113. package/src/hooks/executeHooks.ts +27 -7
  114. package/src/index.ts +1 -0
  115. package/src/langfuse.ts +358 -0
  116. package/src/llm/anthropic/index.ts +27 -3
  117. package/src/llm/anthropic/llm.spec.ts +60 -1
  118. package/src/llm/anthropic/utils/message_inputs.ts +46 -0
  119. package/src/run.ts +60 -38
  120. package/src/specs/langfuse-config.test.ts +57 -0
  121. package/src/specs/langfuse-metadata.test.ts +19 -1
  122. package/src/stream.ts +13 -3
  123. package/src/tools/BashExecutor.ts +21 -10
  124. package/src/tools/BashProgrammaticToolCalling.ts +21 -9
  125. package/src/tools/CodeExecutor.ts +55 -12
  126. package/src/tools/CodeSessionFileSummary.ts +80 -0
  127. package/src/tools/ProgrammaticToolCalling.ts +25 -12
  128. package/src/tools/ToolNode.ts +8 -5
  129. package/src/tools/__tests__/BashExecutor.test.ts +9 -0
  130. package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +537 -0
  131. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +43 -0
  132. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +100 -16
  133. package/src/tools/__tests__/SubagentExecutor.test.ts +540 -6
  134. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +52 -0
  135. package/src/tools/__tests__/subagentHooks.test.ts +237 -0
  136. package/src/tools/cloudflare/CloudflareBridgeRuntime.ts +480 -0
  137. package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +1162 -0
  138. package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +744 -0
  139. package/src/tools/cloudflare/CloudflareSandboxTools.ts +225 -0
  140. package/src/tools/cloudflare/index.ts +4 -0
  141. package/src/tools/local/LocalExecutionEngine.ts +20 -4
  142. package/src/tools/local/resolveLocalExecutionTools.ts +169 -7
  143. package/src/tools/subagent/SubagentExecutor.ts +514 -36
  144. package/src/types/graph.ts +9 -0
  145. package/src/types/tools.ts +143 -5
@@ -0,0 +1,358 @@
1
+ import { CallbackHandler } from '@langfuse/langchain';
2
+ import { LangfuseSpanProcessor } from '@langfuse/otel';
3
+ import {
4
+ createObservationAttributes,
5
+ createTraceAttributes,
6
+ } from '@langfuse/tracing';
7
+ import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
8
+ import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
9
+ import { SpanStatusCode } from '@opentelemetry/api';
10
+ import type { Serialized } from '@langchain/core/load/serializable';
11
+ import type { BaseMessage } from '@langchain/core/messages';
12
+ import type { LLMResult } from '@langchain/core/outputs';
13
+ import type { Span } from '@opentelemetry/api';
14
+ import type * as t from '@/types';
15
+ import { isPresent } from '@/utils/misc';
16
+
17
+ type TraceMetadata = Record<string, unknown>;
18
+
19
+ type LangfuseHandlerParams = {
20
+ userId?: string;
21
+ sessionId?: string;
22
+ traceMetadata?: TraceMetadata;
23
+ };
24
+
25
+ type AgentLangfuseHandlerParams = LangfuseHandlerParams & {
26
+ langfuse?: t.LangfuseConfig;
27
+ };
28
+
29
+ type ResolvedLangfuseConfig = t.LangfuseConfig & {
30
+ enabled: true;
31
+ publicKey: string;
32
+ secretKey: string;
33
+ };
34
+
35
+ function getModelName(serialized: Serialized): string {
36
+ const serializedRecord = serialized as unknown as Record<string, unknown>;
37
+ const kwargs = serializedRecord.kwargs as Record<string, unknown> | undefined;
38
+ const modelName =
39
+ kwargs?.model ??
40
+ kwargs?.model_name ??
41
+ kwargs?.modelName ??
42
+ kwargs?.model_id ??
43
+ kwargs?.modelId ??
44
+ serializedRecord.name;
45
+
46
+ if (typeof modelName === 'string' && modelName.trim() !== '') {
47
+ return modelName;
48
+ }
49
+
50
+ if (Array.isArray(serializedRecord.id) && serializedRecord.id.length > 0) {
51
+ return String(serializedRecord.id[serializedRecord.id.length - 1]);
52
+ }
53
+
54
+ return 'ChatModel';
55
+ }
56
+
57
+ function getModelParameters(
58
+ extraParams?: Record<string, unknown>
59
+ ): Record<string, string | number> {
60
+ const invocationParams = extraParams?.invocation_params;
61
+ const params =
62
+ invocationParams != null && typeof invocationParams === 'object'
63
+ ? (invocationParams as Record<string, unknown>)
64
+ : (extraParams ?? {});
65
+
66
+ return Object.fromEntries(
67
+ Object.entries(params).filter(([, value]) => {
68
+ return typeof value === 'string' || typeof value === 'number';
69
+ })
70
+ ) as Record<string, string | number>;
71
+ }
72
+
73
+ function getOutput(output: LLMResult): unknown {
74
+ return output.generations.map((generation) =>
75
+ generation.map((item) => {
76
+ if ('message' in item && item.message != null) {
77
+ return (item.message as { content?: unknown }).content;
78
+ }
79
+ return item.text;
80
+ })
81
+ );
82
+ }
83
+
84
+ function getUsageDetails(
85
+ output: LLMResult
86
+ ): Record<string, number> | undefined {
87
+ const llmOutput = output.llmOutput as Record<string, unknown> | undefined;
88
+ const usage = llmOutput?.tokenUsage ?? llmOutput?.usage;
89
+ if (usage == null || typeof usage !== 'object') {
90
+ return undefined;
91
+ }
92
+
93
+ const usageEntries = Object.entries(usage as Record<string, unknown>).filter(
94
+ ([, value]) => typeof value === 'number'
95
+ );
96
+
97
+ return usageEntries.length > 0
98
+ ? (Object.fromEntries(usageEntries) as Record<string, number>)
99
+ : undefined;
100
+ }
101
+
102
+ function getTraceName(traceMetadata?: TraceMetadata): string {
103
+ const agentName = traceMetadata?.agentName;
104
+ return typeof agentName === 'string' && agentName.trim() !== ''
105
+ ? `LibreChat Agent: ${agentName}`
106
+ : 'LibreChat Agent';
107
+ }
108
+
109
+ export class LangfuseAgentCallbackHandler extends BaseCallbackHandler {
110
+ name = 'librechat_langfuse_agent_handler';
111
+
112
+ private readonly provider: BasicTracerProvider;
113
+ private readonly processor: LangfuseSpanProcessor;
114
+ private readonly userId?: string;
115
+ private readonly sessionId?: string;
116
+ private readonly traceMetadata?: TraceMetadata;
117
+ private readonly spans = new Map<string, Span>();
118
+
119
+ constructor({
120
+ langfuse,
121
+ userId,
122
+ sessionId,
123
+ traceMetadata,
124
+ }: LangfuseHandlerParams & { langfuse: ResolvedLangfuseConfig }) {
125
+ super();
126
+ this.userId = userId;
127
+ this.sessionId = sessionId;
128
+ this.traceMetadata = traceMetadata;
129
+ this.processor = new LangfuseSpanProcessor({
130
+ publicKey: langfuse.publicKey,
131
+ secretKey: langfuse.secretKey,
132
+ ...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),
133
+ environment:
134
+ process.env.LANGFUSE_TRACING_ENVIRONMENT ??
135
+ process.env.NODE_ENV ??
136
+ 'development',
137
+ exportMode: 'immediate',
138
+ });
139
+ this.provider = new BasicTracerProvider({
140
+ spanProcessors: [this.processor],
141
+ });
142
+ }
143
+
144
+ private startGenerationSpan({
145
+ llm,
146
+ input,
147
+ runId,
148
+ extraParams,
149
+ metadata,
150
+ name,
151
+ }: {
152
+ llm: Serialized;
153
+ input: unknown;
154
+ runId: string;
155
+ extraParams?: Record<string, unknown>;
156
+ metadata?: Record<string, unknown>;
157
+ name?: string;
158
+ }): void {
159
+ if (this.spans.has(runId)) {
160
+ return;
161
+ }
162
+
163
+ const tracer = this.provider.getTracer('librechat-agents-langfuse');
164
+ const spanName =
165
+ typeof name === 'string' && name.trim() !== '' ? name : getModelName(llm);
166
+ const span = tracer.startSpan(spanName, {
167
+ attributes: {
168
+ ...createTraceAttributes({
169
+ name: getTraceName(this.traceMetadata),
170
+ userId: this.userId,
171
+ sessionId: this.sessionId,
172
+ metadata: this.traceMetadata,
173
+ }),
174
+ ...createObservationAttributes('generation', {
175
+ input,
176
+ model: getModelName(llm),
177
+ modelParameters: getModelParameters(extraParams),
178
+ metadata: {
179
+ ...metadata,
180
+ ...this.traceMetadata,
181
+ },
182
+ }),
183
+ },
184
+ });
185
+ this.spans.set(runId, span);
186
+ }
187
+
188
+ async handleChatModelStart(
189
+ llm: Serialized,
190
+ messages: BaseMessage[][],
191
+ runId: string,
192
+ _parentRunId?: string,
193
+ extraParams?: Record<string, unknown>,
194
+ _tags?: string[],
195
+ metadata?: Record<string, unknown>,
196
+ name?: string
197
+ ): Promise<void> {
198
+ this.startGenerationSpan({
199
+ llm,
200
+ input: messages,
201
+ runId,
202
+ extraParams,
203
+ metadata,
204
+ name,
205
+ });
206
+ }
207
+
208
+ async handleLLMStart(
209
+ llm: Serialized,
210
+ prompts: string[],
211
+ runId: string,
212
+ _parentRunId?: string,
213
+ extraParams?: Record<string, unknown>,
214
+ _tags?: string[],
215
+ metadata?: Record<string, unknown>,
216
+ name?: string
217
+ ): Promise<void> {
218
+ this.startGenerationSpan({
219
+ llm,
220
+ input: prompts,
221
+ runId,
222
+ extraParams,
223
+ metadata,
224
+ name,
225
+ });
226
+ }
227
+
228
+ async handleLLMEnd(output: LLMResult, runId: string): Promise<void> {
229
+ const span = this.spans.get(runId);
230
+ if (!span) {
231
+ return;
232
+ }
233
+
234
+ span.setAttributes(
235
+ createObservationAttributes('generation', {
236
+ output: getOutput(output),
237
+ usageDetails: getUsageDetails(output),
238
+ })
239
+ );
240
+ span.end();
241
+ this.spans.delete(runId);
242
+ await this.flush();
243
+ }
244
+
245
+ async handleLLMError(err: unknown, runId: string): Promise<void> {
246
+ const span = this.spans.get(runId);
247
+ if (!span) {
248
+ return;
249
+ }
250
+
251
+ const message = err instanceof Error ? err.message : String(err);
252
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
253
+ span.setAttributes(
254
+ createObservationAttributes('generation', {
255
+ level: 'ERROR',
256
+ statusMessage: message,
257
+ })
258
+ );
259
+ span.end();
260
+ this.spans.delete(runId);
261
+ await this.flush();
262
+ }
263
+
264
+ private async flush(): Promise<void> {
265
+ try {
266
+ await this.provider.forceFlush();
267
+ } catch (error) {
268
+ process.emitWarning(
269
+ `[LangfuseAgentCallbackHandler] Failed to flush Langfuse spans: ${
270
+ error instanceof Error ? error.message : String(error)
271
+ }`
272
+ );
273
+ }
274
+ }
275
+
276
+ async dispose(): Promise<void> {
277
+ for (const span of this.spans.values()) {
278
+ span.end();
279
+ }
280
+ this.spans.clear();
281
+ await this.flush();
282
+ try {
283
+ await this.provider.shutdown();
284
+ } catch (error) {
285
+ process.emitWarning(
286
+ `[LangfuseAgentCallbackHandler] Failed to shut down Langfuse provider: ${
287
+ error instanceof Error ? error.message : String(error)
288
+ }`
289
+ );
290
+ }
291
+ }
292
+ }
293
+
294
+ function hasRequiredLangfuseConfig(
295
+ langfuse?: t.LangfuseConfig
296
+ ): langfuse is ResolvedLangfuseConfig {
297
+ return (
298
+ langfuse?.enabled === true &&
299
+ isPresent(langfuse.publicKey) &&
300
+ isPresent(langfuse.secretKey)
301
+ );
302
+ }
303
+
304
+ export function createLegacyLangfuseHandler(
305
+ params: LangfuseHandlerParams
306
+ ): CallbackHandler {
307
+ return new CallbackHandler(params);
308
+ }
309
+
310
+ export function createLangfuseHandler({
311
+ langfuse,
312
+ userId,
313
+ sessionId,
314
+ traceMetadata,
315
+ }: AgentLangfuseHandlerParams): LangfuseAgentCallbackHandler | undefined {
316
+ if (!hasRequiredLangfuseConfig(langfuse)) {
317
+ return undefined;
318
+ }
319
+
320
+ return new LangfuseAgentCallbackHandler({
321
+ langfuse,
322
+ userId,
323
+ sessionId,
324
+ traceMetadata,
325
+ });
326
+ }
327
+
328
+ export function hasExplicitLangfuseConfig(
329
+ contexts: Iterable<{ langfuse?: t.LangfuseConfig }>
330
+ ): boolean {
331
+ for (const context of contexts) {
332
+ if (context.langfuse != null) {
333
+ return true;
334
+ }
335
+ }
336
+ return false;
337
+ }
338
+
339
+ export function hasLangfuseEnvConfig(): boolean {
340
+ return (
341
+ isPresent(process.env.LANGFUSE_SECRET_KEY) &&
342
+ isPresent(process.env.LANGFUSE_PUBLIC_KEY) &&
343
+ isPresent(process.env.LANGFUSE_BASE_URL)
344
+ );
345
+ }
346
+
347
+ export function isLangfuseCallbackHandler(value: unknown): boolean {
348
+ return (
349
+ value instanceof CallbackHandler ||
350
+ value instanceof LangfuseAgentCallbackHandler
351
+ );
352
+ }
353
+
354
+ export async function disposeLangfuseHandler(value: unknown): Promise<void> {
355
+ if (value instanceof LangfuseAgentCallbackHandler) {
356
+ await value.dispose();
357
+ }
358
+ }
@@ -17,9 +17,13 @@ import type {
17
17
  ChatAnthropicToolType,
18
18
  AnthropicMCPServerURLDefinition,
19
19
  AnthropicContextManagementConfigParam,
20
+ AnthropicRequestOptions,
20
21
  } from '@/llm/anthropic/types';
21
22
  import { _makeMessageChunkFromAnthropicEvent } from './utils/message_outputs';
22
- import { _convertMessagesToAnthropicPayload } from './utils/message_inputs';
23
+ import {
24
+ _convertMessagesToAnthropicPayload,
25
+ stripUnsupportedAssistantPrefill,
26
+ } from './utils/message_inputs';
23
27
  import { handleToolChoice } from './utils/tools';
24
28
 
25
29
  const DEFAULT_STREAM_DELAY = 25;
@@ -591,6 +595,26 @@ export class CustomAnthropic extends ChatAnthropicMessages {
591
595
  });
592
596
  }
593
597
 
598
+ protected override async createStreamWithRetry(
599
+ request: AnthropicStreamingMessageCreateParams,
600
+ options?: AnthropicRequestOptions
601
+ ): ReturnType<ChatAnthropicMessages['createStreamWithRetry']> {
602
+ return super.createStreamWithRetry(
603
+ stripUnsupportedAssistantPrefill(request),
604
+ options
605
+ );
606
+ }
607
+
608
+ protected override async completionWithRetry(
609
+ request: AnthropicMessageCreateParams,
610
+ options: AnthropicRequestOptions
611
+ ): ReturnType<ChatAnthropicMessages['completionWithRetry']> {
612
+ return super.completionWithRetry(
613
+ stripUnsupportedAssistantPrefill(request),
614
+ options
615
+ );
616
+ }
617
+
594
618
  async *_streamResponseChunks(
595
619
  messages: BaseMessage[],
596
620
  options: this['ParsedCallOptions'],
@@ -599,11 +623,11 @@ export class CustomAnthropic extends ChatAnthropicMessages {
599
623
  this.resetTokenEvents();
600
624
  const params = this.invocationParams(options);
601
625
  const formattedMessages = _convertMessagesToAnthropicPayload(messages);
602
- const payload = {
626
+ const payload = stripUnsupportedAssistantPrefill({
603
627
  ...params,
604
628
  ...formattedMessages,
605
629
  stream: true,
606
- } as const;
630
+ } as const);
607
631
  const coerceContentToString =
608
632
  !_toolsInParams(payload) &&
609
633
  !_documentsInParams(payload) &&
@@ -64,7 +64,11 @@ import type {
64
64
  ToolEndEvent,
65
65
  TPayload,
66
66
  } from '@/types';
67
- import { _convertMessagesToAnthropicPayload } from './utils/message_inputs';
67
+ import {
68
+ _convertMessagesToAnthropicPayload,
69
+ modelDisallowsAssistantPrefill,
70
+ stripUnsupportedAssistantPrefill,
71
+ } from './utils/message_inputs';
68
72
  import {
69
73
  _makeMessageChunkFromAnthropicEvent,
70
74
  getAnthropicUsageMetadata,
@@ -2637,6 +2641,61 @@ describe('Anthropic Reasoning with contentBlocks', () => {
2637
2641
  });
2638
2642
  });
2639
2643
 
2644
+ describe('Claude assistant prefill compatibility', () => {
2645
+ test.each([
2646
+ 'claude-sonnet-4-6',
2647
+ 'claude-sonnet-4-6@20260217',
2648
+ 'claude-opus-4-7',
2649
+ 'claude-opus-4-10',
2650
+ 'global.anthropic.claude-opus-4-6-v1:0',
2651
+ 'anthropic/claude-sonnet-4.6',
2652
+ 'anthropic/claude-sonnet-4.12',
2653
+ ])('detects %s as not supporting assistant prefill', (model) => {
2654
+ expect(modelDisallowsAssistantPrefill(model)).toBe(true);
2655
+ });
2656
+
2657
+ test.each([
2658
+ 'claude-sonnet-4-5-20250929',
2659
+ 'claude-opus-4-20250514',
2660
+ 'anthropic.claude-opus-4-20250514-v1:0',
2661
+ 'gpt-5.4',
2662
+ ])('leaves %s prefill support unchanged', (model) => {
2663
+ expect(modelDisallowsAssistantPrefill(model)).toBe(false);
2664
+ });
2665
+
2666
+ test('strips trailing assistant messages for Claude 4.6+ requests', () => {
2667
+ const request = {
2668
+ model: 'claude-opus-4-6',
2669
+ max_tokens: 100,
2670
+ messages: [
2671
+ { role: 'user' as const, content: 'What changed?' },
2672
+ { role: 'assistant' as const, content: 'Draft prefill' },
2673
+ { role: 'assistant' as const, content: 'Another prefill' },
2674
+ ],
2675
+ };
2676
+
2677
+ const sanitized = stripUnsupportedAssistantPrefill(request);
2678
+
2679
+ expect(sanitized).not.toBe(request);
2680
+ expect(sanitized.messages).toEqual([
2681
+ { role: 'user', content: 'What changed?' },
2682
+ ]);
2683
+ });
2684
+
2685
+ test('does not strip assistant messages for older Claude models', () => {
2686
+ const request = {
2687
+ model: 'claude-sonnet-4-5-20250929',
2688
+ max_tokens: 100,
2689
+ messages: [
2690
+ { role: 'user' as const, content: 'Write JSON only.' },
2691
+ { role: 'assistant' as const, content: '{' },
2692
+ ],
2693
+ };
2694
+
2695
+ expect(stripUnsupportedAssistantPrefill(request)).toBe(request);
2696
+ });
2697
+ });
2698
+
2640
2699
  const opus46Model = 'claude-opus-4-6';
2641
2700
 
2642
2701
  describe('Opus 4.6', () => {
@@ -49,6 +49,10 @@ type GoogleFunctionCallBlock = MessageContentComplex & {
49
49
  };
50
50
 
51
51
  const ANTHROPIC_EMPTY_TEXT_PLACEHOLDER = '_';
52
+ const CLAUDE_4_RELEASE_DATE_MODEL_PATTERN =
53
+ /claude-(?:opus|sonnet|haiku)-4-\d{8}(?:[-.@]|$)/i;
54
+ const CLAUDE_4_MINOR_MODEL_PATTERN =
55
+ /claude-(?:opus|sonnet|haiku)-4[-.](\d+)(?:[-.@]|$)/i;
52
56
 
53
57
  function _formatImage(imageUrl: string) {
54
58
  const parsed = parseBase64DataUrl({ dataUrl: imageUrl });
@@ -796,6 +800,48 @@ export function _convertMessagesToAnthropicPayload(
796
800
  } as AnthropicMessageCreateParams;
797
801
  }
798
802
 
803
+ export function modelDisallowsAssistantPrefill(model?: string): boolean {
804
+ const modelId = model ?? '';
805
+ if (CLAUDE_4_RELEASE_DATE_MODEL_PATTERN.test(modelId)) {
806
+ return false;
807
+ }
808
+
809
+ const match = CLAUDE_4_MINOR_MODEL_PATTERN.exec(modelId);
810
+ if (!match) {
811
+ return false;
812
+ }
813
+ return Number(match[1]) >= 6;
814
+ }
815
+
816
+ export function stripUnsupportedAssistantPrefill<
817
+ T extends Pick<AnthropicMessageCreateParams, 'messages'> & { model?: string },
818
+ >(request: T): T {
819
+ if (!modelDisallowsAssistantPrefill(request.model)) {
820
+ return request;
821
+ }
822
+
823
+ const messages = request.messages;
824
+ if (
825
+ messages.length <= 1 ||
826
+ messages[messages.length - 1]?.role !== 'assistant'
827
+ ) {
828
+ return request;
829
+ }
830
+
831
+ const nextMessages = [...messages];
832
+ while (
833
+ nextMessages.length > 1 &&
834
+ nextMessages[nextMessages.length - 1]?.role === 'assistant'
835
+ ) {
836
+ nextMessages.pop();
837
+ }
838
+
839
+ return {
840
+ ...request,
841
+ messages: nextMessages,
842
+ };
843
+ }
844
+
799
845
  function mergeMessages(messages: AnthropicMessageCreateParams['messages']) {
800
846
  if (messages.length <= 1) {
801
847
  return messages;
package/src/run.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  // src/run.ts
2
2
  import './instrumentation';
3
- import { CallbackHandler } from '@langfuse/langchain';
4
3
  import { PromptTemplate } from '@langchain/core/prompts';
5
4
  import { RunnableLambda } from '@langchain/core/runnables';
6
5
  import { AzureChatOpenAI, ChatOpenAI } from '@langchain/openai';
@@ -31,7 +30,14 @@ import { initializeModel } from '@/llm/init';
31
30
  import { HandlerRegistry } from '@/events';
32
31
  import { executeHooks } from '@/hooks';
33
32
  import { isOpenAILike } from '@/utils/llm';
34
- import { isPresent } from '@/utils/misc';
33
+ import {
34
+ createLegacyLangfuseHandler,
35
+ createLangfuseHandler,
36
+ disposeLangfuseHandler,
37
+ hasExplicitLangfuseConfig,
38
+ hasLangfuseEnvConfig,
39
+ isLangfuseCallbackHandler,
40
+ } from '@/langfuse';
35
41
  import type { HookRegistry } from '@/hooks';
36
42
 
37
43
  export const defaultOmitOptions = new Set([
@@ -607,9 +613,8 @@ export class Run<_T extends t.BaseGraphState> {
607
613
  .concat(customHandler);
608
614
 
609
615
  if (
610
- isPresent(process.env.LANGFUSE_SECRET_KEY) &&
611
- isPresent(process.env.LANGFUSE_PUBLIC_KEY) &&
612
- isPresent(process.env.LANGFUSE_BASE_URL)
616
+ hasLangfuseEnvConfig() &&
617
+ !hasExplicitLangfuseConfig(this.Graph.agentContexts.values())
613
618
  ) {
614
619
  const userId = config.configurable?.user_id;
615
620
  const sessionId = config.configurable?.thread_id;
@@ -621,7 +626,7 @@ export class Run<_T extends t.BaseGraphState> {
621
626
  parentMessageId: config.configurable?.requestBody?.parentMessageId,
622
627
  agentName: primaryContext?.name,
623
628
  };
624
- const handler = new CallbackHandler({
629
+ const handler = createLegacyLangfuseHandler({
625
630
  userId,
626
631
  sessionId,
627
632
  traceMetadata,
@@ -1134,12 +1139,8 @@ export class Run<_T extends t.BaseGraphState> {
1134
1139
  titleMethod = TitleMethod.COMPLETION,
1135
1140
  titlePromptTemplate,
1136
1141
  }: t.RunTitleOptions): Promise<{ language?: string; title?: string }> {
1137
- if (
1138
- chainOptions != null &&
1139
- isPresent(process.env.LANGFUSE_SECRET_KEY) &&
1140
- isPresent(process.env.LANGFUSE_PUBLIC_KEY) &&
1141
- isPresent(process.env.LANGFUSE_BASE_URL)
1142
- ) {
1142
+ let titleLangfuseHandler: unknown;
1143
+ if (chainOptions != null) {
1143
1144
  const userId = chainOptions.configurable?.user_id;
1144
1145
  const sessionId = chainOptions.configurable?.thread_id;
1145
1146
  const titleContext = this.Graph?.agentContexts.get(
@@ -1149,14 +1150,31 @@ export class Run<_T extends t.BaseGraphState> {
1149
1150
  messageId: 'title-' + this.id,
1150
1151
  agentName: titleContext?.name,
1151
1152
  };
1152
- const handler = new CallbackHandler({
1153
- userId,
1154
- sessionId,
1155
- traceMetadata,
1156
- });
1157
- chainOptions.callbacks = (
1158
- (chainOptions.callbacks as t.ProvidedCallbacks) ?? []
1159
- ).concat([handler]);
1153
+ const hasExplicitLangfuse =
1154
+ this.Graph != null &&
1155
+ hasExplicitLangfuseConfig(this.Graph.agentContexts.values());
1156
+ if (titleContext?.langfuse != null) {
1157
+ titleLangfuseHandler = createLangfuseHandler({
1158
+ langfuse: titleContext.langfuse,
1159
+ userId,
1160
+ sessionId,
1161
+ traceMetadata,
1162
+ });
1163
+ } else if (hasLangfuseEnvConfig() && !hasExplicitLangfuse) {
1164
+ titleLangfuseHandler = createLegacyLangfuseHandler({
1165
+ userId,
1166
+ sessionId,
1167
+ traceMetadata,
1168
+ });
1169
+ }
1170
+
1171
+ if (titleLangfuseHandler != null) {
1172
+ chainOptions.callbacks = (
1173
+ (chainOptions.callbacks as t.ProvidedCallbacks) ?? []
1174
+ ).concat([
1175
+ titleLangfuseHandler as NonNullable<t.ProvidedCallbacks>[number],
1176
+ ]);
1177
+ }
1160
1178
  }
1161
1179
 
1162
1180
  const convoTemplate = PromptTemplate.fromTemplate(
@@ -1221,24 +1239,28 @@ export class Run<_T extends t.BaseGraphState> {
1221
1239
  });
1222
1240
 
1223
1241
  try {
1224
- return await fullChain.invoke(
1225
- { input: inputText, output: response },
1226
- invokeConfig
1227
- );
1228
- } catch (_e) {
1229
- // Fallback: strip callbacks to avoid EventStream tracer errors in certain environments
1230
- // But preserve langfuse handler if it exists
1231
- const langfuseHandler = (
1232
- invokeConfig.callbacks as t.ProvidedCallbacks
1233
- )?.find((cb) => cb instanceof CallbackHandler);
1234
- const { callbacks: _cb, ...rest } = invokeConfig;
1235
- const safeConfig = Object.assign({}, rest, {
1236
- callbacks: langfuseHandler ? [langfuseHandler] : [],
1237
- });
1238
- return await fullChain.invoke(
1239
- { input: inputText, output: response },
1240
- safeConfig as Partial<RunnableConfig>
1241
- );
1242
+ try {
1243
+ return await fullChain.invoke(
1244
+ { input: inputText, output: response },
1245
+ invokeConfig
1246
+ );
1247
+ } catch (_e) {
1248
+ // Fallback: strip callbacks to avoid EventStream tracer errors in certain environments
1249
+ // but preserve Langfuse tracing if it exists.
1250
+ const langfuseHandler = (
1251
+ invokeConfig.callbacks as t.ProvidedCallbacks
1252
+ )?.find(isLangfuseCallbackHandler);
1253
+ const { callbacks: _cb, ...rest } = invokeConfig;
1254
+ const safeConfig = Object.assign({}, rest, {
1255
+ callbacks: langfuseHandler ? [langfuseHandler] : [],
1256
+ });
1257
+ return await fullChain.invoke(
1258
+ { input: inputText, output: response },
1259
+ safeConfig as Partial<RunnableConfig>
1260
+ );
1261
+ }
1262
+ } finally {
1263
+ await disposeLangfuseHandler(titleLangfuseHandler);
1242
1264
  }
1243
1265
  }
1244
1266
  }