SignatureRepair — LangChain.js middleware fixing Gemini 3.x thought_signature drops on streamed tool calls
Workaround for langchain-ai/langchainjs#9624.
Gemini 3.x (gemini-3-flash-preview, gemini-3.1-flash-lite, gemini-3.5-flash, …) requires that
every replayed functionCall part carries back the thoughtSignature it was issued with. When you
stream the turn that emits a tool call, @langchain/google-common's chunk merging concatenates the
per-chunk signature arrays out of alignment with the re-serialized parts — you end up with e.g.
signatures = [sig, ""] against a single functionCall part. The serializer then applies an
exact-length guard (signatures array length must equal parts array length) and, on mismatch,
silently drops ALL signatures. Vertex/GenAI rejects the next turn with 400 INVALID_ARGUMENT.
Symptom: the first turn (including the tool call) works; the second turn 400s. Non-streaming
(invoke) is unaffected — only streaming triggers the misaligned merge.
Re-derive the parts-parallel signature array the serializer expects, before the next model call:
one "" for the leading text part (if any), then one non-empty signature per tool call, in order.
This is the same shape the non-streaming path produces, so the exact-length guard passes and the
signatures survive. (Confirmed by multiple people on the upstream issue: dropping the exact-length
guard and index-mapping the non-empty signatures onto the functionCall parts also fixes it — this
middleware does that mapping from the outside, without patching the library.)
Add it to your agent's middleware chain (LangChain.js v1 agent middleware):
import { createAgent } from "langchain"
import { signatureRepair } from "./signatureRepair.ts"
const agent = createAgent({
model, // a Gemini 3.x model via @langchain/google-vertexai or @langchain/google
tools,
systemPrompt,
middleware: [signatureRepair /*, ...others */],
})Verified against @langchain/core@1.2.1, @langchain/google-common@2.2.0,
@langchain/google-vertexai@2.2.0, langchain@1.2.x.
- Only touches messages where the non-empty signature count matches the tool-call count — shapes it doesn't recognize (e.g. thinking models emitting signed thought blocks) are left untouched.
- Temporary. Remove once the exact-length guard is fixed upstream.