@librechat/agents 3.2.46 → 3.2.52

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 (57) hide show
  1. package/dist/cjs/graphs/MultiAgentGraph.cjs +6 -6
  2. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  3. package/dist/cjs/llm/anthropic/index.cjs +4 -3
  4. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  5. package/dist/cjs/llm/anthropic/utils/tools.cjs +2 -1
  6. package/dist/cjs/llm/anthropic/utils/tools.cjs.map +1 -1
  7. package/dist/cjs/llm/bedrock/cachePoints.cjs +28 -0
  8. package/dist/cjs/llm/bedrock/cachePoints.cjs.map +1 -0
  9. package/dist/cjs/llm/bedrock/index.cjs +10 -1
  10. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  11. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  12. package/dist/cjs/run.cjs +21 -3
  13. package/dist/cjs/run.cjs.map +1 -1
  14. package/dist/cjs/tools/ToolNode.cjs +26 -5
  15. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  16. package/dist/esm/graphs/MultiAgentGraph.mjs +7 -7
  17. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  18. package/dist/esm/llm/anthropic/index.mjs +4 -3
  19. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  20. package/dist/esm/llm/anthropic/utils/tools.mjs +2 -1
  21. package/dist/esm/llm/anthropic/utils/tools.mjs.map +1 -1
  22. package/dist/esm/llm/bedrock/cachePoints.mjs +28 -0
  23. package/dist/esm/llm/bedrock/cachePoints.mjs.map +1 -0
  24. package/dist/esm/llm/bedrock/index.mjs +10 -1
  25. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  26. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  27. package/dist/esm/run.mjs +21 -3
  28. package/dist/esm/run.mjs.map +1 -1
  29. package/dist/esm/tools/ToolNode.mjs +26 -5
  30. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  31. package/dist/types/llm/anthropic/utils/tools.d.ts +1 -1
  32. package/dist/types/llm/bedrock/cachePoints.d.ts +9 -0
  33. package/dist/types/run.d.ts +15 -1
  34. package/dist/types/tools/ToolNode.d.ts +9 -2
  35. package/package.json +16 -21
  36. package/src/graphs/MultiAgentGraph.ts +7 -12
  37. package/src/llm/anthropic/index.ts +13 -2
  38. package/src/llm/anthropic/inherited-content-utils.spec.ts +244 -0
  39. package/src/llm/anthropic/inherited-stream-events.spec.ts +752 -0
  40. package/src/llm/anthropic/inherited-strict.spec.ts +302 -0
  41. package/src/llm/anthropic/llm.spec.ts +65 -0
  42. package/src/llm/anthropic/utils/tools.ts +7 -1
  43. package/src/llm/bedrock/cachePoints.ts +86 -0
  44. package/src/llm/bedrock/index.ts +9 -0
  45. package/src/llm/bedrock/inherited-cache.spec.ts +144 -0
  46. package/src/llm/bedrock/inherited.spec.ts +724 -0
  47. package/src/llm/google/inherited-stream-events.spec.ts +350 -0
  48. package/src/llm/openai/inherited-deepseek.spec.ts +347 -0
  49. package/src/llm/openai/inherited-xai.spec.ts +416 -0
  50. package/src/llm/openai/llm.spec.ts +1568 -0
  51. package/src/llm/openrouter/index.ts +1 -3
  52. package/src/llm/vertexai/inherited-stream-events.spec.ts +271 -0
  53. package/src/run.ts +31 -3
  54. package/src/scripts/handoff-test.ts +5 -6
  55. package/src/tools/ToolNode.ts +43 -5
  56. package/src/tools/__tests__/ToolNode.runtimeState.test.ts +120 -0
  57. package/src/tools/__tests__/hitl.test.ts +162 -0
