@librechat/agents 3.0.0-rc11 → 3.0.0-rc13

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 (89) 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 +16 -1
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/instrumentation.cjs +21 -0
  6. package/dist/cjs/instrumentation.cjs.map +1 -0
  7. package/dist/cjs/llm/openai/index.cjs +0 -4
  8. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  9. package/dist/cjs/main.cjs +5 -1
  10. package/dist/cjs/main.cjs.map +1 -1
  11. package/dist/cjs/messages/cache.cjs +49 -0
  12. package/dist/cjs/messages/cache.cjs.map +1 -0
  13. package/dist/cjs/messages/content.cjs +53 -0
  14. package/dist/cjs/messages/content.cjs.map +1 -0
  15. package/dist/cjs/messages/format.cjs +0 -27
  16. package/dist/cjs/messages/format.cjs.map +1 -1
  17. package/dist/cjs/run.cjs +57 -5
  18. package/dist/cjs/run.cjs.map +1 -1
  19. package/dist/cjs/stream.cjs +7 -0
  20. package/dist/cjs/stream.cjs.map +1 -1
  21. package/dist/cjs/utils/misc.cjs +8 -1
  22. package/dist/cjs/utils/misc.cjs.map +1 -1
  23. package/dist/cjs/utils/title.cjs +54 -25
  24. package/dist/cjs/utils/title.cjs.map +1 -1
  25. package/dist/esm/agents/AgentContext.mjs +6 -2
  26. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  27. package/dist/esm/graphs/Graph.mjs +16 -1
  28. package/dist/esm/graphs/Graph.mjs.map +1 -1
  29. package/dist/esm/instrumentation.mjs +19 -0
  30. package/dist/esm/instrumentation.mjs.map +1 -0
  31. package/dist/esm/llm/openai/index.mjs +0 -4
  32. package/dist/esm/llm/openai/index.mjs.map +1 -1
  33. package/dist/esm/main.mjs +4 -2
  34. package/dist/esm/main.mjs.map +1 -1
  35. package/dist/esm/messages/cache.mjs +47 -0
  36. package/dist/esm/messages/cache.mjs.map +1 -0
  37. package/dist/esm/messages/content.mjs +51 -0
  38. package/dist/esm/messages/content.mjs.map +1 -0
  39. package/dist/esm/messages/format.mjs +1 -27
  40. package/dist/esm/messages/format.mjs.map +1 -1
  41. package/dist/esm/run.mjs +57 -5
  42. package/dist/esm/run.mjs.map +1 -1
  43. package/dist/esm/stream.mjs +7 -0
  44. package/dist/esm/stream.mjs.map +1 -1
  45. package/dist/esm/utils/misc.mjs +8 -2
  46. package/dist/esm/utils/misc.mjs.map +1 -1
  47. package/dist/esm/utils/title.mjs +54 -25
  48. package/dist/esm/utils/title.mjs.map +1 -1
  49. package/dist/types/agents/AgentContext.d.ts +4 -1
  50. package/dist/types/instrumentation.d.ts +1 -0
  51. package/dist/types/messages/cache.d.ts +8 -0
  52. package/dist/types/messages/content.d.ts +7 -0
  53. package/dist/types/messages/format.d.ts +1 -7
  54. package/dist/types/messages/index.d.ts +2 -0
  55. package/dist/types/run.d.ts +2 -1
  56. package/dist/types/types/graph.d.ts +2 -0
  57. package/dist/types/types/messages.d.ts +4 -0
  58. package/dist/types/utils/misc.d.ts +1 -0
  59. package/package.json +5 -1
  60. package/src/agents/AgentContext.ts +8 -0
  61. package/src/graphs/Graph.ts +23 -1
  62. package/src/instrumentation.ts +22 -0
  63. package/src/llm/anthropic/llm.spec.ts +1 -1
  64. package/src/llm/openai/index.ts +0 -5
  65. package/src/messages/cache.test.ts +262 -0
  66. package/src/messages/cache.ts +56 -0
  67. package/src/messages/content.test.ts +362 -0
  68. package/src/messages/content.ts +63 -0
  69. package/src/messages/format.ts +0 -34
  70. package/src/messages/index.ts +2 -0
  71. package/src/run.ts +82 -10
  72. package/src/scripts/ant_web_search.ts +1 -1
  73. package/src/scripts/handoff-test.ts +1 -1
  74. package/src/scripts/multi-agent-chain.ts +4 -4
  75. package/src/scripts/multi-agent-conditional.ts +4 -4
  76. package/src/scripts/multi-agent-document-review-chain.ts +4 -4
  77. package/src/scripts/multi-agent-parallel.ts +10 -8
  78. package/src/scripts/multi-agent-sequence.ts +3 -3
  79. package/src/scripts/multi-agent-supervisor.ts +5 -3
  80. package/src/scripts/multi-agent-test.ts +2 -2
  81. package/src/scripts/simple.ts +8 -0
  82. package/src/scripts/test-custom-prompt-key.ts +4 -4
  83. package/src/scripts/test-handoff-input.ts +3 -3
  84. package/src/scripts/test-multi-agent-list-handoff.ts +2 -2
  85. package/src/stream.ts +9 -2
  86. package/src/types/graph.ts +2 -0
  87. package/src/types/messages.ts +4 -0
  88. package/src/utils/misc.ts +33 -21
  89. package/src/utils/title.ts +80 -40
