Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion libs/core/langchain_core/utils/function_calling.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ def convert_to_openai_function(
"web_search_preview",
"web_search",
"tool_search",
"apply_patch",
"namespace",
)

Expand Down Expand Up @@ -546,7 +547,7 @@ def convert_to_openai_tool(

Return OpenAI Responses API-style tools unchanged. This includes
any dict with `"type"` in `"file_search"`, `"function"`,
`"computer_use_preview"`, `"web_search_preview"`.
`"computer_use_preview"`, `"web_search_preview"`, `"apply_patch"`.

!!! warning "Behavior changed in `langchain-core` 0.3.63"

Expand Down
9 changes: 9 additions & 0 deletions libs/core/tests/unit_tests/utils/test_function_calling.py
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,15 @@ def test_convert_to_openai_function_json_schema_missing_title_includes_schema()
convert_to_openai_function(schema_without_title)


def test_convert_to_openai_tool_apply_patch_passthrough() -> None:
"""Test apply_patch is passed through as an OpenAI built-in tool."""
tool = {"type": "apply_patch"}

result = convert_to_openai_tool(tool)

assert result == tool


def test_convert_to_openai_tool_computer_passthrough() -> None:
"""Test that the 'computer' tool type is passed through unchanged."""
computer_tool = {
Expand Down
13 changes: 12 additions & 1 deletion libs/partners/openai/langchain_openai/chat_models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ def _get_default_model_profile(model_name: str) -> ModelProfile:
"mcp",
"image_generation",
"tool_search",
"apply_patch",
)


Expand Down Expand Up @@ -4490,6 +4491,8 @@ def _construct_responses_api_input(messages: Sequence[BaseMessage]) -> list:
"mcp_approval_request",
"tool_search_call",
"tool_search_output",
"apply_patch_call",
"apply_patch_call_output",
):
input_.append(_pop_index_and_sub_index(block))
elif block_type == "image_generation_call":
Expand Down Expand Up @@ -4535,7 +4538,11 @@ def _construct_responses_api_input(messages: Sequence[BaseMessage]) -> list:
elif msg["role"] in ("user", "system", "developer"):
if isinstance(msg["content"], list):
new_blocks = []
non_message_item_types = ("mcp_approval_response", "tool_search_output")
non_message_item_types = (
"mcp_approval_response",
"tool_search_output",
"apply_patch_call_output",
)
for block in msg["content"]:
if block["type"] in ("text", "image_url", "file"):
new_blocks.append(
Expand Down Expand Up @@ -4704,6 +4711,8 @@ def _construct_lc_result_from_responses_api(
"image_generation_call",
"tool_search_call",
"tool_search_output",
"apply_patch_call",
"apply_patch_call_output",
):
content_blocks.append(output.model_dump(exclude_none=True, mode="json"))

Expand Down Expand Up @@ -4958,6 +4967,8 @@ def _advance(output_idx: int, sub_idx: int | None = None) -> None:
"image_generation_call",
"tool_search_call",
"tool_search_output",
"apply_patch_call",
"apply_patch_call_output",
):
_advance(chunk.output_index)
tool_output = chunk.item.model_dump(exclude_none=True, mode="json")
Expand Down
192 changes: 192 additions & 0 deletions libs/partners/openai/tests/unit_tests/chat_models/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
_construct_responses_api_input,
_convert_dict_to_message,
_convert_message_to_dict,
_convert_responses_chunk_to_generation_chunk,
_convert_to_openai_response_format,
_create_usage_metadata,
_create_usage_metadata_responses,
Expand Down Expand Up @@ -1924,6 +1925,185 @@ def test__construct_lc_result_from_responses_api_no_usage_metadata() -> None:
assert cast(AIMessage, result.generations[0].message).usage_metadata is None


def test__construct_lc_result_from_responses_api_apply_patch_response() -> None:
"""Test a response with apply_patch output."""
apply_patch_call = MagicMock()
apply_patch_call.type = "apply_patch_call"
apply_patch_call.model_dump.return_value = {
"type": "apply_patch_call",
"call_id": "call_123",
"operation": {
"type": "create_file",
"path": "hello.txt",
"diff": "+hello\\n",
},
"status": "completed",
}