@@ -0,0 +1,302 @@
1
+ // Inherited from @langchain/anthropic@1.5.1 tests/chat_models.test.ts (strict tool calling), adapted to LibreChat's fork.
2
+ import { z } from 'zod';
3
+ import { tool } from '@langchain/core/tools';
4
+ import { describe, test, expect, jest } from '@jest/globals';
5
+ import { CustomAnthropic as ChatAnthropic } from '@/llm/anthropic';
6
+
7
+ test('extras with cache_control are merged into tool definitions', () => {
8
+ const searchFiles = tool(
9
+ async (input: { query: string }) => {
10
+ return `Results for ${input.query}`;
11
+ },
12
+ {
13
+ name: 'search_files',
14
+ description: 'Search files.',
15
+ schema: z.object({
16
+ query: z.string(),
17
+ }),
18
+ extras: { cache_control: { type: 'ephemeral' } },
19
+ }
20
+ );
21
+
22
+ const model = new ChatAnthropic({
23
+ modelName: 'claude-haiku-4-5-20251001',
24
+ anthropicApiKey: 'testing',
25
+ });
26
+
27
+ const formattedTools = model.formatStructuredToolToAnthropic([searchFiles]);
28
+
29
+ expect(formattedTools).toBeDefined();
30
+ const searchTool = formattedTools?.find(
31
+ (t) => 'name' in t && t.name === 'search_files'
32
+ );
33
+ expect(searchTool).toBeDefined();
34
+ expect(searchTool).toHaveProperty('cache_control', { type: 'ephemeral' });
35
+ });
36
+
37
+ describe('strict tool calling', () => {
38
+ const weatherTool = tool(
39
+ async (input: { location: string }) => `Weather in ${input.location}`,
40
+ {
41
+ name: 'get_current_weather',
42
+ description: 'Get the current weather in a location',
43
+ schema: z.object({
44
+ location: z.string().describe('The location to get the weather for'),
45
+ }),
46
+ }
47
+ );
48
+
49
+ const strictWeatherTool = tool(
50
+ async (input: { location: string }) => `Weather in ${input.location}`,
51
+ {
52
+ name: 'get_current_weather',
53
+ description: 'Get the current weather in a location',
54
+ schema: z.object({ location: z.string() }),
55
+ extras: { strict: true },
56
+ }
57
+ );
58
+
59
+ const openAIShapedWeatherTool = {
60
+ type: 'function' as const,
61
+ function: {
62
+ name: 'get_current_weather',
63
+ description: 'Get the current weather in a location',
64
+ parameters: {
65
+ type: 'object',
66
+ properties: { location: { type: 'string' } },
67
+ required: ['location'],
68
+ },
69
+ strict: true,
70
+ },
71
+ };
72
+
73
+ const anthropicShapedWeatherTool = {
74
+ name: 'get_current_weather',
75
+ description: 'Get the current weather in a location',
76
+ input_schema: {
77
+ type: 'object' as const,
78
+ properties: { location: { type: 'string' } },
79
+ required: ['location'],
80
+ },
81
+ strict: true,
82
+ };
83
+
84
+ type MockFetch = jest.Mock<
85
+ (url: string | URL | Request, options?: RequestInit) => Promise<Response>
86
+ >;
87
+
88
+ function makeMockFetch(): MockFetch {
89
+ const mockFetch =
90
+ jest.fn<
91
+ (
92
+ url: string | URL | Request,
93
+ options?: RequestInit
94
+ ) => Promise<Response>
95
+ >();
96
+ mockFetch.mockImplementation((_url, _options) =>
97
+ Promise.resolve(
98
+ new Response(
99
+ JSON.stringify({
100
+ id: 'msg_test',
101
+ type: 'message',
102
+ role: 'assistant',
103
+ model: 'claude-haiku-4-5-20251001',
104
+ // `tool_use` shape (not text) is required so `withStructuredOutput`
105
+ // can parse a tool call out of the response.
106
+ content: [
107
+ {
108
+ type: 'tool_use',
109
+ id: 'toolu_test',
110
+ name: 'get_current_weather',
111
+ input: { location: 'test' },
112
+ },
113
+ ],
114
+ stop_reason: 'tool_use',
115
+ stop_sequence: null,
116
+ usage: { input_tokens: 1, output_tokens: 1 },
117
+ }),
118
+ {
119
+ status: 200,
120
+ headers: { 'content-type': 'application/json' },
121
+ }
122
+ )
123
+ )
124
+ );
125
+ return mockFetch;
126
+ }
127
+
128
+ function makeMockedModel(): { model: ChatAnthropic; mockFetch: MockFetch } {
129
+ const mockFetch = makeMockFetch();
130
+ const model = new ChatAnthropic({
131
+ model: 'claude-haiku-4-5-20251001',
132
+ anthropicApiKey: 'testing',
133
+ clientOptions: { fetch: mockFetch },
134
+ maxRetries: 0,
135
+ });
136
+ return { model, mockFetch };
137
+ }
138
+
139
+ function getRequestTools(
140
+ mockFetch: MockFetch
141
+ ): Array<Record<string, unknown>> {
142
+ expect(mockFetch).toHaveBeenCalled();
143
+ const [, init] = mockFetch.mock.calls[0];
144
+ if (!init || !init.body) {
145
+ throw new Error('Body not found in request.');
146
+ }
147
+ const body = JSON.parse(init.body as string) as {
148
+ tools: Array<Record<string, unknown>>;
149
+ };
150
+ return body.tools;
151
+ }
152
+
153
+ test('applies strict from .bindTools call args', async () => {
154
+ const { model, mockFetch } = makeMockedModel();
155
+ const modelWithTools = model.bindTools([weatherTool], { strict: true });
156
+ await modelWithTools.invoke("What's the weather like?");
157
+ expect(getRequestTools(mockFetch)[0]).toHaveProperty('strict', true);
158
+ });
159
+
160
+ test('applies strict from .withConfig call options', async () => {
161
+ const { model, mockFetch } = makeMockedModel();
162
+ const modelWithTools = model.withConfig({
163
+ tools: [weatherTool],
164
+ strict: true,
165
+ });
166
+ await modelWithTools.invoke("What's the weather like?");
167
+ expect(getRequestTools(mockFetch)[0]).toHaveProperty('strict', true);
168
+ });
169
+
170
+ test('applies strict from .bindTools(...).withConfig({ strict })', async () => {
171
+ const { model, mockFetch } = makeMockedModel();
172
+ const modelWithTools = model
173
+ .bindTools([weatherTool])
174
+ .withConfig({ strict: true });
175
+ await modelWithTools.invoke("What's the weather like?");
176
+ expect(getRequestTools(mockFetch)[0]).toHaveProperty('strict', true);
177
+ });
178
+
179
+ test('applies strict from per-call invoke options', async () => {
180
+ const { model, mockFetch } = makeMockedModel();
181
+ const modelWithTools = model.bindTools([weatherTool]);
182
+ await modelWithTools.invoke("What's the weather like?", { strict: true });
183
+ expect(getRequestTools(mockFetch)[0]).toHaveProperty('strict', true);
184
+ });
185
+
186
+ test('per-call invoke strict overrides .bindTools strict', async () => {
187
+ const { model, mockFetch } = makeMockedModel();
188
+ const modelWithTools = model.bindTools([weatherTool], { strict: true });
189
+ await modelWithTools.invoke("What's the weather like?", { strict: false });
190
+ expect(getRequestTools(mockFetch)[0]).toHaveProperty('strict', false);
191
+ });
192
+
193
+ test('applies strict from .withStructuredOutput config', async () => {
194
+ const { model, mockFetch } = makeMockedModel();
195
+ const modelWithTools = model.withStructuredOutput(
196
+ z.object({
197
+ location: z.string().describe('The location to get the weather for'),
198
+ }),
199
+ { strict: true, method: 'functionCalling' }
200
+ );
201
+ await modelWithTools.invoke("What's the weather like?");
202
+ expect(getRequestTools(mockFetch)[0]).toHaveProperty('strict', true);
203
+ });
204
+
205
+ test('omits strict when not provided anywhere', async () => {
206
+ const { model, mockFetch } = makeMockedModel();
207
+ const modelWithTools = model.bindTools([weatherTool]);
208
+ await modelWithTools.invoke("What's the weather like?");
209
+ expect(getRequestTools(mockFetch)[0]).not.toHaveProperty('strict');
210
+ });
211
+
212
+ test('omits strict when not passed in .withStructuredOutput', async () => {
213
+ const { model, mockFetch } = makeMockedModel();
214
+ const modelWithTools = model.withStructuredOutput(
215
+ z.object({
216
+ location: z.string().describe('The location to get the weather for'),
217
+ }),
218
+ { method: 'functionCalling' }
219
+ );
220
+ await modelWithTools.invoke("What's the weather like?");
221
+ expect(getRequestTools(mockFetch)[0]).not.toHaveProperty('strict');
222
+ });
223
+
224
+ test('per-tool extras.strict applies to that tool only', async () => {
225
+ const { model, mockFetch } = makeMockedModel();
226
+ const looseSearchTool = tool(
227
+ async (input: { query: string }) => `Results for ${input.query}`,
228
+ {
229
+ name: 'search',
230
+ description: 'Search the web',
231
+ schema: z.object({ query: z.string() }),
232
+ }
233
+ );
234
+ const modelWithTools = model.bindTools([
235
+ strictWeatherTool,
236
+ looseSearchTool,
237
+ ]);
238
+ await modelWithTools.invoke("What's the weather like?");
239
+ const tools = getRequestTools(mockFetch);
240
+ const strictTool = tools.find((t) => t.name === 'get_current_weather');
241
+ const looseTool = tools.find((t) => t.name === 'search');
242
+ expect(strictTool).toHaveProperty('strict', true);
243
+ expect(looseTool).not.toHaveProperty('strict');
244
+ });
245
+
246
+ test('per-tool function.strict applies on OpenAI-shaped tools', async () => {
247
+ const { model, mockFetch } = makeMockedModel();
248
+ const modelWithTools = model.bindTools([openAIShapedWeatherTool]);
249
+ await modelWithTools.invoke("What's the weather like?");
250
+ expect(getRequestTools(mockFetch)[0]).toHaveProperty('strict', true);
251
+ });
252
+
253
+ test("per-call strict overrides OpenAI-shaped tool's function.strict", async () => {
254
+ const { model, mockFetch } = makeMockedModel();
255
+ const modelWithTools = model.bindTools([openAIShapedWeatherTool], {
256
+ strict: false,
257
+ });
258
+ await modelWithTools.invoke("What's the weather like?");
259
+ expect(getRequestTools(mockFetch)[0]).toHaveProperty('strict', false);
260
+ });
261
+
262
+ test('per-tool native strict applies on Anthropic-shaped tools', async () => {
263
+ const { model, mockFetch } = makeMockedModel();
264
+ const modelWithTools = model.bindTools([anthropicShapedWeatherTool]);
265
+ await modelWithTools.invoke("What's the weather like?");
266
+ expect(getRequestTools(mockFetch)[0]).toHaveProperty('strict', true);
267
+ });
268
+
269
+ test("per-call strict overrides Anthropic-shaped tool's own strict", async () => {
270
+ const { model, mockFetch } = makeMockedModel();
271
+ const modelWithTools = model.bindTools([anthropicShapedWeatherTool], {
272
+ strict: false,
273
+ });
274
+ await modelWithTools.invoke("What's the weather like?");
275
+ expect(getRequestTools(mockFetch)[0]).toHaveProperty('strict', false);
276
+ });
277
+
278
+ test('per-call strict overrides per-tool extras.strict', async () => {
279
+ const { model, mockFetch } = makeMockedModel();
280
+ const modelWithTools = model.bindTools([strictWeatherTool], {
281
+ strict: false,
282
+ });
283
+ await modelWithTools.invoke("What's the weather like?");
284
+ expect(getRequestTools(mockFetch)[0]).toHaveProperty('strict', false);
285
+ });
286
+
287
+ test.each([['jsonSchema'], ['jsonMode']])(
288
+ 'withStructuredOutput throws when strict is set with method = %s',
289
+ (method) => {
290
+ const model = new ChatAnthropic({
291
+ model: 'claude-haiku-4-5-20251001',
292
+ anthropicApiKey: 'testing',
293
+ });
294
+ expect(() =>
295
+ model.withStructuredOutput(z.object({ location: z.string() }), {
296
+ strict: true,
297
+ method: method as 'jsonSchema' | 'jsonMode',
298
+ })
299
+ ).toThrow(/strict.*functionCalling/);
300
+ }
301
+ );
302
+ });
@@ -2087,6 +2087,71 @@ describe('Citations', () => {
2087
2087
  });
