diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index aecacb2d25..6560e331eb 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -5194,6 +5194,20 @@ def _convert_responses_chunk_to_generation_chunk( if getattr(chunk.item, "namespace", None) is not None: function_call_content["namespace"] = chunk.item.namespace content.append(function_call_content) + elif ( + chunk.type == "response.output_item.done" and chunk.item.type == "function_call" + ): + # The "added" event above records the transient status (typically + # "in_progress"); update it here with the terminal status (e.g. + # "completed") so a replayed history doesn't send a stale value. + _advance(chunk.output_index) + content.append( + { + "type": "function_call", + "status": getattr(chunk.item, "status", None), + "index": current_index, + } + ) elif chunk.type == "response.output_item.done" and chunk.item.type in ( "compaction", "web_search_call", diff --git a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py index 1ef3e10837..769c23f52d 100644 --- a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py @@ -2662,6 +2662,66 @@ def test__convert_responses_chunk_to_generation_chunk_function_call_status() -> assert function_call_block["status"] == "in_progress" +def test__convert_responses_chunk_to_generation_chunk_function_call_done_status() -> ( + None +): + """A streamed function_call's terminal status overwrites the transient one. + + The "added" event reports a transient status (typically "in_progress"), + and the "done" event reports the terminal status (e.g. "completed"). + Merging the two chunks must not concatenate "status" as if it were + accumulated text. + """ + added_chunk = MagicMock() + added_chunk.type = "response.output_item.added" + added_chunk.output_index = 0 + added_chunk.item = ResponseFunctionToolCall( + type="function_call", + id="func_123", + call_id="call_123", + name="get_weather", + arguments="", + status="in_progress", + ) + + idx, output_idx, sub_idx, added_generation_chunk = ( + _convert_responses_chunk_to_generation_chunk( + added_chunk, + current_index=-1, + current_output_index=-1, + current_sub_index=-1, + ) + ) + assert added_generation_chunk is not None + + done_chunk = MagicMock() + done_chunk.type = "response.output_item.done" + done_chunk.output_index = 0 + done_chunk.item = ResponseFunctionToolCall( + type="function_call", + id="func_123", + call_id="call_123", + name="get_weather", + arguments="{}", + status="completed", + ) + + _, _, _, done_generation_chunk = _convert_responses_chunk_to_generation_chunk( + done_chunk, + current_index=idx, + current_output_index=output_idx, + current_sub_index=sub_idx, + ) + assert done_generation_chunk is not None + + merged = added_generation_chunk + done_generation_chunk + content = merged.message.content + assert isinstance(content, list) + function_call_block = content[0] + assert isinstance(function_call_block, dict) + assert function_call_block["status"] == "completed" + + def test__construct_lc_result_from_responses_api_web_search_response() -> None: """Test a response with web search output.""" from openai.types.responses.response_function_web_search import (