@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
@@ -1,3 +1,3 @@
1
1
  import type { Anthropic } from '@anthropic-ai/sdk';
2
2
  import { AnthropicToolChoice } from '../types.js';
3
- export declare function handleToolChoice(toolChoice?: AnthropicToolChoice): Anthropic.Messages.ToolChoiceAuto | Anthropic.Messages.ToolChoiceAny | Anthropic.Messages.ToolChoiceTool | undefined;
3
+ export declare function handleToolChoice(toolChoice?: AnthropicToolChoice): Anthropic.Messages.ToolChoiceAuto | Anthropic.Messages.ToolChoiceAny | Anthropic.Messages.ToolChoiceTool | Anthropic.Messages.ToolChoiceNone | undefined;
@@ -0,0 +1,9 @@
1
+ import type { BedrockPromptCacheControl, ConverseCommandParams } from '@langchain/aws';
2
+ import type * as Bedrock from '@aws-sdk/client-bedrock-runtime';
3
+ export declare function applyCachePointsToConversePayload(fields: {
4
+ cacheControl?: BedrockPromptCacheControl;
5
+ system: Bedrock.SystemContentBlock[];
6
+ messages: Bedrock.Message[];
7
+ params?: Partial<ConverseCommandParams>;
8
+ modelId: string;
9
+ }): void;
@@ -180,10 +180,24 @@ export declare class Run<_T extends t.BaseGraphState> {
180
180
  * paths processed; returns 0 when checkpointing is disabled.
181
181
  */
182
182
  rewindFiles(): Promise<number>;
183
+ /**
184
+ * Resume an interrupted run. `commandOptions` forwards langgraph 1.4.5
185
+ * `Command` fields applied together with `resume` in one superstep:
186
+ * - `update`: channel updates committed at the resume point. On a *rebuilt*
187
+ * Run (new instance + durable checkpointer), `update.messages` are the first
188
+ * write the fresh wrapper sees, so they seed the `getRunMessages()` /
189
+ * `returnContent` baseline and are excluded from them (still committed to the
190
+ * checkpoint). Hosts that rebuild + inject messages should persist them
191
+ * directly or read from `getState`. Unreachable without a durable checkpointer.
192
+ * - `goto`: a *dynamic* edge that does not cancel static `addEdge` routes. On
193
+ * the built-in standard graph the fixed `toolNode -> agentNode/END` edge still
194
+ * fires, so `goto` adds rather than replaces (e.g. `goto: END` will not stop a
195
+ * tool-node resume). Intended for custom, Command-routed graphs.
196
+ */
183
197
  resume<TResume = t.ToolApprovalDecision[] | t.ToolApprovalDecisionMap>(resumeValue: TResume, callerConfig: Partial<RunnableConfig> & {
184
198
  version: 'v1' | 'v2';
185
199
  run_id?: string;
186
- }, streamOptions?: t.EventStreamOptions): Promise<MessageContentComplex[] | undefined>;
200
+ }, streamOptions?: t.EventStreamOptions, commandOptions?: Pick<ConstructorParameters<typeof Command>[0], 'update' | 'goto'>): Promise<MessageContentComplex[] | undefined>;
187
201
  private resolveInterruptResumeConfig;
188
202
  private createSystemCallback;
189
203
  getCallbacks(clientCallbacks: t.ClientCallbacks): t.SystemCallbacks;
@@ -11,7 +11,7 @@ import { RunnableCallable } from '@/utils';
11
11
  * batch-scoped value the method needs so the signature stays at
12
12
  * three positional parameters even as new context fields are added.
13
13
  */
