@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
|
@@ -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;
|
package/dist/types/run.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
221
|
-
"@aws-sdk/client-bedrock-runtime": "^3.
|
|
222
|
-
"@langchain/anthropic": "^1.
|
|
223
|
-
"@langchain/aws": "^1.
|
|
224
|
-
"@langchain/core": "1.1
|
|
225
|
-
"@langchain/deepseek": "^1.
|
|
226
|
-
"@langchain/google-common": "2.
|
|
227
|
-
"@langchain/google-gauth": "2.
|
|
228
|
-
"@langchain/google-genai": "2.
|
|
229
|
-
"@langchain/google-vertexai": "2.
|
|
230
|
-
"@langchain/langgraph": "^1.
|
|
231
|
-
"@langchain/mistralai": "^1.0
|
|
232
|
-
"@langchain/openai": "1.
|
|
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
|
|
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 {
|
|
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,
|
|
332
|
+
async (rawInput, runtime: ToolRuntime) => {
|
|
334
333
|
const input = rawInput as Record<string, unknown>;
|
|
335
|
-
const state =
|
|
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,
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
+
});
|