Skip to content

Commit 55aab9c

Browse files
committed
fix(core): support input_json_delta aggregation for anthropic tool streams
1 parent 3999fab commit 55aab9c

2 files changed

Lines changed: 60 additions & 8 deletions

File tree

libs/langchain-core/src/messages/base.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,7 @@ export abstract class BaseMessage<
217217
TRole extends MessageType = MessageType,
218218
>
219219
extends Serializable
220-
implements Message<TStructure, TRole>
221-
{
220+
implements Message<TStructure, TRole> {
222221
lc_namespace = ["langchain_core", "messages"];
223222

224223
lc_serializable = true;
@@ -566,7 +565,14 @@ function hasMergeableId(value: unknown): value is { id: string | number } {
566565
}
567566

568567
function getMergeableTypeBase(type: string): string {
569-
return type.endsWith("_delta") ? type.slice(0, -"_delta".length) : type;
568+
if (type === "input_json_delta" || type === "input_json") {
569+
return "tool_use";
570+
}
571+
572+
if (type.endsWith("_delta")) {
573+
return type.replace("_delta", "");
574+
}
575+
return type;
570576
}
571577

572578
function hasMismatchedMergeableType(left: unknown, right: unknown): boolean {
@@ -740,9 +746,9 @@ export type BaseMessageLike =
740746
* @deprecated Specifying "type" is deprecated and will be removed in 0.4.0.
741747
*/
742748
| ({
743-
type: MessageType | "user" | "assistant" | "placeholder";
744-
} & BaseMessageFields &
745-
Record<string, unknown>)
749+
type: MessageType | "user" | "assistant" | "placeholder";
750+
} & BaseMessageFields &
751+
Record<string, unknown>)
746752
| SerializedConstructor;
747753

748754
/**

libs/providers/langchain-anthropic/src/tests/chat_models.test.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,7 +1111,7 @@ describe("Tool search beta auto-append", () => {
11111111

11121112
expect(
11131113
paramsWithoutToolSearch.betas === undefined ||
1114-
!paramsWithoutToolSearch.betas.includes("advanced-tool-use-2025-11-20")
1114+
!paramsWithoutToolSearch.betas.includes("advanced-tool-use-2025-11-20")
11151115
).toBe(true);
11161116
});
11171117
});
@@ -1285,6 +1285,52 @@ describe("Streaming tool call consolidation (input_json_delta handling)", () =>
12851285
input: { prompt: "hello" },
12861286
});
12871287
});
1288+
1289+
test("should successfully aggregate tool_use and input_json_delta chunks via .concat()", async () => {
1290+
// 1. Replicate the exact asymmetric sequence Anthropic emits during an active stream
1291+
const chunk1 = new AIMessageChunk({
1292+
content: [
1293+
{
1294+
type: "tool_use",
1295+
index: 0,
1296+
id: "toolu_01Xyz",
1297+
name: "my_tool",
1298+
}
1299+
]
1300+
});
1301+
1302+
const chunk2 = new AIMessageChunk({
1303+
content: [
1304+
{
1305+
type: "input_json_delta",
1306+
index: 0,
1307+
partial_json: '{"prompt":',
1308+
}
1309+
]
1310+
});
1311+
1312+
const chunk3 = new AIMessageChunk({
1313+
content: [
1314+
{
1315+
type: "input_json_delta",
1316+
index: 0,
1317+
partial_json: '"hello"}',
1318+
}
1319+
]
1320+
});
1321+
1322+
// 2. Execute the concatenation (this is what threw OUTPUT_PARSING_FAILURE before your fix)
1323+
const merged = chunk1.concat(chunk2).concat(chunk3);
1324+
1325+
// 3. Assert that types map correctly and accumulate into a single valid tool call structure
1326+
expect(merged.content[0]).toEqual({
1327+
type: "tool_use",
1328+
index: 0,
1329+
id: "toolu_01Xyz",
1330+
name: "my_tool",
1331+
partial_json: '{"prompt":"hello"}'
1332+
});
1333+
});
12881334
});
12891335

12901336
describe("ContentBlock.Multimodal.Image format support", () => {
@@ -1959,7 +2005,7 @@ describe("Opus 4.6", () => {
19592005

19602006
expect(
19612007
params.betas === undefined ||
1962-
!params.betas.includes("compact-2026-01-12")
2008+
!params.betas.includes("compact-2026-01-12")
19632009
).toBe(true);
19642010
});
19652011

0 commit comments

Comments
 (0)