2088
2088
  });
2089
2089
 
2090
+ // Inherited from @langchain/anthropic@1.5.1 tests/chat_models.test.ts — verifies
2091
+ // the fork honors upstream's `thinkingExplicitlySet` gating in invocationParams.
2092
+ describe('invocationParams thinking gating', () => {
2093
+ test('omits thinking when not explicitly configured', () => {
2094
+ const model = new ChatAnthropic({
2095
+ model: 'claude-haiku-4-5-20251001',
2096
+ apiKey: 'testing',
2097
+ });
2098
+
2099
+ expect(model.invocationParams({}).thinking).toBeUndefined();
2100
+ });
2101
+
2102
+ test('includes thinking when explicitly disabled', () => {
2103
+ const model = new ChatAnthropic({
2104
+ model: 'claude-haiku-4-5-20251001',
2105
+ apiKey: 'testing',
2106
+ thinking: { type: 'disabled' },
2107
+ });
2108
+
2109
+ expect(model.invocationParams({}).thinking).toEqual({ type: 'disabled' });
2110
+ });
2111
+
2112
+ test('includes thinking when explicitly enabled', () => {
2113
+ const model = new ChatAnthropic({
2114
+ model: 'claude-haiku-4-5-20251001',
2115
+ temperature: 1,
2116
+ apiKey: 'testing',
2117
+ thinking: { type: 'enabled', budget_tokens: 1000 },
2118
+ });
2119
+
2120
+ expect(model.invocationParams({}).thinking).toEqual({
2121
+ type: 'enabled',
2122
+ budget_tokens: 1000,
2123
+ });
2124
+ });
2125
+ });
2126
+
2127
+ // Inherited from @langchain/anthropic@1.5.1 — top-level request cache_control.
2128
+ describe('invocationParams cache_control', () => {
2129
+ const newModel = () =>
2130
+ new ChatAnthropic({
2131
+ model: 'claude-haiku-4-5-20251001',
2132
+ apiKey: 'testing',
2133
+ });
2134
+
2135
+ test('includes cache_control when provided in call options', () => {
2136
+ expect(
2137
+ newModel().invocationParams({ cache_control: { type: 'ephemeral' } })
2138
+ .cache_control
2139
+ ).toEqual({ type: 'ephemeral' });
2140
+ });
2141
+
2142
+ test('includes cache_control with 1h ttl', () => {
2143
+ expect(
2144
+ newModel().invocationParams({
2145
+ cache_control: { type: 'ephemeral', ttl: '1h' },
2146
+ }).cache_control
2147
+ ).toEqual({ type: 'ephemeral', ttl: '1h' });
2148
+ });
2149
+
2150
+ test('omits cache_control when not provided', () => {
2151
+ expect(newModel().invocationParams({}).cache_control).toBeUndefined();
2152
+ });
2153
+ });
2154
+
2090
2155
  describe('Opus 4.7', () => {
2091
2156
  test('default max_tokens for claude-opus-4-7 is 16384', () => {
2092
2157
  const model = new ChatAnthropic({
@@ -7,10 +7,12 @@ export function handleToolChoice(
7
7
  | Anthropic.Messages.ToolChoiceAuto
8
8
  | Anthropic.Messages.ToolChoiceAny
9
9
  | Anthropic.Messages.ToolChoiceTool
10
+ | Anthropic.Messages.ToolChoiceNone
10
11
  | undefined {
11
12
  if (toolChoice == null) {
12
13
  return undefined;
13
- } else if (toolChoice === 'any') {
14
+ } else if (toolChoice === 'any' || toolChoice === 'required') {
15
+ // "required" is the OpenAI-style alias for forcing tool use.
14
16
  return {
15
17
  type: 'any',
16
18
  };
@@ -18,6 +20,10 @@ export function handleToolChoice(
18
20
  return {
19
21
  type: 'auto',
20
22
  };
23
+ } else if (toolChoice === 'none') {
24
+ return {
25
+ type: 'none',
26
+ };
21
27
  } else if (typeof toolChoice === 'string') {
22
28
  return {
23
29
  type: 'tool',
@@ -0,0 +1,86 @@
1
+ // Vendored from @langchain/aws@1.4.2 (utils/message_inputs.ts) because the
2
+ // upstream `applyCachePointsToConversePayload` export is internal.
3
+ import type {
4
+ BedrockPromptCacheControl,
5
+ ConverseCommandParams,
6
+ } from '@langchain/aws';
7
+ import type * as Bedrock from '@aws-sdk/client-bedrock-runtime';
8
+
9
+ function isConverseCachePoint(block: unknown): boolean {
10
+ return Boolean(
11
+ typeof block === 'object' &&
12
+ block !== null &&
13
+ 'cachePoint' in block &&
14
+ block.cachePoint &&
15
+ typeof block.cachePoint === 'object' &&
16
+ block.cachePoint !== null &&
17
+ 'type' in block.cachePoint
18
+ );
19
+ }
20
+
21
+ function createConverseCachePointBlock(
22
+ cacheControl: BedrockPromptCacheControl,
23
+ isNovaModel: boolean
24
+ ): { cachePoint: { type: 'default'; ttl?: '1h' } } {
25
+ const ttl =
26
+ !isNovaModel && cacheControl.ttl && cacheControl.ttl !== '5m'
27
+ ? cacheControl.ttl
28
+ : undefined;
29
+ return {
30
+ cachePoint: {
31
+ type: 'default',
32
+ ...(ttl ? { ttl } : {}),
33
+ },
34
+ };
35
+ }
36
+
37
+ export function applyCachePointsToConversePayload(fields: {
38
+ cacheControl?: BedrockPromptCacheControl;
39
+ system: Bedrock.SystemContentBlock[];
40
+ messages: Bedrock.Message[];
41
+ params?: Partial<ConverseCommandParams>;
42
+ modelId: string;
43
+ }): void {
44
+ const { cacheControl, system, messages, params, modelId } = fields;
45
+ if (!cacheControl) {
46
+ return;
47
+ }
48
+
49
+ const isNovaModel = modelId.toLowerCase().includes('amazon.nova');
50
+ const cacheBlock = createConverseCachePointBlock(cacheControl, isNovaModel);
51
+
52
+ if (
53
+ system.length > 0 &&
54
+ !system.some((block) => isConverseCachePoint(block))
55
+ ) {
56
+ system.push(cacheBlock);
57
+ }
58
+
59
+ const lastMessage = messages[messages.length - 1];
60
+ const lastContent = lastMessage.content;
61
+ if (Array.isArray(lastContent)) {
62
+ const hasNovaToolBlock =
63
+ isNovaModel &&
64
+ lastContent.some(
65
+ (block) =>
66
+ typeof block === 'object' &&
67
+ block !== null &&
68
+ ('toolResult' in block || 'toolUse' in block)
69
+ );
70
+ if (
71
+ !hasNovaToolBlock &&
72
+ !lastContent.some((block) => isConverseCachePoint(block))
73
+ ) {
74
+ lastContent.push(cacheBlock);
75
+ }
76
+ }
77
+
78
+ const tools = params?.toolConfig?.tools;
79
+ if (
80
+ !isNovaModel &&
81
+ Array.isArray(tools) &&
82
+ !tools.some((tool) => isConverseCachePoint(tool))
83
+ ) {
84
+ tools.push(cacheBlock as unknown as Bedrock.Tool);
85
+ }
86
+ }
@@ -44,6 +44,7 @@ import {
44
44
  supportsBedrockToolCache,
45
45
  type PromptCacheTtl,
46
46
  } from '@/messages/cache';
47
+ import { applyCachePointsToConversePayload } from './cachePoints';
47
48
  import { insertBedrockToolCachePoint } from './toolCache';
48
49
 
49
50
  /**
@@ -245,6 +246,14 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
245
246
 
246
247
  const modelId = this.getModelId();
247
248
 
249
+ applyCachePointsToConversePayload({
250
+ cacheControl: options.cache_control,
251
+ system: converseSystem,
252
+ messages: converseMessages,
253
+ params,
254
+ modelId,
255
+ });
256
+
248
257
  const command = new ConverseStreamCommand({
249
258
  modelId,
250
259
  messages: converseMessages,
@@ -0,0 +1,144 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { expect, test, describe, jest } from '@jest/globals';
3
+ import { HumanMessage, SystemMessage } from '@langchain/core/messages';
4
+ import {
5
+ BedrockRuntimeClient,
6
+ ConverseCommand,
7
+ ConverseStreamCommand,
8
+ } from '@aws-sdk/client-bedrock-runtime';
9
+ import type {
10
+ ConverseCommandInput,
11
+ ConverseStreamCommandInput,
12
+ } from '@aws-sdk/client-bedrock-runtime';
13
+ import { CustomChatBedrockConverse as ChatBedrockConverse } from './index';
14
+
15
+ jest.setTimeout(120000);
16
+
17
+ const baseConstructorArgs = {
18
+ region: 'us-east-1',
19
+ credentials: {
20
+ secretAccessKey: 'test-secret',
21
+ accessKeyId: 'test-key',
22
+ },
23
+ model: 'anthropic.claude-3-sonnet-20240229-v1:0',
24
+ };
25
+
26
+ function nonStreamingClient(): {
27
+ client: BedrockRuntimeClient;
28
+ send: ReturnType<typeof jest.fn>;
29
+ } {
30
+ const send = jest.fn<any>().mockResolvedValue({
31
+ output: {
32
+ message: {
33
+ role: 'assistant',
34
+ content: [{ text: 'Response' }],
35
+ },
36
+ },
37
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
38
+ });
39
+ return { client: { send } as unknown as BedrockRuntimeClient, send };
40
+ }
41
+
42
+ function streamingClient(): {
43
+ client: BedrockRuntimeClient;
44
+ send: ReturnType<typeof jest.fn>;
45
+ } {
46
+ const send = jest.fn<any>().mockResolvedValue({
47
+ stream: (async function* streamChunks() {
48
+ yield {
49
+ contentBlockDelta: {
50
+ contentBlockIndex: 0,
51
+ delta: { text: 'Response' },
52
+ },
53
+ };
54
+ yield {
55
+ metadata: {
56
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
57
+ },
58
+ };
59
+ })(),
60
+ });
61
+ return { client: { send } as unknown as BedrockRuntimeClient, send };
62
+ }
63
+
64
+ describe('CustomChatBedrockConverse prompt caching request mapping', () => {
65
+ test('invoke maps cache_control to system, messages, and tools', async () => {
66
+ const { client, send } = nonStreamingClient();
67
+ const model = new ChatBedrockConverse({ ...baseConstructorArgs, client });
68
+
69
+ await model.invoke(
70
+ [new SystemMessage('System prompt'), new HumanMessage('Hello')],
71
+ {
72
+ cache_control: { type: 'ephemeral', ttl: '1h' },
73
+ tools: [
74
+ {
75
+ toolSpec: {
76
+ name: 'get_weather',
77
+ description: 'Get weather',
78
+ inputSchema: {
79
+ json: { type: 'object', properties: {} },
80
+ },
81
+ },
82
+ },
83
+ ],
84
+ }
85
+ );
86
+
87
+ expect(send).toHaveBeenCalledTimes(1);
88
+ const command = send.mock.calls[0][0] as ConverseCommand;
89
+ expect(command).toBeInstanceOf(ConverseCommand);
90
+ const input = command.input as ConverseCommandInput;
91
+
92
+ expect(input.system).toEqual([
93
+ { text: 'System prompt' },
94
+ { cachePoint: { type: 'default', ttl: '1h' } },
95
+ ]);
96
+ expect(input.messages?.[0].content).toEqual([
97
+ { text: 'Hello' },
98
+ { cachePoint: { type: 'default', ttl: '1h' } },
99
+ ]);
100
+ expect(input.toolConfig?.tools).toEqual([
101
+ {
102
+ toolSpec: {
103
+ name: 'get_weather',
104
+ description: 'Get weather',
105
+ inputSchema: {
106
+ json: { type: 'object', properties: {} },
107
+ },
108
+ },
109
+ },
110
+ { cachePoint: { type: 'default', ttl: '1h' } },
111
+ ]);
112
+ });
113
+
114
+ test('stream maps cache_control to system and last message', async () => {
115
+ const { client, send } = streamingClient();
116
+ const model = new ChatBedrockConverse({ ...baseConstructorArgs, client });
117
+
118
+ const stream = await model.stream(
119
+ [new SystemMessage('System prompt'), new HumanMessage('Hello')],
120
+ {
121
+ cache_control: { type: 'ephemeral' },
122
+ }
123
+ );
124
+ for await (const _chunk of stream) {
125
+ // Fully consume stream so the command is executed.
126
+ }
127
+
128
+ expect(send).toHaveBeenCalledTimes(1);
129
+ const command = send.mock.calls[0][0] as ConverseStreamCommand;
130
+ expect(command).toBeInstanceOf(ConverseStreamCommand);
131
+ const input = command.input as ConverseStreamCommandInput;
132
+
133
+ expect(input.system).toEqual([
134
+ { text: 'System prompt' },
135
+ { cachePoint: { type: 'default' } },
136
+ ]);
137
+ expect(input.messages?.[0].content).toEqual([
138
+ { text: 'Hello' },
139
+ { cachePoint: { type: 'default' } },
140
+ ]);
141
+ });
142
+
143
+ // Dropped (inherited): live
144
+ });