chat_node builds its invoke kwargs with a class-name test:
if model.__class__.__name__ in ["ChatOpenAI"]:
ainvoke_kwargs["parallel_tool_calls"] = False
(chat.py:79-80). The list has exactly one entry, but the picker offers four backends —
ChatOpenAI, ChatAnthropic (model.py:30), ChatGoogleGenerativeAI (model.py:39) and
ChatXAI (model-selection-flow). For the other
three, nothing suppresses parallel tool calls.
The rest of the node is written as if exactly one can ever arrive. Every branch indexes — the handler (), its (), and the routing chain that picks / / (). So when a model returns two tool calls in one assistant message, the first is answered and the second is silently dropped: the conversation now holds a with no matching , which Anthropic and Google both reject on the request, not this one.
That delayed rejection is what makes it worth a row. It is not a crash at the call site — it is a provider-side 400 one turn later, on a message the user did not send, which reads as "the chat broke for no reason" and only on some model choices. Nothing in the repo catches it: there are no tests, no CI, and no mypy (coverage — the test surface is grep-proven absent), and the happy path is the single-tool-call case the system prompt already nudges the model toward.
Two ways to close it, pick one
- Suppress it everywhere (smaller diff, matches current intent). Widen the check to the
other classes; each provider has its own spelling of the flag, so this is a per-provider
mapping rather than one shared kwarg — verify against the installed LangChain integration
before wiring, don't assume
parallel_tool_callsis accepted verbatim. - Handle the list (more honest). Iterate
ai_message.tool_callsand emit oneToolMessageper entry, keeping the routing decision on . This also makes the node correct if the suppression flag is ever dropped or ignored by a provider.
Related but distinct: fact_check_node indexes tool_calls[0] with no existence guard at
all (fact-check-node-unguarded-toolcall).
That row is about the empty case; this one is about the many case. A single sitting in
chat.py/fact_check.py can settle both.