@librechat/agents 3.0.0-rc9 → 3.0.1

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 (195) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +6 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +23 -2
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs +5 -5
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/instrumentation.cjs +21 -0
  8. package/dist/cjs/instrumentation.cjs.map +1 -0
  9. package/dist/cjs/llm/anthropic/index.cjs +21 -2
  10. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  11. package/dist/cjs/llm/google/index.cjs +3 -0
  12. package/dist/cjs/llm/google/index.cjs.map +1 -1
  13. package/dist/cjs/llm/google/utils/common.cjs +13 -0
  14. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  15. package/dist/cjs/llm/ollama/index.cjs +3 -0
  16. package/dist/cjs/llm/ollama/index.cjs.map +1 -1
  17. package/dist/cjs/llm/openai/index.cjs +18 -3
  18. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  19. package/dist/cjs/llm/openai/utils/index.cjs +6 -1
  20. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  21. package/dist/cjs/llm/openrouter/index.cjs +5 -1
  22. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  23. package/dist/cjs/llm/vertexai/index.cjs +1 -1
  24. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  25. package/dist/cjs/main.cjs +8 -2
  26. package/dist/cjs/main.cjs.map +1 -1
  27. package/dist/cjs/messages/cache.cjs +49 -0
  28. package/dist/cjs/messages/cache.cjs.map +1 -0
  29. package/dist/cjs/messages/content.cjs +53 -0
  30. package/dist/cjs/messages/content.cjs.map +1 -0
  31. package/dist/cjs/messages/core.cjs +5 -1
  32. package/dist/cjs/messages/core.cjs.map +1 -1
  33. package/dist/cjs/messages/format.cjs +50 -59
  34. package/dist/cjs/messages/format.cjs.map +1 -1
  35. package/dist/cjs/messages/prune.cjs +28 -0
  36. package/dist/cjs/messages/prune.cjs.map +1 -1
  37. package/dist/cjs/run.cjs +57 -5
  38. package/dist/cjs/run.cjs.map +1 -1
  39. package/dist/cjs/stream.cjs +7 -0
  40. package/dist/cjs/stream.cjs.map +1 -1
  41. package/dist/cjs/tools/ToolNode.cjs +2 -0
  42. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  43. package/dist/cjs/tools/search/firecrawl.cjs +3 -1
  44. package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
  45. package/dist/cjs/tools/search/rerankers.cjs +8 -6
  46. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  47. package/dist/cjs/tools/search/search.cjs +5 -5
  48. package/dist/cjs/tools/search/search.cjs.map +1 -1
  49. package/dist/cjs/tools/search/serper-scraper.cjs +132 -0
  50. package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -0
  51. package/dist/cjs/tools/search/tool.cjs +46 -9
  52. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  53. package/dist/cjs/utils/handlers.cjs +70 -0
  54. package/dist/cjs/utils/handlers.cjs.map +1 -0
  55. package/dist/cjs/utils/misc.cjs +8 -1
  56. package/dist/cjs/utils/misc.cjs.map +1 -1
  57. package/dist/cjs/utils/title.cjs +54 -25
  58. package/dist/cjs/utils/title.cjs.map +1 -1
  59. package/dist/esm/agents/AgentContext.mjs +6 -2
  60. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  61. package/dist/esm/graphs/Graph.mjs +23 -2
  62. package/dist/esm/graphs/Graph.mjs.map +1 -1
  63. package/dist/esm/graphs/MultiAgentGraph.mjs +5 -5
  64. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  65. package/dist/esm/instrumentation.mjs +19 -0
  66. package/dist/esm/instrumentation.mjs.map +1 -0
  67. package/dist/esm/llm/anthropic/index.mjs +21 -2
  68. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  69. package/dist/esm/llm/google/index.mjs +3 -0
  70. package/dist/esm/llm/google/index.mjs.map +1 -1
  71. package/dist/esm/llm/google/utils/common.mjs +13 -0
  72. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  73. package/dist/esm/llm/ollama/index.mjs +3 -0
  74. package/dist/esm/llm/ollama/index.mjs.map +1 -1
  75. package/dist/esm/llm/openai/index.mjs +18 -3
  76. package/dist/esm/llm/openai/index.mjs.map +1 -1
  77. package/dist/esm/llm/openai/utils/index.mjs +6 -1
  78. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  79. package/dist/esm/llm/openrouter/index.mjs +5 -1
  80. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  81. package/dist/esm/llm/vertexai/index.mjs +1 -1
  82. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  83. package/dist/esm/main.mjs +5 -2
  84. package/dist/esm/main.mjs.map +1 -1
  85. package/dist/esm/messages/cache.mjs +47 -0
  86. package/dist/esm/messages/cache.mjs.map +1 -0
  87. package/dist/esm/messages/content.mjs +51 -0
  88. package/dist/esm/messages/content.mjs.map +1 -0
  89. package/dist/esm/messages/core.mjs +5 -1
  90. package/dist/esm/messages/core.mjs.map +1 -1
  91. package/dist/esm/messages/format.mjs +50 -58
  92. package/dist/esm/messages/format.mjs.map +1 -1
  93. package/dist/esm/messages/prune.mjs +28 -0
  94. package/dist/esm/messages/prune.mjs.map +1 -1
  95. package/dist/esm/run.mjs +57 -5
  96. package/dist/esm/run.mjs.map +1 -1
  97. package/dist/esm/stream.mjs +7 -0
  98. package/dist/esm/stream.mjs.map +1 -1
  99. package/dist/esm/tools/ToolNode.mjs +2 -0
  100. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  101. package/dist/esm/tools/search/firecrawl.mjs +3 -1
  102. package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
  103. package/dist/esm/tools/search/rerankers.mjs +8 -6
  104. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  105. package/dist/esm/tools/search/search.mjs +5 -5
  106. package/dist/esm/tools/search/search.mjs.map +1 -1
  107. package/dist/esm/tools/search/serper-scraper.mjs +129 -0
  108. package/dist/esm/tools/search/serper-scraper.mjs.map +1 -0
  109. package/dist/esm/tools/search/tool.mjs +46 -9
  110. package/dist/esm/tools/search/tool.mjs.map +1 -1
  111. package/dist/esm/utils/handlers.mjs +68 -0
  112. package/dist/esm/utils/handlers.mjs.map +1 -0
  113. package/dist/esm/utils/misc.mjs +8 -2
  114. package/dist/esm/utils/misc.mjs.map +1 -1
  115. package/dist/esm/utils/title.mjs +54 -25
  116. package/dist/esm/utils/title.mjs.map +1 -1
  117. package/dist/types/agents/AgentContext.d.ts +4 -1
  118. package/dist/types/instrumentation.d.ts +1 -0
  119. package/dist/types/llm/anthropic/index.d.ts +3 -0
  120. package/dist/types/llm/google/index.d.ts +1 -0
  121. package/dist/types/llm/ollama/index.d.ts +1 -0
  122. package/dist/types/llm/openai/index.d.ts +4 -0
  123. package/dist/types/llm/openrouter/index.d.ts +4 -2
  124. package/dist/types/llm/vertexai/index.d.ts +1 -1
  125. package/dist/types/messages/cache.d.ts +8 -0
  126. package/dist/types/messages/content.d.ts +7 -0
  127. package/dist/types/messages/format.d.ts +22 -25
  128. package/dist/types/messages/index.d.ts +2 -0
  129. package/dist/types/run.d.ts +2 -1
  130. package/dist/types/tools/search/firecrawl.d.ts +2 -1
  131. package/dist/types/tools/search/rerankers.d.ts +4 -1
  132. package/dist/types/tools/search/search.d.ts +1 -2
  133. package/dist/types/tools/search/serper-scraper.d.ts +59 -0
  134. package/dist/types/tools/search/tool.d.ts +25 -4
  135. package/dist/types/tools/search/types.d.ts +31 -1
  136. package/dist/types/types/graph.d.ts +3 -1
  137. package/dist/types/types/messages.d.ts +4 -0
  138. package/dist/types/utils/handlers.d.ts +34 -0
  139. package/dist/types/utils/index.d.ts +1 -0
  140. package/dist/types/utils/misc.d.ts +1 -0
  141. package/package.json +7 -3
  142. package/src/agents/AgentContext.ts +8 -0
  143. package/src/graphs/Graph.ts +31 -2
  144. package/src/graphs/MultiAgentGraph.ts +5 -5
  145. package/src/instrumentation.ts +22 -0
  146. package/src/llm/anthropic/index.ts +23 -2
  147. package/src/llm/anthropic/llm.spec.ts +1 -1
  148. package/src/llm/google/index.ts +4 -0
  149. package/src/llm/google/utils/common.ts +14 -0
  150. package/src/llm/ollama/index.ts +3 -0
  151. package/src/llm/openai/index.ts +17 -4
  152. package/src/llm/openai/utils/index.ts +7 -1
  153. package/src/llm/openrouter/index.ts +15 -6
  154. package/src/llm/vertexai/index.ts +2 -2
  155. package/src/messages/cache.test.ts +262 -0
  156. package/src/messages/cache.ts +56 -0
  157. package/src/messages/content.test.ts +362 -0
  158. package/src/messages/content.ts +63 -0
  159. package/src/messages/core.ts +5 -2
  160. package/src/messages/format.ts +65 -71
  161. package/src/messages/formatMessage.test.ts +418 -2
  162. package/src/messages/index.ts +2 -0
  163. package/src/messages/prune.ts +51 -0
  164. package/src/run.ts +82 -10
  165. package/src/scripts/ant_web_search.ts +1 -1
  166. package/src/scripts/handoff-test.ts +1 -1
  167. package/src/scripts/multi-agent-chain.ts +4 -4
  168. package/src/scripts/multi-agent-conditional.ts +4 -4
  169. package/src/scripts/multi-agent-document-review-chain.ts +4 -4
  170. package/src/scripts/multi-agent-parallel.ts +10 -8
  171. package/src/scripts/multi-agent-sequence.ts +3 -3
  172. package/src/scripts/multi-agent-supervisor.ts +5 -3
  173. package/src/scripts/multi-agent-test.ts +2 -2
  174. package/src/scripts/search.ts +5 -1
  175. package/src/scripts/simple.ts +8 -0
  176. package/src/scripts/test-custom-prompt-key.ts +4 -4
  177. package/src/scripts/test-handoff-input.ts +3 -3
  178. package/src/scripts/test-multi-agent-list-handoff.ts +2 -2
  179. package/src/scripts/tools.ts +4 -1
  180. package/src/specs/agent-handoffs.test.ts +889 -0
  181. package/src/stream.ts +9 -2
  182. package/src/tools/search/firecrawl.ts +5 -2
  183. package/src/tools/search/jina-reranker.test.ts +126 -0
  184. package/src/tools/search/rerankers.ts +11 -5
  185. package/src/tools/search/search.ts +6 -8
  186. package/src/tools/search/serper-scraper.ts +155 -0
  187. package/src/tools/search/tool.ts +49 -8
  188. package/src/tools/search/types.ts +46 -0
  189. package/src/types/graph.ts +6 -1
  190. package/src/types/messages.ts +4 -0
  191. package/src/utils/handlers.ts +107 -0
  192. package/src/utils/index.ts +2 -1
  193. package/src/utils/llmConfig.ts +35 -1
  194. package/src/utils/misc.ts +33 -21
  195. package/src/utils/title.ts +80 -40