14
- type RunToolBatchContext = {
14
+ type RunToolBatchContext<T = unknown> = {
15
15
  /** Position of this call within the parent ToolNode batch. */
16
16
  batchIndex?: number;
17
17
  /** Batch turn shared across every call in the batch. */
@@ -50,6 +50,13 @@ type RunToolBatchContext = {
50
50
  * contract for hosts relying on it for policy / recovery guidance.
51
51
  */
52
52
  additionalContextsSink?: string[];
53
+ /**
54
+ * Graph state the ToolNode was invoked with, threaded from `run()`
55
+ * so `tool.invoke` can forward it as langgraph 1.4's `runtime.state`
56
+ * (the deprecation-free replacement for `getCurrentTaskInput()`,
57
+ * which relies on `node:async_hooks` and is browser-incompatible).
58
+ */
59
+ runInput?: T;
53
60
  };
54
61
  export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
55
62
  private toolMap;
@@ -203,7 +210,7 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
203
210
  * register the output for future `{{tool<idx>turn<turn>}}`
204
211
  * substitutions. Omit when no registration should occur.
205
212
  */
206
- protected runTool(call: ToolCall, config: RunnableConfig, batchContext?: RunToolBatchContext): Promise<BaseMessage | Command>;
213
+ protected runTool(call: ToolCall, config: RunnableConfig, batchContext?: RunToolBatchContext<T>): Promise<BaseMessage | Command>;
207
214
  /**
208
215
  * Runs a single in-process tool call with the same lifecycle hooks
209
216
  * the event-dispatch path fires (`PreToolUse`, `PermissionDenied`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.2.46",
3
+ "version": "3.2.52",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -200,15 +200,10 @@
200
200
  "format": "prettier --write ."
201
201
  },
202
202
  "overrides": {
203
- "@langchain/openai": "1.4.5",
204
- "@anthropic-ai/sdk": "$@anthropic-ai/sdk",
203
+ "@langchain/openai": "1.5.3",
205
204
  "@browserbasehq/stagehand": {
206
205
  "openai": "$openai"
207
206
  },
208
- "@langchain/google-common": "$@langchain/google-common",
209
- "@langchain/google-gauth": "$@langchain/google-gauth",
210
- "@langchain/google-genai": "$@langchain/google-genai",
211
- "@langchain/google-vertexai": "$@langchain/google-vertexai",
212
207
  "uuid": "$uuid",
213
208
  "fast-xml-parser": "5.7.2",
214
209
  "ajv": "6.14.0",
@@ -217,21 +212,21 @@
217
212
  "js-yaml": "^4.2.0"
218
213
  },
219
214
  "dependencies": {
220
- "@anthropic-ai/sdk": "^0.92.0",
221
- "@aws-sdk/client-bedrock-runtime": "^3.1013.0",
222
- "@langchain/anthropic": "^1.3.28",
223
- "@langchain/aws": "^1.3.5",
224
- "@langchain/core": "1.1.48",
225
- "@langchain/deepseek": "^1.0.25",
226
- "@langchain/google-common": "2.1.31",
227
- "@langchain/google-gauth": "2.1.31",
228
- "@langchain/google-genai": "2.1.31",
229
- "@langchain/google-vertexai": "2.1.31",
230
- "@langchain/langgraph": "^1.2.9",
231
- "@langchain/mistralai": "^1.0.8",
232
- "@langchain/openai": "1.4.5",
215
+ "@anthropic-ai/sdk": "^0.103.0",
216
+ "@aws-sdk/client-bedrock-runtime": "^3.1075.0",
217
+ "@langchain/anthropic": "^1.5.1",
218
+ "@langchain/aws": "^1.4.2",
219
+ "@langchain/core": "^1.2.1",
220
+ "@langchain/deepseek": "^1.1.3",
221
+ "@langchain/google-common": "2.2.0",
222
+ "@langchain/google-gauth": "2.2.0",
223
+ "@langchain/google-genai": "2.2.0",
224
+ "@langchain/google-vertexai": "2.2.0",
225
+ "@langchain/langgraph": "^1.4.5",
226
+ "@langchain/mistralai": "^1.2.0",
227
+ "@langchain/openai": "1.5.3",
233
228
  "@langchain/textsplitters": "^1.0.1",
234
- "@langchain/xai": "^1.3.17",
229
+ "@langchain/xai": "^1.4.3",
235
230
  "@langfuse/langchain": "^5.4.1",
236
231
  "@langfuse/otel": "^5.4.1",
237
232
  "@langfuse/tracing": "^5.4.1",
@@ -12,12 +12,11 @@ import {
12
12
  Command,
13
13
  StateGraph,
14
14
  Annotation,
15
- getCurrentTaskInput,
16
15
  messagesStateReducer,
17
16
  } from '@langchain/langgraph';
18
17
  import type { BaseMessage, AIMessageChunk } from '@langchain/core/messages';
19
18
  import type { LangGraphRunnableConfig } from '@langchain/langgraph';
20
- import type { ToolRunnableConfig } from '@langchain/core/tools';
19
+ import type { ToolRuntime } from '@langchain/core/tools';
21
20
  import type * as t from '@/types';
22
21
  import { StandardGraph } from './Graph';
23
22
  import { Constants } from '@/common';
@@ -330,12 +329,10 @@ export class MultiAgentGraph extends StandardGraph {
330
329
 
331
330
  tools.push(
332
331
  tool(
333
- async (rawInput, config) => {
332
+ async (rawInput, runtime: ToolRuntime) => {
334
333
  const input = rawInput as Record<string, unknown>;
335
- const state = getCurrentTaskInput() as t.BaseGraphState;
336
- const toolCallId =
337
- (config as ToolRunnableConfig | undefined)?.toolCall?.id ??
338
- 'unknown';
334
+ const state = runtime.state as t.BaseGraphState;
335
+ const toolCallId = runtime.toolCall?.id ?? 'unknown';
339
336
 
340
337
  /** Evaluated condition */
341
338
  const result = edge.condition!(state);
@@ -414,11 +411,9 @@ export class MultiAgentGraph extends StandardGraph {
414
411
 
415
412
  tools.push(
416
413
  tool(
417
- async (rawInput, config) => {
414
+ async (rawInput, runtime: ToolRuntime) => {
418
415
  const input = rawInput as Record<string, unknown>;
419
- const toolCallId =
420
- (config as ToolRunnableConfig | undefined)?.toolCall?.id ??
421
- 'unknown';
416
+ const toolCallId = runtime.toolCall?.id ?? 'unknown';
422
417
 
423
418
  let content = `Successfully transferred to ${destination}`;
424
419
  if (
@@ -439,7 +434,7 @@ export class MultiAgentGraph extends StandardGraph {
439
434
  },
440
435
  });
441
436
 
442
- const state = getCurrentTaskInput() as t.BaseGraphState;
437
+ const state = runtime.state as t.BaseGraphState;
443
438
 
444
439
  /**
445
440
  * For parallel handoff support:
@@ -486,6 +486,7 @@ export class CustomAnthropic extends ChatAnthropicMessages {
486
486
  | Anthropic.Messages.ToolChoiceAuto
487
487
  | Anthropic.Messages.ToolChoiceAny
488
488
  | Anthropic.Messages.ToolChoiceTool
489
+ | Anthropic.Messages.ToolChoiceNone
489
490
  | undefined = handleToolChoice(options?.tool_choice);
490
491
 
491
492
  const callOptions = options as CustomAnthropicCallOptions | undefined;
@@ -508,12 +509,19 @@ export class CustomAnthropic extends ChatAnthropicMessages {
508
509
  const toolBetas = getToolBetas(options?.tools);
509
510
  const compactionBetas = getCompactionBetas(contextManagement);
510
511
  const taskBudgetBetas = getTaskBudgetBetas(this.model, mergedOutputConfig);
511
- const formattedTools = this.formatStructuredToolToAnthropic(options?.tools);
512
+ const formattedTools = this.formatStructuredToolToAnthropic(
513
+ options?.tools,
514
+ {
515
+ strict: options?.strict,
516
+ }
517
+ );
512
518
 
513
519
  const sharedParams = {
514
520
  tools: formattedTools,
515
521
  tool_choice,
516
- thinking: this.thinking,
522
+ // Match upstream: omit `thinking` unless the user set it, so we don't send
523
+ // `{ type: 'disabled' }` (an unsupported param on some models) by default.
524
+ thinking: this.thinkingExplicitlySet ? this.thinking : undefined,
517
525
  context_management: contextManagement,
518
526
  ...this.invocationKwargs,
519
527
  container: callOptions?.container,
@@ -527,6 +535,9 @@ export class CustomAnthropic extends ChatAnthropicMessages {
527
535
  output_config: mergedOutputConfig,
528
536
  inference_geo: inferenceGeo,
529
537
  mcp_servers: callOptions?.mcp_servers,
538
+ // Top-level request cache_control (1.5.x): API auto-advances the cache
539
+ // breakpoint across turns. Additive — independent of our block-level cache.
540
+ cache_control: options?.cache_control,
530
541
  };
531
542
  validateInvocationParamCompatibility({
532
543
  model: this.model,
@@ -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
+ });