Skip to content
5 changes: 5 additions & 0 deletions .changeset/soft-monkeys-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@langchain/core": patch
---

fix(core): support input_json_delta aggregation for anthropic tool streams
9 changes: 8 additions & 1 deletion libs/langchain-core/src/messages/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,14 @@ function hasMergeableId(value: unknown): value is { id: string | number } {
}

function getMergeableTypeBase(type: string): string {
return type.endsWith("_delta") ? type.slice(0, -"_delta".length) : type;
if (type === "input_json_delta" || type === "input_json") {
return "tool_use";
}

if (type.endsWith("_delta")) {
return type.replace("_delta", "");
}
return type;
}

function hasMismatchedMergeableType(left: unknown, right: unknown): boolean {
Expand Down
48 changes: 48 additions & 0 deletions libs/langchain-core/src/messages/tests/messages.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isMessage } from "../message.js";
import { AIMessageChunk } from "../index.js";

describe("isMessage", () => {
describe("valid cases", () => {
Expand Down Expand Up @@ -73,3 +74,50 @@ describe("isMessage", () => {
});
});
});

describe("AIMessageChunk", () => {
describe(".concat()", () => {
test("should successfully aggregate tool_use and input_json_delta chunks", async () => {
const chunk1 = new AIMessageChunk({
content: [
{
type: "tool_use",
index: 0,
id: "toolu_01Xyz",
name: "my_tool",
},
],
});

const chunk2 = new AIMessageChunk({
content: [
{
type: "input_json_delta",
index: 0,
partial_json: '{"prompt":',
},
],
});

const chunk3 = new AIMessageChunk({
content: [
{
type: "input_json_delta",
index: 0,
partial_json: '"hello"}',
},
],
});

const merged = chunk1.concat(chunk2).concat(chunk3);

expect(merged.content[0]).toEqual({
type: "tool_use",
index: 0,
id: "toolu_01Xyz",
name: "my_tool",
partial_json: '{"prompt":"hello"}',
});
});
});
});