From dae4e5de9d5623fbc25cc0bc8911872bf5cc994f Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Mon, 22 Jun 2026 23:01:15 -0400 Subject: [PATCH] feat(openrouter): surface `parallel_tool_calls` on `bind_tools` (#38214) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ChatOpenRouter.bind_tools()` now accepts a `parallel_tool_calls` argument to disable parallel tool use. --- Users binding tools to `ChatOpenRouter` previously had to know that arbitrary kwargs were forwarded to the OpenRouter SDK in order to disable parallel tool use (e.g. `model.bind(parallel_tool_calls=False)`). This made the option hard to discover. This exposes `parallel_tool_calls` as an explicit keyword-only argument on `ChatOpenRouter.bind_tools()`, matching the ergonomics of `langchain-openai`. When set, it is forwarded to the request exactly as before; when left as `None` it is omitted, preserving existing behavior. No change to the underlying request payload or SDK floor — purely an API/discoverability improvement. Made by [Open SWE](https://openswe.vercel.app/agents/053b59c8-baf4-84e1-c003-425e349e014d) Co-authored-by: open-swe[bot] --- .../openrouter/langchain_openrouter/chat_models.py | 6 ++++++ .../tests/unit_tests/test_chat_models.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/libs/partners/openrouter/langchain_openrouter/chat_models.py b/libs/partners/openrouter/langchain_openrouter/chat_models.py index 0e591c6a0cb..c9526a3ca4e 100644 --- a/libs/partners/openrouter/langchain_openrouter/chat_models.py +++ b/libs/partners/openrouter/langchain_openrouter/chat_models.py @@ -837,6 +837,7 @@ class ChatOpenRouter(BaseChatModel): *, tool_choice: dict | str | bool | None = None, strict: bool | None = None, + parallel_tool_calls: bool | None = None, **kwargs: Any, ) -> Runnable[LanguageModelInput, AIMessage]: """Bind tool-like objects to this chat model. @@ -852,8 +853,13 @@ class ChatOpenRouter(BaseChatModel): If `None`, the `strict` argument will not be passed to the model. + parallel_tool_calls: Set to `False` to disable parallel tool use. + Defaults to `None` (no specification, which allows parallel + tool use). **kwargs: Any additional parameters. """ + if parallel_tool_calls is not None: + kwargs["parallel_tool_calls"] = parallel_tool_calls formatted_tools = [ convert_to_openai_tool(tool, strict=strict) for tool in tools ] diff --git a/libs/partners/openrouter/tests/unit_tests/test_chat_models.py b/libs/partners/openrouter/tests/unit_tests/test_chat_models.py index 4ba52979164..4d62e042374 100644 --- a/libs/partners/openrouter/tests/unit_tests/test_chat_models.py +++ b/libs/partners/openrouter/tests/unit_tests/test_chat_models.py @@ -1044,6 +1044,20 @@ class TestBindTools: tools = bound.kwargs["tools"] assert "strict" not in tools[0]["function"] + def test_bind_tools_parallel_tool_calls_forwarded(self) -> None: + """Test that parallel_tool_calls is forwarded to the request kwargs.""" + model = _make_model() + bound = model.bind_tools([GetWeather], parallel_tool_calls=False) + assert isinstance(bound, RunnableBinding) + assert bound.kwargs["parallel_tool_calls"] is False + + def test_bind_tools_parallel_tool_calls_none_omits_key(self) -> None: + """Test that parallel_tool_calls=None does not set the key in kwargs.""" + model = _make_model() + bound = model.bind_tools([GetWeather]) + assert isinstance(bound, RunnableBinding) + assert "parallel_tool_calls" not in bound.kwargs + # =========================================================================== # with_structured_output tests