@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.
- package/dist/cjs/agents/AgentContext.cjs +6 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +16 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +21 -0
- package/dist/cjs/instrumentation.cjs.map +1 -0
- package/dist/cjs/llm/openai/index.cjs +0 -4
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +5 -1
- 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/format.cjs +0 -27
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +57 -5
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +7 -0
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/utils/misc.cjs +8 -1
- package/dist/cjs/utils/misc.cjs.map +1 -1
- package/dist/cjs/utils/title.cjs +54 -25
- package/dist/cjs/utils/title.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +6 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +16 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +19 -0
- package/dist/esm/instrumentation.mjs.map +1 -0
- package/dist/esm/llm/openai/index.mjs +0 -4
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +4 -2
- 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/format.mjs +1 -27
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +57 -5
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +7 -0
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/utils/misc.mjs +8 -2
- package/dist/esm/utils/misc.mjs.map +1 -1
- package/dist/esm/utils/title.mjs +54 -25
- package/dist/esm/utils/title.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +4 -1
- package/dist/types/instrumentation.d.ts +1 -0
- 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 +1 -7
- package/dist/types/messages/index.d.ts +2 -0
- package/dist/types/run.d.ts +2 -1
- package/dist/types/types/graph.d.ts +2 -0
- package/dist/types/types/messages.d.ts +4 -0
- package/dist/types/utils/misc.d.ts +1 -0
- package/package.json +5 -1
- package/src/agents/AgentContext.ts +8 -0
- package/src/graphs/Graph.ts +23 -1
- package/src/instrumentation.ts +22 -0
- package/src/llm/anthropic/llm.spec.ts +1 -1
- package/src/llm/openai/index.ts +0 -5
- 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/format.ts +0 -34
- package/src/messages/index.ts +2 -0
- package/src/run.ts +82 -10
- package/src/scripts/ant_web_search.ts +1 -1
- package/src/scripts/handoff-test.ts +1 -1
- package/src/scripts/multi-agent-chain.ts +4 -4
- package/src/scripts/multi-agent-conditional.ts +4 -4
- package/src/scripts/multi-agent-document-review-chain.ts +4 -4
- package/src/scripts/multi-agent-parallel.ts +10 -8
- package/src/scripts/multi-agent-sequence.ts +3 -3
- package/src/scripts/multi-agent-supervisor.ts +5 -3
- package/src/scripts/multi-agent-test.ts +2 -2
- package/src/scripts/simple.ts +8 -0
- package/src/scripts/test-custom-prompt-key.ts +4 -4
- package/src/scripts/test-handoff-input.ts +3 -3
- package/src/scripts/test-multi-agent-list-handoff.ts +2 -2
- package/src/stream.ts +9 -2
- package/src/types/graph.ts +2 -0
- package/src/types/messages.ts +4 -0
- package/src/utils/misc.ts +33 -21
- 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
|
-
|
|
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,
|
|
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.
|
package/dist/types/run.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@librechat/agents",
|
|
3
|
-
"version": "3.0.00-
|
|
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
|
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -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
|
-
|
|
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-
|
|
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';
|
package/src/llm/openai/index.ts
CHANGED
|
@@ -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
|
+
}
|