From 6d447f89d9ccd6fc5d9e0bcf3acdf0844316df02 Mon Sep 17 00:00:00 2001 From: Dragos Bobolea Date: Sat, 27 Dec 2025 10:42:06 +0200 Subject: [PATCH] fix(fireworks): `bind_tools(strict: bool)` and `reasoning_content` (#34343) Extract strict from kwargs and pass it to convert_to_openai_tool when converting tools. This ensures that when strict is provided, it's properly used during tool conversion and removed from kwargs before calling the parent bind method. Also extract reasoning_content from API responses and store it in additional_kwargs for AIMessage objects. Fixes https://github.com/langchain-ai/langchain/issues/34341 and https://github.com/langchain-ai/langchain/issues/34342 --------- Co-authored-by: Mason Daugherty --- .../langchain_fireworks/chat_models.py | 9 ++++- .../integration_tests/test_chat_models.py | 21 ++++++++-- .../tests/unit_tests/test_chat_models.py | 38 +++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 libs/partners/fireworks/tests/unit_tests/test_chat_models.py diff --git a/libs/partners/fireworks/langchain_fireworks/chat_models.py b/libs/partners/fireworks/langchain_fireworks/chat_models.py index ec8d65f0818..37c8c4bc488 100644 --- a/libs/partners/fireworks/langchain_fireworks/chat_models.py +++ b/libs/partners/fireworks/langchain_fireworks/chat_models.py @@ -114,8 +114,12 @@ def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage: # Also Fireworks returns None for tool invocations content = _dict.get("content", "") or "" additional_kwargs: dict = {} + if reasoning_content := _dict.get("reasoning_content"): + additional_kwargs["reasoning_content"] = reasoning_content + if function_call := _dict.get("function_call"): additional_kwargs["function_call"] = dict(function_call) + tool_calls = [] invalid_tool_calls = [] if raw_tool_calls := _dict.get("tool_calls"): @@ -678,7 +682,10 @@ class ChatFireworks(BaseChatModel): **kwargs: Any additional parameters to pass to `langchain_fireworks.chat_models.ChatFireworks.bind` """ # noqa: E501 - formatted_tools = [convert_to_openai_tool(tool) for tool in tools] + strict = kwargs.pop("strict", None) + formatted_tools = [ + convert_to_openai_tool(tool, strict=strict) for tool in tools + ] if tool_choice is not None and tool_choice: if isinstance(tool_choice, str) and ( tool_choice not in ("auto", "any", "none") diff --git a/libs/partners/fireworks/tests/integration_tests/test_chat_models.py b/libs/partners/fireworks/tests/integration_tests/test_chat_models.py index 2cd6539388f..112fe77675e 100644 --- a/libs/partners/fireworks/tests/integration_tests/test_chat_models.py +++ b/libs/partners/fireworks/tests/integration_tests/test_chat_models.py @@ -18,15 +18,30 @@ from langchain_fireworks import ChatFireworks _MODEL = "accounts/fireworks/models/gpt-oss-120b" -def test_tool_choice_bool() -> None: - """Test that tool choice is respected just passing in True.""" +@pytest.mark.parametrize("strict", [None, True, False]) +def test_tool_choice_bool(strict: bool | None) -> None: # noqa: FBT001 + """Test that tool choice is respected with different strict values.""" llm = ChatFireworks(model="fireworks/kimi-k2-instruct-0905") class MyTool(BaseModel): name: str age: int - with_tool = llm.bind_tools([MyTool], tool_choice=True) + kwargs = {"tool_choice": True} + if strict is not None: + kwargs["strict"] = strict + with_tool = llm.bind_tools([MyTool], **kwargs) + + # Verify that strict is correctly set in the tool definition + assert hasattr(with_tool, "kwargs") + tools = with_tool.kwargs.get("tools", []) + assert len(tools) == 1 + tool_def = tools[0] + assert "function" in tool_def + if strict is None: + assert "strict" not in tool_def["function"] + else: + assert tool_def["function"].get("strict") is strict resp = with_tool.invoke("Who was the 27 year old named Erick?") assert isinstance(resp, AIMessage) diff --git a/libs/partners/fireworks/tests/unit_tests/test_chat_models.py b/libs/partners/fireworks/tests/unit_tests/test_chat_models.py new file mode 100644 index 00000000000..948b5e45c58 --- /dev/null +++ b/libs/partners/fireworks/tests/unit_tests/test_chat_models.py @@ -0,0 +1,38 @@ +"""Unit tests for ChatFireworks.""" + +from __future__ import annotations + +from langchain_core.messages import AIMessage + +from langchain_fireworks.chat_models import _convert_dict_to_message + + +def test_convert_dict_to_message_with_reasoning_content() -> None: + """Test that reasoning_content is correctly extracted from API response.""" + response_dict = { + "role": "assistant", + "content": "The answer is 42.", + "reasoning_content": "Let me think about this step by step...", + } + + message = _convert_dict_to_message(response_dict) + + assert isinstance(message, AIMessage) + assert message.content == "The answer is 42." + assert "reasoning_content" in message.additional_kwargs + expected_reasoning = "Let me think about this step by step..." + assert message.additional_kwargs["reasoning_content"] == expected_reasoning + + +def test_convert_dict_to_message_without_reasoning_content() -> None: + """Test that messages without reasoning_content work correctly.""" + response_dict = { + "role": "assistant", + "content": "The answer is 42.", + } + + message = _convert_dict_to_message(response_dict) + + assert isinstance(message, AIMessage) + assert message.content == "The answer is 42." + assert "reasoning_content" not in message.additional_kwargs