@librechat/agents 3.2.46 → 3.2.51
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/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/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/types/llm/anthropic/utils/tools.d.ts +1 -1
- package/dist/types/llm/bedrock/cachePoints.d.ts +9 -0
- package/package.json +16 -21
- 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
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
// Inherited from @langchain/aws@1.4.2
|
|
2
|
+
// tests/chat_models_stream_events.test.ts (native streamEvents)
|
|
3
|
+
// tests/chat_models.invocation.test.ts (invocationParams / system / bearer / headers)
|
|
4
|
+
// tests/convertToConverseTools.test.ts (tool conversion util)
|
|
5
|
+
// utils/tests/message_outputs.test.ts (usage-metadata conversion util)
|
|
6
|
+
//
|
|
7
|
+
// Consolidates the deterministic, unit-testable cases from the four upstream
|
|
8
|
+
// suites and runs them against OUR fork, `CustomChatBedrockConverse` (imported
|
|
9
|
+
// here as `ChatBedrockConverse` from `@/llm/bedrock`).
|
|
10
|
+
//
|
|
11
|
+
// Applicability to our fork:
|
|
12
|
+
// `CustomChatBedrockConverse extends ChatBedrockConverse` (the installed
|
|
13
|
+
// @langchain/aws@1.4.2). It overrides `invocationParams`, `_streamResponseChunks`,
|
|
14
|
+
// `_generateNonStreaming`, and `getModelId`, but does NOT override the native
|
|
15
|
+
// `_streamChatModelEvents` (the streamEvents path) — that is inherited verbatim,
|
|
16
|
+
// so the streamEvents cases are a parity check that our overridden
|
|
17
|
+
// `_streamResponseChunks` still feeds the inherited event machinery correctly.
|
|
18
|
+
// Bearer-token auth and the defaultHeaders middleware are constructor-level
|
|
19
|
+
// features inherited unchanged from the base class.
|
|
20
|
+
//
|
|
21
|
+
// Adaptation (vitest -> jest):
|
|
22
|
+
// - `@jest/globals`; `vi.*` -> `jest.*`; `zod/v3` -> `zod` (not needed; see below).
|
|
23
|
+
// - Import the class from `@/llm/bedrock`, not `../chat_models.js`.
|
|
24
|
+
// - Upstream drove the SDK via `vi.mock('@aws-sdk/client-bedrock-runtime', ...)`.
|
|
25
|
+
// We instead use the harness from `llm.spec.ts`: inject a mock `client.send`
|
|
26
|
+
// (capturing the real `ConverseCommand`/`ConverseStreamCommand`), or spy on a
|
|
27
|
+
// real injected client's `middlewareStack.add`. This keeps the SDK command
|
|
28
|
+
// classes real and exercises the fork's real generation paths — no live API.
|
|
29
|
+
// - Upstream's streamEvents suite used vitest custom matchers
|
|
30
|
+
// (`toHaveStreamText` / `toHaveStreamReasoning` / `toHaveStreamToolCalls` /
|
|
31
|
+
// `toHaveStreamUsage`). Those don't exist in jest; they wrap the public
|
|
32
|
+
// `ChatModelStream` sub-streams (`.text` / `.reasoning` / `.toolCalls` /
|
|
33
|
+
// `.usage`), which we assert directly.
|
|
34
|
+
//
|
|
35
|
+
// Dropped (inherited): tests/convertToConverseTools.test.ts — the whole file.
|
|
36
|
+
// `convertToConverseTools` and `supportedToolChoiceValuesForModel` live in
|
|
37
|
+
// @langchain/aws's internal `utils/tools.js`, are NOT on the package's public
|
|
38
|
+
// export surface, and our fork neither uses nor re-exports them (verified via
|
|
39
|
+
// grep over src/ and the package `exports` map). Nothing in our fork to assert.
|
|
40
|
+
//
|
|
41
|
+
// Dropped (inherited): live — none of the four source files contained live
|
|
42
|
+
// `.invoke()`/`.stream()`-to-API cases; every retained case runs against a
|
|
43
|
+
// mocked transport.
|
|
44
|
+
|
|
45
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
46
|
+
import { test, expect, describe, jest, afterEach } from '@jest/globals';
|
|
47
|
+
import { ChatModelStream } from '@langchain/core/language_models/stream';
|
|
48
|
+
import {
|
|
49
|
+
HumanMessage,
|
|
50
|
+
SystemMessage,
|
|
51
|
+
AIMessageChunk,
|
|
52
|
+
} from '@langchain/core/messages';
|
|
53
|
+
import {
|
|
54
|
+
BedrockRuntimeClient,
|
|
55
|
+
ConverseCommand,
|
|
56
|
+
ConverseStreamCommand,
|
|
57
|
+
} from '@aws-sdk/client-bedrock-runtime';
|
|
58
|
+
import type {
|
|
59
|
+
Message as BedrockMessage,
|
|
60
|
+
ConverseResponse,
|
|
61
|
+
ConverseCommandInput,
|
|
62
|
+
ConverseStreamCommandInput,
|
|
63
|
+
} from '@aws-sdk/client-bedrock-runtime';
|
|
64
|
+
import type { BaseChatModelCallOptions } from '@langchain/core/language_models/chat_models';
|
|
65
|
+
import type { ChatModelStreamEvent } from '@langchain/core/language_models/event';
|
|
66
|
+
import {
|
|
67
|
+
convertConverseMessageToLangChainMessage,
|
|
68
|
+
handleConverseStreamMetadata,
|
|
69
|
+
} from './utils';
|
|
70
|
+
import { CustomChatBedrockConverse as ChatBedrockConverse } from './index';
|
|
71
|
+
|
|
72
|
+
jest.setTimeout(120000);
|
|
73
|
+
|
|
74
|
+
const baseConstructorArgs = {
|
|
75
|
+
region: 'us-east-1',
|
|
76
|
+
credentials: {
|
|
77
|
+
secretAccessKey: 'test-secret',
|
|
78
|
+
accessKeyId: 'test-key',
|
|
79
|
+
},
|
|
80
|
+
model: 'anthropic.claude-3-sonnet-20240229-v1:0',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
jest.restoreAllMocks();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ─── Mock transport helpers ─────────────────────────────────────
|
|
88
|
+
//
|
|
89
|
+
// Mirrors `llm.spec.ts`: inject a mock `client.send` that captures the real
|
|
90
|
+
// command objects and returns canned non-stream / stream responses.
|
|
91
|
+
|
|
92
|
+
type CapturingClient = {
|
|
93
|
+
client: BedrockRuntimeClient;
|
|
94
|
+
sent: unknown[];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function nonStreamingResponse(): unknown {
|
|
98
|
+
return {
|
|
99
|
+
output: {
|
|
100
|
+
message: { role: 'assistant', content: [{ text: 'Response' }] },
|
|
101
|
+
},
|
|
102
|
+
stopReason: 'end_turn',
|
|
103
|
+
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function streamingResponse(events: Array<Record<string, unknown>>): unknown {
|
|
108
|
+
return {
|
|
109
|
+
stream: (async function* () {
|
|
110
|
+
for (const event of events) {
|
|
111
|
+
yield event;
|
|
112
|
+
}
|
|
113
|
+
})(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function capturingClient(makeResponse: () => unknown): CapturingClient {
|
|
118
|
+
const sent: unknown[] = [];
|
|
119
|
+
const send = jest
|
|
120
|
+
.fn<(command: unknown) => Promise<unknown>>()
|
|
121
|
+
.mockImplementation(async (command) => {
|
|
122
|
+
sent.push(command);
|
|
123
|
+
return makeResponse();
|
|
124
|
+
});
|
|
125
|
+
return { client: { send } as unknown as BedrockRuntimeClient, sent };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function streamEventsGen(
|
|
129
|
+
model: ChatBedrockConverse,
|
|
130
|
+
messages: HumanMessage[],
|
|
131
|
+
options: BaseChatModelCallOptions = {} as BaseChatModelCallOptions
|
|
132
|
+
): AsyncGenerator<ChatModelStreamEvent> {
|
|
133
|
+
return (
|
|
134
|
+
model as unknown as {
|
|
135
|
+
_streamChatModelEvents: (
|
|
136
|
+
messages: unknown[],
|
|
137
|
+
options: BaseChatModelCallOptions
|
|
138
|
+
) => AsyncGenerator<ChatModelStreamEvent>;
|
|
139
|
+
}
|
|
140
|
+
)._streamChatModelEvents(messages, options);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── streamEvents fixtures (Bedrock Converse stream events) ─────
|
|
144
|
+
|
|
145
|
+
function bedrockTextStream(): Array<Record<string, unknown>> {
|
|
146
|
+
return [
|
|
147
|
+
{ contentBlockDelta: { contentBlockIndex: 0, delta: { text: 'Hello' } } },
|
|
148
|
+
{ contentBlockDelta: { contentBlockIndex: 0, delta: { text: ' world' } } },
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function bedrockReasoningStream(): Array<Record<string, unknown>> {
|
|
153
|
+
return [
|
|
154
|
+
{
|
|
155
|
+
contentBlockDelta: {
|
|
156
|
+
contentBlockIndex: 0,
|
|
157
|
+
delta: { reasoningContent: { text: 'Let me reason...' } },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function bedrockToolStream(): Array<Record<string, unknown>> {
|
|
164
|
+
return [
|
|
165
|
+
{
|
|
166
|
+
contentBlockDelta: {
|
|
167
|
+
contentBlockIndex: 0,
|
|
168
|
+
delta: { text: 'Let me search.' },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
contentBlockStart: {
|
|
173
|
+
contentBlockIndex: 1,
|
|
174
|
+
start: { toolUse: { toolUseId: 'toolu_1', name: 'web_search' } },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
contentBlockDelta: {
|
|
179
|
+
contentBlockIndex: 1,
|
|
180
|
+
delta: { toolUse: { input: '{"query":"weather"}' } },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function bedrockUsageStream(): Array<Record<string, unknown>> {
|
|
187
|
+
return [
|
|
188
|
+
{
|
|
189
|
+
metadata: {
|
|
190
|
+
usage: { inputTokens: 5, outputTokens: 2, totalTokens: 7 },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function mockBedrock(
|
|
197
|
+
events: Array<Record<string, unknown>>
|
|
198
|
+
): ChatBedrockConverse {
|
|
199
|
+
const { client } = capturingClient(() => streamingResponse(events));
|
|
200
|
+
return new ChatBedrockConverse({
|
|
201
|
+
...baseConstructorArgs,
|
|
202
|
+
model: 'anthropic.claude-3-haiku-20240307-v1:0',
|
|
203
|
+
client,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ─── streamEvents (native, inherited `_streamChatModelEvents`) ──
|
|
208
|
+
//
|
|
209
|
+
// From tests/chat_models_stream_events.test.ts. Upstream's custom matchers wrap
|
|
210
|
+
// the `ChatModelStream` sub-streams asserted here directly.
|
|
211
|
+
describe('CustomChatBedrockConverse.streamEvents (inherited native path)', () => {
|
|
212
|
+
test('streams text', async () => {
|
|
213
|
+
const stream = new ChatModelStream(
|
|
214
|
+
streamEventsGen(mockBedrock(bedrockTextStream()), [
|
|
215
|
+
new HumanMessage('Hello'),
|
|
216
|
+
])
|
|
217
|
+
);
|
|
218
|
+
expect(await stream.text).toBe('Hello world');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('streams reasoning', async () => {
|
|
222
|
+
const stream = new ChatModelStream(
|
|
223
|
+
streamEventsGen(mockBedrock(bedrockReasoningStream()), [
|
|
224
|
+
new HumanMessage('Hello'),
|
|
225
|
+
])
|
|
226
|
+
);
|
|
227
|
+
expect(await stream.reasoning).toBe('Let me reason...');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('streams tool calls', async () => {
|
|
231
|
+
const stream = new ChatModelStream(
|
|
232
|
+
streamEventsGen(mockBedrock(bedrockToolStream()), [
|
|
233
|
+
new HumanMessage('Hello'),
|
|
234
|
+
])
|
|
235
|
+
);
|
|
236
|
+
const calls = await stream.toolCalls;
|
|
237
|
+
expect(calls).toEqual([
|
|
238
|
+
expect.objectContaining({
|
|
239
|
+
name: 'web_search',
|
|
240
|
+
args: { query: 'weather' },
|
|
241
|
+
}),
|
|
242
|
+
]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('streams usage', async () => {
|
|
246
|
+
const stream = new ChatModelStream(
|
|
247
|
+
streamEventsGen(
|
|
248
|
+
mockBedrock(bedrockUsageStream()),
|
|
249
|
+
[new HumanMessage('Hello')],
|
|
250
|
+
{ streamUsage: true } as BaseChatModelCallOptions
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
// Fork note: our metadata handler always emits an `input_token_details`
|
|
254
|
+
// object (here empty `{}`) where upstream's matcher only checked the three
|
|
255
|
+
// top-level token counts. Assert the counts upstream asserted via toMatchObject.
|
|
256
|
+
expect(await stream.usage).toMatchObject({
|
|
257
|
+
input_tokens: 5,
|
|
258
|
+
output_tokens: 2,
|
|
259
|
+
total_tokens: 7,
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ─── invocationParams / system / inferenceConfig ───────────────
|
|
265
|
+
//
|
|
266
|
+
// From tests/chat_models.invocation.test.ts. Upstream read the command input via
|
|
267
|
+
// `Reflect.get(ConverseCommand, 'lastInput')` from a vi.mock'd SDK; we read the
|
|
268
|
+
// real captured command's `.input` instead.
|
|
269
|
+
describe('CustomChatBedrockConverse invocationParams', () => {
|
|
270
|
+
describe('inferenceConfig conditional logic', () => {
|
|
271
|
+
test('covers all inferenceConfig scenarios compactly', () => {
|
|
272
|
+
const cases: Array<{
|
|
273
|
+
name: string;
|
|
274
|
+
ctor?: Partial<{
|
|
275
|
+
maxTokens: number;
|
|
276
|
+
temperature: number;
|
|
277
|
+
topP: number;
|
|
278
|
+
}>;
|
|
279
|
+
opts?: { stop?: string[] };
|
|
280
|
+
expectDefined: boolean;
|
|
281
|
+
expectValues?: Partial<{
|
|
282
|
+
maxTokens: number;
|
|
283
|
+
temperature: number;
|
|
284
|
+
topP: number;
|
|
285
|
+
stopSequences: string[];
|
|
286
|
+
}>;
|
|
287
|
+
expectUndefinedKeys?: Array<
|
|
288
|
+
'maxTokens' | 'temperature' | 'topP' | 'stopSequences'
|
|
289
|
+
>;
|
|
290
|
+
}> = [
|
|
291
|
+
{
|
|
292
|
+
name: 'undefined when no inference values are set',
|
|
293
|
+
expectDefined: false,
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: 'includes only maxTokens when set',
|
|
297
|
+
ctor: { maxTokens: 100 },
|
|
298
|
+
expectDefined: true,
|
|
299
|
+
expectValues: { maxTokens: 100 },
|
|
300
|
+
expectUndefinedKeys: ['temperature', 'topP', 'stopSequences'],
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
name: 'includes only temperature when set',
|
|
304
|
+
ctor: { temperature: 0.7 },
|
|
305
|
+
expectDefined: true,
|
|
306
|
+
expectValues: { temperature: 0.7 },
|
|
307
|
+
expectUndefinedKeys: ['maxTokens', 'topP', 'stopSequences'],
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: 'includes only topP when set',
|
|
311
|
+
ctor: { topP: 0.9 },
|
|
312
|
+
expectDefined: true,
|
|
313
|
+
expectValues: { topP: 0.9 },
|
|
314
|
+
expectUndefinedKeys: ['maxTokens', 'temperature', 'stopSequences'],
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: 'includes stopSequences when provided',
|
|
318
|
+
opts: { stop: ['END', 'STOP'] },
|
|
319
|
+
expectDefined: true,
|
|
320
|
+
expectValues: { stopSequences: ['END', 'STOP'] },
|
|
321
|
+
expectUndefinedKeys: ['maxTokens', 'temperature', 'topP'],
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: 'includes all values when all are set',
|
|
325
|
+
ctor: { maxTokens: 200, temperature: 0.5, topP: 0.95 },
|
|
326
|
+
opts: { stop: ['END'] },
|
|
327
|
+
expectDefined: true,
|
|
328
|
+
expectValues: {
|
|
329
|
+
maxTokens: 200,
|
|
330
|
+
temperature: 0.5,
|
|
331
|
+
topP: 0.95,
|
|
332
|
+
stopSequences: ['END'],
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: 'undefined when stop sequences is empty array',
|
|
337
|
+
opts: { stop: [] },
|
|
338
|
+
expectDefined: false,
|
|
339
|
+
},
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
for (const c of cases) {
|
|
343
|
+
const model = new ChatBedrockConverse({
|
|
344
|
+
...baseConstructorArgs,
|
|
345
|
+
...(c.ctor ?? {}),
|
|
346
|
+
});
|
|
347
|
+
const params = model.invocationParams(c.opts);
|
|
348
|
+
if (!c.expectDefined) {
|
|
349
|
+
expect(params.inferenceConfig).toBeUndefined();
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
expect(params.inferenceConfig).toBeDefined();
|
|
353
|
+
const ic = params.inferenceConfig as Record<string, unknown>;
|
|
354
|
+
if (c.expectValues?.maxTokens !== undefined) {
|
|
355
|
+
expect(ic.maxTokens).toBe(c.expectValues.maxTokens);
|
|
356
|
+
}
|
|
357
|
+
if (c.expectValues?.temperature !== undefined) {
|
|
358
|
+
expect(ic.temperature).toBe(c.expectValues.temperature);
|
|
359
|
+
}
|
|
360
|
+
if (c.expectValues?.topP !== undefined) {
|
|
361
|
+
expect(ic.topP).toBe(c.expectValues.topP);
|
|
362
|
+
}
|
|
363
|
+
if (c.expectValues?.stopSequences !== undefined) {
|
|
364
|
+
expect(ic.stopSequences).toEqual(c.expectValues.stopSequences);
|
|
365
|
+
}
|
|
366
|
+
for (const k of c.expectUndefinedKeys ?? []) {
|
|
367
|
+
expect(ic[k]).toBeUndefined();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe('system parameter conditional logic (invoke)', () => {
|
|
374
|
+
test.each([
|
|
375
|
+
{
|
|
376
|
+
name: 'no system messages',
|
|
377
|
+
messages: [new HumanMessage('Hello')],
|
|
378
|
+
expectedSystem: { present: false, length: 0, texts: [] as string[] },
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
name: 'one system message',
|
|
382
|
+
messages: [
|
|
383
|
+
new SystemMessage('You are a helpful assistant.'),
|
|
384
|
+
new HumanMessage('Hello'),
|
|
385
|
+
],
|
|
386
|
+
expectedSystem: {
|
|
387
|
+
present: true,
|
|
388
|
+
length: 1,
|
|
389
|
+
texts: ['You are a helpful assistant.'],
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: 'multiple system messages',
|
|
394
|
+
messages: [
|
|
395
|
+
new SystemMessage('You are a helpful assistant.'),
|
|
396
|
+
new SystemMessage('Be concise in your responses.'),
|
|
397
|
+
new HumanMessage('Hello'),
|
|
398
|
+
],
|
|
399
|
+
expectedSystem: {
|
|
400
|
+
present: true,
|
|
401
|
+
length: 2,
|
|
402
|
+
texts: [
|
|
403
|
+
'You are a helpful assistant.',
|
|
404
|
+
'Be concise in your responses.',
|
|
405
|
+
],
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
])(
|
|
409
|
+
'invoke should handle system parameter: $name',
|
|
410
|
+
async ({ messages, expectedSystem }) => {
|
|
411
|
+
const { client, sent } = capturingClient(nonStreamingResponse);
|
|
412
|
+
const model = new ChatBedrockConverse({
|
|
413
|
+
...baseConstructorArgs,
|
|
414
|
+
client,
|
|
415
|
+
});
|
|
416
|
+
await model.invoke(messages);
|
|
417
|
+
const command = sent[0] as ConverseCommand;
|
|
418
|
+
expect(command).toBeInstanceOf(ConverseCommand);
|
|
419
|
+
const input = command.input as ConverseCommandInput;
|
|
420
|
+
if (!expectedSystem.present) {
|
|
421
|
+
expect(input).not.toHaveProperty('system');
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
expect(input).toHaveProperty('system');
|
|
425
|
+
const system = input.system as NonNullable<typeof input.system>;
|
|
426
|
+
expect(system).toHaveLength(expectedSystem.length);
|
|
427
|
+
expectedSystem.texts.forEach((t, i) => {
|
|
428
|
+
expect(system[i]).toHaveProperty('text', t);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe('stream method system parameter logic', () => {
|
|
435
|
+
test.each([
|
|
436
|
+
{
|
|
437
|
+
name: 'no system messages',
|
|
438
|
+
messages: [new HumanMessage('Hello')],
|
|
439
|
+
expectedPresent: false,
|
|
440
|
+
expectedLength: 0,
|
|
441
|
+
expectedTexts: [] as string[],
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
name: 'one system message',
|
|
445
|
+
messages: [
|
|
446
|
+
new SystemMessage('You are a helpful assistant.'),
|
|
447
|
+
new HumanMessage('Hello'),
|
|
448
|
+
],
|
|
449
|
+
expectedPresent: true,
|
|
450
|
+
expectedLength: 1,
|
|
451
|
+
expectedTexts: ['You are a helpful assistant.'],
|
|
452
|
+
},
|
|
453
|
+
])(
|
|
454
|
+
'stream should handle system parameter: $name',
|
|
455
|
+
async ({ messages, expectedPresent, expectedLength, expectedTexts }) => {
|
|
456
|
+
const { client, sent } = capturingClient(() =>
|
|
457
|
+
streamingResponse([
|
|
458
|
+
{
|
|
459
|
+
contentBlockDelta: {
|
|
460
|
+
contentBlockIndex: 0,
|
|
461
|
+
delta: { text: 'Response' },
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
metadata: {
|
|
466
|
+
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
])
|
|
470
|
+
);
|
|
471
|
+
const model = new ChatBedrockConverse({
|
|
472
|
+
...baseConstructorArgs,
|
|
473
|
+
client,
|
|
474
|
+
});
|
|
475
|
+
const stream = await model.stream(messages);
|
|
476
|
+
let chunks = 0;
|
|
477
|
+
for await (const _chunk of stream) {
|
|
478
|
+
chunks += 1;
|
|
479
|
+
}
|
|
480
|
+
expect(chunks).toBeGreaterThan(0);
|
|
481
|
+
const command = sent[0] as ConverseStreamCommand;
|
|
482
|
+
expect(command).toBeInstanceOf(ConverseStreamCommand);
|
|
483
|
+
const input = command.input as ConverseStreamCommandInput;
|
|
484
|
+
if (!expectedPresent) {
|
|
485
|
+
// Fork divergence: our `_streamResponseChunks` always spreads
|
|
486
|
+
// `system: converseSystem` into the command, so with no system message
|
|
487
|
+
// it sends an empty `system: []` rather than omitting it like upstream
|
|
488
|
+
// (and like our non-stream invoke path). Bedrock accepts an empty
|
|
489
|
+
// system array. Assert OURS.
|
|
490
|
+
expect(input.system).toEqual([]);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
expect(input).toHaveProperty('system');
|
|
494
|
+
const system = input.system as NonNullable<typeof input.system>;
|
|
495
|
+
expect(system).toHaveLength(expectedLength);
|
|
496
|
+
expectedTexts.forEach((t, i) => {
|
|
497
|
+
expect(system[i]).toHaveProperty('text', t);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Dropped (inherited): "prompt caching request mapping" (invoke/stream map
|
|
504
|
+
// cache_control to system/messages/tools) — cache_control cases are owned by a
|
|
505
|
+
// separate agent (inherited-cache.spec.ts).
|
|
506
|
+
|
|
507
|
+
describe('bearer token auth (inherited from base constructor)', () => {
|
|
508
|
+
// Upstream asserted on a vi.mock'd `BedrockRuntimeClient.lastConfig`. We
|
|
509
|
+
// assert on the real constructed client's resolved config instead, which
|
|
510
|
+
// exercises the actual SDK auth wiring our fork inherits unchanged.
|
|
511
|
+
test('configures bearer auth from constructor token', async () => {
|
|
512
|
+
const model = new ChatBedrockConverse({
|
|
513
|
+
...baseConstructorArgs,
|
|
514
|
+
bedrockBearerToken: 'test-bearer-token',
|
|
515
|
+
} as never);
|
|
516
|
+
expect(
|
|
517
|
+
(model as unknown as { bedrockBearerToken?: string }).bedrockBearerToken
|
|
518
|
+
).toBe('test-bearer-token');
|
|
519
|
+
|
|
520
|
+
const config = (
|
|
521
|
+
model as unknown as {
|
|
522
|
+
client: {
|
|
523
|
+
config: {
|
|
524
|
+
authSchemePreference?: (() => Promise<string[]>) | string[];
|
|
525
|
+
credentials?: unknown;
|
|
526
|
+
token?: () => Promise<{ token: string }>;
|
|
527
|
+
};
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
).client.config;
|
|
531
|
+
|
|
532
|
+
const authPref = config.authSchemePreference;
|
|
533
|
+
const resolvedAuth =
|
|
534
|
+
typeof authPref === 'function' ? await authPref() : authPref;
|
|
535
|
+
expect(resolvedAuth).toEqual(['httpBearerAuth']);
|
|
536
|
+
await expect(config.token?.()).resolves.toEqual({
|
|
537
|
+
token: 'test-bearer-token',
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test('configures bearer auth from AWS_BEARER_TOKEN_BEDROCK', async () => {
|
|
542
|
+
process.env.AWS_BEARER_TOKEN_BEDROCK = 'env-bearer-token';
|
|
543
|
+
try {
|
|
544
|
+
const model = new ChatBedrockConverse({
|
|
545
|
+
...baseConstructorArgs,
|
|
546
|
+
} as never);
|
|
547
|
+
expect(
|
|
548
|
+
(model as unknown as { bedrockBearerToken?: string })
|
|
549
|
+
.bedrockBearerToken
|
|
550
|
+
).toBe('env-bearer-token');
|
|
551
|
+
|
|
552
|
+
const config = (
|
|
553
|
+
model as unknown as {
|
|
554
|
+
client: {
|
|
555
|
+
config: {
|
|
556
|
+
authSchemePreference?: (() => Promise<string[]>) | string[];
|
|
557
|
+
token?: () => Promise<{ token: string }>;
|
|
558
|
+
};
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
).client.config;
|
|
562
|
+
const authPref = config.authSchemePreference;
|
|
563
|
+
const resolvedAuth =
|
|
564
|
+
typeof authPref === 'function' ? await authPref() : authPref;
|
|
565
|
+
expect(resolvedAuth).toEqual(['httpBearerAuth']);
|
|
566
|
+
await expect(config.token?.()).resolves.toEqual({
|
|
567
|
+
token: 'env-bearer-token',
|
|
568
|
+
});
|
|
569
|
+
} finally {
|
|
570
|
+
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
describe('defaultHeaders middleware (inherited from base constructor)', () => {
|
|
576
|
+
// Spy on a real injected client's `middlewareStack.add` (instead of a
|
|
577
|
+
// vi.mock'd stack) so the actual header-injection middleware is captured and
|
|
578
|
+
// invoked.
|
|
579
|
+
test('registers middleware on client when defaultHeaders are provided', () => {
|
|
580
|
+
const injected = new BedrockRuntimeClient({
|
|
581
|
+
region: 'us-east-1',
|
|
582
|
+
credentials: { secretAccessKey: 's', accessKeyId: 'a' },
|
|
583
|
+
});
|
|
584
|
+
const addSpy = jest.spyOn(injected.middlewareStack, 'add');
|
|
585
|
+
|
|
586
|
+
new ChatBedrockConverse({
|
|
587
|
+
...baseConstructorArgs,
|
|
588
|
+
defaultHeaders: {
|
|
589
|
+
'X-Foo': 'Bar',
|
|
590
|
+
'anthropic-beta': 'prompt-caching-2024-07-31',
|
|
591
|
+
},
|
|
592
|
+
client: injected,
|
|
593
|
+
} as never);
|
|
594
|
+
|
|
595
|
+
expect(addSpy).toHaveBeenCalledTimes(1);
|
|
596
|
+
const [middlewareFn, options] = addSpy.mock.calls[0] as [
|
|
597
|
+
(
|
|
598
|
+
next: (a: unknown) => Promise<unknown>
|
|
599
|
+
) => (a: {
|
|
600
|
+
request: { headers: Record<string, string> };
|
|
601
|
+
}) => Promise<unknown>,
|
|
602
|
+
unknown,
|
|
603
|
+
];
|
|
604
|
+
expect(options).toEqual({
|
|
605
|
+
step: 'build',
|
|
606
|
+
name: 'langchain_aws_default_headers',
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
const fakeRequest = { headers: {} as Record<string, string> };
|
|
610
|
+
const fakeNext = jest.fn<() => Promise<unknown>>().mockResolvedValue({});
|
|
611
|
+
void middlewareFn(fakeNext)({ request: fakeRequest });
|
|
612
|
+
expect(fakeRequest.headers['X-Foo']).toBe('Bar');
|
|
613
|
+
expect(fakeRequest.headers['anthropic-beta']).toBe(
|
|
614
|
+
'prompt-caching-2024-07-31'
|
|
615
|
+
);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test('does not register middleware when defaultHeaders is absent', () => {
|
|
619
|
+
const injected = new BedrockRuntimeClient({
|
|
620
|
+
region: 'us-east-1',
|
|
621
|
+
credentials: { secretAccessKey: 's', accessKeyId: 'a' },
|
|
622
|
+
});
|
|
623
|
+
const addSpy = jest.spyOn(injected.middlewareStack, 'add');
|
|
624
|
+
|
|
625
|
+
new ChatBedrockConverse({
|
|
626
|
+
...baseConstructorArgs,
|
|
627
|
+
client: injected,
|
|
628
|
+
} as never);
|
|
629
|
+
|
|
630
|
+
expect(addSpy).not.toHaveBeenCalled();
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// ─── usage-metadata conversion utils ───────────────────────────
|
|
636
|
+
//
|
|
637
|
+
// From utils/tests/message_outputs.test.ts. Our fork re-exports these from
|
|
638
|
+
// `./utils` (utils/message_outputs.ts), so they are tested directly.
|
|
639
|
+
describe('message output usage metadata conversion', () => {
|
|
640
|
+
test('maps Bedrock prompt cache tokens for non-stream responses', () => {
|
|
641
|
+
const message: BedrockMessage = {
|
|
642
|
+
role: 'assistant',
|
|
643
|
+
content: [{ text: 'Hello' }],
|
|
644
|
+
};
|
|
645
|
+
const responseMetadata = {
|
|
646
|
+
usage: {
|
|
647
|
+
inputTokens: 10,
|
|
648
|
+
outputTokens: 5,
|
|
649
|
+
totalTokens: 25,
|
|
650
|
+
cacheReadInputTokens: 7,
|
|
651
|
+
cacheWriteInputTokens: 3,
|
|
652
|
+
},
|
|
653
|
+
} as Omit<ConverseResponse, 'output'>;
|
|
654
|
+
|
|
655
|
+
const result = convertConverseMessageToLangChainMessage(
|
|
656
|
+
message,
|
|
657
|
+
responseMetadata
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
// Fork divergence: upstream@1.4.2 folds cache read+write INTO input_tokens
|
|
661
|
+
// (would be 20). Our fork keeps input_tokens = raw inputTokens (10) and
|
|
662
|
+
// surfaces cache tokens only in input_token_details (Bedrock cache is
|
|
663
|
+
// additive, not a subset of input_tokens). Assert OURS.
|
|
664
|
+
expect(result.usage_metadata).toEqual({
|
|
665
|
+
input_tokens: 10,
|
|
666
|
+
output_tokens: 5,
|
|
667
|
+
total_tokens: 25,
|
|
668
|
+
input_token_details: {
|
|
669
|
+
cache_read: 7,
|
|
670
|
+
cache_creation: 3,
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test('does not add input_token_details when Bedrock cache fields are absent', () => {
|
|
676
|
+
const message: BedrockMessage = {
|
|
677
|
+
role: 'assistant',
|
|
678
|
+
content: [{ text: 'Hello' }],
|
|
679
|
+
};
|
|
680
|
+
const responseMetadata = {
|
|
681
|
+
usage: {
|
|
682
|
+
inputTokens: 10,
|
|
683
|
+
outputTokens: 5,
|
|
684
|
+
totalTokens: 15,
|
|
685
|
+
},
|
|
686
|
+
} as Omit<ConverseResponse, 'output'>;
|
|
687
|
+
|
|
688
|
+
const result = convertConverseMessageToLangChainMessage(
|
|
689
|
+
message,
|
|
690
|
+
responseMetadata
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
expect(result.usage_metadata?.input_token_details).toBeUndefined();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test('maps Bedrock prompt cache tokens for stream metadata', () => {
|
|
697
|
+
const chunk = handleConverseStreamMetadata(
|
|
698
|
+
{
|
|
699
|
+
usage: {
|
|
700
|
+
inputTokens: 20,
|
|
701
|
+
outputTokens: 4,
|
|
702
|
+
totalTokens: 39,
|
|
703
|
+
cacheReadInputTokens: 9,
|
|
704
|
+
cacheWriteInputTokens: 6,
|
|
705
|
+
},
|
|
706
|
+
metrics: { latencyMs: 100 },
|
|
707
|
+
},
|
|
708
|
+
{ streamUsage: true }
|
|
709
|
+
);
|
|
710
|
+
const message = chunk.message as AIMessageChunk;
|
|
711
|
+
|
|
712
|
+
// Same fork divergence as the non-stream case: upstream would report
|
|
713
|
+
// input_tokens 35 (20+9+6); our fork keeps the raw 20.
|
|
714
|
+
expect(message.usage_metadata).toEqual({
|
|
715
|
+
input_tokens: 20,
|
|
716
|
+
output_tokens: 4,
|
|
717
|
+
total_tokens: 39,
|
|
718
|
+
input_token_details: {
|
|
719
|
+
cache_read: 9,
|
|
720
|
+
cache_creation: 6,
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
});
|