@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,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inherited Anthropic content/util tests, ported from @langchain/anthropic@1.5.1.
|
|
3
|
+
*
|
|
4
|
+
* Upstream sources (vitest), consolidated here and adapted to this fork (jest):
|
|
5
|
+
* - src/v1/tests/standard_content.test.ts (v1 standard content-block conversion)
|
|
6
|
+
* - src/utils/tests/message_outputs.test.ts (message_outputs — copied at utils/message_outputs.ts)
|
|
7
|
+
* - src/utils/tests/tools.test.ts (tools util — copied at utils/tools.ts)
|
|
8
|
+
*
|
|
9
|
+
* Adaptation notes:
|
|
10
|
+
* - vitest -> jest; `zod/v3` -> `zod`; no `vi.*` usage was needed.
|
|
11
|
+
* - The fork copies `message_outputs.ts` and `tools.ts` near-verbatim, so those
|
|
12
|
+
* cases are imported and asserted directly against our copies.
|
|
13
|
+
* - Upstream's `_formatStandardContent` operates on the v1 `ContentBlock.Multimodal`
|
|
14
|
+
* model (`fileId`/`text-plain`/`data: Uint8Array`/`metadata`). This fork does not
|
|
15
|
+
* implement that function or the v1 block model; instead it converts the deprecated
|
|
16
|
+
* v0.3 `source_type`-based standard blocks via the private `standardContentBlockConverter`,
|
|
17
|
+
* reached through the public `_convertMessagesToAnthropicPayload`. The standard-content
|
|
18
|
+
* cases below are therefore routed through that path and assert the fork's equivalent
|
|
19
|
+
* output. Cases with no fork equivalent are dropped inline with a reason.
|
|
20
|
+
*/
|
|
21
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
22
|
+
import { _makeMessageChunkFromAnthropicEvent } from './utils/message_outputs';
|
|
23
|
+
import { _convertMessagesToAnthropicPayload } from './utils/message_inputs';
|
|
24
|
+
import { handleToolChoice } from './utils/tools';
|
|
25
|
+
|
|
26
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Helper mirroring upstream's `createAnthropicMessage`, but using this fork's
|
|
30
|
+
* conversion entry point. Upstream wrapped raw v1 `contentBlocks` and called
|
|
31
|
+
* `_formatStandardContent`; here we send deprecated v0.3 standard blocks through
|
|
32
|
+
* `_convertMessagesToAnthropicPayload` and read back the converted human-turn content.
|
|
33
|
+
*/
|
|
34
|
+
function convertStandardBlock(block: Record<string, unknown>): unknown {
|
|
35
|
+
const payload = _convertMessagesToAnthropicPayload([
|
|
36
|
+
new HumanMessage({ content: [block as any] }),
|
|
37
|
+
]);
|
|
38
|
+
const human = payload.messages.find((m: any) => m.role === 'user')!;
|
|
39
|
+
return (human.content as unknown[])[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('standard content-block conversion (ported from standard_content.test.ts)', () => {
|
|
43
|
+
// Adapted: upstream converted a v1 `file` block backed by a `fileId` into a
|
|
44
|
+
// `{ source: { type: 'file', file_id } }` document. This fork has no fileId
|
|
45
|
+
// source type; the metadata passthrough (cache_control/citations/context/title)
|
|
46
|
+
// is the load-bearing behavior, so we assert it on a URL-backed file document.
|
|
47
|
+
it('converts file blocks into Anthropic documents and forwards metadata', () => {
|
|
48
|
+
const content = convertStandardBlock({
|
|
49
|
+
type: 'file',
|
|
50
|
+
source_type: 'url',
|
|
51
|
+
url: 'https://example.com/doc.pdf',
|
|
52
|
+
mime_type: 'application/pdf',
|
|
53
|
+
metadata: {
|
|
54
|
+
cache_control: { type: 'ephemeral', ttl: '5m' },
|
|
55
|
+
citations: { enabled: true },
|
|
56
|
+
context: 'source context',
|
|
57
|
+
title: 'My Document',
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(content).toMatchObject({
|
|
62
|
+
type: 'document',
|
|
63
|
+
source: { type: 'url', url: 'https://example.com/doc.pdf' },
|
|
64
|
+
cache_control: { type: 'ephemeral', ttl: '5m' },
|
|
65
|
+
citations: { enabled: true },
|
|
66
|
+
context: 'source context',
|
|
67
|
+
title: 'My Document',
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('converts inlined text files into text document sources', () => {
|
|
72
|
+
const content = convertStandardBlock({
|
|
73
|
+
type: 'file',
|
|
74
|
+
source_type: 'text',
|
|
75
|
+
text: 'Plain text body',
|
|
76
|
+
mime_type: 'text/plain',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(content).toMatchObject({
|
|
80
|
+
type: 'document',
|
|
81
|
+
source: {
|
|
82
|
+
type: 'text',
|
|
83
|
+
data: 'Plain text body',
|
|
84
|
+
media_type: 'text/plain',
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('wraps base64 image file payloads in document content blocks', () => {
|
|
90
|
+
// Upstream passed a Uint8Array and asserted Buffer.from(..).toString('base64');
|
|
91
|
+
// this fork takes a pre-encoded base64 string, so we pass the encoded value.
|
|
92
|
+
const data = Buffer.from(Uint8Array.from([1, 2, 3])).toString('base64');
|
|
93
|
+
const content = convertStandardBlock({
|
|
94
|
+
type: 'file',
|
|
95
|
+
source_type: 'base64',
|
|
96
|
+
data,
|
|
97
|
+
mime_type: 'image/png',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(content).toMatchObject({
|
|
101
|
+
type: 'document',
|
|
102
|
+
source: {
|
|
103
|
+
type: 'content',
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: 'image',
|
|
107
|
+
source: {
|
|
108
|
+
type: 'base64',
|
|
109
|
+
data,
|
|
110
|
+
media_type: 'image/png',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('converts standard image blocks with metadata', () => {
|
|
119
|
+
const content = convertStandardBlock({
|
|
120
|
+
type: 'image',
|
|
121
|
+
source_type: 'url',
|
|
122
|
+
url: 'https://example.com/image.png',
|
|
123
|
+
metadata: {
|
|
124
|
+
cache_control: { type: 'ephemeral', ttl: '1h' },
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(content).toMatchObject({
|
|
129
|
+
type: 'image',
|
|
130
|
+
source: { type: 'url', url: 'https://example.com/image.png' },
|
|
131
|
+
cache_control: { type: 'ephemeral', ttl: '1h' },
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Dropped (inherited): upstream's "promotes plain text blocks to Anthropic text
|
|
136
|
+
// documents" exercises the v1 `text-plain` content-block type, which this fork does
|
|
137
|
+
// not model. The equivalent text->document-source path is already covered by the
|
|
138
|
+
// "inlined text files" case above (v0.3 `file`/`source_type: 'text'`).
|
|
139
|
+
|
|
140
|
+
it('throws for unsupported audio blocks', () => {
|
|
141
|
+
// Upstream threw /does not support audio/i. This fork throws from the standard
|
|
142
|
+
// converter because it intentionally implements no `fromStandardAudioBlock`,
|
|
143
|
+
// so we assert the fork's actual message.
|
|
144
|
+
expect(() =>
|
|
145
|
+
convertStandardBlock({
|
|
146
|
+
type: 'audio',
|
|
147
|
+
source_type: 'base64',
|
|
148
|
+
data: 'AQID',
|
|
149
|
+
mime_type: 'audio/mpeg',
|
|
150
|
+
})
|
|
151
|
+
).toThrow(/fromStandardAudioBlock/);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('_makeMessageChunkFromAnthropicEvent (ported from message_outputs.test.ts)', () => {
|
|
156
|
+
const fields = { streamUsage: true, coerceContentToString: false };
|
|
157
|
+
|
|
158
|
+
it('message_start chunk contains correct cache token counts', () => {
|
|
159
|
+
const event = {
|
|
160
|
+
type: 'message_start' as const,
|
|
161
|
+
message: {
|
|
162
|
+
id: 'msg_01',
|
|
163
|
+
type: 'message' as const,
|
|
164
|
+
role: 'assistant' as const,
|
|
165
|
+
content: [],
|
|
166
|
+
model: 'claude-3-5-haiku-latest',
|
|
167
|
+
stop_reason: null,
|
|
168
|
+
stop_sequence: null,
|
|
169
|
+
usage: {
|
|
170
|
+
input_tokens: 100,
|
|
171
|
+
output_tokens: 0,
|
|
172
|
+
cache_creation_input_tokens: 500,
|
|
173
|
+
cache_read_input_tokens: 1000,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const result = _makeMessageChunkFromAnthropicEvent(event as any, fields);
|
|
179
|
+
expect(result).not.toBeNull();
|
|
180
|
+
|
|
181
|
+
const usage = result!.chunk.usage_metadata!;
|
|
182
|
+
// input_tokens in LangChain = input_tokens + cache_creation + cache_read
|
|
183
|
+
expect(usage.input_tokens).toBe(1600);
|
|
184
|
+
expect(usage.input_token_details?.cache_creation).toBe(500);
|
|
185
|
+
expect(usage.input_token_details?.cache_read).toBe(1000);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('message_delta chunk has input_tokens=0 and no cache token details', () => {
|
|
189
|
+
const event = {
|
|
190
|
+
type: 'message_delta' as const,
|
|
191
|
+
delta: { stop_reason: 'end_turn' as const, stop_sequence: null },
|
|
192
|
+
usage: {
|
|
193
|
+
input_tokens: 100,
|
|
194
|
+
output_tokens: 42,
|
|
195
|
+
// Anthropic API returns cumulative cache values here — same as message_start
|
|
196
|
+
cache_creation_input_tokens: 500,
|
|
197
|
+
cache_read_input_tokens: 1000,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const result = _makeMessageChunkFromAnthropicEvent(event as any, fields);
|
|
202
|
+
expect(result).not.toBeNull();
|
|
203
|
+
|
|
204
|
+
const usage = result!.chunk.usage_metadata!;
|
|
205
|
+
expect(usage.output_tokens).toBe(42);
|
|
206
|
+
expect(usage.input_tokens).toBe(0);
|
|
207
|
+
expect(usage.input_token_details?.cache_creation).toBeUndefined();
|
|
208
|
+
expect(usage.input_token_details?.cache_read).toBeUndefined();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('handleToolChoice (ported from tools.test.ts)', () => {
|
|
213
|
+
it('should return undefined for undefined input', () => {
|
|
214
|
+
expect(handleToolChoice(undefined)).toBeUndefined();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should handle 'any' tool choice", () => {
|
|
218
|
+
expect(handleToolChoice('any')).toEqual({ type: 'any' });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("maps OpenAI-style 'required' to Anthropic 'any'", () => {
|
|
222
|
+
expect(handleToolChoice('required')).toEqual({ type: 'any' });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("should handle 'auto' tool choice", () => {
|
|
226
|
+
expect(handleToolChoice('auto')).toEqual({ type: 'auto' });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("maps 'none' to Anthropic 'none' (disables tools)", () => {
|
|
230
|
+
expect(handleToolChoice('none')).toEqual({ type: 'none' });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should handle specific tool name as string', () => {
|
|
234
|
+
expect(handleToolChoice('my_custom_tool')).toEqual({
|
|
235
|
+
type: 'tool',
|
|
236
|
+
name: 'my_custom_tool',
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should pass through object tool choice', () => {
|
|
241
|
+
const toolChoice = { type: 'tool' as const, name: 'specific_tool' };
|
|
242
|
+
expect(handleToolChoice(toolChoice)).toEqual(toolChoice);
|
|
243
|
+
});
|
|
244
|
+
});
|