Skip to content

Commit e4193f8

Browse files
afirstenberghntrl
andauthored
fix(@langchain/google) Fixes for ID in Vertex function calls (#10292)
Co-authored-by: Hunter Lovell <40191806+hntrl@users.noreply.github.com> Co-authored-by: Hunter Lovell <hunter@hntrl.io>
1 parent c2960fe commit e4193f8

File tree

5 files changed

+104
-20
lines changed

5 files changed

+104
-20
lines changed

.changeset/silver-deers-reply.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@langchain/google": patch
3+
---
4+
5+
fixes for Vertex function calls

libs/providers/langchain-google/src/chat_models/tests/index.int.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -567,15 +567,15 @@ describe.each(coreModelInfo)(
567567
tool_calls: [
568568
{
569569
type: "tool_call",
570-
id: "test-id",
570+
id: "lc-tool-call-test-id",
571571
name: "test",
572572
args: {
573573
testName: "cobalt",
574574
},
575575
},
576576
],
577577
}),
578-
new ToolMessage(JSON.stringify(toolResult), "test-id"),
578+
new ToolMessage(JSON.stringify(toolResult), "lc-tool-call-test-id"),
579579
];
580580
const res = await llm.stream(messages);
581581
const resArray: BaseMessageChunk[] = [];