@@ -135,12 +135,18 @@ export class CustomAnthropic extends ChatAnthropicMessages {
135
135
  private message_delta: AnthropicMessageDeltaEvent | undefined;
136
136
  private tools_in_params?: boolean;
137
137
  private emitted_usage?: boolean;
138
+ top_k: number | undefined;
138
139
  constructor(fields?: CustomAnthropicInput) {
139
140
  super(fields);
140
141
  this.resetTokenEvents();
142
+ this.setDirectFields(fields);
141
143
  this._lc_stream_delay = fields?._lc_stream_delay ?? 25;
142
144
  }
143
145
 
146
+ static lc_name(): 'LibreChatAnthropic' {
147
+ return 'LibreChatAnthropic';
148
+ }
149
+
144
150
  /**
145
151
  * Get the parameters used to invoke the model
146
152
  */
@@ -158,7 +164,7 @@ export class CustomAnthropic extends ChatAnthropicMessages {
158
164
  | undefined = handleToolChoice(options?.tool_choice);
159
165
 
160
166
  if (this.thinking.type === 'enabled') {
161
- if (this.topK !== -1 && (this.topK as number | undefined) != null) {
167
+ if (this.top_k !== -1 && (this.top_k as number | undefined) != null) {
162
168
  throw new Error('topK is not supported when thinking is enabled');
163
169
  }
164
170
  if (this.topP !== -1 && (this.topP as number | undefined) != null) {
@@ -187,7 +193,7 @@ export class CustomAnthropic extends ChatAnthropicMessages {
187
193
  return {
188
194
  model: this.model,
189
195
  temperature: this.temperature,
190
- top_k: this.topK,
196
+ top_k: this.top_k,
191
197
  top_p: this.topP,
192
198
  stop_sequences: options?.stop ?? this.stopSequences,
193
199
  stream: this.streaming,
@@ -244,6 +250,21 @@ export class CustomAnthropic extends ChatAnthropicMessages {
244
250
  this.tools_in_params = undefined;
245
251
  }
246
252
 
253
+ setDirectFields(fields?: CustomAnthropicInput): void {
254
+ this.temperature = fields?.temperature ?? undefined;
255
+ this.topP = fields?.topP ?? undefined;
256
+ this.top_k = fields?.topK;
257
+ if (this.temperature === -1 || this.temperature === 1) {
258
+ this.temperature = undefined;
259
+ }
260
+ if (this.topP === -1) {
261
+ this.topP = undefined;
262
+ }
263
+ if (this.top_k === -1) {
264
+ this.top_k = undefined;
265
+ }
266
+ }
267
+
247
268
  private createGenerationChunk({
248
269
  token,
249
270
  chunk,
@@ -58,7 +58,7 @@ async function invoke(
58
58
  const extendedThinkingModelName = 'claude-3-7-sonnet-20250219';
59
59
 
60
60
  // use this for tests involving citations
61
- const citationsModelName = 'claude-3-5-sonnet-20241022';
61
+ const citationsModelName = 'claude-sonnet-4-5-20250929';
62
62
 
63
63
  // use this for tests involving PDF documents
64
64
  const pdfModelName = 'claude-3-5-haiku-20241022';
@@ -107,6 +107,10 @@ export class CustomChatGoogleGenerativeAI extends ChatGoogleGenerativeAI {
107
107
  this.streamUsage = fields.streamUsage ?? this.streamUsage;
108
108
  }
109
109
 
110
+ static lc_name(): 'LibreChatGoogleGenerativeAI' {
111
+ return 'LibreChatGoogleGenerativeAI';
112
+ }
113
+
110
114
  invocationParams(
111
115
  options?: this['ParsedCallOptions']
112
116
  ): Omit<GenerateContentRequest, 'contents'> {
@@ -301,6 +301,20 @@ function _convertLangChainContentToPart(
301
301
  mimeType,
302
302
  },
303
303
  };
304
+ } else if (
305
+ content.type === 'document' ||
306
+ content.type === 'audio' ||
307
+ content.type === 'video'
308
+ ) {
309
+ if (!isMultimodalModel) {
310
+ throw new Error(`This model does not support ${content.type}s`);
311
+ }
312
+ return {
313
+ inlineData: {
314
+ data: content.data,
315
+ mimeType: content.mimeType,
316
+ },
317
+ };
304
318
  } else if (content.type === 'media') {
305
319
  return messageContentMedia(content);
306
320
  } else if (content.type === 'tool_use') {
@@ -13,6 +13,9 @@ import {
13
13
  } from './utils';
14
14
 
15
15
  export class ChatOllama extends BaseChatOllama {
16
+ static lc_name(): 'LibreChatOllama' {
17
+ return 'LibreChatOllama';
18
+ }
16
19
  async *_streamResponseChunks(
17
20
  messages: BaseMessage[],
18
21
  options: this['ParsedCallOptions'],
@@ -210,6 +210,9 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
210
210
  public get exposedClient(): CustomOpenAIClient {
211
211
  return this.client;
212
212
  }
213
+ static lc_name(): string {
214
+ return 'LibreChatOpenAI';
215
+ }
213
216
  protected _getClientOptions(
214
217
  options?: OpenAICoreRequestOptions
215
218
  ): OpenAICoreRequestOptions {
@@ -245,10 +248,6 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
245
248
  getReasoningParams(
246
249
  options?: this['ParsedCallOptions']
247
250
  ): OpenAIClient.Reasoning | undefined {
248
- if (!isReasoningModel(this.model)) {
249
- return;
250
- }
251
-
252
251
  // apply options in reverse order of importance -- newer options supersede older options
253
252
  let reasoning: OpenAIClient.Reasoning | undefined;
254
253
  if (this.reasoning !== undefined) {
@@ -360,6 +359,10 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
360
359
  } else if ('reasoning' in delta) {
361
360
  chunk.additional_kwargs.reasoning_content = delta.reasoning;
362
361
  }
362
+ if ('provider_specific_fields' in delta) {
363
+ chunk.additional_kwargs.provider_specific_fields =
364
+ delta.provider_specific_fields;
365
+ }
363
366
  defaultRole = delta.role ?? defaultRole;
364
367
  const newTokenIndices = {
365
368
  prompt: options.promptIndex ?? 0,
@@ -463,6 +466,9 @@ export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
463
466
  public get exposedClient(): CustomOpenAIClient {
464
467
  return this.client;
465
468
  }
469
+ static lc_name(): 'LibreChatAzureOpenAI' {
470
+ return 'LibreChatAzureOpenAI';
471
+ }
466
472
  /**
467
473
  * Returns backwards compatible reasoning parameters from constructor params and call options
468
474
  * @internal
@@ -607,6 +613,9 @@ export class ChatDeepSeek extends OriginalChatDeepSeek {
607
613
  public get exposedClient(): CustomOpenAIClient {
608
614
  return this.client;
609
615
  }
616
+ static lc_name(): 'LibreChatDeepSeek' {
617
+ return 'LibreChatDeepSeek';
618
+ }
610
619
  protected _getClientOptions(
611
620
  options?: OpenAICoreRequestOptions
612
621
  ): OpenAICoreRequestOptions {
@@ -679,6 +688,10 @@ export class ChatXAI extends OriginalChatXAI {
679
688
  }
680
689
  }
681
690
 
691
+ static lc_name(): 'LibreChatXAI' {
692
+ return 'LibreChatXAI';
693
+ }
694
+
682
695
  public get exposedClient(): CustomOpenAIClient {
683
696
  return this.client;
684
697
  }
@@ -298,10 +298,16 @@ export function _convertMessagesToOpenAIParams(
298
298
  role = 'developer';
299
299
  }
300
300
 
301
+ let hasAnthropicThinkingBlock: boolean = false;
302
+
301
303
  const content =
302
304
  typeof message.content === 'string'
303
305
  ? message.content
304
306
  : message.content.map((m) => {
307
+ if ('type' in m && m.type === 'thinking') {
308
+ hasAnthropicThinkingBlock = true;
309
+ return m;
310
+ }
305
311
  if (isDataContentBlock(m)) {
306
312
  return convertToProviderContentBlock(
307
313
  m,
@@ -326,7 +332,7 @@ export function _convertMessagesToOpenAIParams(
326
332
  completionParam.tool_calls = message.tool_calls.map(
327
333
  convertLangChainToolCallToOpenAI
328
334
  );
329
- completionParam.content = '';
335
+ completionParam.content = hasAnthropicThinkingBlock ? content : '';
330
336
  } else {
331
337
  if (message.additional_kwargs.tool_calls != null) {
332
338
  completionParam.tool_calls = message.additional_kwargs.tool_calls;
@@ -1,27 +1,36 @@
1
- import type { ChatOpenAICallOptions, OpenAIClient } from '@langchain/openai';
1
+ import { ChatOpenAI } from '@/llm/openai';
2
2
  import type {
3
- AIMessageChunk,
4
- HumanMessageChunk,
5
- SystemMessageChunk,
6
3
  FunctionMessageChunk,
4
+ SystemMessageChunk,
5
+ HumanMessageChunk,
7
6
  ToolMessageChunk,
8
7
  ChatMessageChunk,
8
+ AIMessageChunk,
9
9
  } from '@langchain/core/messages';
10
- import { ChatOpenAI } from '@/llm/openai';
10
+ import type {
11
+ ChatOpenAICallOptions,
12
+ OpenAIChatInput,
13
+ OpenAIClient,
14
+ } from '@langchain/openai';
11
15
 
12
16
  export interface ChatOpenRouterCallOptions extends ChatOpenAICallOptions {
13
17
  include_reasoning?: boolean;
18
+ modelKwargs?: OpenAIChatInput['modelKwargs'];
14
19
  }
15
20
  export class ChatOpenRouter extends ChatOpenAI {
16
21
  constructor(_fields: Partial<ChatOpenRouterCallOptions>) {
17
- const { include_reasoning, ...fields } = _fields;
22
+ const { include_reasoning, modelKwargs = {}, ...fields } = _fields;
18
23
  super({
19
24
  ...fields,
20
25
  modelKwargs: {
26
+ ...modelKwargs,
21
27
  include_reasoning,
22
28
  },
23
29
  });
24
30
  }
31
+ static lc_name(): 'LibreChatOpenRouter' {
32
+ return 'LibreChatOpenRouter';
33
+ }
25
34
  protected override _convertOpenAIDeltaToBaseMessageChunk(
26
35
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
36
  delta: Record<string, any>,
@@ -313,8 +313,8 @@ export class ChatVertexAI extends ChatGoogle {
313
313
  lc_namespace = ['langchain', 'chat_models', 'vertexai'];
314
314
  dynamicThinkingBudget = false;
315
315
 
316
- static lc_name(): 'ChatVertexAI' {
317
- return 'ChatVertexAI';
316
+ static lc_name(): 'LibreChatVertexAI' {
317
+ return 'LibreChatVertexAI';
318
318
  }
319
319
 
320
320
  constructor(fields?: VertexAIClientOptions) {
@@ -0,0 +1,262 @@
1
+ import type Anthropic from '@anthropic-ai/sdk';
2
+ import type { AnthropicMessages } from '@/types/messages';
3
+ import { addCacheControl } from './cache';
4
+
5
+ describe('addCacheControl', () => {
6
+ test('should add cache control to the last two user messages with array content', () => {
7
+ const messages: AnthropicMessages = [
8
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
9
+ { role: 'assistant', content: [{ type: 'text', text: 'Hi there' }] },
10
+ { role: 'user', content: [{ type: 'text', text: 'How are you?' }] },
11
+ {
12
+ role: 'assistant',
13
+ content: [{ type: 'text', text: 'I\'m doing well, thanks!' }],
14
+ },
15
+ { role: 'user', content: [{ type: 'text', text: 'Great!' }] },
16
+ ];
17
+
18
+ const result = addCacheControl(messages);
19
+
20
+ expect(result[0].content[0]).not.toHaveProperty('cache_control');
21
+ expect(
22
+ (result[2].content[0] as Anthropic.TextBlockParam).cache_control
23
+ ).toEqual({ type: 'ephemeral' });
24
+ expect(
25
+ (result[4].content[0] as Anthropic.TextBlockParam).cache_control
26
+ ).toEqual({ type: 'ephemeral' });
27
+ });
28
+
29
+ test('should add cache control to the last two user messages with string content', () => {
30
+ const messages: AnthropicMessages = [
31
+ { role: 'user', content: 'Hello' },
32
+ { role: 'assistant', content: 'Hi there' },
33
+ { role: 'user', content: 'How are you?' },
34
+ { role: 'assistant', content: 'I\'m doing well, thanks!' },
35
+ { role: 'user', content: 'Great!' },
36
+ ];
37
+
38
+ const result = addCacheControl(messages);
39
+
40
+ expect(result[0].content).toBe('Hello');
41
+ expect(result[2].content[0]).toEqual({
42
+ type: 'text',
43
+ text: 'How are you?',
44
+ cache_control: { type: 'ephemeral' },
45
+ });
46
+ expect(result[4].content[0]).toEqual({
47
+ type: 'text',
48
+ text: 'Great!',
49
+ cache_control: { type: 'ephemeral' },
50
+ });
51
+ });
52
+
53
+ test('should handle mixed string and array content', () => {
54
+ const messages: AnthropicMessages = [
55
+ { role: 'user', content: 'Hello' },
56
+ { role: 'assistant', content: 'Hi there' },
57
+ { role: 'user', content: [{ type: 'text', text: 'How are you?' }] },
58
+ ];
59
+
60
+ const result = addCacheControl(messages);
61
+
62
+ expect(result[0].content[0]).toEqual({
63
+ type: 'text',
64
+ text: 'Hello',
65
+ cache_control: { type: 'ephemeral' },
66
+ });
67
+ expect(
68
+ (result[2].content[0] as Anthropic.TextBlockParam).cache_control
69
+ ).toEqual({ type: 'ephemeral' });
70
+ });
71
+
72
+ test('should handle less than two user messages', () => {
73
+ const messages: AnthropicMessages = [
74
+ { role: 'user', content: 'Hello' },
75
+ { role: 'assistant', content: 'Hi there' },
76
+ ];
77
+
78
+ const result = addCacheControl(messages);
79
+
80
+ expect(result[0].content[0]).toEqual({
81
+ type: 'text',
82
+ text: 'Hello',
83
+ cache_control: { type: 'ephemeral' },
84
+ });
85
+ expect(result[1].content).toBe('Hi there');
86
+ });
87
+
88
+ test('should return original array if no user messages', () => {
89
+ const messages: AnthropicMessages = [
90
+ { role: 'assistant', content: 'Hi there' },
91
+ { role: 'assistant', content: 'How can I help?' },
92
+ ];
93
+
94
+ const result = addCacheControl(messages);
95
+
96
+ expect(result).toEqual(messages);
97
+ });
98
+
99
+ test('should handle empty array', () => {
100
+ const messages: AnthropicMessages = [];
101
+ const result = addCacheControl(messages);
102
+ expect(result).toEqual([]);
103
+ });
104
+
105
+ test('should handle non-array input', () => {
106
+ const messages = 'not an array';
107
+ /** @ts-expect-error - This is a test */
108
+ const result = addCacheControl(messages);
109
+ expect(result).toBe('not an array');
110
+ });
111
+
112
+ test('should not modify assistant messages', () => {
113
+ const messages: AnthropicMessages = [
114
+ { role: 'user', content: 'Hello' },
115
+ { role: 'assistant', content: 'Hi there' },
116
+ { role: 'user', content: 'How are you?' },
117
+ ];
118
+
119
+ const result = addCacheControl(messages);
120
+
121
+ expect(result[1].content).toBe('Hi there');
122
+ });
123
+
124
+ test('should handle multiple content items in user messages', () => {
125
+ const messages: AnthropicMessages = [
126
+ {
127
+ role: 'user',
128
+ content: [
129
+ { type: 'text', text: 'Hello' },
130
+ {
131
+ type: 'image',
132
+ source: { type: 'url', url: 'http://example.com/image.jpg' },
133
+ },
134
+ { type: 'text', text: 'This is an image' },
135
+ ],
136
+ },
137
+ { role: 'assistant', content: 'Hi there' },
138
+ { role: 'user', content: 'How are you?' },
139
+ ];
140
+
141
+ const result = addCacheControl(messages);
142
+
143
+ expect(result[0].content[0]).not.toHaveProperty('cache_control');
144
+ expect(result[0].content[1]).not.toHaveProperty('cache_control');
145
+ expect(
146
+ (result[0].content[2] as Anthropic.TextBlockParam).cache_control
147
+ ).toEqual({ type: 'ephemeral' });
148
+ expect(result[2].content[0]).toEqual({
149
+ type: 'text',
150
+ text: 'How are you?',
151
+ cache_control: { type: 'ephemeral' },
152
+ });
153
+ });
154
+
155
+ test('should handle an array with mixed content types', () => {
156
+ const messages: AnthropicMessages = [
157
+ { role: 'user', content: 'Hello' },
158
+ { role: 'assistant', content: 'Hi there' },
159
+ { role: 'user', content: [{ type: 'text', text: 'How are you?' }] },
160
+ { role: 'assistant', content: 'I\'m doing well, thanks!' },
161
+ { role: 'user', content: 'Great!' },
162
+ ];
163
+
164
+ const result = addCacheControl(messages);
165
+
166
+ expect(result[0].content).toEqual('Hello');
167
+ expect(result[2].content[0]).toEqual({
168
+ type: 'text',
169
+ text: 'How are you?',
170
+ cache_control: { type: 'ephemeral' },
171
+ });
172
+ expect(result[4].content).toEqual([
173
+ {
174
+ type: 'text',
175
+ text: 'Great!',
176
+ cache_control: { type: 'ephemeral' },
177
+ },
178
+ ]);
179
+ expect(result[1].content).toBe('Hi there');
180
+ expect(result[3].content).toBe('I\'m doing well, thanks!');
181
+ });
182
+
183
+ test('should handle edge case with multiple content types', () => {
184
+ const messages: AnthropicMessages = [
185
+ {
186
+ role: 'user',
187
+ content: [
188
+ {
189
+ type: 'image',
190
+ source: {
191
+ type: 'base64',
192
+ media_type: 'image/png',
193
+ data: 'some_base64_string',
194
+ },
195
+ },
196
+ {
197
+ type: 'image',
198
+ source: {
199
+ type: 'base64',
200
+ media_type: 'image/png',
201
+ data: 'another_base64_string',
202
+ },
203
+ },
204
+ { type: 'text', text: 'what do all these images have in common' },
205
+ ],
206
+ },
207
+ { role: 'assistant', content: 'I see multiple images.' },
208
+ { role: 'user', content: 'Correct!' },
209
+ ];
210
+
211
+ const result = addCacheControl(messages);
212
+
213
+ expect(result[0].content[0]).not.toHaveProperty('cache_control');
214
+ expect(result[0].content[1]).not.toHaveProperty('cache_control');
215
+ expect(
216
+ (result[0].content[2] as Anthropic.ImageBlockParam).cache_control
217
+ ).toEqual({ type: 'ephemeral' });
218
+ expect(result[2].content[0]).toEqual({
219
+ type: 'text',
220
+ text: 'Correct!',
221
+ cache_control: { type: 'ephemeral' },
222
+ });
223
+ });
224
+
225
+ test('should handle user message with no text block', () => {
226
+ const messages: AnthropicMessages = [
227
+ {
228
+ role: 'user',
229
+ content: [
230
+ {
231
+ type: 'image',
232
+ source: {
233
+ type: 'base64',
234
+ media_type: 'image/png',
235
+ data: 'some_base64_string',
236
+ },
237
+ },
238
+ {
239
+ type: 'image',
240
+ source: {
241
+ type: 'base64',
242
+ media_type: 'image/png',
243
+ data: 'another_base64_string',
244
+ },
245
+ },
246
+ ],
247
+ },
248
+ { role: 'assistant', content: 'I see two images.' },
249
+ { role: 'user', content: 'Correct!' },
250
+ ];
251
+
252
+ const result = addCacheControl(messages);
253
+
254
+ expect(result[0].content[0]).not.toHaveProperty('cache_control');
255
+ expect(result[0].content[1]).not.toHaveProperty('cache_control');
256
+ expect(result[2].content[0]).toEqual({
257
+ type: 'text',
258
+ text: 'Correct!',
259
+ cache_control: { type: 'ephemeral' },
260
+ });
261
+ });
262
+ });
@@ -0,0 +1,56 @@
1
+ import type { AnthropicMessage } from '@/types/messages';
2
+ import type Anthropic from '@anthropic-ai/sdk';
3
+ import { BaseMessage } from '@langchain/core/messages';
4
+
5
+ /**
6
+ * Anthropic API: Adds cache control to the appropriate user messages in the payload.
7
+ * @param messages - The array of message objects.
8
+ * @returns - The updated array of message objects with cache control added.
9
+ */
10
+ export function addCacheControl<T extends AnthropicMessage | BaseMessage>(
11
+ messages: T[]
12
+ ): T[] {
13
+ if (!Array.isArray(messages) || messages.length < 2) {
14
+ return messages;
15
+ }
16
+
17
+ const updatedMessages = [...messages];
18
+ let userMessagesModified = 0;
19
+
20
+ for (
21
+ let i = updatedMessages.length - 1;
22
+ i >= 0 && userMessagesModified < 2;
23
+ i--
24
+ ) {
25
+ const message = updatedMessages[i];
26
+ if ('getType' in message && message.getType() !== 'human') {
27
+ continue;
28
+ } else if ('role' in message && message.role !== 'user') {
29
+ continue;
30
+ }
31
+
32
+ if (typeof message.content === 'string') {
33
+ message.content = [
34
+ {
35
+ type: 'text',
36
+ text: message.content,
37
+ cache_control: { type: 'ephemeral' },
38
+ },
39
+ ];
40
+ userMessagesModified++;
41
+ } else if (Array.isArray(message.content)) {
42
+ for (let j = message.content.length - 1; j >= 0; j--) {
43
+ const contentPart = message.content[j];
44
+ if ('type' in contentPart && contentPart.type === 'text') {
45
+ (contentPart as Anthropic.TextBlockParam).cache_control = {
46
+ type: 'ephemeral',
47
+ };
48
+ userMessagesModified++;
49
+ break;
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ return updatedMessages;
56
+ }