@librechat/agents 3.1.78 → 3.1.79

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 (40) hide show
  1. package/dist/cjs/llm/anthropic/index.cjs +44 -55
  2. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  3. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +33 -21
  4. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  5. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +0 -4
  6. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  7. package/dist/cjs/messages/anthropicToolCache.cjs +48 -15
  8. package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -1
  9. package/dist/cjs/messages/format.cjs +97 -14
  10. package/dist/cjs/messages/format.cjs.map +1 -1
  11. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +14 -16
  12. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  13. package/dist/esm/llm/anthropic/index.mjs +43 -54
  14. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  15. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +33 -21
  16. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  17. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +0 -4
  18. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  19. package/dist/esm/messages/anthropicToolCache.mjs +48 -15
  20. package/dist/esm/messages/anthropicToolCache.mjs.map +1 -1
  21. package/dist/esm/messages/format.mjs +97 -14
  22. package/dist/esm/messages/format.mjs.map +1 -1
  23. package/dist/esm/tools/local/LocalExecutionEngine.mjs +14 -16
  24. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  25. package/dist/types/llm/anthropic/index.d.ts +1 -9
  26. package/dist/types/messages/anthropicToolCache.d.ts +5 -5
  27. package/package.json +1 -1
  28. package/src/llm/anthropic/index.ts +55 -64
  29. package/src/llm/anthropic/llm.spec.ts +585 -0
  30. package/src/llm/anthropic/utils/message_inputs.ts +36 -21
  31. package/src/llm/anthropic/utils/message_outputs.ts +0 -4
  32. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +95 -13
  33. package/src/messages/__tests__/anthropicToolCache.test.ts +46 -0
  34. package/src/messages/anthropicToolCache.ts +70 -25
  35. package/src/messages/format.ts +117 -18
  36. package/src/messages/formatAgentMessages.test.ts +202 -1
  37. package/src/specs/summarization.test.ts +3 -3
  38. package/src/tools/__tests__/LocalExecutionRoots.test.ts +8 -0
  39. package/src/tools/local/LocalExecutionEngine.ts +55 -54
  40. package/src/types/diff.d.ts +15 -0
@@ -31,16 +31,39 @@ import type {
31
31
  MessageContentComplex,
32
32
  } from '@langchain/core/messages';
33
33
  import { toLangChainContent } from '@/messages/langchain';
34
+ import { formatAgentMessages } from '@/messages/format';
35
+ import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
34
36
  import { _documentsInParams, CustomAnthropic as ChatAnthropic } from './index';
37
+ import { partitionAndMarkAnthropicToolCache } from '@/messages/anthropicToolCache';
38
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
39
+ import { ModelEndHandler, ToolEndHandler } from '@/events';
40
+ import { Run } from '@/run';
35
41
  import type { CustomAnthropicCallOptions } from './index';
36
42
  import type {
37
43
  AnthropicContextManagementConfigParam,
38
44
  AnthropicMessageCreateParams,
45
+ AnthropicMessageStreamEvent,
39
46
  AnthropicMessageResponse,
40
47
  AnthropicOutputConfig,
48
+ AnthropicRequestOptions,
49
+ AnthropicStreamingMessageCreateParams,
41
50
  AnthropicThinkingConfigParam,
42
51
  ChatAnthropicContentBlock,
43
52
  } from './types';
53
+ import type {
54
+ AnthropicClientOptions,
55
+ IState,
56
+ MessageContentComplex as LibreChatContentBlock,
57
+ MessageDeltaEvent,
58
+ ReasoningDeltaEvent,
59
+ RunConfig,
60
+ RunStep,
61
+ RunStepDeltaEvent,
62
+ SharedLLMConfig,
63
+ StreamEventData,
64
+ ToolEndEvent,
65
+ TPayload,
66
+ } from '@/types';
44
67
  import { _convertMessagesToAnthropicPayload } from './utils/message_inputs';
