@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.
- package/dist/cjs/graphs/MultiAgentGraph.cjs +6 -6
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +4 -3
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/tools.cjs +2 -1
- package/dist/cjs/llm/anthropic/utils/tools.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/cachePoints.cjs +28 -0
- package/dist/cjs/llm/bedrock/cachePoints.cjs.map +1 -0
- package/dist/cjs/llm/bedrock/index.cjs +10 -1
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/run.cjs +21 -3
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +26 -5
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +7 -7
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +4 -3
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/tools.mjs +2 -1
- package/dist/esm/llm/anthropic/utils/tools.mjs.map +1 -1
- package/dist/esm/llm/bedrock/cachePoints.mjs +28 -0
- package/dist/esm/llm/bedrock/cachePoints.mjs.map +1 -0
- package/dist/esm/llm/bedrock/index.mjs +10 -1
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/run.mjs +21 -3
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +26 -5
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/llm/anthropic/utils/tools.d.ts +1 -1
- package/dist/types/llm/bedrock/cachePoints.d.ts +9 -0
- package/dist/types/run.d.ts +15 -1
- package/dist/types/tools/ToolNode.d.ts +9 -2
- package/package.json +16 -21
- package/src/graphs/MultiAgentGraph.ts +7 -12
- package/src/llm/anthropic/index.ts +13 -2
- package/src/llm/anthropic/inherited-content-utils.spec.ts +244 -0
- package/src/llm/anthropic/inherited-stream-events.spec.ts +752 -0
- package/src/llm/anthropic/inherited-strict.spec.ts +302 -0
- package/src/llm/anthropic/llm.spec.ts +65 -0
- package/src/llm/anthropic/utils/tools.ts +7 -1
- package/src/llm/bedrock/cachePoints.ts +86 -0
- package/src/llm/bedrock/index.ts +9 -0
- package/src/llm/bedrock/inherited-cache.spec.ts +144 -0
- package/src/llm/bedrock/inherited.spec.ts +724 -0
- package/src/llm/google/inherited-stream-events.spec.ts +350 -0
- package/src/llm/openai/inherited-deepseek.spec.ts +347 -0
- package/src/llm/openai/inherited-xai.spec.ts +416 -0
- package/src/llm/openai/llm.spec.ts +1568 -0
- package/src/llm/openrouter/index.ts +1 -3
- package/src/llm/vertexai/inherited-stream-events.spec.ts +271 -0
- package/src/run.ts +31 -3
- package/src/scripts/handoff-test.ts +5 -6
- package/src/tools/ToolNode.ts +43 -5
- package/src/tools/__tests__/ToolNode.runtimeState.test.ts +120 -0
- 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
|
+
}
|
package/src/llm/bedrock/index.ts
CHANGED
|
@@ -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
|
+
});
|