Skip to content

Commit 3a34a22

Browse files
committed
feat(openai): support apply_patch built-in tool
1 parent e411a4e commit 3a34a22

4 files changed

Lines changed: 215 additions & 2 deletions

File tree

libs/core/langchain_core/utils/function_calling.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,7 @@ def convert_to_openai_function(
508508
"web_search_preview",
509509
"web_search",
510510
"tool_search",
511+
"apply_patch",
511512
"namespace",
512513
)
513514

@@ -546,7 +547,7 @@ def convert_to_openai_tool(
546547
547548
Return OpenAI Responses API-style tools unchanged. This includes
548549
any dict with `"type"` in `"file_search"`, `"function"`,
549-
`"computer_use_preview"`, `"web_search_preview"`.
550+
`"computer_use_preview"`, `"web_search_preview"`, `"apply_patch"`.
550551
551552
!!! warning "Behavior changed in `langchain-core` 0.3.63"
552553

libs/core/tests/unit_tests/utils/test_function_calling.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,6 +1245,15 @@ def test_convert_to_openai_function_json_schema_missing_title_includes_schema()
12451245
convert_to_openai_function(schema_without_title)
12461246

12471247

1248+
def test_convert_to_openai_tool_apply_patch_passthrough() -> None:
1249+
"""Test apply_patch is passed through as an OpenAI built-in tool."""
1250+
tool = {"type": "apply_patch"}
1251+
1252+
result = convert_to_openai_tool(tool)
1253+
1254+
assert result == tool
1255+
1256+
12481257
def test_convert_to_openai_tool_computer_passthrough() -> None:
12491258
"""Test that the 'computer' tool type is passed through unchanged."""
12501259
computer_tool = {

libs/partners/openai/langchain_openai/chat_models/base.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ def _get_default_model_profile(model_name: str) -> ModelProfile:
192192
"mcp",
193193
"image_generation",
194194
"tool_search",
195+
"apply_patch",
195196
)
196197

197198

@@ -4478,6 +4479,8 @@ def _construct_responses_api_input(messages: Sequence[BaseMessage]) -> list:
44784479
"mcp_approval_request",
44794480
"tool_search_call",
44804481
"tool_search_output",
4482+
"apply_patch_call",
4483+
"apply_patch_call_output",
44814484
):
44824485
input_.append(_pop_index_and_sub_index(block))
44834486
elif block_type == "image_generation_call":
@@ -4523,7 +4526,11 @@ def _construct_responses_api_input(messages: Sequence[BaseMessage]) -> list:
45234526
elif msg["role"] in ("user", "system", "developer"):
45244527
if isinstance(msg["content"], list):
45254528
new_blocks = []
4526-
non_message_item_types = ("mcp_approval_response", "tool_search_output")
4529+
non_message_item_types = (
4530+
"mcp_approval_response",
4531+
"tool_search_output",
4532+
"apply_patch_call_output",
4533+
)
45274534
for block in msg["content"]:
45284535
if block["type"] in ("text", "image_url", "file"):
45294536
new_blocks.append(
@@ -4692,6 +4699,8 @@ def _construct_lc_result_from_responses_api(
46924699
"image_generation_call",
46934700
"tool_search_call",
46944701
"tool_search_output",
4702+
"apply_patch_call",
4703+
"apply_patch_call_output",
46954704
):
46964705
content_blocks.append(output.model_dump(exclude_none=True, mode="json"))
46974706

@@ -4946,6 +4955,8 @@ def _advance(output_idx: int, sub_idx: int | None = None) -> None:
49464955
"image_generation_call",
49474956
"tool_search_call",
49484957
"tool_search_output",
4958+
"apply_patch_call",
4959+
"apply_patch_call_output",
49494960
):
49504961
_advance(chunk.output_index)
49514962
tool_output = chunk.item.model_dump(exclude_none=True, mode="json")

libs/partners/openai/tests/unit_tests/chat_models/test_base.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
_construct_responses_api_input,
7272
_convert_dict_to_message,
7373
_convert_message_to_dict,
74+
_convert_responses_chunk_to_generation_chunk,
7475
_convert_to_openai_response_format,
7576
_create_usage_metadata,
7677
_create_usage_metadata_responses,
@@ -1924,6 +1925,185 @@ def test__construct_lc_result_from_responses_api_no_usage_metadata() -> None:
19241925
assert cast(AIMessage, result.generations[0].message).usage_metadata is None
19251926

19261927

