Overuse may indicate design issues; consider polymorphism
assert isinstance(response, AIMessage)
1"""Test Responses API usage."""23import base644import json5import os6from typing import TYPE_CHECKING, Annotated, Any, Literal, cast78import openai9import pytest10from langchain.agents import create_agent11from langchain.agents.middleware.types import (12 AgentMiddleware,13 AgentState,14 ToolCallRequest,15 hook_config,16)17from langchain_core.messages import (18 AIMessage,19 AIMessageChunk,20 BaseMessage,21 BaseMessageChunk,22 HumanMessage,23 MessageLikeRepresentation,24 ToolMessage,25)26from langchain_core.tools import tool27from langchain_core.utils.function_calling import convert_to_openai_tool28from langchain_tests.utils.stream_lifecycle import assert_valid_event_stream29from pydantic import BaseModel30from typing_extensions import TypedDict3132from langchain_openai import ChatOpenAI, custom_tool33from langchain_openai.chat_models.base import _convert_to_openai_response_format3435if TYPE_CHECKING:36 from collections.abc import Awaitable3738 from langchain_core.language_models.chat_model_stream import (39 AsyncChatModelStream,40 ChatModelStream,41 )4243MODEL_NAME = "gpt-4o-mini"444546def _check_response(response: BaseMessage | None) -> None:47 assert isinstance(response, AIMessage)48 assert isinstance(response.content, list)49 for block in response.content:50 assert isinstance(block, dict)51 if block["type"] == "text":52 assert isinstance(block.get("text"), str)53 annotations = block.get("annotations", [])54 for annotation in annotations:55 if annotation["type"] == "file_citation":56 assert all(57 key in annotation58 for key in ["file_id", "filename", "file_index", "type"]59 )60 elif annotation["type"] == "web_search":61 assert all(62 key in annotation63 for key in ["end_index", "start_index", "title", "type", "url"]64 )65 elif annotation["type"] == "citation":66 assert all(key in annotation for key in ["title", "type"])67 if "url" in annotation:68 assert "start_index" in annotation69 assert "end_index" in annotation70 text_content = response.text # type: ignore[operator,misc]71 assert isinstance(text_content, str)72 assert text_content73 assert response.usage_metadata74 assert response.usage_metadata["input_tokens"] > 075 assert response.usage_metadata["output_tokens"] > 076 assert response.usage_metadata["total_tokens"] > 077 assert response.response_metadata["model_name"]78 assert response.response_metadata["service_tier"] # type: ignore[typeddict-item]798081@pytest.mark.vcr82def test_incomplete_response() -> None:83 model = ChatOpenAI(84 model=MODEL_NAME, use_responses_api=True, max_completion_tokens=1685 )86 response = model.invoke("Tell me a 100 word story about a bear.")87 assert response.response_metadata["incomplete_details"]88 assert response.response_metadata["incomplete_details"]["reason"]89 assert response.response_metadata["status"] == "incomplete"9091 full: AIMessageChunk | None = None92 for chunk in model.stream("Tell me a 100 word story about a bear."):93 assert isinstance(chunk, AIMessageChunk)94 full = chunk if full is None else full + chunk95 assert isinstance(full, AIMessageChunk)96 assert full.response_metadata["incomplete_details"]97 assert full.response_metadata["incomplete_details"]["reason"]98 assert full.response_metadata["status"] == "incomplete"99100101@pytest.mark.default_cassette("test_web_search.yaml.gz")102@pytest.mark.vcr103@pytest.mark.parametrize(104 ("output_version", "use_v2_stream"),105 [106 ("responses/v1", False),107 ("v1", False),108 ("v1", True),109 ],110)111def test_web_search(112 output_version: Literal["responses/v1", "v1"], use_v2_stream: bool113) -> None:114 llm = ChatOpenAI(model=MODEL_NAME, output_version=output_version)115 first_response = llm.invoke(116 "What was a positive news story from today?",117 tools=[{"type": "web_search_preview"}],118 )119 _check_response(first_response)120121 # Test streaming122 full: BaseMessage123 if use_v2_stream:124 full = llm.stream_events(125 "What was a positive news story from today?",126 tools=[{"type": "web_search_preview"}],127 version="v3",128 ).output129 else:130 aggregated: BaseMessageChunk | None = None131 for chunk in llm.stream(132 "What was a positive news story from today?",133 tools=[{"type": "web_search_preview"}],134 ):135 assert isinstance(chunk, AIMessageChunk)136 aggregated = chunk if aggregated is None else aggregated + chunk137 assert aggregated is not None138 full = aggregated139 _check_response(full)140141 # Use OpenAI's stateful API142 response = llm.invoke(143 "what about a negative one",144 tools=[{"type": "web_search_preview"}],145 previous_response_id=first_response.response_metadata["id"],146 )147 _check_response(response)148149 # Manually pass in chat history150 response = llm.invoke(151 [152 {"role": "user", "content": "What was a positive news story from today?"},153 first_response,154 {"role": "user", "content": "what about a negative one"},155 ],156 tools=[{"type": "web_search_preview"}],157 )158 _check_response(response)159160 # Bind tool161 response = llm.bind_tools([{"type": "web_search_preview"}]).invoke(162 "What was a positive news story from today?"163 )164 _check_response(response)165166 for msg in [first_response, full, response]:167 assert msg is not None168 block_types = [block["type"] for block in msg.content] # type: ignore[index]169 if output_version == "responses/v1":170 assert block_types == ["web_search_call", "text"]171 else:172 assert block_types == ["server_tool_call", "server_tool_result", "text"]173174175@pytest.mark.flaky(retries=3, delay=1)176async def test_web_search_async() -> None:177 llm = ChatOpenAI(model=MODEL_NAME, output_version="v0")178 response = await llm.ainvoke(179 "What was a positive news story from today?",180 tools=[{"type": "web_search_preview"}],181 )182 _check_response(response)183 assert response.response_metadata["status"]184185 # Test streaming186 full: BaseMessageChunk | None = None187 async for chunk in llm.astream(188 "What was a positive news story from today?",189 tools=[{"type": "web_search_preview"}],190 ):191 assert isinstance(chunk, AIMessageChunk)192 full = chunk if full is None else full + chunk193 assert isinstance(full, AIMessageChunk)194 _check_response(full)195196 for msg in [response, full]:197 assert msg.additional_kwargs["tool_outputs"]198 assert len(msg.additional_kwargs["tool_outputs"]) == 1199 tool_output = msg.additional_kwargs["tool_outputs"][0]200 assert tool_output["type"] == "web_search_call"201202203@pytest.mark.default_cassette("test_apply_patch.yaml.gz")204@pytest.mark.vcr205def test_apply_patch() -> None:206 """Test the apply_patch built-in tool end-to-end.207208 apply_patch is a client-executed tool: the model proposes a file operation209 via an `apply_patch_call` block, the client applies it, and the result is210 returned as an `apply_patch_call_output` block. Requires a model that211 supports the tool.212 """213 prompt = "Create a new file named hello.txt containing the line: hello world"214 llm = ChatOpenAI(model="gpt-5.1", output_version="responses/v1")215 tool = {"type": "apply_patch"}216217 # Non-streaming: the model should emit an apply_patch_call block.218 response = llm.invoke(prompt, tools=[tool])219 assert isinstance(response, AIMessage)220 calls = [221 block222 for block in response.content223 if isinstance(block, dict) and block["type"] == "apply_patch_call"224 ]225 assert len(calls) == 1226 call = calls[0]227 assert call["call_id"]228 assert call["operation"]["type"] in ("create_file", "update_file", "delete_file")229230 # Streaming: the apply_patch_call block survives chunk aggregation.231 aggregated: BaseMessageChunk | None = None232 for chunk in llm.stream(prompt, tools=[tool]):233 assert isinstance(chunk, AIMessageChunk)234 aggregated = chunk if aggregated is None else aggregated + chunk235 assert isinstance(aggregated, AIMessageChunk)236 assert any(237 isinstance(block, dict) and block["type"] == "apply_patch_call"238 for block in aggregated.content239 )240241 # Round-trip: return an apply_patch_call_output and continue the conversation.242 output_message = HumanMessage(243 content=[244 {245 "type": "apply_patch_call_output",246 "call_id": call["call_id"],247 "status": "completed",248 "output": f"Created {call['operation']['path']}",249 }250 ]251 )252 follow_up = llm.invoke(253 [HumanMessage(prompt), response, output_message],254 tools=[tool],255 )256 assert isinstance(follow_up, AIMessage)257258259@pytest.mark.default_cassette("test_function_calling.yaml.gz")260@pytest.mark.vcr261@pytest.mark.parametrize("output_version", ["v0", "responses/v1", "v1"])262def test_function_calling(output_version: Literal["v0", "responses/v1", "v1"]) -> None:263 def multiply(x: int, y: int) -> int:264 """return x * y"""265 return x * y266267 llm = ChatOpenAI(model=MODEL_NAME, output_version=output_version)268 bound_llm = llm.bind_tools([multiply, {"type": "web_search_preview"}])269 ai_msg = cast(AIMessage, bound_llm.invoke("whats 5 * 4"))270 assert len(ai_msg.tool_calls) == 1271 assert ai_msg.tool_calls[0]["name"] == "multiply"272 assert set(ai_msg.tool_calls[0]["args"]) == {"x", "y"}273274 full: Any = None275 for chunk in bound_llm.stream("whats 5 * 4"):276 assert isinstance(chunk, AIMessageChunk)277 full = chunk if full is None else full + chunk278 assert len(full.tool_calls) == 1279 assert full.tool_calls[0]["name"] == "multiply"280 assert set(full.tool_calls[0]["args"]) == {"x", "y"}281282 for msg in [ai_msg, full]:283 assert len(msg.content_blocks) == 1284 assert msg.content_blocks[0]["type"] == "tool_call"285286 response = bound_llm.invoke("What was a positive news story from today?")287 _check_response(response)288289290@pytest.mark.default_cassette("test_agent_loop.yaml.gz")291@pytest.mark.vcr292@pytest.mark.parametrize("output_version", ["responses/v1", "v1"])293def test_agent_loop(output_version: Literal["responses/v1", "v1"]) -> None:294 @tool295 def get_weather(location: str) -> str:296 """Get the weather for a location."""297 return "It's sunny."298299 llm = ChatOpenAI(300 model="gpt-5.4",301 use_responses_api=True,302 output_version=output_version,303 )304 llm_with_tools = llm.bind_tools([get_weather])305 input_message = HumanMessage("What is the weather in San Francisco, CA?")306 tool_call_message = llm_with_tools.invoke([input_message])307 assert isinstance(tool_call_message, AIMessage)308 tool_calls = tool_call_message.tool_calls309 assert len(tool_calls) == 1310 tool_call = tool_calls[0]311 tool_message = get_weather.invoke(tool_call)312 assert isinstance(tool_message, ToolMessage)313 response = llm_with_tools.invoke(314 [315 input_message,316 tool_call_message,317 tool_message,318 ]319 )320 assert isinstance(response, AIMessage)321322323@pytest.mark.default_cassette("test_agent_loop_streaming.yaml.gz")324@pytest.mark.vcr325@pytest.mark.parametrize(326 ("output_version", "use_v2_stream"),327 [328 ("responses/v1", False),329 ("responses/v1", True),330 ("v1", False),331 ("v1", True),332 ],333)334def test_agent_loop_streaming(335 output_version: Literal["responses/v1", "v1"], use_v2_stream: bool336) -> None:337 @tool338 def get_weather(location: str) -> str:339 """Get the weather for a location."""340 return "It's sunny."341342 llm = ChatOpenAI(343 model="gpt-5.2",344 use_responses_api=True,345 reasoning={"effort": "medium", "summary": "auto"},346 streaming=True,347 output_version=output_version,348 )349 llm_with_tools = llm.bind_tools([get_weather])350 input_message = HumanMessage("What is the weather in San Francisco, CA?")351 if use_v2_stream:352 tool_call_message = cast(353 "ChatModelStream",354 llm_with_tools.stream_events([input_message], version="v3"),355 ).output356 else:357 tool_call_message = llm_with_tools.invoke([input_message])358 assert isinstance(tool_call_message, AIMessage)359 tool_calls = tool_call_message.tool_calls360 assert len(tool_calls) == 1361 tool_call = tool_calls[0]362 tool_message = get_weather.invoke(tool_call)363 assert isinstance(tool_message, ToolMessage)364 if use_v2_stream:365 response = cast(366 "ChatModelStream",367 llm_with_tools.stream_events(368 [input_message, tool_call_message, tool_message],369 version="v3",370 ),371 ).output372 else:373 response = llm_with_tools.invoke(374 [375 input_message,376 tool_call_message,377 tool_message,378 ]379 )380 assert isinstance(response, AIMessage)381382383@pytest.mark.default_cassette("test_agent_loop_streaming.yaml.gz")384@pytest.mark.vcr385async def test_agent_loop_streaming_astream_events_v3_v1() -> None:386 """Async multi-turn through `astream_events(version="v3")`.387388 Mirrors `test_agent_loop_streaming` for `output_version="v1"` but389 exercises `AsyncChatModelStream` end-to-end: aggregation in the390 async state machine, async projections, and the background391 producer task. Cassette byte-matches guarantee the aggregated392 message serializes identically to the legacy path on the393 follow-up turn.394 """395396 @tool397 def get_weather(location: str) -> str:398 """Get the weather for a location."""399 return "It's sunny."400401 llm = ChatOpenAI(402 model="gpt-5.2",403 use_responses_api=True,404 reasoning={"effort": "medium", "summary": "auto"},405 streaming=True,406 output_version="v1",407 )408 llm_with_tools = llm.bind_tools([get_weather])409 input_message = HumanMessage("What is the weather in San Francisco, CA?")410 stream = await cast(411 "Awaitable[AsyncChatModelStream]",412 llm_with_tools.astream_events([input_message], version="v3"),413 )414 tool_call_message = await stream415 assert isinstance(tool_call_message, AIMessage)416 tool_calls = tool_call_message.tool_calls417 assert len(tool_calls) == 1418 tool_call = tool_calls[0]419 tool_message = get_weather.invoke(tool_call)420 assert isinstance(tool_message, ToolMessage)421 stream = await cast(422 "Awaitable[AsyncChatModelStream]",423 llm_with_tools.astream_events(424 [input_message, tool_call_message, tool_message],425 version="v3",426 ),427 )428 response = await stream429 assert isinstance(response, AIMessage)430431432class Foo(BaseModel):433 response: str434435436class FooDict(TypedDict):437 response: str438439440@pytest.mark.default_cassette("test_parsed_pydantic_schema.yaml.gz")441@pytest.mark.vcr442@pytest.mark.parametrize("output_version", ["v0", "responses/v1", "v1"])443def test_parsed_pydantic_schema(444 output_version: Literal["v0", "responses/v1", "v1"],445) -> None:446 llm = ChatOpenAI(447 model=MODEL_NAME, use_responses_api=True, output_version=output_version448 )449 response = llm.invoke("how are ya", response_format=Foo)450 parsed = Foo(**json.loads(response.text))451 assert parsed == response.additional_kwargs["parsed"]452 assert parsed.response453454 # Test stream455 full: BaseMessageChunk | None = None456 for chunk in llm.stream("how are ya", response_format=Foo):457 assert isinstance(chunk, AIMessageChunk)458 full = chunk if full is None else full + chunk459 assert isinstance(full, AIMessageChunk)460 parsed = Foo(**json.loads(full.text))461 assert parsed == full.additional_kwargs["parsed"]462 assert parsed.response463464465async def test_parsed_pydantic_schema_async() -> None:466 llm = ChatOpenAI(model=MODEL_NAME, use_responses_api=True)467 response = await llm.ainvoke("how are ya", response_format=Foo)468 parsed = Foo(**json.loads(response.text))469 assert parsed == response.additional_kwargs["parsed"]470 assert parsed.response471472 # Test stream473 full: BaseMessageChunk | None = None474 async for chunk in llm.astream("how are ya", response_format=Foo):475 assert isinstance(chunk, AIMessageChunk)476 full = chunk if full is None else full + chunk477 assert isinstance(full, AIMessageChunk)478 parsed = Foo(**json.loads(full.text))479 assert parsed == full.additional_kwargs["parsed"]480 assert parsed.response481482483@pytest.mark.flaky(retries=3, delay=1)484@pytest.mark.parametrize("schema", [Foo.model_json_schema(), FooDict])485def test_parsed_dict_schema(schema: Any) -> None:486 llm = ChatOpenAI(model=MODEL_NAME, use_responses_api=True)487 response = llm.invoke("how are ya", response_format=schema)488 parsed = json.loads(response.text)489 assert parsed == response.additional_kwargs["parsed"]490 assert parsed["response"]491 assert isinstance(parsed["response"], str)492493 # Test stream494 full: BaseMessageChunk | None = None495 for chunk in llm.stream("how are ya", response_format=schema):496 assert isinstance(chunk, AIMessageChunk)497 full = chunk if full is None else full + chunk498 assert isinstance(full, AIMessageChunk)499 parsed = json.loads(full.text)500 assert parsed == full.additional_kwargs["parsed"]501 assert parsed["response"]502 assert isinstance(parsed["response"], str)503504505def test_parsed_strict() -> None:506 llm = ChatOpenAI(model=MODEL_NAME, use_responses_api=True)507508 class Joke(TypedDict):509 setup: Annotated[str, ..., "The setup of the joke"]510 punchline: Annotated[str, None, "The punchline of the joke"]511512 schema = _convert_to_openai_response_format(Joke)513 invalid_schema = cast(dict, _convert_to_openai_response_format(Joke, strict=True))514 # Intentionally make the strict schema invalid. OpenAI requires every property515 # to appear in `required`; omitting `punchline` should produce a BadRequestError.516 invalid_schema["json_schema"]["schema"]["required"] = ["setup"]517518 # Test not strict519 response = llm.invoke("Tell me a joke", response_format=schema)520 parsed = json.loads(response.text)521 assert parsed == response.additional_kwargs["parsed"]522523 # Test strict524 with pytest.raises(openai.BadRequestError):525 llm.invoke(526 "Tell me a joke about cats.", response_format=invalid_schema, strict=True527 )528 with pytest.raises(openai.BadRequestError):529 next(530 llm.stream(531 "Tell me a joke about cats.",532 response_format=invalid_schema,533 strict=True,534 )535 )536537538@pytest.mark.flaky(retries=3, delay=1)539@pytest.mark.parametrize("schema", [Foo.model_json_schema(), FooDict])540async def test_parsed_dict_schema_async(schema: Any) -> None:541 llm = ChatOpenAI(model=MODEL_NAME, use_responses_api=True)542 response = await llm.ainvoke("how are ya", response_format=schema)543 parsed = json.loads(response.text)544 assert parsed == response.additional_kwargs["parsed"]545 assert parsed["response"]546 assert isinstance(parsed["response"], str)547548 # Test stream549 full: BaseMessageChunk | None = None550 async for chunk in llm.astream("how are ya", response_format=schema):551 assert isinstance(chunk, AIMessageChunk)552 full = chunk if full is None else full + chunk553 assert isinstance(full, AIMessageChunk)554 parsed = json.loads(full.text)555 assert parsed == full.additional_kwargs["parsed"]556 assert parsed["response"]557 assert isinstance(parsed["response"], str)558559560@pytest.mark.parametrize("schema", [Foo, Foo.model_json_schema(), FooDict])561def test_function_calling_and_structured_output(schema: Any) -> None:562 def multiply(x: int, y: int) -> int:563 """return x * y"""564 return x * y565566 llm = ChatOpenAI(model=MODEL_NAME, use_responses_api=True)567 bound_llm = llm.bind_tools([multiply], response_format=schema, strict=True)568 # Test structured output569 response = llm.invoke("how are ya", response_format=schema)570 if schema == Foo:571 parsed = schema(**json.loads(response.text))572 assert parsed.response573 else:574 parsed = json.loads(response.text)575 assert parsed["response"]576 assert parsed == response.additional_kwargs["parsed"]577578 # Test function calling579 ai_msg = cast(AIMessage, bound_llm.invoke("whats 5 * 4"))580 assert len(ai_msg.tool_calls) == 1581 assert ai_msg.tool_calls[0]["name"] == "multiply"582 assert set(ai_msg.tool_calls[0]["args"]) == {"x", "y"}583584585@pytest.mark.default_cassette("test_reasoning.yaml.gz")586@pytest.mark.vcr587@pytest.mark.parametrize("output_version", ["v0", "responses/v1", "v1"])588def test_reasoning(output_version: Literal["v0", "responses/v1", "v1"]) -> None:589 llm = ChatOpenAI(590 model="gpt-5-nano", use_responses_api=True, output_version=output_version591 )592 response = llm.invoke("Hello", reasoning={"effort": "low"})593 assert isinstance(response, AIMessage)594595 # Test init params + streaming596 llm = ChatOpenAI(597 model="gpt-5-nano", reasoning={"effort": "low"}, output_version=output_version598 )599 full: BaseMessageChunk | None = None600 for chunk in llm.stream("Hello"):601 assert isinstance(chunk, AIMessageChunk)602 full = chunk if full is None else full + chunk603 assert isinstance(full, AIMessage)604605 for msg in [response, full]:606 if output_version == "v0":607 assert msg.additional_kwargs["reasoning"]608 else:609 block_types = [block["type"] for block in msg.content]610 assert block_types == ["reasoning", "text"]611612613def test_stateful_api() -> None:614 llm = ChatOpenAI(model=MODEL_NAME, use_responses_api=True)615 response = llm.invoke("how are you, my name is Bobo")616 assert "id" in response.response_metadata617618 second_response = llm.invoke(619 "what's my name", previous_response_id=response.response_metadata["id"]620 )621 assert isinstance(second_response.content, list)622 assert "bobo" in second_response.content[0]["text"].lower() # type: ignore623624625def test_route_from_model_kwargs() -> None:626 llm = ChatOpenAI(627 model=MODEL_NAME, model_kwargs={"text": {"format": {"type": "text"}}}628 )629 _ = next(llm.stream("Hello"))630631632@pytest.mark.flaky(retries=3, delay=1)633def test_computer_calls() -> None:634 llm = ChatOpenAI(model="gpt-5.4")635 tool = {"type": "computer"}636 llm_with_tools = llm.bind_tools([tool], tool_choice="any")637 response = llm_with_tools.invoke("Please open the browser.")638 assert any(block["type"] == "computer_call" for block in response.content) # type: ignore[index]639640641@pytest.mark.default_cassette("test_file_search.yaml.gz")642@pytest.mark.vcr643@pytest.mark.parametrize("output_version", ["responses/v1", "v1"])644def test_file_search(645 output_version: Literal["responses/v1", "v1"],646) -> None:647 vector_store_id = os.getenv("OPENAI_VECTOR_STORE_ID")648 if not vector_store_id:649 pytest.skip()650651 llm = ChatOpenAI(652 model=MODEL_NAME,653 use_responses_api=True,654 output_version=output_version,655 )656 tool = {657 "type": "file_search",658 "vector_store_ids": [vector_store_id],659 }660661 input_message = {"role": "user", "content": "What is deep research by OpenAI?"}662 response = llm.invoke([input_message], tools=[tool])663 _check_response(response)664665 if output_version == "v1":666 assert [block["type"] for block in response.content] == [ # type: ignore[index]667 "server_tool_call",668 "server_tool_result",669 "text",670 ]671 else:672 assert [block["type"] for block in response.content] == [ # type: ignore[index]673 "file_search_call",674 "text",675 ]676677 full: AIMessageChunk | None = None678 for chunk in llm.stream([input_message], tools=[tool]):679 assert isinstance(chunk, AIMessageChunk)680 full = chunk if full is None else full + chunk681 assert isinstance(full, AIMessageChunk)682 _check_response(full)683684 if output_version == "v1":685 assert [block["type"] for block in full.content] == [ # type: ignore[index]686 "server_tool_call",687 "server_tool_result",688 "text",689 ]690 else:691 assert [block["type"] for block in full.content] == ["file_search_call", "text"] # type: ignore[index]692693 next_message = {"role": "user", "content": "Thank you."}694 _ = llm.invoke([input_message, full, next_message])695696 for message in [response, full]:697 assert [block["type"] for block in message.content_blocks] == [698 "server_tool_call",699 "server_tool_result",700 "text",701 ]702703704@pytest.mark.default_cassette("test_stream_reasoning_summary.yaml.gz")705@pytest.mark.vcr706@pytest.mark.parametrize(707 ("output_version", "use_v2_stream"),708 [709 ("v0", False),710 ("responses/v1", False),711 ("v1", False),712 ("v1", True),713 ],714)715def test_stream_reasoning_summary(716 output_version: Literal["v0", "responses/v1", "v1"],717 use_v2_stream: bool,718) -> None:719 llm = ChatOpenAI(720 model="gpt-5-nano",721 # Routes to Responses API if `reasoning` is set.722 reasoning={"effort": "medium", "summary": "auto"},723 output_version=output_version,724 )725 message_1 = {726 "role": "user",727 "content": "What was the third tallest buliding in the year 2000?",728 }729 response_1: BaseMessage730 if use_v2_stream:731 response_1 = llm.stream_events([message_1], version="v3").output732 else:733 aggregated: BaseMessageChunk | None = None734 for chunk in llm.stream([message_1]):735 assert isinstance(chunk, AIMessageChunk)736 aggregated = chunk if aggregated is None else aggregated + chunk737 assert isinstance(aggregated, AIMessageChunk)738 response_1 = aggregated739 if output_version == "v0":740 reasoning = response_1.additional_kwargs["reasoning"]741 assert set(reasoning.keys()) == {"id", "type", "summary"}742 summary = reasoning["summary"]743 assert isinstance(summary, list)744 for block in summary:745 assert isinstance(block, dict)746 assert isinstance(block["type"], str)747 assert isinstance(block["text"], str)748 assert block["text"]749 elif output_version == "responses/v1":750 reasoning = next(751 block752 for block in response_1.content753 if block["type"] == "reasoning" # type: ignore[index]754 )755 if isinstance(reasoning, str):756 reasoning = json.loads(reasoning)757 assert set(reasoning.keys()) == {"id", "type", "summary", "index"}758 summary = reasoning["summary"]759 assert isinstance(summary, list)760 for block in summary:761 assert isinstance(block, dict)762 assert isinstance(block["type"], str)763 assert isinstance(block["text"], str)764 assert block["text"]765 else:766 # v1767 total_reasoning_blocks = 0768 for block in response_1.content_blocks:769 if block["type"] == "reasoning":770 total_reasoning_blocks += 1771 assert isinstance(block.get("id"), str)772 assert block.get("id", "").startswith("rs_")773 assert isinstance(block.get("reasoning"), str)774 assert isinstance(block.get("index"), str)775 assert (776 total_reasoning_blocks > 1777 ) # This query typically generates multiple reasoning blocks778779 # Check we can pass back summaries780 message_2 = {"role": "user", "content": "Thank you."}781 response_2 = llm.invoke([message_1, response_1, message_2])782 assert isinstance(response_2, AIMessage)783784785@pytest.mark.default_cassette("test_code_interpreter.yaml.gz")786@pytest.mark.vcr787@pytest.mark.parametrize(788 ("output_version", "use_v2_stream"),789 [790 ("v0", False),791 ("responses/v1", False),792 ("v1", False),793 ("v1", True),794 ],795)796def test_code_interpreter(797 output_version: Literal["v0", "responses/v1", "v1"], use_v2_stream: bool798) -> None:799 llm = ChatOpenAI(800 model="gpt-5-nano", use_responses_api=True, output_version=output_version801 )802 llm_with_tools = llm.bind_tools(803 [{"type": "code_interpreter", "container": {"type": "auto"}}]804 )805 input_message = {806 "role": "user",807 "content": "Write and run code to answer the question: what is 3^3?",808 }809 response = llm_with_tools.invoke([input_message])810 assert isinstance(response, AIMessage)811 _check_response(response)812 if output_version == "v0":813 tool_outputs = [814 item815 for item in response.additional_kwargs["tool_outputs"]816 if item["type"] == "code_interpreter_call"817 ]818 assert len(tool_outputs) == 1819 elif output_version == "responses/v1":820 tool_outputs = [821 item822 for item in response.content823 if isinstance(item, dict) and item["type"] == "code_interpreter_call"824 ]825 assert len(tool_outputs) == 1826 else:827 # v1828 tool_outputs = [829 item830 for item in response.content_blocks831 if item["type"] == "server_tool_call" and item["name"] == "code_interpreter"832 ]833 code_interpreter_result = next(834 item835 for item in response.content_blocks836 if item["type"] == "server_tool_result"837 )838 assert tool_outputs839 assert code_interpreter_result840 assert len(tool_outputs) == 1841842 # Test streaming843 # Use same container844 container_id = tool_outputs[0].get("container_id") or tool_outputs[0].get(845 "extras", {}846 ).get("container_id")847 llm_with_tools = llm.bind_tools(848 [{"type": "code_interpreter", "container": container_id}]849 )850851 full: BaseMessage852 if use_v2_stream:853 full = cast(854 "ChatModelStream",855 llm_with_tools.stream_events([input_message], version="v3"),856 ).output857 else:858 aggregated: BaseMessageChunk | None = None859 for chunk in llm_with_tools.stream([input_message]):860 assert isinstance(chunk, AIMessageChunk)861 aggregated = chunk if aggregated is None else aggregated + chunk862 assert isinstance(aggregated, AIMessageChunk)863 full = aggregated864 if output_version == "v0":865 tool_outputs = [866 item867 for item in response.additional_kwargs["tool_outputs"]868 if item["type"] == "code_interpreter_call"869 ]870 assert tool_outputs871 elif output_version == "responses/v1":872 tool_outputs = [873 item874 for item in response.content875 if isinstance(item, dict) and item["type"] == "code_interpreter_call"876 ]877 assert tool_outputs878 else:879 # v1880 code_interpreter_call = next(881 item882 for item in full.content_blocks883 if item["type"] == "server_tool_call" and item["name"] == "code_interpreter"884 )885 code_interpreter_result = next(886 item for item in full.content_blocks if item["type"] == "server_tool_result"887 )888 assert code_interpreter_call889 assert code_interpreter_result890891 # Test we can pass back in892 next_message = {"role": "user", "content": "Please add more comments to the code."}893 _ = llm_with_tools.invoke([input_message, full, next_message])894895896@pytest.mark.vcr897def test_mcp_builtin() -> None:898 llm = ChatOpenAI(model="gpt-5-nano", use_responses_api=True, output_version="v0")899900 llm_with_tools = llm.bind_tools(901 [902 {903 "type": "mcp",904 "server_label": "deepwiki",905 "server_url": "https://mcp.deepwiki.com/mcp",906 "require_approval": {"always": {"tool_names": ["read_wiki_structure"]}},907 }908 ]909 )910 input_message = {911 "role": "user",912 "content": (913 "What transport protocols does the 2025-03-26 version of the MCP spec "914 "support?"915 ),916 }917 response = llm_with_tools.invoke([input_message])918 assert all(isinstance(block, dict) for block in response.content)919920 approval_message = HumanMessage(921 [922 {923 "type": "mcp_approval_response",924 "approve": True,925 "approval_request_id": output["id"],926 }927 for output in response.additional_kwargs["tool_outputs"]928 if output["type"] == "mcp_approval_request"929 ]930 )931 _ = llm_with_tools.invoke(932 [approval_message], previous_response_id=response.response_metadata["id"]933 )934935936@pytest.mark.vcr937def test_mcp_builtin_zdr() -> None:938 llm = ChatOpenAI(939 model="gpt-5-nano",940 use_responses_api=True,941 store=False,942 include=["reasoning.encrypted_content"],943 )944945 llm_with_tools = llm.bind_tools(946 [947 {948 "type": "mcp",949 "server_label": "deepwiki",950 "server_url": "https://mcp.deepwiki.com/mcp",951 "allowed_tools": ["ask_question"],952 "require_approval": "always",953 }954 ]955 )956 input_message = {957 "role": "user",958 "content": (959 "What transport protocols does the 2025-03-26 version of the MCP "960 "spec (modelcontextprotocol/modelcontextprotocol) support?"961 ),962 }963 full: BaseMessageChunk | None = None964 for chunk in llm_with_tools.stream([input_message]):965 assert isinstance(chunk, AIMessageChunk)966 full = chunk if full is None else full + chunk967968 assert isinstance(full, AIMessageChunk)969 assert all(isinstance(block, dict) for block in full.content)970971 approval_message = HumanMessage(972 [973 {974 "type": "mcp_approval_response",975 "approve": True,976 "approval_request_id": block["id"], # type: ignore[index]977 }978 for block in full.content979 if block["type"] == "mcp_approval_request" # type: ignore[index]980 ]981 )982 result = llm_with_tools.invoke([input_message, full, approval_message])983 next_message = {"role": "user", "content": "Thanks!"}984 _ = llm_with_tools.invoke(985 [input_message, full, approval_message, result, next_message]986 )987988989@pytest.mark.default_cassette("test_mcp_builtin_zdr.yaml.gz")990@pytest.mark.vcr991@pytest.mark.parametrize("use_v2_stream", [False, True])992def test_mcp_builtin_zdr_v1(use_v2_stream: bool) -> None:993 llm = ChatOpenAI(994 model="gpt-5-nano",995 output_version="v1",996 store=False,997 include=["reasoning.encrypted_content"],998 )9991000 llm_with_tools = llm.bind_tools(1001 [1002 {1003 "type": "mcp",1004 "server_label": "deepwiki",1005 "server_url": "https://mcp.deepwiki.com/mcp",1006 "allowed_tools": ["ask_question"],1007 "require_approval": "always",1008 }1009 ]1010 )1011 input_message = {1012 "role": "user",1013 "content": (1014 "What transport protocols does the 2025-03-26 version of the MCP "1015 "spec (modelcontextprotocol/modelcontextprotocol) support?"1016 ),1017 }1018 full: BaseMessage1019 if use_v2_stream:1020 full = cast(1021 "ChatModelStream",1022 llm_with_tools.stream_events([input_message], version="v3"),1023 ).output1024 else:1025 aggregated: BaseMessageChunk | None = None1026 for chunk in llm_with_tools.stream([input_message]):1027 assert isinstance(chunk, AIMessageChunk)1028 aggregated = chunk if aggregated is None else aggregated + chunk1029 assert isinstance(aggregated, AIMessageChunk)1030 full = aggregated10311032 assert isinstance(full, AIMessage)1033 assert all(isinstance(block, dict) for block in full.content)10341035 approval_message = HumanMessage(1036 [1037 {1038 "type": "non_standard",1039 "value": {1040 "type": "mcp_approval_response",1041 "approve": True,1042 "approval_request_id": block["value"]["id"], # type: ignore[index]1043 },1044 }1045 for block in full.content_blocks1046 if block["type"] == "non_standard"1047 and block["value"]["type"] == "mcp_approval_request" # type: ignore[index]1048 ]1049 )1050 result = llm_with_tools.invoke([input_message, full, approval_message])1051 next_message = {"role": "user", "content": "Thanks!"}1052 _ = llm_with_tools.invoke(1053 [input_message, full, approval_message, result, next_message]1054 )105510561057@pytest.mark.default_cassette("test_image_generation_streaming.yaml.gz")1058@pytest.mark.vcr1059@pytest.mark.parametrize("output_version", ["v0", "responses/v1"])1060def test_image_generation_streaming(1061 output_version: Literal["v0", "responses/v1"],1062) -> None:1063 """Test image generation streaming."""1064 llm = ChatOpenAI(1065 model="gpt-4.1", use_responses_api=True, output_version=output_version1066 )1067 tool = {1068 "type": "image_generation",1069 # For testing purposes let's keep the quality low, so the test runs faster.1070 "quality": "low",1071 "output_format": "jpeg",1072 "output_compression": 100,1073 "size": "1024x1024",1074 }10751076 # Example tool output for an image1077 # {1078 # "background": "opaque",1079 # "id": "ig_683716a8ddf0819888572b20621c7ae4029ec8c11f8dacf8",1080 # "output_format": "png",1081 # "quality": "high",1082 # "revised_prompt": "A fluffy, fuzzy cat sitting calmly, with soft fur, bright "1083 # "eyes, and a cute, friendly expression. The background is "1084 # "simple and light to emphasize the cat's texture and "1085 # "fluffiness.",1086 # "size": "1024x1024",1087 # "status": "completed",1088 # "type": "image_generation_call",1089 # "result": # base64 encode image data1090 # }10911092 expected_keys = {1093 "id",1094 "index",1095 "background",1096 "output_format",1097 "quality",1098 "result",1099 "revised_prompt",1100 "size",1101 "status",1102 "type",1103 }11041105 full: BaseMessageChunk | None = None1106 for chunk in llm.stream("Draw a random short word in green font.", tools=[tool]):1107 assert isinstance(chunk, AIMessageChunk)1108 full = chunk if full is None else full + chunk1109 complete_ai_message = cast(AIMessageChunk, full)1110 # At the moment, the streaming API does not pick up annotations fully.1111 # So the following check is commented out.1112 # _check_response(complete_ai_message)1113 if output_version == "v0":1114 assert complete_ai_message.additional_kwargs["tool_outputs"]1115 tool_output = complete_ai_message.additional_kwargs["tool_outputs"][0]1116 assert set(tool_output.keys()).issubset(expected_keys)1117 else:1118 # "responses/v1"1119 tool_output = next(1120 block1121 for block in complete_ai_message.content1122 if isinstance(block, dict) and block["type"] == "image_generation_call"1123 )1124 assert set(tool_output.keys()).issubset(expected_keys)112511261127@pytest.mark.default_cassette("test_image_generation_streaming.yaml.gz")1128@pytest.mark.vcr1129def test_image_generation_streaming_v1() -> None:1130 """Test image generation streaming."""1131 llm = ChatOpenAI(model="gpt-4.1", use_responses_api=True, output_version="v1")1132 tool = {1133 "type": "image_generation",1134 "quality": "low",1135 "output_format": "jpeg",1136 "output_compression": 100,1137 "size": "1024x1024",1138 }11391140 standard_keys = {"type", "base64", "mime_type", "id", "index"}1141 extra_keys = {1142 "background",1143 "output_format",1144 "quality",1145 "revised_prompt",1146 "size",1147 "status",1148 }11491150 full: BaseMessageChunk | None = None1151 for chunk in llm.stream("Draw a random short word in green font.", tools=[tool]):1152 assert isinstance(chunk, AIMessageChunk)1153 full = chunk if full is None else full + chunk1154 complete_ai_message = cast(AIMessageChunk, full)11551156 tool_output = next(1157 block1158 for block in complete_ai_message.content1159 if isinstance(block, dict) and block["type"] == "image"1160 )1161 assert set(standard_keys).issubset(tool_output.keys())1162 assert set(extra_keys).issubset(tool_output["extras"].keys())116311641165@pytest.mark.default_cassette("test_image_generation_multi_turn.yaml.gz")1166@pytest.mark.vcr1167@pytest.mark.parametrize("output_version", ["v0", "responses/v1"])1168def test_image_generation_multi_turn(1169 output_version: Literal["v0", "responses/v1"],1170) -> None:1171 """Test multi-turn editing of image generation by passing in history."""1172 # Test multi-turn1173 llm = ChatOpenAI(1174 model="gpt-4.1", use_responses_api=True, output_version=output_version1175 )1176 # Test invocation1177 tool = {1178 "type": "image_generation",1179 # For testing purposes let's keep the quality low, so the test runs faster.1180 "quality": "low",1181 "output_format": "jpeg",1182 "output_compression": 100,1183 "size": "1024x1024",1184 }1185 llm_with_tools = llm.bind_tools([tool])11861187 chat_history: list[MessageLikeRepresentation] = [1188 {"role": "user", "content": "Draw a random short word in green font."}1189 ]1190 ai_message = llm_with_tools.invoke(chat_history)1191 assert isinstance(ai_message, AIMessage)1192 _check_response(ai_message)11931194 expected_keys = {1195 "id",1196 "background",1197 "output_format",1198 "quality",1199 "result",1200 "revised_prompt",1201 "size",1202 "status",1203 "type",1204 }12051206 if output_version == "v0":1207 tool_output = ai_message.additional_kwargs["tool_outputs"][0]1208 assert set(tool_output.keys()).issubset(expected_keys)1209 elif output_version == "responses/v1":1210 tool_output = next(1211 block1212 for block in ai_message.content1213 if isinstance(block, dict) and block["type"] == "image_generation_call"1214 )1215 assert set(tool_output.keys()).issubset(expected_keys)1216 else:1217 standard_keys = {"type", "base64", "id", "status"}1218 tool_output = next(1219 block1220 for block in ai_message.content1221 if isinstance(block, dict) and block["type"] == "image"1222 )1223 assert set(standard_keys).issubset(tool_output.keys())12241225 # Example tool output for an image (v0)1226 # {1227 # "background": "opaque",1228 # "id": "ig_683716a8ddf0819888572b20621c7ae4029ec8c11f8dacf8",1229 # "output_format": "png",1230 # "quality": "high",1231 # "revised_prompt": "A fluffy, fuzzy cat sitting calmly, with soft fur, bright "1232 # "eyes, and a cute, friendly expression. The background is "1233 # "simple and light to emphasize the cat's texture and "1234 # "fluffiness.",1235 # "size": "1024x1024",1236 # "status": "completed",1237 # "type": "image_generation_call",1238 # "result": # base64 encode image data1239 # }12401241 chat_history.extend(1242 [1243 # AI message with tool output1244 ai_message,1245 # New request1246 {1247 "role": "user",1248 "content": (1249 "Now, change the font to blue. Keep the word and everything else "1250 "the same."1251 ),1252 },1253 ]1254 )12551256 ai_message2 = llm_with_tools.invoke(chat_history)1257 assert isinstance(ai_message2, AIMessage)1258 _check_response(ai_message2)12591260 if output_version == "v0":1261 tool_output = ai_message2.additional_kwargs["tool_outputs"][0]1262 assert set(tool_output.keys()).issubset(expected_keys)1263 else:1264 # "responses/v1"1265 tool_output = next(1266 block1267 for block in ai_message2.content1268 if isinstance(block, dict) and block["type"] == "image_generation_call"1269 )1270 assert set(tool_output.keys()).issubset(expected_keys)127112721273@pytest.mark.default_cassette("test_image_generation_multi_turn.yaml.gz")1274@pytest.mark.vcr1275def test_image_generation_multi_turn_v1() -> None:1276 """Test multi-turn editing of image generation by passing in history."""1277 # Test multi-turn1278 llm = ChatOpenAI(model="gpt-4.1", use_responses_api=True, output_version="v1")1279 # Test invocation1280 tool = {1281 "type": "image_generation",1282 "quality": "low",1283 "output_format": "jpeg",1284 "output_compression": 100,1285 "size": "1024x1024",1286 }1287 llm_with_tools = llm.bind_tools([tool])12881289 chat_history: list[MessageLikeRepresentation] = [1290 {"role": "user", "content": "Draw a random short word in green font."}1291 ]1292 ai_message = llm_with_tools.invoke(chat_history)1293 assert isinstance(ai_message, AIMessage)1294 _check_response(ai_message)12951296 standard_keys = {"type", "base64", "mime_type", "id"}1297 extra_keys = {1298 "background",1299 "output_format",1300 "quality",1301 "revised_prompt",1302 "size",1303 "status",1304 }13051306 tool_output = next(1307 block1308 for block in ai_message.content1309 if isinstance(block, dict) and block["type"] == "image"1310 )1311 assert set(standard_keys).issubset(tool_output.keys())1312 assert set(extra_keys).issubset(tool_output["extras"].keys())13131314 chat_history.extend(1315 [1316 # AI message with tool output1317 ai_message,1318 # New request1319 {1320 "role": "user",1321 "content": (1322 "Now, change the font to blue. Keep the word and everything else "1323 "the same."1324 ),1325 },1326 ]1327 )13281329 ai_message2 = llm_with_tools.invoke(chat_history)1330 assert isinstance(ai_message2, AIMessage)1331 _check_response(ai_message2)13321333 tool_output = next(1334 block1335 for block in ai_message2.content1336 if isinstance(block, dict) and block["type"] == "image"1337 )1338 assert set(standard_keys).issubset(tool_output.keys())1339 assert set(extra_keys).issubset(tool_output["extras"].keys())134013411342def test_verbosity_parameter() -> None:1343 """Test verbosity parameter with Responses API.13441345 Tests that the verbosity parameter works correctly with the OpenAI Responses API.13461347 """1348 llm = ChatOpenAI(model=MODEL_NAME, verbosity="medium", use_responses_api=True)1349 response = llm.invoke([HumanMessage(content="Hello, explain quantum computing.")])13501351 assert isinstance(response, AIMessage)1352 assert response.content135313541355@pytest.mark.default_cassette("test_custom_tool.yaml.gz")1356@pytest.mark.vcr1357@pytest.mark.parametrize("output_version", ["responses/v1", "v1"])1358def test_custom_tool(output_version: Literal["responses/v1", "v1"]) -> None:1359 @custom_tool1360 def execute_code(code: str) -> str:1361 """Execute python code."""1362 return "27"13631364 llm = ChatOpenAI(model="gpt-5", output_version=output_version).bind_tools(1365 [execute_code]1366 )13671368 input_message = {"role": "user", "content": "Use the tool to evaluate 3^3."}1369 tool_call_message = llm.invoke([input_message])1370 assert isinstance(tool_call_message, AIMessage)1371 assert len(tool_call_message.tool_calls) == 11372 tool_call = tool_call_message.tool_calls[0]1373 tool_message = execute_code.invoke(tool_call)1374 response = llm.invoke([input_message, tool_call_message, tool_message])1375 assert isinstance(response, AIMessage)13761377 # Test streaming1378 full: BaseMessageChunk | None = None1379 for chunk in llm.stream([input_message]):1380 assert isinstance(chunk, AIMessageChunk)1381 full = chunk if full is None else full + chunk1382 assert isinstance(full, AIMessageChunk)1383 assert len(full.tool_calls) == 1138413851386@pytest.mark.default_cassette("test_compaction.yaml.gz")1387@pytest.mark.vcr1388@pytest.mark.parametrize("output_version", ["responses/v1", "v1"])1389def test_compaction(output_version: Literal["responses/v1", "v1"]) -> None:1390 """Test the compaction beta feature."""1391 llm = ChatOpenAI(1392 model="gpt-5.2",1393 context_management=[{"type": "compaction", "compact_threshold": 10_000}],1394 output_version=output_version,1395 )13961397 input_message = {1398 "role": "user",1399 "content": f"Generate a one-sentence summary of this:\n\n{'a' * 50000}",1400 }1401 messages: list = [input_message]14021403 first_response = llm.invoke(messages)1404 messages.append(first_response)14051406 second_message = {1407 "role": "user",1408 "content": f"Generate a one-sentence summary of this:\n\n{'b' * 50000}",1409 }1410 messages.append(second_message)14111412 second_response = llm.invoke(messages)1413 messages.append(second_response)14141415 content_blocks = second_response.content_blocks1416 compaction_block = next(1417 (block for block in content_blocks if block["type"] == "non_standard"),1418 None,1419 )1420 assert compaction_block1421 assert compaction_block["value"].get("type") == "compaction"14221423 third_message = {1424 "role": "user",1425 "content": "What are we talking about?",1426 }1427 messages.append(third_message)1428 third_response = llm.invoke(messages)1429 assert third_response.text143014311432@pytest.mark.default_cassette("test_compaction_streaming.yaml.gz")1433@pytest.mark.vcr1434@pytest.mark.parametrize(1435 ("output_version", "use_v2_stream"),1436 [1437 ("responses/v1", False),1438 ("v1", False),1439 ("v1", True),1440 ],1441)1442def test_compaction_streaming(1443 output_version: Literal["responses/v1", "v1"], use_v2_stream: bool1444) -> None:1445 """Test the compaction beta feature."""1446 llm = ChatOpenAI(1447 model="gpt-5.2",1448 context_management=[{"type": "compaction", "compact_threshold": 10_000}],1449 output_version=output_version,1450 streaming=True,1451 )14521453 def _run(messages: list) -> AIMessage:1454 if use_v2_stream:1455 return llm.stream_events(messages, version="v3").output1456 result = llm.invoke(messages)1457 assert isinstance(result, AIMessage)1458 return result14591460 input_message = {1461 "role": "user",1462 "content": f"Generate a one-sentence summary of this:\n\n{'a' * 50000}",1463 }1464 messages: list = [input_message]14651466 first_response = _run(messages)1467 messages.append(first_response)14681469 second_message = {1470 "role": "user",1471 "content": f"Generate a one-sentence summary of this:\n\n{'b' * 50000}",1472 }1473 messages.append(second_message)14741475 second_response = _run(messages)1476 messages.append(second_response)14771478 content_blocks = second_response.content_blocks1479 compaction_block = next(1480 (block for block in content_blocks if block["type"] == "non_standard"),1481 None,1482 )1483 assert compaction_block1484 assert compaction_block["value"].get("type") == "compaction"14851486 third_message = {1487 "role": "user",1488 "content": "What are we talking about?",1489 }1490 messages.append(third_message)1491 third_response = _run(messages)1492 assert third_response.text149314941495def test_csv_input() -> None:1496 """Test CSV file input with both LangChain standard and OpenAI native formats."""1497 # Create sample CSV content1498 csv_content = (1499 "name,age,city\nAlice,30,New York\nBob,25,Los Angeles\nCarol,35,Chicago"1500 )1501 csv_bytes = csv_content.encode("utf-8")1502 base64_string = base64.b64encode(csv_bytes).decode("utf-8")15031504 llm = ChatOpenAI(model=MODEL_NAME, use_responses_api=True)15051506 # Test LangChain standard format1507 langchain_message = {1508 "role": "user",1509 "content": [1510 {1511 "type": "text",1512 "text": "How many people are in this CSV file?",1513 },1514 {1515 "type": "file",1516 "base64": base64_string,1517 "mime_type": "text/csv",1518 "filename": "people.csv",1519 },1520 ],1521 }1522 payload = llm._get_request_payload([langchain_message])1523 block = payload["input"][0]["content"][1]1524 assert block["type"] == "input_file"15251526 response = llm.invoke([langchain_message])1527 assert isinstance(response, AIMessage)1528 assert response.content1529 assert (1530 "3" in str(response.content).lower() or "three" in str(response.content).lower()1531 )15321533 # Test OpenAI native format1534 openai_message = {1535 "role": "user",1536 "content": [1537 {1538 "type": "text",1539 "text": "How many people are in this CSV file?",1540 },1541 {1542 "type": "input_file",1543 "filename": "people.csv",1544 "file_data": f"data:text/csv;base64,{base64_string}",1545 },1546 ],1547 }1548 payload2 = llm._get_request_payload([openai_message])1549 block2 = payload2["input"][0]["content"][1]1550 assert block2["type"] == "input_file"15511552 response2 = llm.invoke([openai_message])1553 assert isinstance(response2, AIMessage)1554 assert response2.content1555 assert (1556 "3" in str(response2.content).lower()1557 or "three" in str(response2.content).lower()1558 )155915601561@pytest.mark.default_cassette("test_phase.yaml.gz")1562@pytest.mark.vcr1563@pytest.mark.parametrize("output_version", ["responses/v1", "v1"])1564def test_phase(output_version: str) -> None:1565 def get_weather(location: str) -> str:1566 """Get the weather at a location."""1567 return "It's sunny."15681569 model = ChatOpenAI(1570 model="gpt-5.4",1571 use_responses_api=True,1572 verbosity="high",1573 reasoning={"effort": "medium", "summary": "auto"},1574 output_version=output_version,1575 )15761577 agent = create_agent(model, tools=[get_weather])15781579 input_message = {1580 "role": "user",1581 "content": (1582 "What's the weather in the oldest major city in the US? State your answer "1583 "and then generate a tool call this turn."1584 ),1585 }1586 result = agent.invoke({"messages": [input_message]})1587 first_response = result["messages"][1]1588 text_block = next(1589 block for block in first_response.content if block["type"] == "text"1590 )1591 assert text_block["phase"] == "commentary"15921593 final_response = result["messages"][-1]1594 text_block = next(1595 block for block in final_response.content if block["type"] == "text"1596 )1597 assert text_block["phase"] == "final_answer"159815991600@pytest.mark.default_cassette("test_phase_streaming.yaml.gz")1601@pytest.mark.vcr1602@pytest.mark.parametrize("output_version", ["responses/v1", "v1"])1603def test_phase_streaming(output_version: str) -> None:1604 def get_weather(location: str) -> str:1605 """Get the weather at a location."""1606 return "It's sunny."16071608 model = ChatOpenAI(1609 model="gpt-5.4",1610 use_responses_api=True,1611 verbosity="high",1612 reasoning={"effort": "medium", "summary": "auto"},1613 streaming=True,1614 output_version=output_version,1615 )16161617 agent = create_agent(model, tools=[get_weather])16181619 input_message = {1620 "role": "user",1621 "content": (1622 "What's the weather in the oldest major city in the US? State your answer "1623 "and then generate a tool call this turn."1624 ),1625 }1626 result = agent.invoke({"messages": [input_message]})1627 first_response = result["messages"][1]1628 if output_version == "responses/v1":1629 assert [block["type"] for block in first_response.content] == [1630 "reasoning",1631 "text",1632 "function_call",1633 ]1634 else:1635 assert [block["type"] for block in first_response.content] == [1636 "reasoning",1637 "text",1638 "tool_call",1639 ]1640 text_block = next(1641 block for block in first_response.content if block["type"] == "text"1642 )1643 assert text_block["phase"] == "commentary"16441645 final_response = result["messages"][-1]1646 assert [block["type"] for block in final_response.content] == ["text"]1647 text_block = next(1648 block for block in final_response.content if block["type"] == "text"1649 )1650 assert text_block["phase"] == "final_answer"165116521653@pytest.mark.default_cassette("test_tool_search.yaml.gz")1654@pytest.mark.vcr1655@pytest.mark.parametrize("output_version", ["responses/v1", "v1"])1656def test_tool_search(output_version: str) -> None:1657 @tool(extras={"defer_loading": True})1658 def get_weather(location: str) -> str:1659 """Get the current weather for a location."""1660 return f"The weather in {location} is sunny and 72°F"16611662 @tool(extras={"defer_loading": True})1663 def get_recipe(query: str) -> None:1664 """Get a recipe for chicken soup."""16651666 model = ChatOpenAI(1667 model="gpt-5.4",1668 use_responses_api=True,1669 output_version=output_version,1670 )16711672 agent = create_agent(1673 model=model,1674 tools=[get_weather, get_recipe, {"type": "tool_search"}],1675 )1676 input_message = {"role": "user", "content": "What's the weather in San Francisco?"}1677 result = agent.invoke({"messages": [input_message]})1678 assert len(result["messages"]) == 41679 tool_call_message = result["messages"][1]1680 assert isinstance(tool_call_message, AIMessage)1681 assert tool_call_message.tool_calls1682 if output_version == "v1":1683 assert [block["type"] for block in tool_call_message.content] == [ # type: ignore[index]1684 "server_tool_call",1685 "server_tool_result",1686 "tool_call",1687 ]1688 else:1689 assert [block["type"] for block in tool_call_message.content] == [ # type: ignore[index]1690 "tool_search_call",1691 "tool_search_output",1692 "function_call",1693 ]16941695 assert isinstance(result["messages"][2], ToolMessage)16961697 assert result["messages"][3].text169816991700@pytest.mark.default_cassette("test_tool_search_streaming.yaml.gz")1701@pytest.mark.vcr1702@pytest.mark.parametrize("output_version", ["responses/v1", "v1"])1703def test_tool_search_streaming(output_version: str) -> None:1704 @tool(extras={"defer_loading": True})1705 def get_weather(location: str) -> str:1706 """Get the current weather for a location."""1707 return f"The weather in {location} is sunny and 72°F"17081709 @tool(extras={"defer_loading": True})1710 def get_recipe(query: str) -> None:1711 """Get a recipe for chicken soup."""17121713 model = ChatOpenAI(1714 model="gpt-5.4",1715 use_responses_api=True,1716 streaming=True,1717 output_version=output_version,1718 )17191720 agent = create_agent(1721 model=model,1722 tools=[get_weather, get_recipe, {"type": "tool_search"}],1723 )1724 input_message = {"role": "user", "content": "What's the weather in San Francisco?"}1725 result = agent.invoke({"messages": [input_message]})1726 assert len(result["messages"]) == 41727 tool_call_message = result["messages"][1]1728 assert isinstance(tool_call_message, AIMessage)1729 assert tool_call_message.tool_calls1730 if output_version == "v1":1731 assert [block["type"] for block in tool_call_message.content] == [ # type: ignore[index]1732 "server_tool_call",1733 "server_tool_result",1734 "tool_call",1735 ]1736 else:1737 assert [block["type"] for block in tool_call_message.content] == [ # type: ignore[index]1738 "tool_search_call",1739 "tool_search_output",1740 "function_call",1741 ]17421743 assert isinstance(result["messages"][2], ToolMessage)17441745 assert result["messages"][3].text174617471748@pytest.mark.vcr1749def test_client_executed_tool_search() -> None:1750 @tool1751 def get_weather(location: str) -> str:1752 """Get the current weather for a location."""1753 return f"The weather in {location} is sunny and 72°F"17541755 def search_tools(goal: str) -> list[dict]:1756 """Search for available tools to help answer the question."""1757 return [1758 {1759 "type": "function",1760 "defer_loading": True,1761 **convert_to_openai_tool(get_weather)["function"],1762 }1763 ]17641765 tool_search_schema = convert_to_openai_tool(search_tools, strict=True)1766 tool_search_config: dict = {1767 "type": "tool_search",1768 "execution": "client",1769 "description": tool_search_schema["function"]["description"],1770 "parameters": tool_search_schema["function"]["parameters"],1771 }17721773 class ClientToolSearchMiddleware(AgentMiddleware):1774 @hook_config(can_jump_to=["model"])1775 def after_model(self, state: AgentState, runtime: Any) -> dict[str, Any] | None:1776 last_message = state["messages"][-1]1777 if not isinstance(last_message, AIMessage):1778 return None1779 for block in last_message.content:1780 if isinstance(block, dict) and block.get("type") == "tool_search_call":1781 call_id = block.get("call_id")1782 args = block.get("arguments", {})1783 goal = args.get("goal", "") if isinstance(args, dict) else ""1784 loaded_tools = search_tools(goal)1785 tool_search_output = {1786 "type": "tool_search_output",1787 "execution": "client",1788 "call_id": call_id,1789 "status": "completed",1790 "tools": loaded_tools,1791 }1792 return {1793 "messages": [HumanMessage(content=[tool_search_output])],1794 "jump_to": "model",1795 }1796 return None17971798 def wrap_tool_call(1799 self,1800 request: ToolCallRequest,1801 handler: Any,1802 ) -> Any:1803 if request.tool_call["name"] == "get_weather":1804 return handler(request.override(tool=get_weather))1805 return handler(request)18061807 llm = ChatOpenAI(model="gpt-5.4", use_responses_api=True)18081809 agent = create_agent(1810 model=llm,1811 tools=[tool_search_config],1812 middleware=[ClientToolSearchMiddleware()],1813 )18141815 result = agent.invoke(1816 {"messages": [HumanMessage("What's the weather in San Francisco?")]}1817 )1818 messages = result["messages"]1819 search_tool_call = messages[1]1820 assert search_tool_call.content[0]["type"] == "tool_search_call"18211822 search_tool_output = messages[2]1823 assert search_tool_output.content[0]["type"] == "tool_search_output"18241825 tool_call = messages[3]1826 assert tool_call.tool_calls18271828 assert isinstance(messages[4], ToolMessage)18291830 assert messages[5].text183118321833@pytest.mark.default_cassette("test_reasoning_text_v1_v2_parity.yaml.gz")1834@pytest.mark.vcr1835def test_reasoning_text_v1_v2_parity() -> None:1836 """`stream()` and `stream_events(version="v3")` agree on reasoning + text.18371838 Exercises the non-tool-call branch of the parity claim: a reasoning1839 model (`gpt-5-nano` via the Responses API) produces one or more1840 `reasoning` blocks followed by a `text` block. Both paths replay the1841 same recorded HTTP response (cassette with `allow_playback_repeats`),1842 so any remaining divergence is a library issue.1843 """1844 llm = ChatOpenAI(1845 model="gpt-5-nano",1846 reasoning={"effort": "low", "summary": "auto"},1847 output_version="v1",1848 )1849 prompt = {"role": "user", "content": "What is the capital of France?"}18501851 v1: AIMessageChunk | None = None1852 for chunk in llm.stream([prompt]):1853 assert isinstance(chunk, AIMessageChunk)1854 v1 = chunk if v1 is None else v1 + chunk1855 assert isinstance(v1, AIMessageChunk)18561857 stream = llm.stream_events([prompt], version="v3")1858 events = list(stream)1859 assert_valid_event_stream(events)1860 v2 = stream.output1861 assert isinstance(v2, AIMessage)18621863 # No tool calls on either path.1864 assert v1.tool_calls == v2.tool_calls == []1865 assert v1.invalid_tool_calls == v2.invalid_tool_calls == []1866 assert v1.additional_kwargs == v2.additional_kwargs18671868 # Content structure must match: same block sequence, same accumulated1869 # text and reasoning payloads, same block identifiers. `content_blocks`1870 # is the v1-shaped projection and is canonical for both paths.1871 assert v1.content_blocks == v2.content_blocks1872 assert v1.content == v2.content1873 # Sanity-check that we actually exercised the reasoning + text path.1874 block_types = [b["type"] for b in v1.content_blocks]1875 assert "reasoning" in block_types1876 assert "text" in block_types18771878 # Usage: core counts must match; provider detail subdicts are1879 # dropped by `_to_protocol_usage` because `langchain_protocol.UsageInfo`1880 # doesn't list them. Tracked as a protocol-repo change.1881 detail_keys = {"input_token_details", "output_token_details"}1882 v1_usage = {1883 k: v for k, v in (v1.usage_metadata or {}).items() if k not in detail_keys1884 }1885 v2_usage = {1886 k: v for k, v in (v2.usage_metadata or {}).items() if k not in detail_keys1887 }1888 assert v1_usage == v2_usage18891890 # Response metadata must match. The Responses API doesn't put1891 # `finish_reason` in per-chunk metadata, so neither the v1 reduction1892 # nor the v2 bridge ends up with one. (Protocol 0.0.10 dropped the1893 # v2 bridge's default `"stop"` synthesis; provider metadata now1894 # passes through unchanged.)1895 assert v1.response_metadata == v2.response_metadata
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.