diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index 5ac28a717ac..be8eaffc616 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -1483,6 +1483,9 @@ class BaseChatOpenAI(BaseChatModel): chat_message = chat_result.generations[0].message if isinstance(chat_message, AIMessage): usage_metadata = chat_message.usage_metadata + # Skip tool_calls, already sent as chunks + if "tool_calls" in chat_message.additional_kwargs: + chat_message.additional_kwargs.pop("tool_calls") else: usage_metadata = None message = AIMessageChunk( diff --git a/libs/partners/openai/tests/integration_tests/chat_models/test_base.py b/libs/partners/openai/tests/integration_tests/chat_models/test_base.py index ff7352b2bc6..6ee7bc0536d 100644 --- a/libs/partners/openai/tests/integration_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/integration_tests/chat_models/test_base.py @@ -1213,3 +1213,28 @@ def test_multi_party_conversation() -> None: ] response = llm.invoke(messages) assert "Bob" in response.content + + +def test_structured_output_and_tools() -> None: + class ResponseFormat(BaseModel): + response: str + explanation: str + + llm = ChatOpenAI(model="gpt-4o-mini").bind_tools( + [GenerateUsername], strict=True, response_format=ResponseFormat + ) + + response = llm.invoke("What weighs more, a pound of feathers or a pound of gold?") + assert isinstance(response.additional_kwargs["parsed"], ResponseFormat) + + # Test streaming tool calls + full: Optional[BaseMessageChunk] = None + for chunk in llm.stream( + "Generate a user name for Alice, black hair. Use the tool." + ): + assert isinstance(chunk, AIMessageChunk) + full = chunk if full is None else full + chunk + assert isinstance(full, AIMessageChunk) + assert len(full.tool_calls) == 1 + tool_call = full.tool_calls[0] + assert tool_call["name"] == "GenerateUsername"