45
68
  import {
46
69
  _makeMessageChunkFromAnthropicEvent,
@@ -86,6 +109,8 @@ const remoteImageUrl =
86
109
 
87
110
  // Use this model for all other tests
88
111
  const modelName = 'claude-haiku-4-5-20251001';
112
+ const webSearchModelName =
113
+ process.env.ANTHROPIC_WEB_SEARCH_MODEL ?? 'claude-opus-4-7';
89
114
 
90
115
  type AnthropicThinkingResponseBlock = Anthropic.Messages.ThinkingBlock & {
91
116
  index?: number;
@@ -119,6 +144,12 @@ type CompactionContentBlock = ContentBlock & {
119
144
  content: string;
120
145
  };
121
146
 
147
+ type AnthropicContentBlockWithId = ContentBlock & {
148
+ id?: unknown;
149
+ input?: unknown;
150
+ name?: unknown;
151
+ };
152
+
122
153
  function getLangChainErrorCode(error: unknown): string | undefined {
123
154
  if (typeof error !== 'object' || error == null) {
124
155
  return undefined;
@@ -200,6 +231,184 @@ function isCompactionBlock(
200
231
  return typeof content === 'string';
201
232
  }
202
233
 
234
+ function isServerToolUseBlock(
235
+ block: ContentBlock
236
+ ): block is AnthropicContentBlockWithId {
237
+ return (
238
+ block.type === 'server_tool_use' &&
239
+ typeof (block as AnthropicContentBlockWithId).id === 'string' &&
240
+ ((block as AnthropicContentBlockWithId).id as string).startsWith(
241
+ Constants.ANTHROPIC_SERVER_TOOL_PREFIX
242
+ )
243
+ );
244
+ }
245
+
246
+ function expectAnthropicPayloadContentIsNonEmpty(
247
+ payload: AnthropicMessageCreateParams
248
+ ): void {
249
+ for (const message of payload.messages) {
250
+ if (typeof message.content === 'string') {
251
+ expect(message.content.trim().length).toBeGreaterThan(0);
252
+ continue;
253
+ }
254
+
255
+ expect(message.content.length).toBeGreaterThan(0);
256
+ for (const block of message.content) {
257
+ if (block.type !== 'text') {
258
+ continue;
259
+ }
260
+ expect(block.text.trim().length).toBeGreaterThan(0);
261
+ }
262
+ }
263
+ }
264
+
265
+ function expectNoDanglingServerToolUses(
266
+ payload: AnthropicMessageCreateParams
267
+ ): void {
268
+ for (const message of payload.messages) {
269
+ if (typeof message.content === 'string') {
270
+ continue;
271
+ }
272
+
273
+ const serverToolResultIds = new Set(
274
+ message.content
275
+ .map((block) =>
276
+ 'tool_use_id' in block &&
277
+ typeof block.tool_use_id === 'string' &&
278
+ block.tool_use_id.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX)
279
+ ? block.tool_use_id
280
+ : undefined
281
+ )
282
+ .filter((id): id is string => id != null)
283
+ );
284
+
285
+ for (const block of message.content) {
286
+ if (block.type !== 'server_tool_use') {
287
+ continue;
288
+ }
289
+ expect(serverToolResultIds.has(block.id)).toBe(true);
290
+ }
291
+ }
292
+ }
293
+
294
+ function getPromptCachedWebSearchTools(): Parameters<
295
+ ChatAnthropic['bindTools']
296
+ >[0] {
297
+ const tools = partitionAndMarkAnthropicToolCache(
298
+ [
299
+ {
300
+ type: 'web_search_20250305',
301
+ name: 'web_search',
302
+ max_uses: 3,
303
+ },
304
+ ] as never,
305
+ () => false
306
+ );
307
+ return tools as Parameters<ChatAnthropic['bindTools']>[0];
308
+ }
309
+
310
+ function getWebSearchTool(): {
311
+ type: 'web_search_20250305';
312
+ name: 'web_search';
313
+ max_uses: number;
314
+ } {
315
+ return {
316
+ type: 'web_search_20250305',
317
+ name: 'web_search',
318
+ max_uses: 3,
319
+ };
320
+ }
321
+
322
+ function getWebSearchLLMConfig(): AnthropicClientOptions & SharedLLMConfig {
323
+ return {
324
+ provider: Providers.ANTHROPIC,
325
+ model: webSearchModelName,
326
+ maxTokens: 1024,
327
+ promptCache: true,
328
+ streaming: true,
329
+ streamUsage: true,
330
+ thinking: { type: 'adaptive' },
331
+ } as AnthropicClientOptions & SharedLLMConfig;
332
+ }
333
+
334
+ async function createWebSearchRun({
335
+ runId,
336
+ customHandlers,
337
+ }: {
338
+ runId: string;
339
+ customHandlers?: RunConfig['customHandlers'];
340
+ }): Promise<Run<IState>> {
341
+ return await Run.create<IState>({
342
+ runId,
343
+ graphConfig: {
344
+ type: 'standard',
345
+ llmConfig: getWebSearchLLMConfig(),
346
+ tools: [getWebSearchTool()],
347
+ instructions:
348
+ 'You are a concise assistant. Use web search when current facts are needed.',
349
+ },
350
+ returnContent: true,
351
+ skipCleanup: true,
352
+ customHandlers,
353
+ });
354
+ }
355
+
356
+ function createLibreChatContentHandlers(): {
357
+ aggregateContent: ReturnType<
358
+ typeof createContentAggregator
359
+ >['aggregateContent'];
360
+ contentParts: Array<LibreChatContentBlock | undefined>;
361
+ customHandlers: NonNullable<RunConfig['customHandlers']>;
362
+ } {
363
+ const { contentParts, aggregateContent } = createContentAggregator();
364
+ const customHandlers = {
365
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
366
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
367
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
368
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
369
+ handle: (
370
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
371
+ data: StreamEventData
372
+ ): void => {
373
+ aggregateContent({
374
+ event,
375
+ data: data as unknown as { result: ToolEndEvent },
376
+ });
377
+ },
378
+ },
379
+ [GraphEvents.ON_RUN_STEP]: {
380
+ handle: (event: GraphEvents.ON_RUN_STEP, data: StreamEventData): void => {
381
+ aggregateContent({ event, data: data as RunStep });
382
+ },
383
+ },
384
+ [GraphEvents.ON_RUN_STEP_DELTA]: {
385
+ handle: (
386
+ event: GraphEvents.ON_RUN_STEP_DELTA,
387
+ data: StreamEventData
388
+ ): void => {
389
+ aggregateContent({ event, data: data as RunStepDeltaEvent });
390
+ },
391
+ },
392
+ [GraphEvents.ON_MESSAGE_DELTA]: {
393
+ handle: (
394
+ event: GraphEvents.ON_MESSAGE_DELTA,
395
+ data: StreamEventData
396
+ ): void => {
397
+ aggregateContent({ event, data: data as MessageDeltaEvent });
398
+ },
399
+ },
400
+ [GraphEvents.ON_REASONING_DELTA]: {
401
+ handle: (
402
+ event: GraphEvents.ON_REASONING_DELTA,
403
+ data: StreamEventData
404
+ ): void => {
405
+ aggregateContent({ event, data: data as ReasoningDeltaEvent });
406
+ },
407
+ },
408
+ };
409
+ return { aggregateContent, contentParts, customHandlers };
410
+ }
411
+
203
412
  test('Test ChatAnthropic', async () => {
204
413
  const chat = new ChatAnthropic({
205
414
  modelName,
@@ -691,6 +900,287 @@ test('Anthropic usage metadata includes cache input token buckets', () => {
691
900
  });
692
901
  });
693
902
 
903
+ type AnthropicStreamEvent = Anthropic.Beta.Messages.BetaRawMessageStreamEvent;
904
+
905
+ function createMockAnthropicStream(events: AnthropicStreamEvent[]) {
906
+ return {
907
+ controller: { abort: jest.fn() },
908
+ async *[Symbol.asyncIterator]() {
909
+ for (const event of events) {
910
+ yield event;
911
+ }
912
+ },
913
+ };
914
+ }
915
+
916
+ class MockStreamingAnthropic extends ChatAnthropic {
917
+ constructor(private readonly mockEvents: AnthropicStreamEvent[]) {
918
+ super({
919
+ modelName,
920
+ apiKey: 'test-key',
921
+ maxTokens: 10,
922
+ streamUsage: true,
923
+ });
924
+ }
925
+
926
+ protected override async createStreamWithRetry() {
927
+ return createMockAnthropicStream(this.mockEvents) as never;
928
+ }
929
+ }
930
+
931
+ class RecordingStreamingAnthropic extends ChatAnthropic {
932
+ messageStartOutputTokens = 0;
933
+ readonly messageDeltaOutputTokens: number[] = [];
934
+
935
+ protected override async createStreamWithRetry(
936
+ request: AnthropicStreamingMessageCreateParams,
937
+ options?: AnthropicRequestOptions
938
+ ) {
939
+ const stream = await super.createStreamWithRetry(request, options);
940
+ const recorder = this;
941
+
942
+ return {
943
+ controller: stream.controller,
944
+ async *[Symbol.asyncIterator](): AsyncGenerator<AnthropicMessageStreamEvent> {
945
+ for await (const event of stream) {
946
+ if (event.type === 'message_start') {
947
+ recorder.messageStartOutputTokens =
948
+ event.message.usage.output_tokens ??
949
+ recorder.messageStartOutputTokens;
950
+ } else if (event.type === 'message_delta') {
951
+ recorder.messageDeltaOutputTokens.push(event.usage.output_tokens);
952
+ }
953
+ yield event;
954
+ }
955
+ },
956
+ } as unknown as typeof stream;
957
+ }
958
+ }
959
+
960
+ test('Anthropic message_delta usage emits only output token totals', () => {
961
+ const event: AnthropicStreamEvent = {
962
+ type: 'message_delta',
963
+ context_management: null,
964
+ delta: {
965
+ container: null,
966
+ stop_details: null,
967
+ stop_reason: 'end_turn',
968
+ stop_sequence: null,
969
+ },
970
+ usage: {
971
+ input_tokens: 243,
972
+ output_tokens: 375,
973
+ cache_creation_input_tokens: 11,
974
+ cache_read_input_tokens: 13,
975
+ server_tool_use: null,
976
+ iterations: null,
977
+ },
978
+ };
979
+
980
+ const result = _makeMessageChunkFromAnthropicEvent(event, {
981
+ streamUsage: true,
982
+ coerceContentToString: true,
983
+ });
984
+
985
+ expect(result?.chunk.usage_metadata).toEqual({
986
+ input_tokens: 0,
987
+ output_tokens: 375,
988
+ total_tokens: 375,
989
+ });
990
+ });
991
+
992
+ test('Anthropic stream usage does not double-count cumulative input tokens', async () => {
993
+ const events: AnthropicStreamEvent[] = [
994
+ {
995
+ type: 'message_start',
996
+ message: {
997
+ id: 'msg_token_accounting',
998
+ container: null,
999
+ context_management: null,
1000
+ content: [],
1001
+ model: modelName,
1002
+ role: 'assistant',
1003
+ stop_details: null,
1004
+ stop_reason: null,
1005
+ stop_sequence: null,
1006
+ type: 'message',
1007
+ usage: {
1008
+ cache_creation: null,
1009
+ cache_creation_input_tokens: 0,
1010
+ cache_read_input_tokens: 0,
1011
+ inference_geo: null,
1012
+ input_tokens: 243,
1013
+ iterations: null,
1014
+ output_tokens: 0,
1015
+ server_tool_use: null,
1016
+ service_tier: null,
1017
+ speed: null,
1018
+ },
1019
+ },
1020
+ },
1021
+ {
1022
+ type: 'message_delta',
1023
+ context_management: null,
1024
+ delta: {
1025
+ container: null,
1026
+ stop_details: null,
1027
+ stop_reason: 'end_turn',
1028
+ stop_sequence: null,
1029
+ },
1030
+ usage: {
1031
+ input_tokens: 243,
1032
+ output_tokens: 375,
1033
+ cache_creation_input_tokens: 0,
1034
+ cache_read_input_tokens: 0,
1035
+ server_tool_use: null,
1036
+ iterations: null,
1037
+ },
1038
+ },
1039
+ { type: 'message_stop' },
1040
+ ];
1041
+ const model = new MockStreamingAnthropic(events);
1042
+
1043
+ let full: AIMessageChunk | undefined;
1044
+ for await (const chunk of await model.stream('hello')) {
1045
+ full = !full ? chunk : concat(full, chunk);
1046
+ }
1047
+
1048
+ expect(full?.usage_metadata).toEqual({
1049
+ input_tokens: 243,
1050
+ output_tokens: 375,
1051
+ total_tokens: 618,
1052
+ input_token_details: {
1053
+ cache_creation: 0,
1054
+ cache_read: 0,
1055
+ },
1056
+ output_token_details: {},
1057
+ });
1058
+ });
1059
+
1060
+ test('Anthropic stream usage handles multiple cumulative message_delta events', async () => {
1061
+ const events: AnthropicStreamEvent[] = [
1062
+ {
1063
+ type: 'message_start',
1064
+ message: {
1065
+ id: 'msg_token_accounting_multi_delta',
1066
+ container: null,
1067
+ context_management: null,
1068
+ content: [],
1069
+ model: modelName,
1070
+ role: 'assistant',
1071
+ stop_details: null,
1072
+ stop_reason: null,
1073
+ stop_sequence: null,
1074
+ type: 'message',
1075
+ usage: {
1076
+ cache_creation: null,
1077
+ cache_creation_input_tokens: 0,
1078
+ cache_read_input_tokens: 0,
1079
+ inference_geo: null,
1080
+ input_tokens: 243,
1081
+ iterations: null,
1082
+ output_tokens: 0,
1083
+ server_tool_use: null,
1084
+ service_tier: null,
1085
+ speed: null,
1086
+ },
1087
+ },
1088
+ },
1089
+ {
1090
+ type: 'message_delta',
1091
+ context_management: null,
1092
+ delta: {
1093
+ container: null,
1094
+ stop_details: null,
1095
+ stop_reason: null,
1096
+ stop_sequence: null,
1097
+ },
1098
+ usage: {
1099
+ input_tokens: 243,
1100
+ output_tokens: 100,
1101
+ cache_creation_input_tokens: 0,
1102
+ cache_read_input_tokens: 0,
1103
+ server_tool_use: null,
1104
+ iterations: null,
1105
+ },
1106
+ },
1107
+ {
1108
+ type: 'message_delta',
1109
+ context_management: null,
1110
+ delta: {
1111
+ container: null,
1112
+ stop_details: null,
1113
+ stop_reason: 'end_turn',
1114
+ stop_sequence: null,
1115
+ },
1116
+ usage: {
1117
+ input_tokens: 243,
1118
+ output_tokens: 375,
1119
+ cache_creation_input_tokens: 0,
1120
+ cache_read_input_tokens: 0,
1121
+ server_tool_use: null,
1122
+ iterations: null,
1123
+ },
1124
+ },
1125
+ { type: 'message_stop' },
1126
+ ];
1127
+ const model = new MockStreamingAnthropic(events);
1128
+
1129
+ let full: AIMessageChunk | undefined;
1130
+ for await (const chunk of await model.stream('hello')) {
1131
+ full = !full ? chunk : concat(full, chunk);
1132
+ }
1133
+
1134
+ expect(full?.usage_metadata).toEqual({
1135
+ input_tokens: 243,
1136
+ output_tokens: 375,
1137
+ total_tokens: 618,
1138
+ input_token_details: {
1139
+ cache_creation: 0,
1140
+ cache_read: 0,
1141
+ },
1142
+ output_token_details: {},
1143
+ });
1144
+ });
1145
+
1146
+ test('Anthropic live stream usage matches raw cumulative output snapshots', async () => {
1147
+ const model = new RecordingStreamingAnthropic({
1148
+ modelName,
1149
+ temperature: 0,
1150
+ maxTokens: 500,
1151
+ _lc_stream_delay: 0,
1152
+ });
1153
+
1154
+ let full: AIMessageChunk | undefined;
1155
+ const stream = await model.stream(
1156
+ 'Write exactly 18 numbered lines about reliable software telemetry. Each line should contain exactly seven words. Do not add an intro or outro.'
1157
+ );
1158
+ for await (const chunk of stream) {
1159
+ full = !full ? chunk : concat(full, chunk);
1160
+ }
1161
+
1162
+ expect(model.messageDeltaOutputTokens.length).toBeGreaterThan(0);
1163
+ const rawOutputTokens =
1164
+ model.messageDeltaOutputTokens[model.messageDeltaOutputTokens.length - 1];
1165
+ expect(full?.usage_metadata?.output_tokens).toBe(
1166
+ model.messageStartOutputTokens + rawOutputTokens
1167
+ );
1168
+ expect(full?.usage_metadata?.total_tokens).toBe(
1169
+ (full?.usage_metadata?.input_tokens ?? 0) +
1170
+ (full?.usage_metadata?.output_tokens ?? 0)
1171
+ );
1172
+
1173
+ if (model.messageDeltaOutputTokens.length > 1) {
1174
+ const summedOutputTokens = model.messageDeltaOutputTokens.reduce(
1175
+ (sum, tokens) => sum + tokens,
1176
+ 0
1177
+ );
1178
+ expect(full?.usage_metadata?.output_tokens).toBeLessThan(
1179
+ model.messageStartOutputTokens + summedOutputTokens
1180
+ );
1181
+ }
1182
+ });
1183
+
694
1184
  test('document detection ignores null content placeholders', () => {
695
1185
  const params: AnthropicMessageCreateParams = {
696
1186
  model: modelName,
@@ -1166,6 +1656,101 @@ test('human message caching', async () => {
1166
1656
  );
1167
1657
  });
1168
1658
 
1659
+ describe('Anthropic web search live regressions', () => {
1660
+ test('accepts prompt-cache markers on built-in web search tools', async () => {
1661
+ const model = new ChatAnthropic({
1662
+ model: webSearchModelName,
1663
+ maxTokens: 1024,
1664
+ thinking: { type: 'adaptive' },
1665
+ });
1666
+ const tools = getPromptCachedWebSearchTools();
1667
+ const formattedTools = model.formatStructuredToolToAnthropic(tools);
1668
+
1669
+ expect(formattedTools?.[0]).toMatchObject({
1670
+ type: 'web_search_20250305',
1671
+ name: 'web_search',
1672
+ cache_control: { type: 'ephemeral' },
1673
+ });
1674
+ expect(formattedTools?.[0]).not.toHaveProperty('extras');
1675
+
1676
+ const response = await model
1677
+ .bindTools(tools)
1678
+ .invoke([
1679
+ new HumanMessage(
1680
+ 'Use web search once and answer with only the word: ok'
1681
+ ),
1682
+ ]);
1683
+
1684
+ expect(response.content.length).toBeGreaterThan(0);
1685
+ });
1686
+
1687
+ test('replays LibreChat-persisted web search content across runs', async () => {
1688
+ const threadId = `web-search-e2e-${Date.now()}`;
1689
+ const firstPrompt =
1690
+ 'Use web search. Who is the lowest seed survived in 2026 NBA playoffs? Answer with only the team name.';
1691
+ const followUpPrompt = "Who are 76ers' opponents in current series?";
1692
+ const { contentParts: firstContentParts, customHandlers: firstHandlers } =
1693
+ createLibreChatContentHandlers();
1694
+ const firstRun = await createWebSearchRun({
1695
+ runId: `${threadId}-turn-1`,
1696
+ customHandlers: firstHandlers,
1697
+ });
1698
+ const runConfig = {
1699
+ configurable: { provider: Providers.ANTHROPIC, thread_id: threadId },
1700
+ streamMode: 'values',
1701
+ version: 'v2' as const,
1702
+ };
1703
+
1704
+ const firstRunContent = await firstRun.processStream(
1705
+ { messages: [new HumanMessage(firstPrompt)] },
1706
+ runConfig
1707
+ );
1708
+ const persistedAssistantContent = firstContentParts.filter(
1709
+ (part): part is LibreChatContentBlock => part != null
1710
+ );
1711
+ const hasPersistedServerToolCall = persistedAssistantContent.some(
1712
+ (part) =>
1713
+ part.type === ContentTypes.TOOL_CALL &&
1714
+ typeof part.tool_call?.id === 'string' &&
1715
+ part.tool_call.id.startsWith(Constants.ANTHROPIC_SERVER_TOOL_PREFIX)
1716
+ );
1717
+ const hasPersistedAnswerText = persistedAssistantContent.some(
1718
+ (part) =>
1719
+ part.type === ContentTypes.TEXT &&
1720
+ typeof part.text === 'string' &&
1721
+ part.text.trim().length > 0
1722
+ );
1723
+
1724
+ expect(firstRunContent).toBeDefined();
1725
+ expect(persistedAssistantContent.length).toBeGreaterThan(0);
1726
+ expect(hasPersistedServerToolCall).toBe(true);
1727
+ expect(hasPersistedAnswerText).toBe(true);
1728
+
1729
+ const persistedPayload: TPayload = [
1730
+ { role: 'user', content: firstPrompt },
1731
+ { role: 'assistant', content: persistedAssistantContent },
1732
+ { role: 'user', content: followUpPrompt },
1733
+ ];
1734
+ const { messages } = formatAgentMessages(
1735
+ persistedPayload,
1736
+ undefined,
1737
+ new Set(['web_search']),
1738
+ undefined,
1739
+ { provider: Providers.ANTHROPIC }
1740
+ );
1741
+ const anthropicPayload = _convertMessagesToAnthropicPayload(messages);
1742
+ const secondRun = await createWebSearchRun({
1743
+ runId: `${threadId}-turn-2`,
1744
+ });
1745
+
1746
+ expectAnthropicPayloadContentIsNonEmpty(anthropicPayload);
1747
+ expectNoDanglingServerToolUses(anthropicPayload);
1748
+ await expect(
1749
+ secondRun.processStream({ messages }, runConfig)
1750
+ ).resolves.toBeDefined();
1751
+ });
1752
+ });
1753
+
1169
1754
  test('Can accept PDF documents', async () => {
1170
1755
  const model = new ChatAnthropic({
1171
1756
  modelName: pdfModelName,