@librechat/agents 3.0.79 → 3.0.81
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/llm/bedrock/index.cjs +35 -74
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +123 -32
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +35 -74
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +123 -32
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/types/llm/bedrock/index.d.ts +7 -4
- package/dist/types/messages/cache.d.ts +6 -2
- package/package.json +2 -1
- package/src/llm/bedrock/index.ts +42 -96
- package/src/llm/bedrock/utils/message_outputs.ts +34 -20
- package/src/messages/cache.test.ts +215 -0
- package/src/messages/cache.ts +172 -43
- package/src/scripts/thinking-bedrock.ts +159 -0
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +0 -465
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +0 -1
- package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +0 -238
- package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +0 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +0 -460
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +0 -1
- package/dist/esm/llm/bedrock/utils/message_outputs.mjs +0 -231
- package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +0 -1
package/src/llm/bedrock/index.ts
CHANGED
|
@@ -21,17 +21,6 @@ import { ChatGenerationChunk, ChatResult } from '@langchain/core/outputs';
|
|
|
21
21
|
import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
|
|
22
22
|
import type { ChatBedrockConverseInput } from '@langchain/aws';
|
|
23
23
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
24
|
-
import {
|
|
25
|
-
ConverseCommand,
|
|
26
|
-
ConverseStreamCommand,
|
|
27
|
-
} from '@aws-sdk/client-bedrock-runtime';
|
|
28
|
-
import {
|
|
29
|
-
convertToConverseMessages,
|
|
30
|
-
convertConverseMessageToLangChainMessage,
|
|
31
|
-
handleConverseStreamContentBlockStart,
|
|
32
|
-
handleConverseStreamContentBlockDelta,
|
|
33
|
-
handleConverseStreamMetadata,
|
|
34
|
-
} from './utils';
|
|
35
24
|
|
|
36
25
|
/**
|
|
37
26
|
* Service tier type for Bedrock invocations.
|
|
@@ -119,7 +108,7 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
|
|
|
119
108
|
} {
|
|
120
109
|
const baseParams = super.invocationParams(options);
|
|
121
110
|
|
|
122
|
-
|
|
111
|
+
/** Service tier from options or fall back to class-level setting */
|
|
123
112
|
const serviceTierType = options?.serviceTier ?? this.serviceTier;
|
|
124
113
|
|
|
125
114
|
return {
|
|
@@ -130,110 +119,67 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
|
|
|
130
119
|
|
|
131
120
|
/**
|
|
132
121
|
* Override _generateNonStreaming to use applicationInferenceProfile as modelId.
|
|
122
|
+
* Uses the same model-swapping pattern as streaming for consistency.
|
|
133
123
|
*/
|
|
134
124
|
override async _generateNonStreaming(
|
|
135
125
|
messages: BaseMessage[],
|
|
136
126
|
options: this['ParsedCallOptions'] & CustomChatBedrockConverseCallOptions,
|
|
137
|
-
|
|
127
|
+
runManager?: CallbackManagerForLLMRun
|
|
138
128
|
): Promise<ChatResult> {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
system: converseSystem,
|
|
147
|
-
requestMetadata: options.requestMetadata,
|
|
148
|
-
...params,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const response = await this.client.send(command, {
|
|
152
|
-
abortSignal: options.signal,
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const { output, ...responseMetadata } = response;
|
|
156
|
-
if (!output?.message) {
|
|
157
|
-
throw new Error('No message found in Bedrock response.');
|
|
129
|
+
// Temporarily swap model for applicationInferenceProfile support
|
|
130
|
+
const originalModel = this.model;
|
|
131
|
+
if (
|
|
132
|
+
this.applicationInferenceProfile != null &&
|
|
133
|
+
this.applicationInferenceProfile !== ''
|
|
134
|
+
) {
|
|
135
|
+
this.model = this.applicationInferenceProfile;
|
|
158
136
|
}
|
|
159
137
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
generations: [
|
|
167
|
-
{
|
|
168
|
-
text: typeof message.content === 'string' ? message.content : '',
|
|
169
|
-
message,
|
|
170
|
-
},
|
|
171
|
-
],
|
|
172
|
-
};
|
|
138
|
+
try {
|
|
139
|
+
return await super._generateNonStreaming(messages, options, runManager);
|
|
140
|
+
} finally {
|
|
141
|
+
// Restore original model
|
|
142
|
+
this.model = originalModel;
|
|
143
|
+
}
|
|
173
144
|
}
|
|
174
145
|
|
|
175
146
|
/**
|
|
176
147
|
* Override _streamResponseChunks to:
|
|
177
|
-
* 1. Use applicationInferenceProfile as modelId
|
|
178
|
-
* 2.
|
|
179
|
-
*
|
|
148
|
+
* 1. Use applicationInferenceProfile as modelId (by temporarily swapping this.model)
|
|
149
|
+
* 2. Strip contentBlockIndex from response_metadata to prevent merge conflicts
|
|
150
|
+
*
|
|
151
|
+
* Note: We delegate to super._streamResponseChunks() to preserve @langchain/aws's
|
|
152
|
+
* internal chunk handling which correctly preserves array content for reasoning blocks.
|
|
180
153
|
*/
|
|
181
154
|
override async *_streamResponseChunks(
|
|
182
155
|
messages: BaseMessage[],
|
|
183
156
|
options: this['ParsedCallOptions'] & CustomChatBedrockConverseCallOptions,
|
|
184
157
|
runManager?: CallbackManagerForLLMRun
|
|
185
158
|
): AsyncGenerator<ChatGenerationChunk> {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
159
|
+
// Temporarily swap model for applicationInferenceProfile support
|
|
160
|
+
const originalModel = this.model;
|
|
161
|
+
if (
|
|
162
|
+
this.applicationInferenceProfile != null &&
|
|
163
|
+
this.applicationInferenceProfile !== ''
|
|
164
|
+
) {
|
|
165
|
+
this.model = this.applicationInferenceProfile;
|
|
193
166
|
}
|
|
194
167
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (response.stream) {
|
|
208
|
-
for await (const event of response.stream) {
|
|
209
|
-
if (event.contentBlockStart != null) {
|
|
210
|
-
const chunk = handleConverseStreamContentBlockStart(
|
|
211
|
-
event.contentBlockStart
|
|
212
|
-
) as ChatGenerationChunk | undefined;
|
|
213
|
-
if (chunk !== undefined) {
|
|
214
|
-
const cleanedChunk = this.cleanChunk(chunk);
|
|
215
|
-
yield cleanedChunk;
|
|
216
|
-
await runManager?.handleLLMNewToken(cleanedChunk.text || '');
|
|
217
|
-
}
|
|
218
|
-
} else if (event.contentBlockDelta != null) {
|
|
219
|
-
const chunk = handleConverseStreamContentBlockDelta(
|
|
220
|
-
event.contentBlockDelta
|
|
221
|
-
) as ChatGenerationChunk | undefined;
|
|
222
|
-
if (chunk !== undefined) {
|
|
223
|
-
const cleanedChunk = this.cleanChunk(chunk);
|
|
224
|
-
yield cleanedChunk;
|
|
225
|
-
await runManager?.handleLLMNewToken(cleanedChunk.text || '');
|
|
226
|
-
}
|
|
227
|
-
} else if (event.metadata != null) {
|
|
228
|
-
const chunk = handleConverseStreamMetadata(event.metadata, {
|
|
229
|
-
streamUsage,
|
|
230
|
-
}) as ChatGenerationChunk | undefined;
|
|
231
|
-
if (chunk !== undefined) {
|
|
232
|
-
const cleanedChunk = this.cleanChunk(chunk);
|
|
233
|
-
yield cleanedChunk;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
168
|
+
try {
|
|
169
|
+
// Use parent's streaming logic which correctly handles reasoning content
|
|
170
|
+
const baseStream = super._streamResponseChunks(
|
|
171
|
+
messages,
|
|
172
|
+
options,
|
|
173
|
+
runManager
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
for await (const chunk of baseStream) {
|
|
177
|
+
// Clean contentBlockIndex from response_metadata to prevent merge conflicts
|
|
178
|
+
yield this.cleanChunk(chunk);
|
|
236
179
|
}
|
|
180
|
+
} finally {
|
|
181
|
+
// Restore original model
|
|
182
|
+
this.model = originalModel;
|
|
237
183
|
}
|
|
238
184
|
}
|
|
239
185
|
|
|
@@ -38,13 +38,13 @@ export function bedrockReasoningDeltaToLangchainPartialReasoningBlock(
|
|
|
38
38
|
reasoningText: { text },
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
if (signature) {
|
|
41
|
+
if (signature != null) {
|
|
42
42
|
return {
|
|
43
43
|
type: 'reasoning_content',
|
|
44
44
|
reasoningText: { signature },
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
-
if (redactedContent) {
|
|
47
|
+
if (redactedContent != null) {
|
|
48
48
|
return {
|
|
49
49
|
type: 'reasoning_content',
|
|
50
50
|
redactedContent: Buffer.from(redactedContent).toString('base64'),
|
|
@@ -65,13 +65,13 @@ export function bedrockReasoningBlockToLangchainReasoningBlock(
|
|
|
65
65
|
redactedContent?: Uint8Array;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
if (reasoningText) {
|
|
68
|
+
if (reasoningText != null) {
|
|
69
69
|
return {
|
|
70
70
|
type: 'reasoning_content',
|
|
71
71
|
reasoningText: reasoningText,
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
-
if (redactedContent) {
|
|
74
|
+
if (redactedContent != null) {
|
|
75
75
|
return {
|
|
76
76
|
type: 'reasoning_content',
|
|
77
77
|
redactedContent: Buffer.from(redactedContent).toString('base64'),
|
|
@@ -87,7 +87,7 @@ export function convertConverseMessageToLangChainMessage(
|
|
|
87
87
|
message: BedrockMessage,
|
|
88
88
|
responseMetadata: Omit<ConverseResponse, 'output'>
|
|
89
89
|
): AIMessage {
|
|
90
|
-
if (
|
|
90
|
+
if (message.content == null) {
|
|
91
91
|
throw new Error('No message content found in response.');
|
|
92
92
|
}
|
|
93
93
|
if (message.role !== 'assistant') {
|
|
@@ -99,7 +99,7 @@ export function convertConverseMessageToLangChainMessage(
|
|
|
99
99
|
let requestId: string | undefined;
|
|
100
100
|
if (
|
|
101
101
|
'$metadata' in responseMetadata &&
|
|
102
|
-
responseMetadata.$metadata &&
|
|
102
|
+
responseMetadata.$metadata != null &&
|
|
103
103
|
typeof responseMetadata.$metadata === 'object' &&
|
|
104
104
|
'requestId' in responseMetadata.$metadata
|
|
105
105
|
) {
|
|
@@ -109,7 +109,7 @@ export function convertConverseMessageToLangChainMessage(
|
|
|
109
109
|
let tokenUsage:
|
|
110
110
|
| { input_tokens: number; output_tokens: number; total_tokens: number }
|
|
111
111
|
| undefined;
|
|
112
|
-
if (responseMetadata.usage) {
|
|
112
|
+
if (responseMetadata.usage != null) {
|
|
113
113
|
const input_tokens = responseMetadata.usage.inputTokens ?? 0;
|
|
114
114
|
const output_tokens = responseMetadata.usage.outputTokens ?? 0;
|
|
115
115
|
tokenUsage = {
|
|
@@ -144,9 +144,10 @@ export function convertConverseMessageToLangChainMessage(
|
|
|
144
144
|
message.content.forEach((c) => {
|
|
145
145
|
if (
|
|
146
146
|
'toolUse' in c &&
|
|
147
|
-
c.toolUse &&
|
|
148
|
-
c.toolUse.name &&
|
|
149
|
-
c.toolUse.
|
|
147
|
+
c.toolUse != null &&
|
|
148
|
+
c.toolUse.name != null &&
|
|
149
|
+
c.toolUse.name !== '' &&
|
|
150
|
+
c.toolUse.input != null &&
|
|
150
151
|
typeof c.toolUse.input === 'object'
|
|
151
152
|
) {
|
|
152
153
|
toolCalls.push({
|
|
@@ -157,7 +158,7 @@ export function convertConverseMessageToLangChainMessage(
|
|
|
157
158
|
});
|
|
158
159
|
} else if ('text' in c && typeof c.text === 'string') {
|
|
159
160
|
content.push({ type: 'text', text: c.text });
|
|
160
|
-
} else if ('reasoningContent' in c && c.reasoningContent) {
|
|
161
|
+
} else if ('reasoningContent' in c && c.reasoningContent != null) {
|
|
161
162
|
content.push(
|
|
162
163
|
bedrockReasoningBlockToLangchainReasoningBlock(c.reasoningContent)
|
|
163
164
|
);
|
|
@@ -182,7 +183,7 @@ export function convertConverseMessageToLangChainMessage(
|
|
|
182
183
|
export function handleConverseStreamContentBlockDelta(
|
|
183
184
|
contentBlockDelta: ContentBlockDeltaEvent
|
|
184
185
|
): ChatGenerationChunk {
|
|
185
|
-
if (
|
|
186
|
+
if (contentBlockDelta.delta == null) {
|
|
186
187
|
throw new Error('No delta found in content block.');
|
|
187
188
|
}
|
|
188
189
|
|
|
@@ -196,7 +197,7 @@ export function handleConverseStreamContentBlockDelta(
|
|
|
196
197
|
},
|
|
197
198
|
}),
|
|
198
199
|
});
|
|
199
|
-
} else if (contentBlockDelta.delta.toolUse) {
|
|
200
|
+
} else if (contentBlockDelta.delta.toolUse != null) {
|
|
200
201
|
const index = contentBlockDelta.contentBlockIndex;
|
|
201
202
|
return new ChatGenerationChunk({
|
|
202
203
|
text: '',
|
|
@@ -214,15 +215,28 @@ export function handleConverseStreamContentBlockDelta(
|
|
|
214
215
|
},
|
|
215
216
|
}),
|
|
216
217
|
});
|
|
217
|
-
} else if (contentBlockDelta.delta.reasoningContent) {
|
|
218
|
+
} else if (contentBlockDelta.delta.reasoningContent != null) {
|
|
219
|
+
const reasoningBlock =
|
|
220
|
+
bedrockReasoningDeltaToLangchainPartialReasoningBlock(
|
|
221
|
+
contentBlockDelta.delta.reasoningContent
|
|
222
|
+
);
|
|
223
|
+
// Extract the text for additional_kwargs.reasoning_content (for stream handler compatibility)
|
|
224
|
+
const reasoningText =
|
|
225
|
+
'reasoningText' in reasoningBlock
|
|
226
|
+
? (reasoningBlock.reasoningText.text ??
|
|
227
|
+
reasoningBlock.reasoningText.signature ??
|
|
228
|
+
('redactedContent' in reasoningBlock
|
|
229
|
+
? reasoningBlock.redactedContent
|
|
230
|
+
: ''))
|
|
231
|
+
: '';
|
|
218
232
|
return new ChatGenerationChunk({
|
|
219
233
|
text: '',
|
|
220
234
|
message: new AIMessageChunk({
|
|
221
|
-
content: [
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
235
|
+
content: [reasoningBlock],
|
|
236
|
+
additional_kwargs: {
|
|
237
|
+
// Set reasoning_content for stream handler to detect reasoning mode
|
|
238
|
+
reasoning_content: reasoningText,
|
|
239
|
+
},
|
|
226
240
|
response_metadata: {
|
|
227
241
|
contentBlockIndex: contentBlockDelta.contentBlockIndex,
|
|
228
242
|
},
|
|
@@ -243,7 +257,7 @@ export function handleConverseStreamContentBlockStart(
|
|
|
243
257
|
): ChatGenerationChunk | null {
|
|
244
258
|
const index = contentBlockStart.contentBlockIndex;
|
|
245
259
|
|
|
246
|
-
if (contentBlockStart.start?.toolUse) {
|
|
260
|
+
if (contentBlockStart.start?.toolUse != null) {
|
|
247
261
|
return new ChatGenerationChunk({
|
|
248
262
|
text: '',
|
|
249
263
|
message: new AIMessageChunk({
|
|
@@ -835,6 +835,221 @@ describe('Multi-agent provider interoperability', () => {
|
|
|
835
835
|
});
|
|
836
836
|
});
|
|
837
837
|
|
|
838
|
+
describe('Immutability - addCacheControl does not mutate original messages', () => {
|
|
839
|
+
it('should not mutate original messages when adding cache control to string content', () => {
|
|
840
|
+
const originalMessages: TestMsg[] = [
|
|
841
|
+
{ role: 'user', content: 'Hello' },
|
|
842
|
+
{ role: 'assistant', content: 'Hi there' },
|
|
843
|
+
{ role: 'user', content: 'How are you?' },
|
|
844
|
+
];
|
|
845
|
+
|
|
846
|
+
const originalFirstContent = originalMessages[0].content;
|
|
847
|
+
const originalThirdContent = originalMessages[2].content;
|
|
848
|
+
|
|
849
|
+
const result = addCacheControl(originalMessages as never);
|
|
850
|
+
|
|
851
|
+
expect(originalMessages[0].content).toBe(originalFirstContent);
|
|
852
|
+
expect(originalMessages[2].content).toBe(originalThirdContent);
|
|
853
|
+
expect(typeof originalMessages[0].content).toBe('string');
|
|
854
|
+
expect(typeof originalMessages[2].content).toBe('string');
|
|
855
|
+
|
|
856
|
+
expect(Array.isArray(result[0].content)).toBe(true);
|
|
857
|
+
expect(Array.isArray(result[2].content)).toBe(true);
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
it('should not mutate original messages when adding cache control to array content', () => {
|
|
861
|
+
const originalMessages: TestMsg[] = [
|
|
862
|
+
{
|
|
863
|
+
role: 'user',
|
|
864
|
+
content: [{ type: ContentTypes.TEXT, text: 'Hello' }],
|
|
865
|
+
},
|
|
866
|
+
{ role: 'assistant', content: 'Hi there' },
|
|
867
|
+
{
|
|
868
|
+
role: 'user',
|
|
869
|
+
content: [{ type: ContentTypes.TEXT, text: 'How are you?' }],
|
|
870
|
+
},
|
|
871
|
+
];
|
|
872
|
+
|
|
873
|
+
const originalFirstBlock = {
|
|
874
|
+
...(originalMessages[0].content as MessageContentComplex[])[0],
|
|
875
|
+
};
|
|
876
|
+
const originalThirdBlock = {
|
|
877
|
+
...(originalMessages[2].content as MessageContentComplex[])[0],
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
const result = addCacheControl(originalMessages as never);
|
|
881
|
+
|
|
882
|
+
const firstContent = originalMessages[0].content as MessageContentComplex[];
|
|
883
|
+
const thirdContent = originalMessages[2].content as MessageContentComplex[];
|
|
884
|
+
|
|
885
|
+
expect('cache_control' in firstContent[0]).toBe(false);
|
|
886
|
+
expect('cache_control' in thirdContent[0]).toBe(false);
|
|
887
|
+
expect(firstContent[0]).toEqual(originalFirstBlock);
|
|
888
|
+
expect(thirdContent[0]).toEqual(originalThirdBlock);
|
|
889
|
+
|
|
890
|
+
const resultFirstContent = result[0].content as MessageContentComplex[];
|
|
891
|
+
const resultThirdContent = result[2].content as MessageContentComplex[];
|
|
892
|
+
expect('cache_control' in resultFirstContent[0]).toBe(true);
|
|
893
|
+
expect('cache_control' in resultThirdContent[0]).toBe(true);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
it('should not mutate original messages when stripping existing cache control', () => {
|
|
897
|
+
const originalMessages: TestMsg[] = [
|
|
898
|
+
{
|
|
899
|
+
role: 'user',
|
|
900
|
+
content: [
|
|
901
|
+
{
|
|
902
|
+
type: ContentTypes.TEXT,
|
|
903
|
+
text: 'Hello',
|
|
904
|
+
cache_control: { type: 'ephemeral' },
|
|
905
|
+
} as MessageContentComplex,
|
|
906
|
+
],
|
|
907
|
+
},
|
|
908
|
+
{ role: 'assistant', content: 'Hi there' },
|
|
909
|
+
{
|
|
910
|
+
role: 'user',
|
|
911
|
+
content: [{ type: ContentTypes.TEXT, text: 'How are you?' }],
|
|
912
|
+
},
|
|
913
|
+
];
|
|
914
|
+
|
|
915
|
+
const originalFirstBlock = (
|
|
916
|
+
originalMessages[0].content as MessageContentComplex[]
|
|
917
|
+
)[0];
|
|
918
|
+
|
|
919
|
+
addCacheControl(originalMessages as never);
|
|
920
|
+
|
|
921
|
+
expect('cache_control' in originalFirstBlock).toBe(true);
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
describe('Immutability - addBedrockCacheControl does not mutate original messages', () => {
|
|
926
|
+
it('should not mutate original messages when adding cache points to string content', () => {
|
|
927
|
+
const originalMessages: TestMsg[] = [
|
|
928
|
+
{ role: 'user', content: 'Hello' },
|
|
929
|
+
{ role: 'assistant', content: 'Hi there' },
|
|
930
|
+
];
|
|
931
|
+
|
|
932
|
+
const originalFirstContent = originalMessages[0].content;
|
|
933
|
+
const originalSecondContent = originalMessages[1].content;
|
|
934
|
+
|
|
935
|
+
const result = addBedrockCacheControl(originalMessages);
|
|
936
|
+
|
|
937
|
+
expect(originalMessages[0].content).toBe(originalFirstContent);
|
|
938
|
+
expect(originalMessages[1].content).toBe(originalSecondContent);
|
|
939
|
+
expect(typeof originalMessages[0].content).toBe('string');
|
|
940
|
+
expect(typeof originalMessages[1].content).toBe('string');
|
|
941
|
+
|
|
942
|
+
expect(Array.isArray(result[0].content)).toBe(true);
|
|
943
|
+
expect(Array.isArray(result[1].content)).toBe(true);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it('should not mutate original messages when adding cache points to array content', () => {
|
|
947
|
+
const originalMessages: TestMsg[] = [
|
|
948
|
+
{
|
|
949
|
+
role: 'user',
|
|
950
|
+
content: [{ type: ContentTypes.TEXT, text: 'Hello' }],
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
role: 'assistant',
|
|
954
|
+
content: [{ type: ContentTypes.TEXT, text: 'Hi there' }],
|
|
955
|
+
},
|
|
956
|
+
];
|
|
957
|
+
|
|
958
|
+
const originalFirstContentLength = (
|
|
959
|
+
originalMessages[0].content as MessageContentComplex[]
|
|
960
|
+
).length;
|
|
961
|
+
const originalSecondContentLength = (
|
|
962
|
+
originalMessages[1].content as MessageContentComplex[]
|
|
963
|
+
).length;
|
|
964
|
+
|
|
965
|
+
const result = addBedrockCacheControl(originalMessages);
|
|
966
|
+
|
|
967
|
+
const firstContent = originalMessages[0].content as MessageContentComplex[];
|
|
968
|
+
const secondContent = originalMessages[1]
|
|
969
|
+
.content as MessageContentComplex[];
|
|
970
|
+
|
|
971
|
+
expect(firstContent.length).toBe(originalFirstContentLength);
|
|
972
|
+
expect(secondContent.length).toBe(originalSecondContentLength);
|
|
973
|
+
expect(firstContent.some((b) => 'cachePoint' in b)).toBe(false);
|
|
974
|
+
expect(secondContent.some((b) => 'cachePoint' in b)).toBe(false);
|
|
975
|
+
|
|
976
|
+
const resultFirstContent = result[0].content as MessageContentComplex[];
|
|
977
|
+
const resultSecondContent = result[1].content as MessageContentComplex[];
|
|
978
|
+
expect(resultFirstContent.length).toBe(originalFirstContentLength + 1);
|
|
979
|
+
expect(resultSecondContent.length).toBe(originalSecondContentLength + 1);
|
|
980
|
+
expect(resultFirstContent.some((b) => 'cachePoint' in b)).toBe(true);
|
|
981
|
+
expect(resultSecondContent.some((b) => 'cachePoint' in b)).toBe(true);
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
it('should not mutate original messages when stripping existing cache control', () => {
|
|
985
|
+
const originalMessages: TestMsg[] = [
|
|
986
|
+
{
|
|
987
|
+
role: 'user',
|
|
988
|
+
content: [
|
|
989
|
+
{
|
|
990
|
+
type: ContentTypes.TEXT,
|
|
991
|
+
text: 'Hello',
|
|
992
|
+
cache_control: { type: 'ephemeral' },
|
|
993
|
+
} as MessageContentComplex,
|
|
994
|
+
],
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
role: 'assistant',
|
|
998
|
+
content: [
|
|
999
|
+
{ type: ContentTypes.TEXT, text: 'Hi there' },
|
|
1000
|
+
{ cachePoint: { type: 'default' } },
|
|
1001
|
+
],
|
|
1002
|
+
},
|
|
1003
|
+
];
|
|
1004
|
+
|
|
1005
|
+
const originalFirstBlock = (
|
|
1006
|
+
originalMessages[0].content as MessageContentComplex[]
|
|
1007
|
+
)[0];
|
|
1008
|
+
const originalSecondContentLength = (
|
|
1009
|
+
originalMessages[1].content as MessageContentComplex[]
|
|
1010
|
+
).length;
|
|
1011
|
+
|
|
1012
|
+
addBedrockCacheControl(originalMessages);
|
|
1013
|
+
|
|
1014
|
+
expect('cache_control' in originalFirstBlock).toBe(true);
|
|
1015
|
+
expect(
|
|
1016
|
+
(originalMessages[1].content as MessageContentComplex[]).length
|
|
1017
|
+
).toBe(originalSecondContentLength);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
it('should allow different providers to process same messages without cross-contamination', () => {
|
|
1021
|
+
const sharedMessages: TestMsg[] = [
|
|
1022
|
+
{
|
|
1023
|
+
role: 'user',
|
|
1024
|
+
content: [{ type: ContentTypes.TEXT, text: 'Shared message 1' }],
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
role: 'assistant',
|
|
1028
|
+
content: [{ type: ContentTypes.TEXT, text: 'Shared response 1' }],
|
|
1029
|
+
},
|
|
1030
|
+
];
|
|
1031
|
+
|
|
1032
|
+
const bedrockResult = addBedrockCacheControl(sharedMessages);
|
|
1033
|
+
|
|
1034
|
+
const anthropicResult = addCacheControl(sharedMessages as never);
|
|
1035
|
+
|
|
1036
|
+
const originalFirstContent = sharedMessages[0]
|
|
1037
|
+
.content as MessageContentComplex[];
|
|
1038
|
+
expect(originalFirstContent.some((b) => 'cachePoint' in b)).toBe(false);
|
|
1039
|
+
expect('cache_control' in originalFirstContent[0]).toBe(false);
|
|
1040
|
+
|
|
1041
|
+
const bedrockFirstContent = bedrockResult[0]
|
|
1042
|
+
.content as MessageContentComplex[];
|
|
1043
|
+
expect(bedrockFirstContent.some((b) => 'cachePoint' in b)).toBe(true);
|
|
1044
|
+
expect('cache_control' in bedrockFirstContent[0]).toBe(false);
|
|
1045
|
+
|
|
1046
|
+
const anthropicFirstContent = anthropicResult[0]
|
|
1047
|
+
.content as MessageContentComplex[];
|
|
1048
|
+
expect(anthropicFirstContent.some((b) => 'cachePoint' in b)).toBe(false);
|
|
1049
|
+
expect('cache_control' in anthropicFirstContent[0]).toBe(true);
|
|
1050
|
+
});
|
|
1051
|
+
});
|
|
1052
|
+
|
|
838
1053
|
describe('Multi-turn cache cleanup', () => {
|
|
839
1054
|
it('strips stale Bedrock cache points from previous turns before applying new ones', () => {
|
|
840
1055
|
const messages: TestMsg[] = [
|