Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/honest-hats-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@langchain/core": patch
---

omit tool call chunks without tool call id
5 changes: 5 additions & 0 deletions .changeset/metal-camels-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@langchain/openai": patch
---

fix(openai): fix streaming in openai
2 changes: 1 addition & 1 deletion langchain-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@langchain/core",
"version": "0.3.76",
"version": "0.3.77",
"description": "Core LangChain.js abstractions and schemas",
"type": "module",
"engines": {
Expand Down
44 changes: 32 additions & 12 deletions langchain-core/src/messages/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,30 +271,50 @@ export class AIMessageChunk extends BaseMessageChunk {
: undefined,
};
} else {
const groupedToolCallChunk = fields.tool_call_chunks.reduce(
const groupedToolCallChunks = fields.tool_call_chunks.reduce(
(acc, chunk) => {
// Assign a fallback ID if the chunk doesn't have one
// This can happen with tools that have empty schemas
const chunkId = chunk.id || `fallback-${chunk.index || 0}`;
acc[chunkId] = acc[chunkId] ?? [];
acc[chunkId].push(chunk);
const matchedChunkIndex = acc.findIndex(([match]) => {
// If chunk has an id and index, match if both are present
if (
"id" in chunk &&
chunk.id &&
"index" in chunk &&
chunk.index !== undefined
) {
return chunk.id === match.id && chunk.index === match.index;
}
// If chunk has an id, we match on id
if ("id" in chunk && chunk.id) {
return chunk.id === match.id;
}
// If chunk has an index, we match on index
if ("index" in chunk && chunk.index !== undefined) {
return chunk.index === match.index;
}
return false;
});
if (matchedChunkIndex !== -1) {
acc[matchedChunkIndex].push(chunk);
} else {
acc.push([chunk]);
}
return acc;
},
{} as Record<string, ToolCallChunk[]>
[] as ToolCallChunk[][]
);

const toolCalls: ToolCall[] = [];
const invalidToolCalls: InvalidToolCall[] = [];
for (const [id, chunks] of Object.entries(groupedToolCallChunk)) {
for (const chunks of groupedToolCallChunks) {
let parsedArgs = {};
const name = chunks[0]?.name ?? "";
const joinedArgs = chunks.map((c) => c.args || "").join("");
const argsStr = joinedArgs.length ? joinedArgs : "{}";
// Use the original ID from the first chunk if it exists, otherwise use the grouped ID
const originalId = chunks[0]?.id || id;
const id = chunks[0]?.id;
try {
parsedArgs = parsePartialJson(argsStr);
if (
!id ||
parsedArgs === null ||
typeof parsedArgs !== "object" ||
Array.isArray(parsedArgs)
Expand All @@ -304,14 +324,14 @@ export class AIMessageChunk extends BaseMessageChunk {
toolCalls.push({
name,
args: parsedArgs,
id: originalId,
id,
type: "tool_call",
});
} catch (e) {
invalidToolCalls.push({
name,
args: argsStr,
id: originalId,
id,
error: "Malformed args.",
type: "invalid_tool_call",
});
Expand Down
4 changes: 3 additions & 1 deletion langchain-core/src/messages/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,9 @@ export function _mergeDicts(
if (key === "type") {
// Do not merge 'type' fields
continue;
} else if (["id", "output_version", "model_provider"].includes(key)) {
} else if (
["id", "name", "output_version", "model_provider"].includes(key)
) {
// Keep the incoming value for these fields
merged[key] = value;
} else {
Expand Down
242 changes: 202 additions & 40 deletions langchain-core/src/messages/tests/base_message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,56 +467,218 @@ describe("Complex AIMessageChunk concat", () => {
});

it("concatenates tool call chunks without IDs", () => {
const chunks: ToolCallChunk[] = [
{
name: "get_current_time",
type: "tool_call_chunk",
index: 0,
// no `id` provided
},
const chunks = [
new AIMessageChunk({
id: "chatcmpl-x",
content: "",
tool_call_chunks: [
{
name: "get_weather",
args: "",
id: "call_q6ZzjkLjKNYb4DizyMOaqpfW",
index: 0,
type: "tool_call_chunk",
},
],
}),
new AIMessageChunk({
id: "chatcmpl-x",
content: "",
tool_call_chunks: [
{
args: '{"',
index: 0,
type: "tool_call_chunk",
},
],
}),
new AIMessageChunk({
id: "chatcmpl-x",
content: "",
tool_call_chunks: [
{
args: "location",
index: 0,
type: "tool_call_chunk",
},
],
}),
new AIMessageChunk({
id: "chatcmpl-x",
content: "",
tool_call_chunks: [
{
args: '":"',
index: 0,
type: "tool_call_chunk",
},
],
}),
new AIMessageChunk({
id: "chatcmpl-x",
content: "",
tool_call_chunks: [
{
args: "San",
index: 0,
type: "tool_call_chunk",
},
],
}),
new AIMessageChunk({
id: "chatcmpl-x",
content: "",
tool_call_chunks: [
{
args: " Francisco",
index: 0,
type: "tool_call_chunk",
},
],
}),
new AIMessageChunk({
id: "chatcmpl-x",
content: "",
tool_call_chunks: [
{
args: '"}',
index: 0,
type: "tool_call_chunk",
},
],
}),
];

const result = new AIMessageChunk({
content: "",
tool_call_chunks: chunks,
});

expect(result.tool_calls?.length).toBe(1);
expect(result.invalid_tool_calls?.length).toBe(0);
expect(result.tool_calls).toEqual([
let finalChunk = new AIMessageChunk("");
for (const chunk of chunks) {
finalChunk = finalChunk.concat(chunk);
}
expect(finalChunk.tool_calls).toHaveLength(1);
expect(finalChunk.tool_calls).toEqual([
{
id: "fallback-0", // Should get fallback ID
name: "get_current_time",
args: {},
type: "tool_call",
name: "get_weather",
args: {
location: "San Francisco",
},
id: "call_q6ZzjkLjKNYb4DizyMOaqpfW",
},
]);
});
});

it("concatenates tool call chunks without IDs and no index", () => {
const chunks: ToolCallChunk[] = [
{
name: "get_current_time",
type: "tool_call_chunk",
// no `id` or `index` provided
},
];
describe("AIMessageChunk", () => {
describe("constructor", () => {
it("omits tool call chunks without IDs", () => {
const chunks: ToolCallChunk[] = [
{
name: "get_current_time",
type: "tool_call_chunk",
index: 0,
// no `id` provided
},
];

const result = new AIMessageChunk({
content: "",
tool_call_chunks: chunks,
const result = new AIMessageChunk({
content: "",
tool_call_chunks: chunks,
});

expect(result.tool_calls?.length).toBe(0);
expect(result.invalid_tool_calls?.length).toBe(1);
expect(result.invalid_tool_calls).toEqual([
{
type: "invalid_tool_call",
id: undefined,
name: "get_current_time",
args: "{}",
error: "Malformed args.",
},
]);
});

expect(result.tool_calls?.length).toBe(1);
expect(result.invalid_tool_calls?.length).toBe(0);
expect(result.tool_calls).toEqual([
{
id: "fallback-0", // Should get fallback ID with index 0
name: "get_current_time",
args: {},
type: "tool_call",
},
]);
it("omits tool call chunks without IDs and no index", () => {
const chunks: ToolCallChunk[] = [
{
name: "get_current_time",
type: "tool_call_chunk",
// no `id` or `index` provided
},
];

const result = new AIMessageChunk({
content: "",
tool_call_chunks: chunks,
});

expect(result.tool_calls?.length).toBe(0);
expect(result.invalid_tool_calls?.length).toBe(1);
expect(result.invalid_tool_calls).toEqual([
{
type: "invalid_tool_call",
id: undefined,
name: "get_current_time",
args: "{}",
error: "Malformed args.",
},
]);
});

it("can concatenate tool call chunks without IDs", () => {
const chunk = new AIMessageChunk({
id: "chatcmpl-x",
content: "",
tool_call_chunks: [
{
name: "get_weather",
args: "",
id: "call_q6ZzjkLjKNYb4DizyMOaqpfW",
index: 0,
type: "tool_call_chunk",
},
{
args: '{"',
index: 0,
type: "tool_call_chunk",
},
{
args: "location",
index: 0,
type: "tool_call_chunk",
},
{
args: '":"',
index: 0,
type: "tool_call_chunk",
},
{
args: "San",
index: 0,
type: "tool_call_chunk",
},
{
args: " Francisco",
index: 0,
type: "tool_call_chunk",
},
{
args: '"}',
index: 0,
type: "tool_call_chunk",
},
],
});
expect(chunk.tool_calls).toHaveLength(1);
expect(chunk.tool_calls).toEqual([
{
type: "tool_call",
name: "get_weather",
args: {
location: "San Francisco",
},
id: "call_q6ZzjkLjKNYb4DizyMOaqpfW",
},
]);
});
});
});

Expand Down
2 changes: 1 addition & 1 deletion libs/langchain-openai/src/chat_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ export abstract class BaseChatOpenAI<
this.verbosity = fields?.verbosity ?? this.verbosity;

// disable streaming in BaseChatModel if explicitly disabled
if (this.streaming === false) this.disableStreaming = true;
if (fields?.streaming === false) this.disableStreaming = true;
if (this.disableStreaming === true) this.streaming = false;

this.streamUsage = fields?.streamUsage ?? this.streamUsage;
Expand Down
Loading
Loading