@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.
Files changed (37) hide show
  1. package/dist/cjs/llm/anthropic/index.cjs +4 -3
  2. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  3. package/dist/cjs/llm/anthropic/utils/tools.cjs +2 -1
  4. package/dist/cjs/llm/anthropic/utils/tools.cjs.map +1 -1
  5. package/dist/cjs/llm/bedrock/cachePoints.cjs +28 -0
  6. package/dist/cjs/llm/bedrock/cachePoints.cjs.map +1 -0
  7. package/dist/cjs/llm/bedrock/index.cjs +10 -1
  8. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  9. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  10. package/dist/esm/llm/anthropic/index.mjs +4 -3
  11. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  12. package/dist/esm/llm/anthropic/utils/tools.mjs +2 -1
  13. package/dist/esm/llm/anthropic/utils/tools.mjs.map +1 -1
  14. package/dist/esm/llm/bedrock/cachePoints.mjs +28 -0
  15. package/dist/esm/llm/bedrock/cachePoints.mjs.map +1 -0
  16. package/dist/esm/llm/bedrock/index.mjs +10 -1
  17. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  18. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  19. package/dist/types/llm/anthropic/utils/tools.d.ts +1 -1
  20. package/dist/types/llm/bedrock/cachePoints.d.ts +9 -0
  21. package/package.json +16 -21
  22. package/src/llm/anthropic/index.ts +13 -2
  23. package/src/llm/anthropic/inherited-content-utils.spec.ts +244 -0
  24. package/src/llm/anthropic/inherited-stream-events.spec.ts +752 -0
  25. package/src/llm/anthropic/inherited-strict.spec.ts +302 -0
  26. package/src/llm/anthropic/llm.spec.ts +65 -0
  27. package/src/llm/anthropic/utils/tools.ts +7 -1
  28. package/src/llm/bedrock/cachePoints.ts +86 -0
  29. package/src/llm/bedrock/index.ts +9 -0
  30. package/src/llm/bedrock/inherited-cache.spec.ts +144 -0
  31. package/src/llm/bedrock/inherited.spec.ts +724 -0
  32. package/src/llm/google/inherited-stream-events.spec.ts +350 -0
  33. package/src/llm/openai/inherited-deepseek.spec.ts +347 -0
  34. package/src/llm/openai/inherited-xai.spec.ts +416 -0
  35. package/src/llm/openai/llm.spec.ts +1568 -0
  36. package/src/llm/openrouter/index.ts +1 -3
  37. 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
+ });