response = MagicMock()
response.error = None
response.id = "resp_123"
response.output = [apply_patch_call]
response.usage = None
response.service_tier = None
response.text = None
response.model_dump.return_value = {
"id": "resp_123",
"created_at": 1234567890,
"model": "gpt-4o",
"object": "response",
"status": "completed",
}

result = _construct_lc_result_from_responses_api(response)

message = cast(AIMessage, result.generations[0].message)
assert message.content == [
{
"type": "apply_patch_call",
"call_id": "call_123",
"operation": {
"type": "create_file",
"path": "hello.txt",
"diff": "+hello\\n",
},
"status": "completed",
}
]
assert message.tool_calls == []
assert message.invalid_tool_calls == []


def test__construct_lc_result_from_responses_api_apply_patch_call_output() -> None:
"""Test a response with apply_patch_call_output output."""
apply_patch_call_output = MagicMock()
apply_patch_call_output.type = "apply_patch_call_output"
apply_patch_call_output.model_dump.return_value = {
"type": "apply_patch_call_output",
"call_id": "call_123",
"status": "completed",
"output": "Created hello.txt",
}

response = MagicMock()
response.error = None
response.id = "resp_123"
response.output = [apply_patch_call_output]
response.usage = None
response.service_tier = None
response.text = None
response.model_dump.return_value = {
"id": "resp_123",
"created_at": 1234567890,
"model": "gpt-4o",
"object": "response",
"status": "completed",
}

result = _construct_lc_result_from_responses_api(response)

message = result.generations[0].message
assert message.content == [
{
"type": "apply_patch_call_output",
"call_id": "call_123",
"status": "completed",
"output": "Created hello.txt",
}
]


def test__construct_responses_api_input_apply_patch_round_trip() -> None:
"""Test apply_patch content blocks are preserved when sent back as input."""
messages = [
AIMessage(
content=[
{
"type": "apply_patch_call",
"call_id": "call_123",
"operation": {
"type": "create_file",
"path": "hello.txt",
"diff": "+hello\\n",
},
"status": "completed",
}
]
),
HumanMessage(
content=[
{
"type": "apply_patch_call_output",
"call_id": "call_123",
"status": "completed",
"output": "Created hello.txt",
}
]
),
]

result = _construct_responses_api_input(messages)

assert result == [
{
"type": "apply_patch_call",
"call_id": "call_123",
"operation": {
"type": "create_file",
"path": "hello.txt",
"diff": "+hello\\n",
},
"status": "completed",
},
{
"type": "apply_patch_call_output",
"call_id": "call_123",
"status": "completed",
"output": "Created hello.txt",
},
]


def test__convert_responses_chunk_to_generation_chunk_apply_patch_response() -> None:
"""Test streamed apply_patch output item is preserved in message chunks."""
chunk = MagicMock()
chunk.type = "response.output_item.done"
chunk.output_index = 0
chunk.item.type = "apply_patch_call"
chunk.item.model_dump.return_value = {
"type": "apply_patch_call",
"call_id": "call_123",
"operation": {
"type": "create_file",
"path": "hello.txt",
"diff": "+hello\\n",
},
"status": "completed",
}

_, _, _, generation_chunk = _convert_responses_chunk_to_generation_chunk(
chunk,
current_index=-1,
current_output_index=-1,
current_sub_index=-1,
)

assert generation_chunk is not None
assert generation_chunk.message.content == [
{
"type": "apply_patch_call",
"call_id": "call_123",
"operation": {
"type": "create_file",
"path": "hello.txt",
"diff": "+hello\\n",
},
"status": "completed",
"index": 0,
}
]


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 (
Expand Down Expand Up @@ -3695,6 +3875,18 @@ def test_get_request_payload_responses_api_input_file_blocks_passthrough() -> No
]


def test_apply_patch_passthrough() -> None:
"""Test that apply_patch dict is passed through as a built-in tool."""
llm = ChatOpenAI(model="gpt-4o", api_key=SecretStr("test-api-key"))
bound = llm.bind_tools([{"type": "apply_patch"}])
payload = bound._get_request_payload( # type: ignore[attr-defined]
"test",
**bound.kwargs, # type: ignore[attr-defined]
)
assert {"type": "apply_patch"} in payload["tools"]
assert "input" in payload


def test_tool_search_passthrough() -> None:
"""Test that tool_search dict is passed through as a built-in tool."""
llm = ChatOpenAI(model="gpt-4o")
Expand Down
Loading