libs/partners/openai/tests/unit_tests/chat_models/test_base.py PYTHON 3,806 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 3,806.
1"""Test OpenAI Chat API wrapper."""23from __future__ import annotations45import json6import warnings7from functools import partial8from types import TracebackType9from typing import Any, Literal, cast10from unittest.mock import AsyncMock, MagicMock, patch1112import httpx13import openai14import pytest15from langchain_core.exceptions import ContextOverflowError16from langchain_core.load import dumps, loads17from langchain_core.messages import (18    AIMessage,19    AIMessageChunk,20    BaseMessage,21    FunctionMessage,22    HumanMessage,23    InvalidToolCall,24    SystemMessage,25    ToolCall,26    ToolMessage,27)28from langchain_core.messages import content as types29from langchain_core.messages.ai import UsageMetadata30from langchain_core.messages.block_translators.openai import (31    _convert_from_v03_ai_message,32)33from langchain_core.outputs import ChatGeneration, ChatResult34from langchain_core.runnables import RunnableLambda35from langchain_core.runnables.base import RunnableBinding, RunnableSequence36from langchain_core.tracers.base import BaseTracer37from langchain_core.tracers.schemas import Run38from openai.types.responses import ResponseOutputMessage, ResponseReasoningItem39from openai.types.responses.response import IncompleteDetails, Response40from openai.types.responses.response_error import ResponseError41from openai.types.responses.response_file_search_tool_call import (42    ResponseFileSearchToolCall,43    Result,44)45from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall46from openai.types.responses.response_function_web_search import (47    ActionSearch,48    ResponseFunctionWebSearch,49)50from openai.types.responses.response_output_refusal import ResponseOutputRefusal51from openai.types.responses.response_output_text import ResponseOutputText52from openai.types.responses.response_reasoning_item import Summary53from openai.types.responses.response_usage import (54    InputTokensDetails,55    OutputTokensDetails,56    ResponseUsage,57)58from pydantic import BaseModel, Field, SecretStr59from typing_extensions import Self, TypedDict6061from langchain_openai import ChatOpenAI62from langchain_openai.chat_models._compat import (63    _FUNCTION_CALL_IDS_MAP_KEY,64    _convert_from_v1_to_chat_completions,65    _convert_from_v1_to_responses,66    _convert_to_v03_ai_message,67)68from langchain_openai.chat_models.base import (69    OpenAIRefusalError,70    _construct_lc_result_from_responses_api,71    _construct_responses_api_input,72    _convert_dict_to_message,73    _convert_message_to_dict,74    _convert_to_openai_response_format,75    _create_usage_metadata,76    _create_usage_metadata_responses,77    _format_message_content,78    _get_last_messages,79    _make_computer_call_output_from_message,80    _model_prefers_responses_api,81    _oai_structured_outputs_parser,82    _resize,83)848586def test_openai_model_param() -> None:87    llm = ChatOpenAI(model="foo")88    assert llm.model_name == "foo"89    assert llm.model == "foo"90    llm = ChatOpenAI(model_name="foo")  # type: ignore[call-arg]91    assert llm.model_name == "foo"92    assert llm.model == "foo"9394    llm = ChatOpenAI(max_tokens=10)  # type: ignore[call-arg]95    assert llm.max_tokens == 1096    llm = ChatOpenAI(max_completion_tokens=10)97    assert llm.max_tokens == 109899100@pytest.mark.parametrize("async_api", [True, False])101def test_streaming_attribute_should_stream(async_api: bool) -> None:102    llm = ChatOpenAI(model="foo", streaming=True)103    assert llm._should_stream(async_api=async_api)104105106def test_openai_client_caching() -> None:107    """Test that the OpenAI client is cached."""108    llm1 = ChatOpenAI(model="gpt-4.1-mini")109    llm2 = ChatOpenAI(model="gpt-4.1-mini")110    assert llm1.root_client._client is llm2.root_client._client111112    llm3 = ChatOpenAI(model="gpt-4.1-mini", base_url="foo")113    assert llm1.root_client._client is not llm3.root_client._client114115    llm4 = ChatOpenAI(model="gpt-4.1-mini", timeout=None)116    assert llm1.root_client._client is llm4.root_client._client117118    llm5 = ChatOpenAI(model="gpt-4.1-mini", timeout=3)119    assert llm1.root_client._client is not llm5.root_client._client120121    llm6 = ChatOpenAI(122        model="gpt-4.1-mini", timeout=httpx.Timeout(timeout=60.0, connect=5.0)123    )124    assert llm1.root_client._client is not llm6.root_client._client125126    llm7 = ChatOpenAI(model="gpt-4.1-mini", timeout=(5, 1))127    assert llm1.root_client._client is not llm7.root_client._client128129130def test_profile() -> None:131    model = ChatOpenAI(model="gpt-4")132    assert model.profile133    assert not model.profile["structured_output"]134135    model = ChatOpenAI(model="gpt-5")136    assert model.profile137    assert model.profile["structured_output"]138    assert model.profile["tool_calling"]139140    # Test overwriting a field141    model.profile["tool_calling"] = False142    assert not model.profile["tool_calling"]143144    # Test we didn't mutate145    model = ChatOpenAI(model="gpt-5")146    assert model.profile147    assert model.profile["tool_calling"]148149    # Test passing in profile150    model = ChatOpenAI(model="gpt-5", profile={"tool_calling": False})151    assert model.profile == {"tool_calling": False}152153    # Test overrides for gpt-5 input tokens154    model = ChatOpenAI(model="gpt-5")155    assert model.profile["max_input_tokens"] == 272_000156157158def test_openai_o1_temperature() -> None:159    llm = ChatOpenAI(model="o1-preview")160    assert llm.temperature == 1161    llm = ChatOpenAI(model_name="o1-mini")  # type: ignore[call-arg]162    assert llm.temperature == 1163164165def test_function_message_dict_to_function_message() -> None:166    content = json.dumps({"result": "Example #1"})167    name = "test_function"168    result = _convert_dict_to_message(169        {"role": "function", "name": name, "content": content}170    )171    assert isinstance(result, FunctionMessage)172    assert result.name == name173    assert result.content == content174175176def test__convert_dict_to_message_human() -> None:177    message = {"role": "user", "content": "foo"}178    result = _convert_dict_to_message(message)179    expected_output = HumanMessage(content="foo")180    assert result == expected_output181    assert _convert_message_to_dict(expected_output) == message182183184def test__convert_dict_to_message_human_with_name() -> None:185    message = {"role": "user", "content": "foo", "name": "test"}186    result = _convert_dict_to_message(message)187    expected_output = HumanMessage(content="foo", name="test")188    assert result == expected_output189    assert _convert_message_to_dict(expected_output) == message190191192def test__convert_dict_to_message_ai() -> None:193    message = {"role": "assistant", "content": "foo"}194    result = _convert_dict_to_message(message)195    expected_output = AIMessage(content="foo")196    assert result == expected_output197    assert _convert_message_to_dict(expected_output) == message198199200def test__convert_dict_to_message_ai_with_name() -> None:201    message = {"role": "assistant", "content": "foo", "name": "test"}202    result = _convert_dict_to_message(message)203    expected_output = AIMessage(content="foo", name="test")204    assert result == expected_output205    assert _convert_message_to_dict(expected_output) == message206207208def test__convert_dict_to_message_system() -> None:209    message = {"role": "system", "content": "foo"}210    result = _convert_dict_to_message(message)211    expected_output = SystemMessage(content="foo")212    assert result == expected_output213    assert _convert_message_to_dict(expected_output) == message214215216def test__convert_dict_to_message_developer() -> None:217    message = {"role": "developer", "content": "foo"}218    result = _convert_dict_to_message(message)219    expected_output = SystemMessage(220        content="foo", additional_kwargs={"__openai_role__": "developer"}221    )222    assert result == expected_output223    assert _convert_message_to_dict(expected_output) == message224225226def test__convert_dict_to_message_system_with_name() -> None:227    message = {"role": "system", "content": "foo", "name": "test"}228    result = _convert_dict_to_message(message)229    expected_output = SystemMessage(content="foo", name="test")230    assert result == expected_output231    assert _convert_message_to_dict(expected_output) == message232233234def test__convert_dict_to_message_tool() -> None:235    message = {"role": "tool", "content": "foo", "tool_call_id": "bar"}236    result = _convert_dict_to_message(message)237    expected_output = ToolMessage(content="foo", tool_call_id="bar")238    assert result == expected_output239    assert _convert_message_to_dict(expected_output) == message240241242def test__convert_dict_to_message_tool_call() -> None:243    raw_tool_call = {244        "id": "call_wm0JY6CdwOMZ4eTxHWUThDNz",245        "function": {246            "arguments": '{"name": "Sally", "hair_color": "green"}',247            "name": "GenerateUsername",248        },249        "type": "function",250    }251    message = {"role": "assistant", "content": None, "tool_calls": [raw_tool_call]}252    result = _convert_dict_to_message(message)253    expected_output = AIMessage(254        content="",255        tool_calls=[256            ToolCall(257                name="GenerateUsername",258                args={"name": "Sally", "hair_color": "green"},259                id="call_wm0JY6CdwOMZ4eTxHWUThDNz",260                type="tool_call",261            )262        ],263    )264    assert result == expected_output265    assert _convert_message_to_dict(expected_output) == message266267    # Test malformed tool call268    raw_tool_calls: list = [269        {270            "id": "call_wm0JY6CdwOMZ4eTxHWUThDNz",271            "function": {"arguments": "oops", "name": "GenerateUsername"},272            "type": "function",273        },274        {275            "id": "call_abc123",276            "function": {277                "arguments": '{"name": "Sally", "hair_color": "green"}',278                "name": "GenerateUsername",279            },280            "type": "function",281        },282    ]283    raw_tool_calls = sorted(raw_tool_calls, key=lambda x: x["id"])284    message = {"role": "assistant", "content": None, "tool_calls": raw_tool_calls}285    result = _convert_dict_to_message(message)286    expected_output = AIMessage(287        content="",288        invalid_tool_calls=[289            InvalidToolCall(290                name="GenerateUsername",291                args="oops",292                id="call_wm0JY6CdwOMZ4eTxHWUThDNz",293                error=(294                    "Function GenerateUsername arguments:\n\noops\n\nare not "295                    "valid JSON. Received JSONDecodeError Expecting value: line 1 "296                    "column 1 (char 0)\nFor troubleshooting, visit: https://docs"297                    ".langchain.com/oss/python/langchain/errors/OUTPUT_PARSING_FAILURE "298                ),299                type="invalid_tool_call",300            )301        ],302        tool_calls=[303            ToolCall(304                name="GenerateUsername",305                args={"name": "Sally", "hair_color": "green"},306                id="call_abc123",307                type="tool_call",308            )309        ],310    )311    assert result == expected_output312    reverted_message_dict = _convert_message_to_dict(expected_output)313    reverted_message_dict["tool_calls"] = sorted(314        reverted_message_dict["tool_calls"], key=lambda x: x["id"]315    )316    assert reverted_message_dict == message317318319class MockAsyncContextManager:320    def __init__(self, chunk_list: list) -> None:321        self.current_chunk = 0322        self.chunk_list = chunk_list323        self.chunk_num = len(chunk_list)324325    async def __aenter__(self) -> Self:326        return self327328    async def __aexit__(329        self,330        exc_type: type[BaseException] | None,331        exc: BaseException | None,332        tb: TracebackType | None,333    ) -> None:334        pass335336    def __aiter__(self) -> MockAsyncContextManager:337        return self338339    async def __anext__(self) -> dict:340        if self.current_chunk < self.chunk_num:341            chunk = self.chunk_list[self.current_chunk]342            self.current_chunk += 1343            return chunk344        raise StopAsyncIteration345346347class MockSyncContextManager:348    def __init__(self, chunk_list: list) -> None:349        self.current_chunk = 0350        self.chunk_list = chunk_list351        self.chunk_num = len(chunk_list)352353    def __enter__(self) -> Self:354        return self355356    def __exit__(357        self,358        exc_type: type[BaseException] | None,359        exc: BaseException | None,360        tb: TracebackType | None,361    ) -> None:362        pass363364    def __iter__(self) -> MockSyncContextManager:365        return self366367    def __next__(self) -> dict:368        if self.current_chunk < self.chunk_num:369            chunk = self.chunk_list[self.current_chunk]370            self.current_chunk += 1371            return chunk372        raise StopIteration373374375GLM4_STREAM_META = """{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u4eba\u5de5\u667a\u80fd"}}]}376{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u52a9\u624b"}}]}377{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":","}}]}378{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u4f60\u53ef\u4ee5"}}]}379{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u53eb\u6211"}}]}380{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"AI"}}]}381{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u52a9\u624b"}}]}382{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"。"}}]}383{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"finish_reason":"stop","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":13,"completion_tokens":10,"total_tokens":23}}384[DONE]"""  # noqa: E501385386387@pytest.fixture388def mock_glm4_completion() -> list:389    list_chunk_data = GLM4_STREAM_META.split("\n")390    result_list = []391    for msg in list_chunk_data:392        if msg != "[DONE]":393            result_list.append(json.loads(msg))394395    return result_list396397398async def test_glm4_astream(mock_glm4_completion: list) -> None:399    llm_name = "glm-4"400    llm = ChatOpenAI(model=llm_name, stream_usage=True)401    mock_client = AsyncMock()402403    async def mock_create(*args: Any, **kwargs: Any) -> MockAsyncContextManager:404        return MockAsyncContextManager(mock_glm4_completion)405406    mock_client.create = mock_create407    usage_chunk = mock_glm4_completion[-1]408409    usage_metadata: UsageMetadata | None = None410    with patch.object(llm, "async_client", mock_client):411        async for chunk in llm.astream("你的名字叫什么?只回答名字"):412            assert isinstance(chunk, AIMessageChunk)413            if chunk.usage_metadata is not None:414                usage_metadata = chunk.usage_metadata415416    assert usage_metadata is not None417418    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]419    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]420    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]421422423def test_glm4_stream(mock_glm4_completion: list) -> None:424    llm_name = "glm-4"425    llm = ChatOpenAI(model=llm_name, stream_usage=True)426    mock_client = MagicMock()427428    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:429        return MockSyncContextManager(mock_glm4_completion)430431    mock_client.create = mock_create432    usage_chunk = mock_glm4_completion[-1]433434    usage_metadata: UsageMetadata | None = None435    with patch.object(llm, "client", mock_client):436        for chunk in llm.stream("你的名字叫什么?只回答名字"):437            assert isinstance(chunk, AIMessageChunk)438            if chunk.usage_metadata is not None:439                usage_metadata = chunk.usage_metadata440441    assert usage_metadata is not None442443    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]444    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]445    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]446447448DEEPSEEK_STREAM_DATA = """{"id":"d3610c24e6b42518a7883ea57c3ea2c3","choices":[{"index":0,"delta":{"content":"","role":"assistant"},"finish_reason":null,"logprobs":null}],"created":1721630271,"model":"deepseek-chat","system_fingerprint":"fp_7e0991cad4","object":"chat.completion.chunk","usage":null}449{"choices":[{"delta":{"content":"我是","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":null}450{"choices":[{"delta":{"content":"Deep","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":null}451{"choices":[{"delta":{"content":"Seek","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":null}452{"choices":[{"delta":{"content":" Chat","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":null}453{"choices":[{"delta":{"content":",","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":null}454{"choices":[{"delta":{"content":"一个","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":null}455{"choices":[{"delta":{"content":"由","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":null}456{"choices":[{"delta":{"content":"深度","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":null}457{"choices":[{"delta":{"content":"求","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":null}458{"choices":[{"delta":{"content":"索","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":null}459{"choices":[{"delta":{"content":"公司","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":null}460{"choices":[{"delta":{"content":"开发的","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":null}461{"choices":[{"delta":{"content":"智能","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":null}462{"choices":[{"delta":{"content":"助手","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":null}463{"choices":[{"delta":{"content":"。","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":null}464{"choices":[{"delta":{"content":"","role":null},"finish_reason":"stop","index":0,"logprobs":null}],"created":1721630271,"id":"d3610c24e6b42518a7883ea57c3ea2c3","model":"deepseek-chat","object":"chat.completion.chunk","system_fingerprint":"fp_7e0991cad4","usage":{"completion_tokens":15,"prompt_tokens":11,"total_tokens":26}}465[DONE]"""  # noqa: E501466467468@pytest.fixture469def mock_deepseek_completion() -> list[dict]:470    list_chunk_data = DEEPSEEK_STREAM_DATA.split("\n")471    result_list = []472    for msg in list_chunk_data:473        if msg != "[DONE]":474            result_list.append(json.loads(msg))475476    return result_list477478479async def test_deepseek_astream(mock_deepseek_completion: list) -> None:480    llm_name = "deepseek-chat"481    llm = ChatOpenAI(model=llm_name, stream_usage=True)482    mock_client = AsyncMock()483484    async def mock_create(*args: Any, **kwargs: Any) -> MockAsyncContextManager:485        return MockAsyncContextManager(mock_deepseek_completion)486487    mock_client.create = mock_create488    usage_chunk = mock_deepseek_completion[-1]489    usage_metadata: UsageMetadata | None = None490    with patch.object(llm, "async_client", mock_client):491        async for chunk in llm.astream("你的名字叫什么?只回答名字"):492            assert isinstance(chunk, AIMessageChunk)493            if chunk.usage_metadata is not None:494                usage_metadata = chunk.usage_metadata495496    assert usage_metadata is not None497498    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]499    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]500    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]501502503def test_deepseek_stream(mock_deepseek_completion: list) -> None:504    llm_name = "deepseek-chat"505    llm = ChatOpenAI(model=llm_name, stream_usage=True)506    mock_client = MagicMock()507508    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:509        return MockSyncContextManager(mock_deepseek_completion)510511    mock_client.create = mock_create512    usage_chunk = mock_deepseek_completion[-1]513    usage_metadata: UsageMetadata | None = None514    with patch.object(llm, "client", mock_client):515        for chunk in llm.stream("你的名字叫什么?只回答名字"):516            assert isinstance(chunk, AIMessageChunk)517            if chunk.usage_metadata is not None:518                usage_metadata = chunk.usage_metadata519520    assert usage_metadata is not None521522    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]523    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]524    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]525526527OPENAI_STREAM_DATA = """{"id":"chatcmpl-9nhARrdUiJWEMd5plwV1Gc9NCjb9M","object":"chat.completion.chunk","created":1721631035,"model":"gpt-4o-2024-05-13","system_fingerprint":"fp_18cc0f1fa0","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}],"usage":null}528{"id":"chatcmpl-9nhARrdUiJWEMd5plwV1Gc9NCjb9M","object":"chat.completion.chunk","created":1721631035,"model":"gpt-4o-2024-05-13","system_fingerprint":"fp_18cc0f1fa0","choices":[{"index":0,"delta":{"content":"我是"},"logprobs":null,"finish_reason":null}],"usage":null}529{"id":"chatcmpl-9nhARrdUiJWEMd5plwV1Gc9NCjb9M","object":"chat.completion.chunk","created":1721631035,"model":"gpt-4o-2024-05-13","system_fingerprint":"fp_18cc0f1fa0","choices":[{"index":0,"delta":{"content":"助手"},"logprobs":null,"finish_reason":null}],"usage":null}530{"id":"chatcmpl-9nhARrdUiJWEMd5plwV1Gc9NCjb9M","object":"chat.completion.chunk","created":1721631035,"model":"gpt-4o-2024-05-13","system_fingerprint":"fp_18cc0f1fa0","choices":[{"index":0,"delta":{"content":"。"},"logprobs":null,"finish_reason":null}],"usage":null}531{"id":"chatcmpl-9nhARrdUiJWEMd5plwV1Gc9NCjb9M","object":"chat.completion.chunk","created":1721631035,"model":"gpt-4o-2024-05-13","system_fingerprint":"fp_18cc0f1fa0","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null}532{"id":"chatcmpl-9nhARrdUiJWEMd5plwV1Gc9NCjb9M","object":"chat.completion.chunk","created":1721631035,"model":"gpt-4o-2024-05-13","system_fingerprint":"fp_18cc0f1fa0","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":3,"total_tokens":17}}533[DONE]"""  # noqa: E501534535536@pytest.fixture537def mock_openai_completion() -> list[dict]:538    list_chunk_data = OPENAI_STREAM_DATA.split("\n")539    result_list = []540    for msg in list_chunk_data:541        if msg != "[DONE]":542            result_list.append(json.loads(msg))543544    return result_list545546547async def test_openai_astream(mock_openai_completion: list) -> None:548    llm_name = "gpt-4o"549    llm = ChatOpenAI(model=llm_name)550    assert llm.stream_usage551    mock_client = AsyncMock()552553    async def mock_create(*args: Any, **kwargs: Any) -> MockAsyncContextManager:554        return MockAsyncContextManager(mock_openai_completion)555556    mock_client.create = mock_create557    usage_chunk = mock_openai_completion[-1]558    usage_metadata: UsageMetadata | None = None559    with patch.object(llm, "async_client", mock_client):560        async for chunk in llm.astream("你的名字叫什么?只回答名字"):561            assert isinstance(chunk, AIMessageChunk)562            if chunk.usage_metadata is not None:563                usage_metadata = chunk.usage_metadata564565    assert usage_metadata is not None566567    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]568    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]569    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]570571572def test_openai_stream(mock_openai_completion: list) -> None:573    llm_name = "gpt-4o"574    llm = ChatOpenAI(model=llm_name)575    assert llm.stream_usage576    mock_client = MagicMock()577578    call_kwargs = []579580    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:581        call_kwargs.append(kwargs)582        return MockSyncContextManager(mock_openai_completion)583584    mock_client.create = mock_create585    usage_chunk = mock_openai_completion[-1]586    usage_metadata: UsageMetadata | None = None587    with patch.object(llm, "client", mock_client):588        for chunk in llm.stream("你的名字叫什么?只回答名字"):589            assert isinstance(chunk, AIMessageChunk)590            if chunk.usage_metadata is not None:591                usage_metadata = chunk.usage_metadata592593    assert call_kwargs[-1]["stream_options"] == {"include_usage": True}594    assert usage_metadata is not None595    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]596    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]597    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]598599    # Verify no streaming outside of default base URL or clients600    for param, value in {601        "stream_usage": False,602        "openai_proxy": "http://localhost:7890",603        "openai_api_base": "https://example.com/v1",604        "base_url": "https://example.com/v1",605        "client": mock_client,606        "root_client": mock_client,607        "async_client": mock_client,608        "root_async_client": mock_client,609        "http_client": httpx.Client(),610        "http_async_client": httpx.AsyncClient(),611    }.items():612        llm = ChatOpenAI(model=llm_name, **{param: value})  # type: ignore[arg-type]613        assert not llm.stream_usage614        with patch.object(llm, "client", mock_client):615            _ = list(llm.stream("..."))616        assert "stream_options" not in call_kwargs[-1]617618619def test_openai_stream_events_v3_lifecycle(mock_openai_completion: list) -> None:620    """`stream_events(version="v3")` on chat completions emits a valid lifecycle."""621    from langchain_tests.utils.stream_lifecycle import assert_valid_event_stream622623    llm = ChatOpenAI(model="gpt-4o")624    mock_client = MagicMock()625626    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:627        return MockSyncContextManager(mock_openai_completion)628629    mock_client.create = mock_create630    with patch.object(llm, "client", mock_client):631        events = list(llm.stream_events("你的名字叫什么?只回答名字", version="v3"))632633    assert_valid_event_stream(events)634    # At minimum, a text block with the accumulated answer.635    finishes = [e for e in events if e["event"] == "content-block-finish"]636    assert len(finishes) >= 1637    text_finishes = [f for f in finishes if f["content"]["type"] == "text"]638    assert len(text_finishes) == 1639640641@pytest.fixture642def mock_completion() -> dict:643    return {644        "id": "chatcmpl-7fcZavknQda3SQ",645        "object": "chat.completion",646        "created": 1689989000,647        "model": "gpt-3.5-turbo-0613",648        "choices": [649            {650                "index": 0,651                "message": {"role": "assistant", "content": "Bar Baz", "name": "Erick"},652                "finish_reason": "stop",653            }654        ],655    }656657658@pytest.fixture659def mock_client(mock_completion: dict) -> MagicMock:660    rtn = MagicMock()661662    mock_create = MagicMock()663664    mock_resp = MagicMock()665    mock_resp.headers = {"content-type": "application/json"}666    mock_resp.parse.return_value = mock_completion667    mock_create.return_value = mock_resp668669    rtn.with_raw_response.create = mock_create670    rtn.create.return_value = mock_completion671    return rtn672673674@pytest.fixture675def mock_async_client(mock_completion: dict) -> AsyncMock:676    rtn = AsyncMock()677678    mock_create = AsyncMock()679    mock_resp = MagicMock()680    mock_resp.parse.return_value = mock_completion681    mock_create.return_value = mock_resp682683    rtn.with_raw_response.create = mock_create684    rtn.create.return_value = mock_completion685    return rtn686687688def test_openai_invoke(mock_client: MagicMock) -> None:689    llm = ChatOpenAI()690691    with patch.object(llm, "client", mock_client):692        res = llm.invoke("bar")693        assert res.content == "Bar Baz"694695        # headers are not in response_metadata if include_response_headers not set696        assert "headers" not in res.response_metadata697    assert mock_client.with_raw_response.create.called698699700async def test_openai_ainvoke(mock_async_client: AsyncMock) -> None:701    llm = ChatOpenAI()702703    with patch.object(llm, "async_client", mock_async_client):704        res = await llm.ainvoke("bar")705        assert res.content == "Bar Baz"706707        # headers are not in response_metadata if include_response_headers not set708        assert "headers" not in res.response_metadata709    assert mock_async_client.with_raw_response.create.called710711712@pytest.mark.parametrize(713    "model",714    [715        "gpt-3.5-turbo",716        "gpt-4",717        "gpt-3.5-0125",718        "gpt-4-0125-preview",719        "gpt-4-turbo-preview",720        "gpt-4-vision-preview",721    ],722)723def test__get_encoding_model(model: str) -> None:724    ChatOpenAI(model=model)._get_encoding_model()725726727def test_openai_invoke_name(mock_client: MagicMock) -> None:728    llm = ChatOpenAI()729730    with patch.object(llm, "client", mock_client):731        messages = [HumanMessage(content="Foo", name="Katie")]732        res = llm.invoke(messages)733        call_args, call_kwargs = mock_client.with_raw_response.create.call_args734        assert len(call_args) == 0  # no positional args735        call_messages = call_kwargs["messages"]736        assert len(call_messages) == 1737        assert call_messages[0]["role"] == "user"738        assert call_messages[0]["content"] == "Foo"739        assert call_messages[0]["name"] == "Katie"740741        # check return type has name742        assert res.content == "Bar Baz"743        assert res.name == "Erick"744745746def test_function_calls_with_tool_calls(mock_client: MagicMock) -> None:747    # Test that we ignore function calls if tool_calls are present748    llm = ChatOpenAI(model="gpt-4.1-mini")749    tool_call_message = AIMessage(750        content="",751        additional_kwargs={752            "function_call": {753                "name": "get_weather",754                "arguments": '{"location": "Boston"}',755            }756        },757        tool_calls=[758            {759                "name": "get_weather",760                "args": {"location": "Boston"},761                "id": "abc123",762                "type": "tool_call",763            }764        ],765    )766    messages = [767        HumanMessage("What's the weather in Boston?"),768        tool_call_message,769        ToolMessage(content="It's sunny.", name="get_weather", tool_call_id="abc123"),770    ]771    with patch.object(llm, "client", mock_client):772        _ = llm.invoke(messages)773        _, call_kwargs = mock_client.with_raw_response.create.call_args774        call_messages = call_kwargs["messages"]775        tool_call_message_payload = call_messages[1]776        assert "tool_calls" in tool_call_message_payload777        assert "function_call" not in tool_call_message_payload778779    # Test we don't ignore function calls if tool_calls are not present780    cast(AIMessage, messages[1]).tool_calls = []781    with patch.object(llm, "client", mock_client):782        _ = llm.invoke(messages)783        _, call_kwargs = mock_client.with_raw_response.create.call_args784        call_messages = call_kwargs["messages"]785        tool_call_message_payload = call_messages[1]786        assert "function_call" in tool_call_message_payload787        assert "tool_calls" not in tool_call_message_payload788789790def test_custom_token_counting() -> None:791    def token_encoder(text: str) -> list[int]:792        return [1, 2, 3]793794    llm = ChatOpenAI(custom_get_token_ids=token_encoder)795    assert llm.get_token_ids("foo") == [1, 2, 3]796797798def test_format_message_content() -> None:799    content: Any = "hello"800    assert content == _format_message_content(content)801802    content = None803    assert content == _format_message_content(content)804805    content = []806    assert content == _format_message_content(content)807808    content = [809        {"type": "text", "text": "What is in this image?"},810        {"type": "image_url", "image_url": {"url": "url.com"}},811    ]812    assert content == _format_message_content(content)813814    content = [815        {"type": "text", "text": "hello"},816        {817            "type": "tool_use",818            "id": "toolu_01A09q90qw90lq917835lq9",819            "name": "get_weather",820            "input": {"location": "San Francisco, CA", "unit": "celsius"},821        },822    ]823    assert _format_message_content(content) == [{"type": "text", "text": "hello"}]824825    # Standard multi-modal inputs826    contents = [827        {"type": "image", "source_type": "url", "url": "https://..."},  # v0828        {"type": "image", "url": "https://..."},  # v1829    ]830    expected = [{"type": "image_url", "image_url": {"url": "https://..."}}]831    for content in contents:832        assert expected == _format_message_content([content])833834    contents = [835        {836            "type": "image",837            "source_type": "base64",838            "data": "<base64 data>",839            "mime_type": "image/png",840        },841        {"type": "image", "base64": "<base64 data>", "mime_type": "image/png"},842    ]843    expected = [844        {845            "type": "image_url",846            "image_url": {"url": "data:image/png;base64,<base64 data>"},847        }848    ]849    for content in contents:850        assert expected == _format_message_content([content])851852    contents = [853        {854            "type": "file",855            "source_type": "base64",856            "data": "<base64 data>",857            "mime_type": "application/pdf",858            "filename": "my_file",859        },860        {861            "type": "file",862            "base64": "<base64 data>",863            "mime_type": "application/pdf",864            "filename": "my_file",865        },866    ]867    expected = [868        {869            "type": "file",870            "file": {871                "filename": "my_file",872                "file_data": "data:application/pdf;base64,<base64 data>",873            },874        }875    ]876    for content in contents:877        assert expected == _format_message_content([content])878879    # Test warn if PDF is missing a filename and that we add a default filename880    pdf_block = {881        "type": "file",882        "base64": "<base64 data>",883        "mime_type": "application/pdf",884    }885    expected = [886        {887            "type": "file",888            "file": {889                "file_data": "data:application/pdf;base64,<base64 data>",890                "filename": "LC_AUTOGENERATED",891            },892        }893    ]894    with pytest.warns(match="filename"):895        assert expected == _format_message_content([pdf_block])896897    contents = [898        {"type": "file", "source_type": "id", "id": "file-abc123"},899        {"type": "file", "file_id": "file-abc123"},900    ]901    expected = [{"type": "file", "file": {"file_id": "file-abc123"}}]902    for content in contents:903        assert expected == _format_message_content([content])904905906class GenerateUsername(BaseModel):907    "Get a username based on someone's name and hair color."908909    name: str910    hair_color: str911912913class MakeASandwich(BaseModel):914    "Make a sandwich given a list of ingredients."915916    bread_type: str917    cheese_type: str918    condiments: list[str]919    vegetables: list[str]920921922@pytest.mark.parametrize(923    "tool_choice",924    [925        "any",926        "none",927        "auto",928        "required",929        "GenerateUsername",930        {"type": "function", "function": {"name": "MakeASandwich"}},931        False,932        None,933    ],934)935@pytest.mark.parametrize("strict", [True, False, None])936def test_bind_tools_tool_choice(tool_choice: Any, strict: bool | None) -> None:937    """Test passing in manually construct tool call message."""938    llm = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)939    llm.bind_tools(940        tools=[GenerateUsername, MakeASandwich], tool_choice=tool_choice, strict=strict941    )942943944@pytest.mark.parametrize(945    "schema", [GenerateUsername, GenerateUsername.model_json_schema()]946)947@pytest.mark.parametrize("method", ["json_schema", "function_calling", "json_mode"])948@pytest.mark.parametrize("include_raw", [True, False])949@pytest.mark.parametrize("strict", [True, False, None])950def test_with_structured_output(951    schema: type | dict[str, Any] | None,952    method: Literal["function_calling", "json_mode", "json_schema"],953    include_raw: bool,954    strict: bool | None,955) -> None:956    """Test passing in manually construct tool call message."""957    if method == "json_mode":958        strict = None959    llm = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)960    llm.with_structured_output(961        schema, method=method, strict=strict, include_raw=include_raw962    )963964965def test_get_num_tokens_from_messages() -> None:966    llm = ChatOpenAI(model="gpt-4o")967    messages = [968        SystemMessage("you're a good assistant"),969        HumanMessage("how are you"),970        HumanMessage(971            [972                {"type": "text", "text": "what's in this image"},973                {"type": "image_url", "image_url": {"url": "https://foobar.com"}},974                {975                    "type": "image_url",976                    "image_url": {"url": "https://foobar.com", "detail": "low"},977                },978            ]979        ),980        AIMessage("a nice bird"),981        AIMessage(982            "",983            tool_calls=[984                ToolCall(id="foo", name="bar", args={"arg1": "arg1"}, type="tool_call")985            ],986        ),987        AIMessage(988            "",989            additional_kwargs={990                "function_call": {991                    "arguments": json.dumps({"arg1": "arg1"}),992                    "name": "fun",993                }994            },995        ),996        AIMessage(997            "text",998            tool_calls=[999                ToolCall(id="foo", name="bar", args={"arg1": "arg1"}, type="tool_call")1000            ],1001        ),1002        ToolMessage("foobar", tool_call_id="foo"),1003    ]1004    expected = 431  # Updated to match token count with mocked 100x100 image10051006    # Mock _url_to_size to avoid PIL dependency in unit tests1007    with patch("langchain_openai.chat_models.base._url_to_size") as mock_url_to_size:1008        mock_url_to_size.return_value = (100, 100)  # 100x100 pixel image1009        actual = llm.get_num_tokens_from_messages(messages)10101011    assert expected == actual10121013    # Test file inputs1014    messages = [1015        HumanMessage(1016            [1017                "Summarize this document.",1018                {1019                    "type": "file",1020                    "file": {1021                        "filename": "my file",1022                        "file_data": "data:application/pdf;base64,<data>",1023                    },1024                },1025            ]1026        )1027    ]1028    actual = 01029    with pytest.warns(match="file inputs are not supported"):1030        actual = llm.get_num_tokens_from_messages(messages)1031    assert actual == 1310321033    # Test Responses1034    messages = [1035        AIMessage(1036            [1037                {1038                    "type": "function_call",1039                    "name": "multiply",1040                    "arguments": '{"x":5,"y":4}',1041                    "call_id": "call_abc123",1042                    "id": "fc_abc123",1043                    "status": "completed",1044                },1045            ],1046            tool_calls=[1047                {1048                    "type": "tool_call",1049                    "name": "multiply",1050                    "args": {"x": 5, "y": 4},1051                    "id": "call_abc123",1052                }1053            ],1054        )1055    ]1056    actual = llm.get_num_tokens_from_messages(messages)1057    assert actual105810591060class Foo(BaseModel):1061    bar: int106210631064# class FooV1(BaseModelV1):1065#     bar: int106610671068@pytest.mark.parametrize(1069    "schema",1070    [1071        Foo1072        # FooV11073    ],1074)1075def test_schema_from_with_structured_output(schema: type) -> None:1076    """Test schema from with_structured_output."""10771078    llm = ChatOpenAI(model="gpt-4o")10791080    structured_llm = llm.with_structured_output(1081        schema, method="json_schema", strict=True1082    )10831084    expected = {1085        "properties": {"bar": {"title": "Bar", "type": "integer"}},1086        "required": ["bar"],1087        "title": schema.__name__,1088        "type": "object",1089    }1090    actual = structured_llm.get_output_schema().model_json_schema()1091    assert actual == expected109210931094def test__create_usage_metadata() -> None:1095    usage_metadata = {1096        "completion_tokens": 15,1097        "prompt_tokens_details": None,1098        "completion_tokens_details": None,1099        "prompt_tokens": 11,1100        "total_tokens": 26,1101    }1102    result = _create_usage_metadata(usage_metadata)1103    assert result == UsageMetadata(1104        output_tokens=15,1105        input_tokens=11,1106        total_tokens=26,1107        input_token_details={},1108        output_token_details={},1109    )111011111112def test__create_usage_metadata_zero_total_tokens() -> None:1113    """Test that explicit total_tokens=0 is preserved, not replaced by sum."""1114    usage_metadata = {1115        "prompt_tokens": 10,1116        "completion_tokens": 5,1117        "total_tokens": 0,1118        "prompt_tokens_details": None,1119        "completion_tokens_details": None,1120    }1121    result = _create_usage_metadata(usage_metadata)1122    assert result["total_tokens"] == 0112311241125def test__create_usage_metadata_responses() -> None:1126    response_usage_metadata = {1127        "input_tokens": 100,1128        "input_tokens_details": {"cached_tokens": 50},1129        "output_tokens": 50,1130        "output_tokens_details": {"reasoning_tokens": 10},1131        "total_tokens": 150,1132    }1133    result = _create_usage_metadata_responses(response_usage_metadata)11341135    assert result == UsageMetadata(1136        output_tokens=50,1137        input_tokens=100,1138        total_tokens=150,1139        input_token_details={"cache_read": 50},1140        output_token_details={"reasoning": 10},1141    )114211431144def test__resize_caps_dimensions_preserving_ratio() -> None:1145    """Larger side capped at 2048 then smaller at 768 keeping aspect ratio."""1146    assert _resize(2048, 4096) == (768, 1536)1147    assert _resize(4096, 2048) == (1536, 768)114811491150def test__convert_to_openai_response_format() -> None:1151    # Test response formats that aren't tool-like.1152    response_format: dict = {1153        "type": "json_schema",1154        "json_schema": {1155            "name": "math_reasoning",1156            "schema": {1157                "type": "object",1158                "properties": {1159                    "steps": {1160                        "type": "array",1161                        "items": {1162                            "type": "object",1163                            "properties": {1164                                "explanation": {"type": "string"},1165                                "output": {"type": "string"},1166                            },1167                            "required": ["explanation", "output"],1168                            "additionalProperties": False,1169                        },1170                    },1171                    "final_answer": {"type": "string"},1172                },1173                "required": ["steps", "final_answer"],1174                "additionalProperties": False,1175            },1176            "strict": True,1177        },1178    }11791180    actual = _convert_to_openai_response_format(response_format)1181    assert actual == response_format11821183    actual = _convert_to_openai_response_format(response_format["json_schema"])1184    assert actual == response_format11851186    actual = _convert_to_openai_response_format(response_format, strict=True)1187    assert actual == response_format11881189    with pytest.raises(ValueError):1190        _convert_to_openai_response_format(response_format, strict=False)119111921193@pytest.mark.parametrize("method", ["function_calling", "json_schema"])1194@pytest.mark.parametrize("strict", [True, None])1195def test_structured_output_strict(1196    method: Literal["function_calling", "json_schema"], strict: bool | None1197) -> None:1198    """Test to verify structured output with strict=True."""11991200    llm = ChatOpenAI(model="gpt-4o-2024-08-06")12011202    class Joke(BaseModel):1203        """Joke to tell user."""12041205        setup: str = Field(description="question to set up a joke")1206        punchline: str = Field(description="answer to resolve the joke")12071208    llm.with_structured_output(Joke, method=method, strict=strict)1209    # Schema1210    llm.with_structured_output(Joke.model_json_schema(), method=method, strict=strict)121112121213def test_nested_structured_output_strict() -> None:1214    """Test to verify structured output with strict=True for nested object."""12151216    llm = ChatOpenAI(model="gpt-4o-2024-08-06")12171218    class SelfEvaluation(TypedDict):1219        score: int1220        text: str12211222    class JokeWithEvaluation(TypedDict):1223        """Joke to tell user."""12241225        setup: str1226        punchline: str1227        _evaluation: SelfEvaluation12281229    llm.with_structured_output(JokeWithEvaluation, method="json_schema")123012311232def test__get_request_payload() -> None:1233    llm = ChatOpenAI(model="gpt-4o-2024-08-06")1234    messages: list = [1235        SystemMessage("hello"),1236        SystemMessage("bye", additional_kwargs={"__openai_role__": "developer"}),1237        SystemMessage(content=[{"type": "text", "text": "hello!"}]),1238        {"role": "human", "content": "how are you"},1239        {"role": "user", "content": [{"type": "text", "text": "feeling today"}]},1240    ]1241    expected = {1242        "messages": [1243            {"role": "system", "content": "hello"},1244            {"role": "developer", "content": "bye"},1245            {"role": "system", "content": [{"type": "text", "text": "hello!"}]},1246            {"role": "user", "content": "how are you"},1247            {"role": "user", "content": [{"type": "text", "text": "feeling today"}]},1248        ],1249        "model": "gpt-4o-2024-08-06",1250        "stream": False,1251    }1252    payload = llm._get_request_payload(messages)1253    assert payload == expected12541255    # Test we coerce to developer role for o-series models1256    llm = ChatOpenAI(model="o3-mini")1257    payload = llm._get_request_payload(messages)1258    expected = {1259        "messages": [1260            {"role": "developer", "content": "hello"},1261            {"role": "developer", "content": "bye"},1262            {"role": "developer", "content": [{"type": "text", "text": "hello!"}]},1263            {"role": "user", "content": "how are you"},1264            {"role": "user", "content": [{"type": "text", "text": "feeling today"}]},1265        ],1266        "model": "o3-mini",1267        "stream": False,1268    }1269    assert payload == expected12701271    # Test we ignore reasoning blocks from other providers1272    reasoning_messages: list = [1273        {1274            "role": "user",1275            "content": [1276                {"type": "reasoning_content", "reasoning_content": "reasoning..."},1277                {"type": "text", "text": "reasoned response"},1278            ],1279        },1280        {1281            "role": "user",1282            "content": [1283                {"type": "thinking", "thinking": "thinking..."},1284                {"type": "text", "text": "thoughtful response"},1285            ],1286        },1287    ]1288    expected = {1289        "messages": [1290            {1291                "role": "user",1292                "content": [{"type": "text", "text": "reasoned response"}],1293            },1294            {1295                "role": "user",1296                "content": [{"type": "text", "text": "thoughtful response"}],1297            },1298        ],1299        "model": "o3-mini",1300        "stream": False,1301    }1302    payload = llm._get_request_payload(reasoning_messages)1303    assert payload == expected130413051306def test_sanitize_chat_completions_text_blocks() -> None:1307    messages = [1308        ToolMessage(1309            content=[{"type": "text", "text": "foo", "id": "lc_abc123"}],1310            tool_call_id="def456",1311        ),1312    ]1313    payload = ChatOpenAI(model="gpt-5.2")._get_request_payload(messages)1314    assert payload["messages"] == [1315        {1316            "content": [{"type": "text", "text": "foo"}],1317            "role": "tool",1318            "tool_call_id": "def456",1319        }1320    ]132113221323def test_init_o1() -> None:1324    with warnings.catch_warnings(record=True) as record:1325        warnings.simplefilter("error")  # Treat warnings as errors1326        ChatOpenAI(model="o1", reasoning_effort="medium")13271328    assert len(record) == 0132913301331def test_init_minimal_reasoning_effort() -> None:1332    with warnings.catch_warnings(record=True) as record:1333        warnings.simplefilter("error")1334        ChatOpenAI(model="gpt-5", reasoning_effort="minimal")13351336    assert len(record) == 0133713381339@pytest.mark.parametrize("use_responses_api", [False, True])1340@pytest.mark.parametrize("use_max_completion_tokens", [True, False])1341def test_minimal_reasoning_effort_payload(1342    use_max_completion_tokens: bool, use_responses_api: bool1343) -> None:1344    """Test that minimal reasoning effort is included in request payload."""1345    if use_max_completion_tokens:1346        kwargs = {"max_completion_tokens": 100}1347    else:1348        kwargs = {"max_tokens": 100}13491350    init_kwargs: dict[str, Any] = {1351        "model": "gpt-5",1352        "reasoning_effort": "minimal",1353        "use_responses_api": use_responses_api,1354        **kwargs,1355    }13561357    llm = ChatOpenAI(**init_kwargs)13581359    messages = [1360        {"role": "developer", "content": "respond with just 'test'"},1361        {"role": "user", "content": "hello"},1362    ]13631364    payload = llm._get_request_payload(messages, stop=None)13651366    # When using responses API, reasoning_effort becomes reasoning.effort1367    if use_responses_api:1368        assert "reasoning" in payload1369        assert payload["reasoning"]["effort"] == "minimal"1370        # For responses API, tokens param becomes max_output_tokens1371        assert payload["max_output_tokens"] == 1001372    else:1373        # For non-responses API, reasoning_effort remains as is1374        assert payload["reasoning_effort"] == "minimal"1375        if use_max_completion_tokens:1376            assert payload["max_completion_tokens"] == 1001377        else:1378            # max_tokens gets converted to max_completion_tokens in non-responses API1379            assert payload["max_completion_tokens"] == 100138013811382def test_output_version_compat() -> None:1383    llm = ChatOpenAI(model="gpt-5", output_version="responses/v1")1384    assert llm._use_responses_api({}) is True138513861387def test_verbosity_parameter_payload() -> None:1388    """Test verbosity parameter is included in request payload for Responses API."""1389    llm = ChatOpenAI(model="gpt-5", verbosity="high", use_responses_api=True)13901391    messages = [{"role": "user", "content": "hello"}]1392    payload = llm._get_request_payload(messages, stop=None)13931394    assert payload["text"]["verbosity"] == "high"139513961397def test_structured_output_old_model() -> None:1398    class Output(TypedDict):1399        """output."""14001401        foo: str14021403    with pytest.warns(match="Cannot use method='json_schema'"):1404        llm = ChatOpenAI(model="gpt-4").with_structured_output(Output)1405    # assert tool calling was used instead of json_schema1406    assert "tools" in llm.steps[0].kwargs  # type: ignore1407    assert "response_format" not in llm.steps[0].kwargs  # type: ignore140814091410def test_structured_outputs_parser() -> None:1411    parsed_response = GenerateUsername(name="alice", hair_color="black")1412    llm_output = ChatGeneration(1413        message=AIMessage(1414            content='{"name": "alice", "hair_color": "black"}',1415            additional_kwargs={"parsed": parsed_response},1416        )1417    )1418    output_parser = RunnableLambda(1419        partial(_oai_structured_outputs_parser, schema=GenerateUsername)1420    )1421    serialized = dumps(llm_output)1422    deserialized = loads(serialized, allowed_objects=[ChatGeneration, AIMessage])1423    assert isinstance(deserialized, ChatGeneration)1424    result = output_parser.invoke(cast(AIMessage, deserialized.message))1425    assert result == parsed_response142614271428def test_create_chat_result_avoids_parsed_model_dump_warning() -> None:1429    class ModelOutput(BaseModel):1430        output: str14311432    class MockParsedMessage(openai.BaseModel):1433        role: Literal["assistant"] = "assistant"1434        content: str = '{"output": "Paris"}'1435        parsed: None = None1436        refusal: str | None = None14371438    class MockChoice(openai.BaseModel):1439        index: int = 01440        finish_reason: Literal["stop"] = "stop"1441        message: MockParsedMessage14421443    class MockChatCompletion(openai.BaseModel):1444        id: str = "chatcmpl-1"1445        object: str = "chat.completion"1446        created: int = 01447        model: str = "gpt-4o-mini"1448        choices: list[MockChoice]1449        usage: dict[str, int] | None = None14501451    parsed_response = ModelOutput(output="Paris")1452    response = MockChatCompletion.model_construct(1453        choices=[1454            MockChoice.model_construct(1455                message=MockParsedMessage.model_construct(parsed=parsed_response)1456            )1457        ],1458        usage={"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},1459    )14601461    llm = ChatOpenAI(model="gpt-4o-mini")1462    with warnings.catch_warnings(record=True) as caught_warnings:1463        warnings.simplefilter("always")1464        result = llm._create_chat_result(response)14651466    warning_messages = [str(warning.message) for warning in caught_warnings]1467    assert not any("field_name='parsed'" in message for message in warning_messages)1468    assert result.generations[0].message.additional_kwargs["parsed"] == parsed_response146914701471def test_structured_outputs_parser_valid_falsy_response() -> None:1472    class LunchBox(BaseModel):1473        sandwiches: list[str]14741475        def __len__(self) -> int:1476            return len(self.sandwiches)14771478    # prepare a valid *but falsy* response object, an empty LunchBox1479    parsed_response = LunchBox(sandwiches=[])1480    assert len(parsed_response) == 01481    llm_output = AIMessage(1482        content='{"sandwiches": []}', additional_kwargs={"parsed": parsed_response}1483    )1484    output_parser = RunnableLambda(1485        partial(_oai_structured_outputs_parser, schema=LunchBox)1486    )1487    result = output_parser.invoke(llm_output)1488    assert result == parsed_response148914901491def test__construct_lc_result_from_responses_api_error_handling() -> None:1492    """Test that errors in the response are properly raised."""1493    response = Response(1494        id="resp_123",1495        created_at=1234567890,1496        model="gpt-4o",1497        object="response",1498        error=ResponseError(message="Test error", code="server_error"),1499        parallel_tool_calls=True,1500        tools=[],1501        tool_choice="auto",1502        output=[],1503    )15041505    with pytest.raises(ValueError) as excinfo:1506        _construct_lc_result_from_responses_api(response)15071508    assert "Test error" in str(excinfo.value)150915101511def test__construct_lc_result_from_responses_api_basic_text_response() -> None:1512    """Test a basic text response with no tools or special features."""1513    response = Response(1514        id="resp_123",1515        created_at=1234567890,1516        model="gpt-4o",1517        object="response",1518        parallel_tool_calls=True,1519        tools=[],1520        tool_choice="auto",1521        output=[1522            ResponseOutputMessage(1523                type="message",1524                id="msg_123",1525                content=[1526                    ResponseOutputText(1527                        type="output_text", text="Hello, world!", annotations=[]1528                    )1529                ],1530                role="assistant",1531                status="completed",1532            )1533        ],1534        usage=ResponseUsage(1535            input_tokens=10,1536            output_tokens=3,1537            total_tokens=13,1538            input_tokens_details=InputTokensDetails(cached_tokens=0),1539            output_tokens_details=OutputTokensDetails(reasoning_tokens=0),1540        ),1541    )15421543    # v01544    result = _construct_lc_result_from_responses_api(response, output_version="v0")15451546    assert isinstance(result, ChatResult)1547    assert len(result.generations) == 11548    assert isinstance(result.generations[0], ChatGeneration)1549    assert isinstance(result.generations[0].message, AIMessage)1550    assert result.generations[0].message.content == [1551        {"type": "text", "text": "Hello, world!", "annotations": []}1552    ]1553    assert result.generations[0].message.id == "msg_123"1554    assert result.generations[0].message.usage_metadata1555    assert result.generations[0].message.usage_metadata["input_tokens"] == 101556    assert result.generations[0].message.usage_metadata["output_tokens"] == 31557    assert result.generations[0].message.usage_metadata["total_tokens"] == 131558    assert result.generations[0].message.response_metadata["id"] == "resp_123"1559    assert result.generations[0].message.response_metadata["model_name"] == "gpt-4o"15601561    # responses/v11562    result = _construct_lc_result_from_responses_api(response)1563    assert result.generations[0].message.content == [1564        {"type": "text", "text": "Hello, world!", "annotations": [], "id": "msg_123"}1565    ]1566    assert result.generations[0].message.id == "resp_123"1567    assert result.generations[0].message.response_metadata["id"] == "resp_123"156815691570def test__construct_lc_result_from_responses_api_multiple_text_blocks() -> None:1571    """Test a response with multiple text blocks."""1572    response = Response(1573        id="resp_123",1574        created_at=1234567890,1575        model="gpt-4o",1576        object="response",1577        parallel_tool_calls=True,1578        tools=[],1579        tool_choice="auto",1580        output=[1581            ResponseOutputMessage(1582                type="message",1583                id="msg_123",1584                content=[1585                    ResponseOutputText(1586                        type="output_text", text="First part", annotations=[]1587                    ),1588                    ResponseOutputText(1589                        type="output_text", text="Second part", annotations=[]1590                    ),1591                ],1592                role="assistant",1593                status="completed",1594            )1595        ],1596    )15971598    result = _construct_lc_result_from_responses_api(response, output_version="v0")15991600    assert len(result.generations[0].message.content) == 21601    assert result.generations[0].message.content == [1602        {"type": "text", "text": "First part", "annotations": []},1603        {"type": "text", "text": "Second part", "annotations": []},1604    ]160516061607def test__construct_lc_result_from_responses_api_multiple_messages() -> None:1608    """Test a response with multiple text blocks."""1609    response = Response(1610        id="resp_123",1611        created_at=1234567890,1612        model="gpt-4o",1613        object="response",1614        parallel_tool_calls=True,1615        tools=[],1616        tool_choice="auto",1617        output=[1618            ResponseOutputMessage(1619                type="message",1620                id="msg_123",1621                content=[1622                    ResponseOutputText(type="output_text", text="foo", annotations=[])1623                ],1624                role="assistant",1625                status="completed",1626            ),1627            ResponseReasoningItem(1628                type="reasoning",1629                id="rs_123",1630                summary=[Summary(type="summary_text", text="reasoning foo")],1631            ),1632            ResponseOutputMessage(1633                type="message",1634                id="msg_234",1635                content=[1636                    ResponseOutputText(type="output_text", text="bar", annotations=[])1637                ],1638                role="assistant",1639                status="completed",1640            ),1641        ],1642    )16431644    # v01645    result = _construct_lc_result_from_responses_api(response, output_version="v0")16461647    assert result.generations[0].message.content == [1648        {"type": "text", "text": "foo", "annotations": []},1649        {"type": "text", "text": "bar", "annotations": []},1650    ]1651    assert result.generations[0].message.additional_kwargs == {1652        "reasoning": {1653            "type": "reasoning",1654            "summary": [{"type": "summary_text", "text": "reasoning foo"}],1655            "id": "rs_123",1656        }1657    }1658    assert result.generations[0].message.id == "msg_234"16591660    # responses/v11661    result = _construct_lc_result_from_responses_api(response)16621663    assert result.generations[0].message.content == [1664        {"type": "text", "text": "foo", "annotations": [], "id": "msg_123"},1665        {1666            "type": "reasoning",1667            "summary": [{"type": "summary_text", "text": "reasoning foo"}],1668            "id": "rs_123",1669        },1670        {"type": "text", "text": "bar", "annotations": [], "id": "msg_234"},1671    ]1672    assert result.generations[0].message.id == "resp_123"167316741675def test__construct_lc_result_from_responses_api_refusal_response() -> None:1676    """Test a response with a refusal."""1677    response = Response(1678        id="resp_123",1679        created_at=1234567890,1680        model="gpt-4o",1681        object="response",1682        parallel_tool_calls=True,1683        tools=[],1684        tool_choice="auto",1685        output=[1686            ResponseOutputMessage(1687                type="message",1688                id="msg_123",1689                content=[1690                    ResponseOutputRefusal(1691                        type="refusal", refusal="I cannot assist with that request."1692                    )1693                ],1694                role="assistant",1695                status="completed",1696            )1697        ],1698    )16991700    # v01701    result = _construct_lc_result_from_responses_api(response, output_version="v0")17021703    assert result.generations[0].message.additional_kwargs["refusal"] == (1704        "I cannot assist with that request."1705    )17061707    # responses/v11708    result = _construct_lc_result_from_responses_api(response)1709    assert result.generations[0].message.content == [1710        {1711            "type": "refusal",1712            "refusal": "I cannot assist with that request.",1713            "id": "msg_123",1714        }1715    ]171617171718def test__construct_lc_result_from_responses_api_function_call_valid_json() -> None:1719    """Test a response with a valid function call."""1720    response = Response(1721        id="resp_123",1722        created_at=1234567890,1723        model="gpt-4o",1724        object="response",1725        parallel_tool_calls=True,1726        tools=[],1727        tool_choice="auto",1728        output=[1729            ResponseFunctionToolCall(1730                type="function_call",1731                id="func_123",1732                call_id="call_123",1733                name="get_weather",1734                arguments='{"location": "New York", "unit": "celsius"}',1735            )1736        ],1737    )17381739    # v01740    result = _construct_lc_result_from_responses_api(response, output_version="v0")17411742    msg: AIMessage = cast(AIMessage, result.generations[0].message)1743    assert len(msg.tool_calls) == 11744    assert msg.tool_calls[0]["type"] == "tool_call"1745    assert msg.tool_calls[0]["name"] == "get_weather"1746    assert msg.tool_calls[0]["id"] == "call_123"1747    assert msg.tool_calls[0]["args"] == {"location": "New York", "unit": "celsius"}1748    assert _FUNCTION_CALL_IDS_MAP_KEY in result.generations[0].message.additional_kwargs1749    assert (1750        result.generations[0].message.additional_kwargs[_FUNCTION_CALL_IDS_MAP_KEY][1751            "call_123"1752        ]1753        == "func_123"1754    )17551756    # responses/v11757    result = _construct_lc_result_from_responses_api(response)1758    msg = cast(AIMessage, result.generations[0].message)1759    assert msg.tool_calls1760    assert msg.content == [1761        {1762            "type": "function_call",1763            "id": "func_123",1764            "name": "get_weather",1765            "arguments": '{"location": "New York", "unit": "celsius"}',1766            "call_id": "call_123",1767        }1768    ]176917701771def test__construct_lc_result_from_responses_api_function_call_invalid_json() -> None:1772    """Test a response with an invalid JSON function call."""1773    response = Response(1774        id="resp_123",1775        created_at=1234567890,1776        model="gpt-4o",1777        object="response",1778        parallel_tool_calls=True,1779        tools=[],1780        tool_choice="auto",1781        output=[1782            ResponseFunctionToolCall(1783                type="function_call",1784                id="func_123",1785                call_id="call_123",1786                name="get_weather",1787                arguments='{"location": "New York", "unit": "celsius"',1788                # Missing closing brace1789            )1790        ],1791    )17921793    result = _construct_lc_result_from_responses_api(response, output_version="v0")17941795    msg: AIMessage = cast(AIMessage, result.generations[0].message)1796    assert len(msg.invalid_tool_calls) == 11797    assert msg.invalid_tool_calls[0]["type"] == "invalid_tool_call"1798    assert msg.invalid_tool_calls[0]["name"] == "get_weather"1799    assert msg.invalid_tool_calls[0]["id"] == "call_123"1800    assert (1801        msg.invalid_tool_calls[0]["args"]1802        == '{"location": "New York", "unit": "celsius"'1803    )1804    assert "error" in msg.invalid_tool_calls[0]1805    assert _FUNCTION_CALL_IDS_MAP_KEY in result.generations[0].message.additional_kwargs180618071808def test__construct_lc_result_from_responses_api_complex_response() -> None:1809    """Test a complex response with multiple output types."""1810    response = Response(1811        id="resp_123",1812        created_at=1234567890,1813        model="gpt-4o",1814        object="response",1815        parallel_tool_calls=True,1816        tools=[],1817        tool_choice="auto",1818        output=[1819            ResponseOutputMessage(1820                type="message",1821                id="msg_123",1822                content=[1823                    ResponseOutputText(1824                        type="output_text",1825                        text="Here's the information you requested:",1826                        annotations=[],1827                    )1828                ],1829                role="assistant",1830                status="completed",1831            ),1832            ResponseFunctionToolCall(1833                type="function_call",1834                id="func_123",1835                call_id="call_123",1836                name="get_weather",1837                arguments='{"location": "New York"}',1838            ),1839        ],1840        metadata={"key1": "value1", "key2": "value2"},1841        incomplete_details=IncompleteDetails(reason="max_output_tokens"),1842        status="completed",1843        user="user_123",1844    )18451846    # v01847    result = _construct_lc_result_from_responses_api(response, output_version="v0")18481849    # Check message content1850    assert result.generations[0].message.content == [1851        {1852            "type": "text",1853            "text": "Here's the information you requested:",1854            "annotations": [],1855        }1856    ]18571858    # Check tool calls1859    msg: AIMessage = cast(AIMessage, result.generations[0].message)1860    assert len(msg.tool_calls) == 11861    assert msg.tool_calls[0]["name"] == "get_weather"18621863    # Check metadata1864    assert result.generations[0].message.response_metadata["id"] == "resp_123"1865    assert result.generations[0].message.response_metadata["metadata"] == {1866        "key1": "value1",1867        "key2": "value2",1868    }1869    assert result.generations[0].message.response_metadata["incomplete_details"] == {1870        "reason": "max_output_tokens"1871    }1872    assert result.generations[0].message.response_metadata["status"] == "completed"1873    assert result.generations[0].message.response_metadata["user"] == "user_123"18741875    # responses/v11876    result = _construct_lc_result_from_responses_api(response)1877    msg = cast(AIMessage, result.generations[0].message)1878    assert msg.response_metadata["metadata"] == {"key1": "value1", "key2": "value2"}1879    assert msg.content == [1880        {1881            "type": "text",1882            "text": "Here's the information you requested:",1883            "annotations": [],1884            "id": "msg_123",1885        },1886        {1887            "type": "function_call",1888            "id": "func_123",1889            "call_id": "call_123",1890            "name": "get_weather",1891            "arguments": '{"location": "New York"}',1892        },1893    ]189418951896def test__construct_lc_result_from_responses_api_no_usage_metadata() -> None:1897    """Test a response without usage metadata."""1898    response = Response(1899        id="resp_123",1900        created_at=1234567890,1901        model="gpt-4o",1902        object="response",1903        parallel_tool_calls=True,1904        tools=[],1905        tool_choice="auto",1906        output=[1907            ResponseOutputMessage(1908                type="message",1909                id="msg_123",1910                content=[1911                    ResponseOutputText(1912                        type="output_text", text="Hello, world!", annotations=[]1913                    )1914                ],1915                role="assistant",1916                status="completed",1917            )1918        ],1919        # No usage field1920    )19211922    result = _construct_lc_result_from_responses_api(response)19231924    assert cast(AIMessage, result.generations[0].message).usage_metadata is None192519261927def test__construct_lc_result_from_responses_api_web_search_response() -> None:1928    """Test a response with web search output."""1929    from openai.types.responses.response_function_web_search import (1930        ResponseFunctionWebSearch,1931    )19321933    response = Response(1934        id="resp_123",1935        created_at=1234567890,1936        model="gpt-4o",1937        object="response",1938        parallel_tool_calls=True,1939        tools=[],1940        tool_choice="auto",1941        output=[1942            ResponseFunctionWebSearch(1943                id="websearch_123",1944                type="web_search_call",1945                status="completed",1946                action=ActionSearch(type="search", query="search query"),1947            )1948        ],1949    )19501951    # v01952    result = _construct_lc_result_from_responses_api(response, output_version="v0")19531954    assert "tool_outputs" in result.generations[0].message.additional_kwargs1955    assert len(result.generations[0].message.additional_kwargs["tool_outputs"]) == 11956    assert (1957        result.generations[0].message.additional_kwargs["tool_outputs"][0]["type"]1958        == "web_search_call"1959    )1960    assert (1961        result.generations[0].message.additional_kwargs["tool_outputs"][0]["id"]1962        == "websearch_123"1963    )1964    assert (1965        result.generations[0].message.additional_kwargs["tool_outputs"][0]["status"]1966        == "completed"1967    )19681969    # responses/v11970    result = _construct_lc_result_from_responses_api(response)1971    assert result.generations[0].message.content == [1972        {1973            "type": "web_search_call",1974            "id": "websearch_123",1975            "status": "completed",1976            "action": {"query": "search query", "type": "search"},1977        }1978    ]197919801981def test__construct_lc_result_from_responses_api_file_search_response() -> None:1982    """Test a response with file search output."""1983    response = Response(1984        id="resp_123",1985        created_at=1234567890,1986        model="gpt-4o",1987        object="response",1988        parallel_tool_calls=True,1989        tools=[],1990        tool_choice="auto",1991        output=[1992            ResponseFileSearchToolCall(1993                id="filesearch_123",1994                type="file_search_call",1995                status="completed",1996                queries=["python code", "langchain"],1997                results=[1998                    Result(1999                        file_id="file_123",2000                        filename="example.py",

Findings

✓ No findings reported for this file.

Get this view in your editor

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