@@ -56,7 +56,9 @@ export declare class AgentContext {
56
56
  systemRunnable?: Runnable<BaseMessage[], (BaseMessage | SystemMessage)[], RunnableConfig<Record<string, unknown>>>;
57
57
  /** Promise for token calculation initialization */
58
58
  tokenCalculationPromise?: Promise<void>;
59
- constructor({ agentId, provider, clientOptions, maxContextTokens, streamBuffer, tokenCounter, tools, toolMap, instructions, additionalInstructions, reasoningKey, toolEnd, instructionTokens, }: {
59
+ /** Format content blocks as strings (for legacy compatibility) */
60
+ useLegacyContent: boolean;
61
+ constructor({ agentId, provider, clientOptions, maxContextTokens, streamBuffer, tokenCounter, tools, toolMap, instructions, additionalInstructions, reasoningKey, toolEnd, instructionTokens, useLegacyContent, }: {
60
62
  agentId: string;
61
63
  provider: Providers;
62
64
  clientOptions?: t.ClientOptions;
@@ -70,6 +72,7 @@ export declare class AgentContext {
70
72
  reasoningKey?: 'reasoning_content' | 'reasoning';
71
73
  toolEnd?: boolean;
72
74
  instructionTokens?: number;
75
+ useLegacyContent?: boolean;
73
76
  });
74
77
  /**
75
78
  * Create system runnable from instructions and calculate tokens if tokenCounter is available
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { AnthropicMessage } from '@/types/messages';
2
+ import { BaseMessage } from '@langchain/core/messages';
3
+ /**
4
+ * Anthropic API: Adds cache control to the appropriate user messages in the payload.
5
+ * @param messages - The array of message objects.
6
+ * @returns - The updated array of message objects with cache control added.
7
+ */
8
+ export declare function addCacheControl<T extends AnthropicMessage | BaseMessage>(messages: T[]): T[];
@@ -0,0 +1,7 @@
1
+ import type { BaseMessage } from '@langchain/core/messages';
2
+ /**
3
+ * Formats an array of messages for LangChain, making sure all content fields are strings
4
+ * @param {Array<HumanMessage | AIMessage | SystemMessage | ToolMessage>} payload - The array of messages to format.
5
+ * @returns {Array<HumanMessage | AIMessage | SystemMessage | ToolMessage>} - The array of formatted LangChain messages, including ToolMessages for tool calls.
6
+ */
7
+ export declare const formatContentStrings: (payload: Array<BaseMessage>) => Array<BaseMessage>;
@@ -1,4 +1,4 @@
1
- import { AIMessage, ToolMessage, BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
1
+ import { AIMessage, ToolMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
2
2
  import type { MessageContentImageUrl } from '@langchain/core/messages';
3
3
  import type { MessageContentComplex, TPayload } from '@/types';
4
4
  import { Providers } from '@/common';
@@ -95,12 +95,6 @@ export declare const formatAgentMessages: (payload: TPayload, indexTokenCountMap
95
95
  messages: Array<HumanMessage | AIMessage | SystemMessage | ToolMessage>;
96
96
  indexTokenCountMap?: Record<number, number>;
97
97
  };
98
- /**
99
- * Formats an array of messages for LangChain, making sure all content fields are strings
100
- * @param payload - The array of messages to format.
101
- * @returns - The array of formatted LangChain messages, including ToolMessages for tool calls.
102
- */
103
- export declare const formatContentStrings: (payload: Array<BaseMessage>) => Array<BaseMessage>;
104
98
  /**
105
99
  * Adds a value at key 0 for system messages and shifts all key indices by one in an indexTokenCountMap.
106
100
  * This is useful when adding a system message at the beginning of a conversation.
@@ -2,3 +2,5 @@ export * from './core';
2
2
  export * from './ids';
3
3
  export * from './prune';
4
4
  export * from './format';
5
+ export * from './cache';
6
+ export * from './content';
@@ -1,4 +1,5 @@
1
- import type { BaseMessage, MessageContentComplex } from '@langchain/core/messages';
1
+ import './instrumentation';
2
+ import type { MessageContentComplex, BaseMessage } from '@langchain/core/messages';
2
3
  import type { RunnableConfig } from '@langchain/core/runnables';
3
4
  import type * as t from '@/types';
4
5
  import { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
@@ -246,4 +246,6 @@ export interface AgentInputs {
246
246
  clientOptions?: ClientOptions;
247
247
  additional_instructions?: string;
248
248
  reasoningKey?: 'reasoning_content' | 'reasoning';
249
+ /** Format content blocks as strings (for legacy compatibility i.e. Ollama/Azure Serverless) */
250
+ useLegacyContent?: boolean;
249
251
  }
@@ -0,0 +1,4 @@
1
+ import type Anthropic from '@anthropic-ai/sdk';
2
+ import type { BaseMessage } from '@langchain/core/messages';
3
+ export type AnthropicMessages = Array<AnthropicMessage | BaseMessage>;
4
+ export type AnthropicMessage = Anthropic.MessageParam;
@@ -1,3 +1,4 @@
1
+ export declare function isPresent(value: string | null | undefined): value is string;
1
2
  /**
2
3
  * Recursively unescapes all string values in an object
3
4
  * @param obj The object to unescape
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.0.00-rc11",
3
+ "version": "3.0.00-rc13",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -105,6 +105,10 @@
105
105
  "@langchain/ollama": "^0.2.3",
106
106
  "@langchain/openai": "0.5.18",
107
107
  "@langchain/xai": "^0.0.3",
108
+ "@langfuse/langchain": "^4.3.0",
109
+ "@langfuse/otel": "^4.3.0",
110
+ "@langfuse/tracing": "^4.3.0",
111
+ "@opentelemetry/sdk-node": "^0.207.0",
108
112
  "cheerio": "^1.0.0",
109
113
  "dotenv": "^16.4.7",
110
114
  "https-proxy-agent": "^7.0.6",
@@ -37,6 +37,7 @@ export class AgentContext {
37
37
  streamBuffer,
38
38
  maxContextTokens,
39
39
  reasoningKey,
40
+ useLegacyContent,
40
41
  } = agentConfig;
41
42
 
42
43
  const agentContext = new AgentContext({
@@ -53,6 +54,7 @@ export class AgentContext {
53
54
  toolEnd,
54
55
  instructionTokens: 0,
55
56
  tokenCounter,
57
+ useLegacyContent,
56
58
  });
57
59
 
58
60
  if (tokenCounter) {
@@ -123,6 +125,8 @@ export class AgentContext {
123
125
  >;
124
126
  /** Promise for token calculation initialization */
125
127
  tokenCalculationPromise?: Promise<void>;
128
+ /** Format content blocks as strings (for legacy compatibility) */
129
+ useLegacyContent: boolean = false;
126
130
 
127
131
  constructor({
128
132
  agentId,
@@ -138,6 +142,7 @@ export class AgentContext {
138
142
  reasoningKey,
139
143
  toolEnd,
140
144
  instructionTokens,
145
+ useLegacyContent,
141
146
  }: {
142
147
  agentId: string;
143
148
  provider: Providers;
@@ -152,6 +157,7 @@ export class AgentContext {
152
157
  reasoningKey?: 'reasoning_content' | 'reasoning';
153
158
  toolEnd?: boolean;
154
159
  instructionTokens?: number;
160
+ useLegacyContent?: boolean;
155
161
  }) {
156
162
  this.agentId = agentId;
157
163
  this.provider = provider;
@@ -173,6 +179,8 @@ export class AgentContext {
173
179
  this.instructionTokens = instructionTokens;
174
180
  }
175
181
 
182
+ this.useLegacyContent = useLegacyContent ?? false;
183
+
176
184
  this.systemRunnable = this.createSystemRunnable();
177
185
  }
178
186
 
@@ -41,7 +41,9 @@ import {
41
41
  convertMessagesToContent,
42
42
  modifyDeltaProperties,
43
43
  formatArtifactPayload,
44
+ formatContentStrings,
44
45
  createPruneMessages,
46
+ addCacheControl,
45
47
  } from '@/messages';
46
48
  import {
47
49
  resetIfNotEmpty,
@@ -608,7 +610,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
608
610
  messagesToUse = context;
609
611
  }
610
612
 
611
- const finalMessages = messagesToUse;
613
+ let finalMessages = messagesToUse;
614
+ if (agentContext.useLegacyContent) {
615
+ finalMessages = formatContentStrings(finalMessages);
616
+ }
617
+
612
618
  const lastMessageX =
613
619
  finalMessages.length >= 2
614
620
  ? finalMessages[finalMessages.length - 2]
@@ -642,6 +648,22 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
642
648
  formatArtifactPayload(finalMessages);
643
649
  }
644
650
 
651
+ if (agentContext.provider === Providers.ANTHROPIC) {
652
+ const anthropicOptions = agentContext.clientOptions as
653
+ | t.AnthropicClientOptions
654
+ | undefined;
655
+ const defaultHeaders = anthropicOptions?.clientOptions
656
+ ?.defaultHeaders as Record<string, string> | undefined;
657
+ const anthropicBeta = defaultHeaders?.['anthropic-beta'];
658
+
659
+ if (
660
+ typeof anthropicBeta === 'string' &&
661
+ anthropicBeta.includes('prompt-caching')
662
+ ) {
663
+ finalMessages = addCacheControl<BaseMessage>(finalMessages);
664
+ }
665
+ }
666
+
645
667
  if (
646
668
  agentContext.lastStreamCall != null &&
647
669
  agentContext.streamBuffer != null
@@ -0,0 +1,22 @@
1
+ import { NodeSDK } from '@opentelemetry/sdk-node';
2
+ import { LangfuseSpanProcessor } from '@langfuse/otel';
3
+ import { isPresent } from '@/utils/misc';
4
+
5
+ if (
6
+ isPresent(process.env.LANGFUSE_SECRET_KEY) &&
7
+ isPresent(process.env.LANGFUSE_PUBLIC_KEY) &&
8
+ isPresent(process.env.LANGFUSE_BASE_URL)
9
+ ) {
10
+ const langfuseSpanProcessor = new LangfuseSpanProcessor({
11
+ publicKey: process.env.LANGFUSE_PUBLIC_KEY,
12
+ secretKey: process.env.LANGFUSE_SECRET_KEY,
13
+ baseUrl: process.env.LANGFUSE_BASE_URL,
14
+ environment: process.env.NODE_ENV ?? 'development',
15
+ });
16
+
17
+ const sdk = new NodeSDK({
18
+ spanProcessors: [langfuseSpanProcessor],
19
+ });
20
+
21
+ sdk.start();
22
+ }
@@ -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';
@@ -248,11 +248,6 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
248
248
  getReasoningParams(
249
249
  options?: this['ParsedCallOptions']
250
250
  ): OpenAIClient.Reasoning | undefined {
251
- const lc_name = (this.constructor as typeof ChatOpenAI).lc_name();
252
- if (lc_name === 'LibreChatOpenAI' && !isReasoningModel(this.model)) {
253
- return;
254
- }
255
-
256
251
  // apply options in reverse order of importance -- newer options supersede older options
257
252
  let reasoning: OpenAIClient.Reasoning | undefined;
258
253
  if (this.reasoning !== undefined) {
@@ -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
+ }