libs/providers/langchain-google/src/chat_models/tests/index.test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ class MockChunkStreamingResponse implements Response {
135135
readonly type: ResponseType = "basic";
136136
readonly url: string = "http://localhost";
137137
readonly bodyUsed: boolean = false;
138-
readonly body: ReadableStream<Uint8Array>;
138+
readonly body: ReadableStream<Uint8Array<ArrayBuffer>>;
139139

140140
constructor(chunks: object[]) {
141141
const encoder = new TextEncoder();
@@ -165,7 +165,7 @@ class MockChunkStreamingResponse implements Response {
165165
async text(): Promise<string> {
166166
throw new Error("Not implemented");
167167
}
168-
async bytes(): Promise<Uint8Array> {
168+
async bytes(): Promise<Uint8Array<ArrayBuffer>> {
169169
throw new Error("Not implemented");
170170
}
171171
clone(): Response {
@@ -983,11 +983,11 @@ describe("Google Mock", () => {
983983
{
984984
handleLLMNewToken(
985985
token: string,
986-
_idx: unknown,
987-
_runId: unknown,
988-
_parentRunId: unknown,
989-
_tags: unknown,
990-
fields: { chunk?: unknown }
986+
_idx,
987+
_runId,
988+
_parentRunId,
989+
_tags,
990+
fields
991991
) {
992992
newTokenCalls.push({ text: token, chunk: fields?.chunk });
993993
},
@@ -1100,11 +1100,11 @@ describe("Google Mock", () => {
11001100
{
11011101
handleLLMNewToken(
11021102
token: string,
1103-
_idx: unknown,
1104-
_runId: unknown,
1105-
_parentRunId: unknown,
1106-
_tags: unknown,
1107-
fields: { chunk?: unknown }
1103+
_idx,
1104+
_runId,
1105+
_parentRunId,
1106+
_tags,
1107+
fields
11081108
) {
11091109
newTokenCalls.push({ text: token, chunk: fields?.chunk });
11101110
},

libs/providers/langchain-google/src/converters/messages.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -465,9 +465,10 @@ function convertStandardContentMessageToGeminiContent(
465465
const matchedToolCall = aiMsg?.tool_calls?.find(
466466
(tc) => tc.id === message.tool_call_id
467467
);
468+
const isGeneratedId = message.tool_call_id.startsWith("lc-tool-call-");
468469
parts.push({
469470
functionResponse: {
470-
id: message.tool_call_id,
471+
...(isGeneratedId ? {} : { id: message.tool_call_id }),
471472
name: matchedToolCall?.name ?? message.name ?? "unknown",
472473
response: { result: responseContent },
473474
},
@@ -762,12 +763,13 @@ function convertLegacyContentMessageToGeminiContent(
762763
if (!aiMsg) {
763764
throw new ToolCallNotFoundError(message.tool_call_id);
764765
}
766+
const isGeneratedId = message.tool_call_id.startsWith("lc-tool-call-");
765767
const matchedToolCall = aiMsg.tool_calls?.find(
766768
(tc) => tc.id === message.tool_call_id
767769
);
768770
parts.push({
769771
functionResponse: {
770-
id: message.tool_call_id,
772+
...(isGeneratedId ? {} : { id: message.tool_call_id }),
771773
name: matchedToolCall?.name ?? message.name ?? "unknown",
772774
response: { result: responseContent },
773775
},
@@ -978,7 +980,9 @@ export const convertGeminiPartsToToolCalls: Converter<
978980
const functionCallPart = part as Gemini.Part.FunctionCall;
979981
toolCalls.push({
980982
type: "tool_call",
981-
id: functionCallPart.functionCall.id ?? uuidv4().replace(/-/g, ""),
983+
id:
984+
functionCallPart.functionCall.id ??
985+
`lc-tool-call-${uuidv4().replace(/-/g, "")}`,
982986
name: functionCallPart.functionCall.name,
983987
args: functionCallPart.functionCall.args ?? {},
984988
thoughtSignature: functionCallPart.thoughtSignature,

libs/providers/langchain-google/src/converters/tests/messages.test.ts

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe("convertGeminiPartsToToolCalls", () => {
4040

4141
expect(toolCalls).toHaveLength(1);
4242
expect(toolCalls[0].id).toBeDefined();
43-
expect(toolCalls[0].id).not.toBe("call_0");
43+
expect(toolCalls[0].id!.startsWith("lc-tool-call-")).toEqual(true);
4444
});
4545

4646
test("generates unique IDs across multiple invocations", () => {
@@ -57,6 +57,8 @@ describe("convertGeminiPartsToToolCalls", () => {
5757
const secondCallToolCalls = convertGeminiPartsToToolCalls(parts);
5858

5959
expect(firstCallToolCalls[0].id).not.toBe(secondCallToolCalls[0].id);
60+
expect(firstCallToolCalls[0].id!.startsWith("lc-tool-call-"));
61+
expect(secondCallToolCalls[0].id!.startsWith("lc-tool-call-"));
6062
});
6163

6264
test("generates unique IDs for multiple tool calls in the same response", () => {
@@ -134,7 +136,7 @@ describe("convertMessagesToGeminiContents", () => {
134136
const toolResponseContent = contents.find((c) => c.role === "function");
135137
expect(toolResponseContent).toBeDefined();
136138

137-
const functionResponsePart = toolResponseContent!.parts.find(
139+
const functionResponsePart = toolResponseContent!.parts!.find(
138140
(p) => "functionResponse" in p && p.functionResponse
139141
);
140142
expect(functionResponsePart).toBeDefined();
@@ -485,7 +487,7 @@ describe("convertMessagesToGeminiContents", () => {
485487
const toolResponseContent = contents.find((c) => c.role === "function");
486488
expect(toolResponseContent).toBeDefined();
487489

488-
const functionResponsePart = toolResponseContent!.parts.find(
490+
const functionResponsePart = toolResponseContent!.parts!.find(
489491
(p) => "functionResponse" in p && p.functionResponse
490492
);
491493
expect(functionResponsePart).toBeDefined();
@@ -495,6 +497,79 @@ describe("convertMessagesToGeminiContents", () => {
495497
).toBe("tool-call-xyz");
496498
});
497499

500+
test("omits generated tool_call_id from functionResponse.id (legacy path)", () => {
501+
const messages = [
502+
new HumanMessage("hello"),
503+
new AIMessage({
504+
content: "",
505+
tool_calls: [
506+
{
507+
name: "my_tool",
508+
args: { query: "test" },
509+
id: "lc-tool-call-abc",
510+
type: "tool_call",
511+
},
512+
],
513+
}),
514+
new ToolMessage({
515+
content: "result",
516+
tool_call_id: "lc-tool-call-abc",
517+
name: "my_tool",
518+
}),
519+
];
520+
521+
const contents = convertMessagesToGeminiContents(messages);
522+
523+
const toolResponseContent = contents.find((c) => c.role === "function");
524+
expect(toolResponseContent).toBeDefined();
525+
526+
const functionResponsePart = toolResponseContent!.parts!.find(
527+
(p) => "functionResponse" in p && p.functionResponse
528+
);
529+
expect(functionResponsePart).toBeDefined();
530+
expect(
531+
(functionResponsePart as Gemini.Part.FunctionResponse).functionResponse!
532+
.id
533+
).toBeUndefined();
534+
});
535+
536+
test("omits generated tool_call_id from functionResponse.id (v1 standard path)", () => {
537+
const messages = [
538+
new HumanMessage("hello"),
539+
new AIMessage({
540+
content: "",
541+
tool_calls: [
542+
{
543+
name: "my_tool",
544+
args: { query: "test" },
545+
id: "lc-tool-call-xyz",
546+
type: "tool_call",
547+
},
548+
],
549+
}),
550+
new ToolMessage({
551+
content: "result",
552+
tool_call_id: "lc-tool-call-xyz",
553+
name: "my_tool",
554+
response_metadata: { output_version: "v1" },
555+
}),
556+
];
557+
558+
const contents = convertMessagesToGeminiContents(messages);
559+
560+
const toolResponseContent = contents.find((c) => c.role === "function");
561+
expect(toolResponseContent).toBeDefined();
562+
563+
const functionResponsePart = toolResponseContent!.parts!.find(
564+
(p) => "functionResponse" in p && p.functionResponse
565+
);
566+
expect(functionResponsePart).toBeDefined();
567+
expect(
568+
(functionResponsePart as Gemini.Part.FunctionResponse).functionResponse!
569+
.id
570+
).toBeUndefined();
571+
});
572+
498573
test("v1 contentBlocks: text-plain block produces fileData part", () => {
499574
const messages = [
500575
new HumanMessage({

0 commit comments

Comments
 (0)