libs/partners/openai/tests/integration_tests/chat_models/test_responses_api.py PYTHON 1,896 lines View on github.com → Search inside
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

Code quality findings 100

Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(response, AIMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(response.content, list)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(block, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(block.get("text"), str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(text_content, str)
Ensure functions have docstrings for documentation
missing-docstring
def test_incomplete_response() -> None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full, AIMessageChunk)
Ensure functions have docstrings for documentation
missing-docstring
def test_web_search(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Ensure functions have docstrings for documentation
missing-docstring
async def test_web_search_async() -> None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(response, AIMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, dict) and block["type"] == "apply_patch_call"
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(aggregated, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(block, dict) and block["type"] == "apply_patch_call"
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(follow_up, AIMessage)
Ensure functions have docstrings for documentation
missing-docstring
def test_function_calling(output_version: Literal["v0", "responses/v1", "v1"]) -> None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Ensure functions have docstrings for documentation
missing-docstring
def test_agent_loop(output_version: Literal["responses/v1", "v1"]) -> None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(tool_call_message, AIMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(tool_message, ToolMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(response, AIMessage)
Ensure functions have docstrings for documentation
missing-docstring
def test_agent_loop_streaming(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(tool_call_message, AIMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(tool_message, ToolMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(response, AIMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(tool_call_message, AIMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(tool_message, ToolMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(response, AIMessage)
Ensure functions have docstrings for documentation
missing-docstring
def test_parsed_pydantic_schema(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full, AIMessageChunk)
Ensure functions have docstrings for documentation
missing-docstring
async def test_parsed_pydantic_schema_async() -> None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full, AIMessageChunk)
Ensure functions have docstrings for documentation
missing-docstring
def test_parsed_dict_schema(schema: Any) -> None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(parsed["response"], str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(parsed["response"], str)
Ensure functions have docstrings for documentation
missing-docstring
def test_parsed_strict() -> None:
Ensure functions have docstrings for documentation
missing-docstring
async def test_parsed_dict_schema_async(schema: Any) -> None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(parsed["response"], str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(parsed["response"], str)
Ensure functions have docstrings for documentation
missing-docstring
def test_function_calling_and_structured_output(schema: Any) -> None:
Ensure functions have docstrings for documentation
missing-docstring
def test_reasoning(output_version: Literal["v0", "responses/v1", "v1"]) -> None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(response, AIMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full, AIMessage)
Ensure functions have docstrings for documentation
missing-docstring
def test_stateful_api() -> None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(second_response.content, list)
Ensure functions have docstrings for documentation
missing-docstring
def test_route_from_model_kwargs() -> None:
Ensure functions have docstrings for documentation
missing-docstring
def test_computer_calls() -> None:
Ensure functions have docstrings for documentation
missing-docstring
def test_file_search(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full, AIMessageChunk)
Ensure functions have docstrings for documentation
missing-docstring
def test_stream_reasoning_summary(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(aggregated, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(summary, list)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(block, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(block["type"], str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(block["text"], str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(reasoning, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(summary, list)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(block, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(block["type"], str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(block["text"], str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(block.get("id"), str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(block.get("reasoning"), str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(block.get("index"), str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(response_2, AIMessage)
Ensure functions have docstrings for documentation
missing-docstring
def test_code_interpreter(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(response, AIMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(item, dict) and item["type"] == "code_interpreter_call"
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(aggregated, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(item, dict) and item["type"] == "code_interpreter_call"
Ensure functions have docstrings for documentation
missing-docstring
def test_mcp_builtin() -> None:
Ensure functions have docstrings for documentation
missing-docstring
def test_mcp_builtin_zdr() -> None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full, AIMessageChunk)
Ensure functions have docstrings for documentation
missing-docstring
def test_mcp_builtin_zdr_v1(use_v2_stream: bool) -> None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(aggregated, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(full, AIMessage)
Ensure functions have docstrings for documentation
missing-docstring
def test_image_generation_streaming(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, dict) and block["type"] == "image_generation_call"
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(chunk, AIMessageChunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, dict) and block["type"] == "image"
Ensure functions have docstrings for documentation
missing-docstring
def test_image_generation_multi_turn(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
assert isinstance(ai_message, AIMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, dict) and block["type"] == "image_generation_call"
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, dict) and block["type"] == "image"

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.