@librechat/agents 3.0.0-rc10 → 3.0.0-rc12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/agents/AgentContext.cjs +6 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +23 -2
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +21 -2
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/google/index.cjs +3 -0
- package/dist/cjs/llm/google/index.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +13 -0
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/ollama/index.cjs +3 -0
- package/dist/cjs/llm/ollama/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +20 -1
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +6 -1
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +5 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/vertexai/index.cjs +1 -1
- package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +7 -2
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +49 -0
- package/dist/cjs/messages/cache.cjs.map +1 -0
- package/dist/cjs/messages/content.cjs +53 -0
- package/dist/cjs/messages/content.cjs.map +1 -0
- package/dist/cjs/messages/core.cjs +5 -1
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +50 -59
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +28 -0
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +2 -0
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/search/firecrawl.cjs +3 -1
- package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
- package/dist/cjs/tools/search/rerankers.cjs +8 -6
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs +5 -5
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/serper-scraper.cjs +132 -0
- package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -0
- package/dist/cjs/tools/search/tool.cjs +46 -9
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/utils/handlers.cjs +70 -0
- package/dist/cjs/utils/handlers.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +6 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +23 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +21 -2
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/google/index.mjs +3 -0
- package/dist/esm/llm/google/index.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +13 -0
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/ollama/index.mjs +3 -0
- package/dist/esm/llm/ollama/index.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +20 -1
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +6 -1
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +5 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/vertexai/index.mjs +1 -1
- package/dist/esm/llm/vertexai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +4 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +47 -0
- package/dist/esm/messages/cache.mjs.map +1 -0
- package/dist/esm/messages/content.mjs +51 -0
- package/dist/esm/messages/content.mjs.map +1 -0
- package/dist/esm/messages/core.mjs +5 -1
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +50 -58
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +28 -0
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +2 -0
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/search/firecrawl.mjs +3 -1
- package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
- package/dist/esm/tools/search/rerankers.mjs +8 -6
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs +5 -5
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/serper-scraper.mjs +129 -0
- package/dist/esm/tools/search/serper-scraper.mjs.map +1 -0
- package/dist/esm/tools/search/tool.mjs +46 -9
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/utils/handlers.mjs +68 -0
- package/dist/esm/utils/handlers.mjs.map +1 -0
- package/dist/types/agents/AgentContext.d.ts +4 -1
- package/dist/types/llm/anthropic/index.d.ts +3 -0
- package/dist/types/llm/google/index.d.ts +1 -0
- package/dist/types/llm/ollama/index.d.ts +1 -0
- package/dist/types/llm/openai/index.d.ts +4 -0
- package/dist/types/llm/openrouter/index.d.ts +4 -2
- package/dist/types/llm/vertexai/index.d.ts +1 -1
- package/dist/types/messages/cache.d.ts +8 -0
- package/dist/types/messages/content.d.ts +7 -0
- package/dist/types/messages/format.d.ts +22 -25
- package/dist/types/messages/index.d.ts +2 -0
- package/dist/types/tools/search/firecrawl.d.ts +2 -1
- package/dist/types/tools/search/rerankers.d.ts +4 -1
- package/dist/types/tools/search/search.d.ts +1 -2
- package/dist/types/tools/search/serper-scraper.d.ts +59 -0
- package/dist/types/tools/search/tool.d.ts +25 -4
- package/dist/types/tools/search/types.d.ts +31 -1
- package/dist/types/types/graph.d.ts +2 -0
- package/dist/types/types/messages.d.ts +4 -0
- package/dist/types/utils/handlers.d.ts +34 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/package.json +2 -2
- package/src/agents/AgentContext.ts +8 -0
- package/src/graphs/Graph.ts +31 -2
- package/src/llm/anthropic/index.ts +23 -2
- package/src/llm/google/index.ts +4 -0
- package/src/llm/google/utils/common.ts +14 -0
- package/src/llm/ollama/index.ts +3 -0
- package/src/llm/openai/index.ts +19 -1
- package/src/llm/openai/utils/index.ts +7 -1
- package/src/llm/openrouter/index.ts +15 -6
- package/src/llm/vertexai/index.ts +2 -2
- package/src/messages/cache.test.ts +262 -0
- package/src/messages/cache.ts +56 -0
- package/src/messages/content.test.ts +362 -0
- package/src/messages/content.ts +63 -0
- package/src/messages/core.ts +5 -2
- package/src/messages/format.ts +65 -71
- package/src/messages/formatMessage.test.ts +418 -2
- package/src/messages/index.ts +2 -0
- package/src/messages/prune.ts +51 -0
- package/src/scripts/search.ts +5 -1
- package/src/scripts/tools.ts +4 -1
- package/src/tools/search/firecrawl.ts +5 -2
- package/src/tools/search/jina-reranker.test.ts +126 -0
- package/src/tools/search/rerankers.ts +11 -5
- package/src/tools/search/search.ts +6 -8
- package/src/tools/search/serper-scraper.ts +155 -0
- package/src/tools/search/tool.ts +49 -8
- package/src/tools/search/types.ts +46 -0
- package/src/types/graph.ts +2 -0
- package/src/types/messages.ts +4 -0
- package/src/utils/handlers.ts +107 -0
- package/src/utils/index.ts +2 -1
- package/src/utils/llmConfig.ts +35 -1
|
@@ -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.
|
|
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.
|
|
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,
|
package/src/llm/google/index.ts
CHANGED
|
@@ -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') {
|
package/src/llm/ollama/index.ts
CHANGED
|
@@ -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'],
|
package/src/llm/openai/index.ts
CHANGED
|
@@ -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,7 +248,8 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
|
|
|
245
248
|
getReasoningParams(
|
|
246
249
|
options?: this['ParsedCallOptions']
|
|
247
250
|
): OpenAIClient.Reasoning | undefined {
|
|
248
|
-
|
|
251
|
+
const lc_name = (this.constructor as typeof ChatOpenAI).lc_name();
|
|
252
|
+
if (lc_name === 'LibreChatOpenAI' && !isReasoningModel(this.model)) {
|
|
249
253
|
return;
|
|
250
254
|
}
|
|
251
255
|
|
|
@@ -360,6 +364,10 @@ export class ChatOpenAI extends OriginalChatOpenAI<t.ChatOpenAICallOptions> {
|
|
|
360
364
|
} else if ('reasoning' in delta) {
|
|
361
365
|
chunk.additional_kwargs.reasoning_content = delta.reasoning;
|
|
362
366
|
}
|
|
367
|
+
if ('provider_specific_fields' in delta) {
|
|
368
|
+
chunk.additional_kwargs.provider_specific_fields =
|
|
369
|
+
delta.provider_specific_fields;
|
|
370
|
+
}
|
|
363
371
|
defaultRole = delta.role ?? defaultRole;
|
|
364
372
|
const newTokenIndices = {
|
|
365
373
|
prompt: options.promptIndex ?? 0,
|
|
@@ -463,6 +471,9 @@ export class AzureChatOpenAI extends OriginalAzureChatOpenAI {
|
|
|
463
471
|
public get exposedClient(): CustomOpenAIClient {
|
|
464
472
|
return this.client;
|
|
465
473
|
}
|
|
474
|
+
static lc_name(): 'LibreChatAzureOpenAI' {
|
|
475
|
+
return 'LibreChatAzureOpenAI';
|
|
476
|
+
}
|
|
466
477
|
/**
|
|
467
478
|
* Returns backwards compatible reasoning parameters from constructor params and call options
|
|
468
479
|
* @internal
|
|
@@ -607,6 +618,9 @@ export class ChatDeepSeek extends OriginalChatDeepSeek {
|
|
|
607
618
|
public get exposedClient(): CustomOpenAIClient {
|
|
608
619
|
return this.client;
|
|
609
620
|
}
|
|
621
|
+
static lc_name(): 'LibreChatDeepSeek' {
|
|
622
|
+
return 'LibreChatDeepSeek';
|
|
623
|
+
}
|
|
610
624
|
protected _getClientOptions(
|
|
611
625
|
options?: OpenAICoreRequestOptions
|
|
612
626
|
): OpenAICoreRequestOptions {
|
|
@@ -679,6 +693,10 @@ export class ChatXAI extends OriginalChatXAI {
|
|
|
679
693
|
}
|
|
680
694
|
}
|
|
681
695
|
|
|
696
|
+
static lc_name(): 'LibreChatXAI' {
|
|
697
|
+
return 'LibreChatXAI';
|
|
698
|
+
}
|
|
699
|
+
|
|
682
700
|
public get exposedClient(): CustomOpenAIClient {
|
|
683
701
|
return this.client;
|
|
684
702
|
}
|
|
@@ -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
|
|
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 {
|
|
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(): '
|
|
317
|
-
return '
|
|
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
|
+
}
|