1928+
def test__construct_lc_result_from_responses_api_apply_patch_response() -> None:
1929+
"""Test a response with apply_patch output."""
1930+
apply_patch_call = MagicMock()
1931+
apply_patch_call.type = "apply_patch_call"
1932+
apply_patch_call.model_dump.return_value = {
1933+
"type": "apply_patch_call",
1934+
"call_id": "call_123",
1935+
"operation": {
1936+
"type": "create_file",
1937+
"path": "hello.txt",
1938+
"diff": "+hello\\n",
1939+
},
1940+
"status": "completed",
1941+
}
1942+
1943+
response = MagicMock()
1944+
response.error = None
1945+
response.id = "resp_123"
1946+
response.output = [apply_patch_call]
1947+
response.usage = None
1948+
response.service_tier = None
1949+
response.text = None
1950+
response.model_dump.return_value = {
1951+
"id": "resp_123",
1952+
"created_at": 1234567890,
1953+
"model": "gpt-4o",
1954+
"object": "response",
1955+
"status": "completed",
1956+
}
1957+
1958+
result = _construct_lc_result_from_responses_api(response)
1959+
1960+
message = cast(AIMessage, result.generations[0].message)
1961+
assert message.content == [
1962+
{
1963+
"type": "apply_patch_call",
1964+
"call_id": "call_123",
1965+
"operation": {
1966+
"type": "create_file",
1967+
"path": "hello.txt",
1968+
"diff": "+hello\\n",
1969+
},
1970+
"status": "completed",
1971+
}
1972+
]
1973+
assert message.tool_calls == []
1974+
assert message.invalid_tool_calls == []
1975+
1976+
1977+
def test__construct_lc_result_from_responses_api_apply_patch_call_output() -> None:
1978+
"""Test a response with apply_patch_call_output output."""
1979+
apply_patch_call_output = MagicMock()
1980+
apply_patch_call_output.type = "apply_patch_call_output"
1981+
apply_patch_call_output.model_dump.return_value = {
1982+
"type": "apply_patch_call_output",
1983+
"call_id": "call_123",
1984+
"status": "completed",
1985+
"output": "Created hello.txt",
1986+
}
1987+
1988+
response = MagicMock()
1989+
response.error = None
1990+
response.id = "resp_123"
1991+
response.output = [apply_patch_call_output]
1992+
response.usage = None
1993+
response.service_tier = None
1994+
response.text = None
1995+
response.model_dump.return_value = {
1996+
"id": "resp_123",
1997+
"created_at": 1234567890,
1998+
"model": "gpt-4o",
1999+
"object": "response",
2000+
"status": "completed",
2001+
}
2002+
2003+
result = _construct_lc_result_from_responses_api(response)
2004+
2005+
message = result.generations[0].message
2006+
assert message.content == [
2007+
{
2008+
"type": "apply_patch_call_output",
2009+
"call_id": "call_123",
2010+
"status": "completed",
2011+
"output": "Created hello.txt",
2012+
}
2013+
]
2014+
2015+
2016+
def test__construct_responses_api_input_apply_patch_round_trip() -> None:
2017+
"""Test apply_patch content blocks are preserved when sent back as input."""
2018+
messages = [
2019+
AIMessage(
2020+
content=[
2021+
{
2022+
"type": "apply_patch_call",
2023+
"call_id": "call_123",
2024+
"operation": {
2025+
"type": "create_file",
2026+
"path": "hello.txt",
2027+
"diff": "+hello\\n",
2028+
},
2029+
"status": "completed",
2030+
}
2031+
]
2032+
),
2033+
HumanMessage(
2034+
content=[
2035+
{
2036+
"type": "apply_patch_call_output",
2037+
"call_id": "call_123",
2038+
"status": "completed",
2039+
"output": "Created hello.txt",
2040+
}
2041+
]
2042+
),
2043+
]
2044+
2045+
result = _construct_responses_api_input(messages)
2046+
2047+
assert result == [
2048+
{
2049+
"type": "apply_patch_call",
2050+
"call_id": "call_123",
2051+
"operation": {
2052+
"type": "create_file",
2053+
"path": "hello.txt",
2054+
"diff": "+hello\\n",
2055+
},
2056+
"status": "completed",
2057+
},
2058+
{
2059+
"type": "apply_patch_call_output",
2060+
"call_id": "call_123",
2061+
"status": "completed",
2062+
"output": "Created hello.txt",
2063+
},
2064+
]
2065+
2066+
2067+
def test__convert_responses_chunk_to_generation_chunk_apply_patch_response() -> None:
2068+
"""Test streamed apply_patch output item is preserved in message chunks."""
2069+
chunk = MagicMock()
2070+
chunk.type = "response.output_item.done"
2071+
chunk.output_index = 0
2072+
chunk.item.type = "apply_patch_call"
2073+
chunk.item.model_dump.return_value = {
2074+
"type": "apply_patch_call",
2075+
"call_id": "call_123",
2076+
"operation": {
2077+
"type": "create_file",
2078+
"path": "hello.txt",
2079+
"diff": "+hello\\n",
2080+
},
2081+
"status": "completed",
2082+
}
2083+
2084+
_, _, _, generation_chunk = _convert_responses_chunk_to_generation_chunk(
2085+
chunk,
2086+
current_index=-1,
2087+
current_output_index=-1,
2088+
current_sub_index=-1,
2089+
)
2090+
2091+
assert generation_chunk is not None
2092+
assert generation_chunk.message.content == [
2093+
{
2094+
"type": "apply_patch_call",
2095+
"call_id": "call_123",
2096+
"operation": {
2097+
"type": "create_file",
2098+
"path": "hello.txt",
2099+
"diff": "+hello\\n",
2100+
},
2101+
"status": "completed",
2102+
"index": 0,
2103+
}
2104+
]
2105+
2106+
19272107
def test__construct_lc_result_from_responses_api_web_search_response() -> None:
19282108
"""Test a response with web search output."""
19292109
from openai.types.responses.response_function_web_search import (
@@ -3668,6 +3848,18 @@ def test_get_request_payload_responses_api_input_file_blocks_passthrough() -> No
36683848
]
36693849

36703850

3851+
def test_apply_patch_passthrough() -> None:
3852+
"""Test that apply_patch dict is passed through as a built-in tool."""
3853+
llm = ChatOpenAI(model="gpt-4o", api_key=SecretStr("test-api-key"))
3854+
bound = llm.bind_tools([{"type": "apply_patch"}])
3855+
payload = bound._get_request_payload( # type: ignore[attr-defined]
3856+
"test",
3857+
**bound.kwargs, # type: ignore[attr-defined]
3858+
)
3859+
assert {"type": "apply_patch"} in payload["tools"]
3860+
assert "input" in payload
3861+
3862+
36713863
def test_tool_search_passthrough() -> None:
36723864
"""Test that tool_search dict is passed through as a built-in tool."""
36733865
llm = ChatOpenAI(model="gpt-4o")

0 commit comments

Comments
 (0)