libs/partners/openai/tests/unit_tests/chat_models/test_base.py PYTHON 4,780 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 4,780.
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    message_chunk_to_message,28)29from langchain_core.messages import content as types30from langchain_core.messages.ai import UsageMetadata31from langchain_core.messages.block_translators.openai import (32    _convert_from_v03_ai_message,33)34from langchain_core.outputs import ChatGeneration, ChatResult35from langchain_core.runnables import RunnableLambda36from langchain_core.runnables.base import RunnableBinding, RunnableSequence37from langchain_core.tracers.base import BaseTracer38from langchain_core.tracers.schemas import Run39from langchain_core.utils.pydantic import PYDANTIC_VERSION40from openai.types.responses import (41    ResponseApplyPatchToolCall,42    ResponseApplyPatchToolCallOutput,43    ResponseOutputMessage,44    ResponseReasoningItem,45)46from openai.types.responses.response import IncompleteDetails, Response47from openai.types.responses.response_apply_patch_tool_call import OperationCreateFile48from openai.types.responses.response_error import ResponseError49from openai.types.responses.response_file_search_tool_call import (50    ResponseFileSearchToolCall,51    Result,52)53from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall54from openai.types.responses.response_function_web_search import (55    ActionSearch,56    ResponseFunctionWebSearch,57)58from openai.types.responses.response_output_refusal import ResponseOutputRefusal59from openai.types.responses.response_output_text import ResponseOutputText60from openai.types.responses.response_reasoning_item import Summary61from openai.types.responses.response_usage import (62    InputTokensDetails,63    OutputTokensDetails,64    ResponseUsage,65)66from pydantic import BaseModel, Field, SecretStr67from typing_extensions import Self, TypedDict6869from langchain_openai import ChatOpenAI70from langchain_openai.chat_models._compat import (71    _FUNCTION_CALL_IDS_MAP_KEY,72    _convert_from_v1_to_chat_completions,73    _convert_from_v1_to_responses,74    _convert_to_v03_ai_message,75)76from langchain_openai.chat_models.base import (77    OpenAIRefusalError,78    _construct_lc_result_from_responses_api,79    _construct_responses_api_input,80    _convert_dict_to_message,81    _convert_message_to_dict,82    _convert_responses_chunk_to_generation_chunk,83    _convert_to_openai_response_format,84    _create_usage_metadata,85    _create_usage_metadata_responses,86    _format_message_content,87    _get_last_messages,88    _make_computer_call_output_from_message,89    _model_prefers_responses_api,90    _oai_structured_outputs_parser,91    _resize,92)9394OPENAI_TEST_MODEL = "gpt-5.5"95OPENAI_TEMPERATURE_CAPABLE_TEST_MODEL = "gpt-4o-mini"969798def test_openai_model_param() -> None:99    llm = ChatOpenAI(model="foo")100    assert llm.model_name == "foo"101    assert llm.model == "foo"102    llm = ChatOpenAI(model_name="foo")  # type: ignore[call-arg]103    assert llm.model_name == "foo"104    assert llm.model == "foo"105106    llm = ChatOpenAI(max_tokens=10)  # type: ignore[call-arg]107    assert llm.max_tokens == 10108    llm = ChatOpenAI(max_completion_tokens=10)109    assert llm.max_tokens == 10110111112@pytest.mark.parametrize("async_api", [True, False])113def test_streaming_attribute_should_stream(async_api: bool) -> None:114    llm = ChatOpenAI(model="foo", streaming=True)115    assert llm._should_stream(async_api=async_api)116117118def test_openai_client_caching() -> None:119    """Test that the OpenAI client is cached."""120    llm1 = ChatOpenAI(model=OPENAI_TEST_MODEL)121    llm2 = ChatOpenAI(model=OPENAI_TEST_MODEL)122    assert llm1.root_client._client is llm2.root_client._client123124    llm3 = ChatOpenAI(model=OPENAI_TEST_MODEL, base_url="foo")125    assert llm1.root_client._client is not llm3.root_client._client126127    llm4 = ChatOpenAI(model=OPENAI_TEST_MODEL, timeout=None)128    assert llm1.root_client._client is llm4.root_client._client129130    llm5 = ChatOpenAI(model=OPENAI_TEST_MODEL, timeout=3)131    assert llm1.root_client._client is not llm5.root_client._client132133    llm6 = ChatOpenAI(134        model=OPENAI_TEST_MODEL, timeout=httpx.Timeout(timeout=60.0, connect=5.0)135    )136    assert llm1.root_client._client is not llm6.root_client._client137138    llm7 = ChatOpenAI(model=OPENAI_TEST_MODEL, timeout=(5, 1))139    assert llm1.root_client._client is not llm7.root_client._client140141142def test_profile() -> None:143    model = ChatOpenAI(model="gpt-5.2-pro")144    assert model.profile145    assert not model.profile["structured_output"]146147    model = ChatOpenAI(model="gpt-5")148    assert model.profile149    assert model.profile["structured_output"]150    assert model.profile["tool_calling"]151152    # Test overwriting a field153    model.profile["tool_calling"] = False154    assert not model.profile["tool_calling"]155156    # Test we didn't mutate157    model = ChatOpenAI(model="gpt-5")158    assert model.profile159    assert model.profile["tool_calling"]160161    # Test passing in profile162    model = ChatOpenAI(model="gpt-5", profile={"tool_calling": False})163    assert model.profile == {"tool_calling": False}164165    # Test overrides for gpt-5 input tokens166    model = ChatOpenAI(model="gpt-5")167    assert model.profile["max_input_tokens"] == 272_000168169170def test_gpt_5_3_chat_latest_profile_has_no_reasoning_effort() -> None:171    model = ChatOpenAI(model="gpt-5.3-chat-latest")172173    assert model.profile174    assert model.profile["reasoning_output"] is False175    assert "reasoning_effort_levels" not in model.profile176177178def test_function_message_dict_to_function_message() -> None:179    content = json.dumps({"result": "Example #1"})180    name = "test_function"181    result = _convert_dict_to_message(182        {"role": "function", "name": name, "content": content}183    )184    assert isinstance(result, FunctionMessage)185    assert result.name == name186    assert result.content == content187188189def test__convert_dict_to_message_human() -> None:190    message = {"role": "user", "content": "foo"}191    result = _convert_dict_to_message(message)192    expected_output = HumanMessage(content="foo")193    assert result == expected_output194    assert _convert_message_to_dict(expected_output) == message195196197def test__convert_dict_to_message_human_with_name() -> None:198    message = {"role": "user", "content": "foo", "name": "test"}199    result = _convert_dict_to_message(message)200    expected_output = HumanMessage(content="foo", name="test")201    assert result == expected_output202    assert _convert_message_to_dict(expected_output) == message203204205def test__convert_dict_to_message_ai() -> None:206    message = {"role": "assistant", "content": "foo"}207    result = _convert_dict_to_message(message)208    expected_output = AIMessage(content="foo")209    assert result == expected_output210    assert _convert_message_to_dict(expected_output) == message211212213def test__convert_dict_to_message_ai_with_name() -> None:214    message = {"role": "assistant", "content": "foo", "name": "test"}215    result = _convert_dict_to_message(message)216    expected_output = AIMessage(content="foo", name="test")217    assert result == expected_output218    assert _convert_message_to_dict(expected_output) == message219220221def test__convert_dict_to_message_system() -> None:222    message = {"role": "system", "content": "foo"}223    result = _convert_dict_to_message(message)224    expected_output = SystemMessage(content="foo")225    assert result == expected_output226    assert _convert_message_to_dict(expected_output) == message227228229def test__convert_dict_to_message_developer() -> None:230    message = {"role": "developer", "content": "foo"}231    result = _convert_dict_to_message(message)232    expected_output = SystemMessage(233        content="foo", additional_kwargs={"__openai_role__": "developer"}234    )235    assert result == expected_output236    assert _convert_message_to_dict(expected_output) == message237238239def test__convert_dict_to_message_system_with_name() -> None:240    message = {"role": "system", "content": "foo", "name": "test"}241    result = _convert_dict_to_message(message)242    expected_output = SystemMessage(content="foo", name="test")243    assert result == expected_output244    assert _convert_message_to_dict(expected_output) == message245246247def test__convert_dict_to_message_tool() -> None:248    message = {"role": "tool", "content": "foo", "tool_call_id": "bar"}249    result = _convert_dict_to_message(message)250    expected_output = ToolMessage(content="foo", tool_call_id="bar")251    assert result == expected_output252    assert _convert_message_to_dict(expected_output) == message253254255def test__convert_dict_to_message_tool_call() -> None:256    raw_tool_call = {257        "id": "call_wm0JY6CdwOMZ4eTxHWUThDNz",258        "function": {259            "arguments": '{"name": "Sally", "hair_color": "green"}',260            "name": "GenerateUsername",261        },262        "type": "function",263    }264    message = {"role": "assistant", "content": None, "tool_calls": [raw_tool_call]}265    result = _convert_dict_to_message(message)266    expected_output = AIMessage(267        content="",268        tool_calls=[269            ToolCall(270                name="GenerateUsername",271                args={"name": "Sally", "hair_color": "green"},272                id="call_wm0JY6CdwOMZ4eTxHWUThDNz",273                type="tool_call",274            )275        ],276    )277    assert result == expected_output278    assert _convert_message_to_dict(expected_output) == message279280    # Test malformed tool call281    raw_tool_calls: list = [282        {283            "id": "call_wm0JY6CdwOMZ4eTxHWUThDNz",284            "function": {"arguments": "oops", "name": "GenerateUsername"},285            "type": "function",286        },287        {288            "id": "call_abc123",289            "function": {290                "arguments": '{"name": "Sally", "hair_color": "green"}',291                "name": "GenerateUsername",292            },293            "type": "function",294        },295    ]296    raw_tool_calls = sorted(raw_tool_calls, key=lambda x: x["id"])297    message = {"role": "assistant", "content": None, "tool_calls": raw_tool_calls}298    result = _convert_dict_to_message(message)299    expected_output = AIMessage(300        content="",301        invalid_tool_calls=[302            InvalidToolCall(303                name="GenerateUsername",304                args="oops",305                id="call_wm0JY6CdwOMZ4eTxHWUThDNz",306                error=(307                    "Function GenerateUsername arguments:\n\noops\n\nare not "308                    "valid JSON. Received JSONDecodeError Expecting value: line 1 "309                    "column 1 (char 0)\nFor troubleshooting, visit: https://docs"310                    ".langchain.com/oss/python/langchain/errors/OUTPUT_PARSING_FAILURE "311                ),312                type="invalid_tool_call",313            )314        ],315        tool_calls=[316            ToolCall(317                name="GenerateUsername",318                args={"name": "Sally", "hair_color": "green"},319                id="call_abc123",320                type="tool_call",321            )322        ],323    )324    assert result == expected_output325    reverted_message_dict = _convert_message_to_dict(expected_output)326    reverted_message_dict["tool_calls"] = sorted(327        reverted_message_dict["tool_calls"], key=lambda x: x["id"]328    )329    assert reverted_message_dict == message330331332class MockAsyncContextManager:333    def __init__(self, chunk_list: list) -> None:334        self.current_chunk = 0335        self.chunk_list = chunk_list336        self.chunk_num = len(chunk_list)337338    async def __aenter__(self) -> Self:339        return self340341    async def __aexit__(342        self,343        exc_type: type[BaseException] | None,344        exc: BaseException | None,345        tb: TracebackType | None,346    ) -> None:347        pass348349    def __aiter__(self) -> MockAsyncContextManager:350        return self351352    async def __anext__(self) -> dict:353        if self.current_chunk < self.chunk_num:354            chunk = self.chunk_list[self.current_chunk]355            self.current_chunk += 1356            return chunk357        raise StopAsyncIteration358359360class MockSyncContextManager:361    def __init__(self, chunk_list: list) -> None:362        self.current_chunk = 0363        self.chunk_list = chunk_list364        self.chunk_num = len(chunk_list)365366    def __enter__(self) -> Self:367        return self368369    def __exit__(370        self,371        exc_type: type[BaseException] | None,372        exc: BaseException | None,373        tb: TracebackType | None,374    ) -> None:375        pass376377    def __iter__(self) -> MockSyncContextManager:378        return self379380    def __next__(self) -> dict:381        if self.current_chunk < self.chunk_num:382            chunk = self.chunk_list[self.current_chunk]383            self.current_chunk += 1384            return chunk385        raise StopIteration386387388GLM4_STREAM_META = """{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u4eba\u5de5\u667a\u80fd"}}]}389{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u52a9\u624b"}}]}390{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":","}}]}391{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u4f60\u53ef\u4ee5"}}]}392{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u53eb\u6211"}}]}393{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"AI"}}]}394{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u52a9\u624b"}}]}395{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"。"}}]}396{"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}}397[DONE]"""  # noqa: E501398399400@pytest.fixture401def mock_glm4_completion() -> list:402    list_chunk_data = GLM4_STREAM_META.split("\n")403    result_list = []404    for msg in list_chunk_data:405        if msg != "[DONE]":406            result_list.append(json.loads(msg))407408    return result_list409410411async def test_glm4_astream(mock_glm4_completion: list) -> None:412    llm_name = "glm-4"413    llm = ChatOpenAI(model=llm_name, stream_usage=True)414    mock_client = AsyncMock()415416    async def mock_create(*args: Any, **kwargs: Any) -> MockAsyncContextManager:417        return MockAsyncContextManager(mock_glm4_completion)418419    mock_client.create = mock_create420    usage_chunk = mock_glm4_completion[-1]421422    usage_metadata: UsageMetadata | None = None423    with patch.object(llm, "async_client", mock_client):424        async for chunk in llm.astream("你的名字叫什么?只回答名字"):425            assert isinstance(chunk, AIMessageChunk)426            if chunk.usage_metadata is not None:427                usage_metadata = chunk.usage_metadata428429    assert usage_metadata is not None430431    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]432    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]433    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]434435436def test_glm4_stream(mock_glm4_completion: list) -> None:437    llm_name = "glm-4"438    llm = ChatOpenAI(model=llm_name, stream_usage=True)439    mock_client = MagicMock()440441    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:442        return MockSyncContextManager(mock_glm4_completion)443444    mock_client.create = mock_create445    usage_chunk = mock_glm4_completion[-1]446447    usage_metadata: UsageMetadata | None = None448    with patch.object(llm, "client", mock_client):449        for chunk in llm.stream("你的名字叫什么?只回答名字"):450            assert isinstance(chunk, AIMessageChunk)451            if chunk.usage_metadata is not None:452                usage_metadata = chunk.usage_metadata453454    assert usage_metadata is not None455456    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]457    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]458    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]459460461DEEPSEEK_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}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":"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}464{"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}465{"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}466{"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}467{"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}468{"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}469{"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}470{"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}471{"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}472{"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}473{"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}474{"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}475{"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}476{"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}477{"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}}478[DONE]"""  # noqa: E501479480481@pytest.fixture482def mock_deepseek_completion() -> list[dict]:483    list_chunk_data = DEEPSEEK_STREAM_DATA.split("\n")484    result_list = []485    for msg in list_chunk_data:486        if msg != "[DONE]":487            result_list.append(json.loads(msg))488489    return result_list490491492async def test_deepseek_astream(mock_deepseek_completion: list) -> None:493    llm_name = "deepseek-chat"494    llm = ChatOpenAI(model=llm_name, stream_usage=True)495    mock_client = AsyncMock()496497    async def mock_create(*args: Any, **kwargs: Any) -> MockAsyncContextManager:498        return MockAsyncContextManager(mock_deepseek_completion)499500    mock_client.create = mock_create501    usage_chunk = mock_deepseek_completion[-1]502    usage_metadata: UsageMetadata | None = None503    with patch.object(llm, "async_client", mock_client):504        async for chunk in llm.astream("你的名字叫什么?只回答名字"):505            assert isinstance(chunk, AIMessageChunk)506            if chunk.usage_metadata is not None:507                usage_metadata = chunk.usage_metadata508509    assert usage_metadata is not None510511    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]512    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]513    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]514515516def test_deepseek_stream(mock_deepseek_completion: list) -> None:517    llm_name = "deepseek-chat"518    llm = ChatOpenAI(model=llm_name, stream_usage=True)519    mock_client = MagicMock()520521    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:522        return MockSyncContextManager(mock_deepseek_completion)523524    mock_client.create = mock_create525    usage_chunk = mock_deepseek_completion[-1]526    usage_metadata: UsageMetadata | None = None527    with patch.object(llm, "client", mock_client):528        for chunk in llm.stream("你的名字叫什么?只回答名字"):529            assert isinstance(chunk, AIMessageChunk)530            if chunk.usage_metadata is not None:531                usage_metadata = chunk.usage_metadata532533    assert usage_metadata is not None534535    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]536    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]537    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]538539540OPENAI_STREAM_DATA = """{"id":"chatcmpl-9nhARrdUiJWEMd5plwV1Gc9NCjb9M","object":"chat.completion.chunk","created":1721631035,"model":"gpt-5.5","system_fingerprint":"fp_18cc0f1fa0","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}],"usage":null}541{"id":"chatcmpl-9nhARrdUiJWEMd5plwV1Gc9NCjb9M","object":"chat.completion.chunk","created":1721631035,"model":"gpt-5.5","system_fingerprint":"fp_18cc0f1fa0","choices":[{"index":0,"delta":{"content":"我是"},"logprobs":null,"finish_reason":null}],"usage":null}542{"id":"chatcmpl-9nhARrdUiJWEMd5plwV1Gc9NCjb9M","object":"chat.completion.chunk","created":1721631035,"model":"gpt-5.5","system_fingerprint":"fp_18cc0f1fa0","choices":[{"index":0,"delta":{"content":"助手"},"logprobs":null,"finish_reason":null}],"usage":null}543{"id":"chatcmpl-9nhARrdUiJWEMd5plwV1Gc9NCjb9M","object":"chat.completion.chunk","created":1721631035,"model":"gpt-5.5","system_fingerprint":"fp_18cc0f1fa0","choices":[{"index":0,"delta":{"content":"。"},"logprobs":null,"finish_reason":null}],"usage":null}544{"id":"chatcmpl-9nhARrdUiJWEMd5plwV1Gc9NCjb9M","object":"chat.completion.chunk","created":1721631035,"model":"gpt-5.5","system_fingerprint":"fp_18cc0f1fa0","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null}545{"id":"chatcmpl-9nhARrdUiJWEMd5plwV1Gc9NCjb9M","object":"chat.completion.chunk","created":1721631035,"model":"gpt-5.5","system_fingerprint":"fp_18cc0f1fa0","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":3,"total_tokens":17}}546[DONE]"""  # noqa: E501547548549@pytest.fixture550def mock_openai_completion() -> list[dict]:551    list_chunk_data = OPENAI_STREAM_DATA.split("\n")552    result_list = []553    for msg in list_chunk_data:554        if msg != "[DONE]":555            result_list.append(json.loads(msg))556557    return result_list558559560async def test_openai_astream(mock_openai_completion: list) -> None:561    llm_name = OPENAI_TEST_MODEL562    llm = ChatOpenAI(model=llm_name, stream_usage=True)563    assert llm.stream_usage564    mock_client = AsyncMock()565566    async def mock_create(*args: Any, **kwargs: Any) -> MockAsyncContextManager:567        return MockAsyncContextManager(mock_openai_completion)568569    mock_client.create = mock_create570    usage_chunk = mock_openai_completion[-1]571    usage_metadata: UsageMetadata | None = None572    with patch.object(llm, "async_client", mock_client):573        async for chunk in llm.astream("你的名字叫什么?只回答名字"):574            assert isinstance(chunk, AIMessageChunk)575            if chunk.usage_metadata is not None:576                usage_metadata = chunk.usage_metadata577578    assert usage_metadata is not None579580    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]581    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]582    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]583584585def test_openai_stream(mock_openai_completion: list) -> None:586    llm_name = OPENAI_TEST_MODEL587    llm = ChatOpenAI(model=llm_name, stream_usage=True)588    assert llm.stream_usage589    mock_client = MagicMock()590591    call_kwargs = []592593    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:594        call_kwargs.append(kwargs)595        return MockSyncContextManager(mock_openai_completion)596597    mock_client.create = mock_create598    usage_chunk = mock_openai_completion[-1]599    usage_metadata: UsageMetadata | None = None600    with patch.object(llm, "client", mock_client):601        for chunk in llm.stream("你的名字叫什么?只回答名字"):602            assert isinstance(chunk, AIMessageChunk)603            if chunk.usage_metadata is not None:604                usage_metadata = chunk.usage_metadata605606    assert call_kwargs[-1]["stream_options"] == {"include_usage": True}607    assert usage_metadata is not None608    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]609    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]610    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]611612    # Verify no streaming outside of default base URL or clients613    for param, value in {614        "stream_usage": False,615        "openai_proxy": "http://localhost:7890",616        "openai_api_base": "https://example.com/v1",617        "base_url": "https://example.com/v1",618        "client": mock_client,619        "root_client": mock_client,620        "async_client": mock_client,621        "root_async_client": mock_client,622        "http_client": httpx.Client(),623        "http_async_client": httpx.AsyncClient(),624    }.items():625        llm = ChatOpenAI(model=llm_name, **{param: value})  # type: ignore[arg-type]626        assert not llm.stream_usage627        with patch.object(llm, "client", mock_client):628            _ = list(llm.stream("..."))629        assert "stream_options" not in call_kwargs[-1]630631632def test_openai_stream_events_v3_lifecycle(mock_openai_completion: list) -> None:633    """`stream_events(version="v3")` on chat completions emits a valid lifecycle."""634    from langchain_tests.utils.stream_lifecycle import assert_valid_event_stream635636    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)637    mock_client = MagicMock()638639    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:640        return MockSyncContextManager(mock_openai_completion)641642    mock_client.create = mock_create643    with patch.object(llm, "client", mock_client):644        events = list(llm.stream_events("你的名字叫什么?只回答名字", version="v3"))645646    assert_valid_event_stream(events)647    # At minimum, a text block with the accumulated answer.648    finishes = [e for e in events if e["event"] == "content-block-finish"]649    assert len(finishes) >= 1650    text_finishes = [f for f in finishes if f["content"]["type"] == "text"]651    assert len(text_finishes) == 1652653654@pytest.fixture655def mock_completion() -> dict:656    return {657        "id": "chatcmpl-7fcZavknQda3SQ",658        "object": "chat.completion",659        "created": 1689989000,660        "model": OPENAI_TEST_MODEL,661        "choices": [662            {663                "index": 0,664                "message": {"role": "assistant", "content": "Bar Baz", "name": "Erick"},665                "finish_reason": "stop",666            }667        ],668    }669670671@pytest.fixture672def mock_client(mock_completion: dict) -> MagicMock:673    rtn = MagicMock()674675    mock_create = MagicMock()676677    mock_resp = MagicMock()678    mock_resp.headers = {"content-type": "application/json"}679    mock_resp.parse.return_value = mock_completion680    mock_create.return_value = mock_resp681682    rtn.with_raw_response.create = mock_create683    rtn.create.return_value = mock_completion684    return rtn685686687@pytest.fixture688def mock_async_client(mock_completion: dict) -> AsyncMock:689    rtn = AsyncMock()690691    mock_create = AsyncMock()692    mock_resp = MagicMock()693    mock_resp.parse.return_value = mock_completion694    mock_create.return_value = mock_resp695696    rtn.with_raw_response.create = mock_create697    rtn.create.return_value = mock_completion698    return rtn699700701def test_openai_invoke(mock_client: MagicMock) -> None:702    llm = ChatOpenAI()703704    with patch.object(llm, "client", mock_client):705        res = llm.invoke("bar")706        assert res.content == "Bar Baz"707708        # headers are not in response_metadata if include_response_headers not set709        assert "headers" not in res.response_metadata710    assert mock_client.with_raw_response.create.called711712713async def test_openai_ainvoke(mock_async_client: AsyncMock) -> None:714    llm = ChatOpenAI()715716    with patch.object(llm, "async_client", mock_async_client):717        res = await llm.ainvoke("bar")718        assert res.content == "Bar Baz"719720        # headers are not in response_metadata if include_response_headers not set721        assert "headers" not in res.response_metadata722    assert mock_async_client.with_raw_response.create.called723724725@pytest.mark.parametrize(726    "model",727    [728        OPENAI_TEST_MODEL,729        "gpt-5-nano",730        "o3",731        "gpt-5.2",732    ],733)734def test__get_encoding_model(model: str) -> None:735    ChatOpenAI(model=model)._get_encoding_model()736737738def test_openai_invoke_name(mock_client: MagicMock) -> None:739    llm = ChatOpenAI()740741    with patch.object(llm, "client", mock_client):742        messages = [HumanMessage(content="Foo", name="Katie")]743        res = llm.invoke(messages)744        call_args, call_kwargs = mock_client.with_raw_response.create.call_args745        assert len(call_args) == 0  # no positional args746        call_messages = call_kwargs["messages"]747        assert len(call_messages) == 1748        assert call_messages[0]["role"] == "user"749        assert call_messages[0]["content"] == "Foo"750        assert call_messages[0]["name"] == "Katie"751752        # check return type has name753        assert res.content == "Bar Baz"754        assert res.name == "Erick"755756757def test_function_calls_with_tool_calls(mock_client: MagicMock) -> None:758    # Test that we ignore function calls if tool_calls are present759    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)760    tool_call_message = AIMessage(761        content="",762        additional_kwargs={763            "function_call": {764                "name": "get_weather",765                "arguments": '{"location": "Boston"}',766            }767        },768        tool_calls=[769            {770                "name": "get_weather",771                "args": {"location": "Boston"},772                "id": "abc123",773                "type": "tool_call",774            }775        ],776    )777    messages = [778        HumanMessage("What's the weather in Boston?"),779        tool_call_message,780        ToolMessage(content="It's sunny.", name="get_weather", tool_call_id="abc123"),781    ]782    with patch.object(llm, "client", mock_client):783        _ = llm.invoke(messages)784        _, call_kwargs = mock_client.with_raw_response.create.call_args785        call_messages = call_kwargs["messages"]786        tool_call_message_payload = call_messages[1]787        assert "tool_calls" in tool_call_message_payload788        assert "function_call" not in tool_call_message_payload789790    # Test we don't ignore function calls if tool_calls are not present791    cast(AIMessage, messages[1]).tool_calls = []792    with patch.object(llm, "client", mock_client):793        _ = llm.invoke(messages)794        _, call_kwargs = mock_client.with_raw_response.create.call_args795        call_messages = call_kwargs["messages"]796        tool_call_message_payload = call_messages[1]797        assert "function_call" in tool_call_message_payload798        assert "tool_calls" not in tool_call_message_payload799800801def test_custom_token_counting() -> None:802    def token_encoder(text: str) -> list[int]:803        return [1, 2, 3]804805    llm = ChatOpenAI(custom_get_token_ids=token_encoder)806    assert llm.get_token_ids("foo") == [1, 2, 3]807808809def test_format_message_content() -> None:810    content: Any = "hello"811    assert content == _format_message_content(content)812813    content = None814    assert content == _format_message_content(content)815816    content = []817    assert content == _format_message_content(content)818819    content = [820        {"type": "text", "text": "What is in this image?"},821        {"type": "image_url", "image_url": {"url": "url.com"}},822    ]823    assert content == _format_message_content(content)824825    content = [826        {"type": "text", "text": "hello"},827        {828            "type": "tool_use",829            "id": "toolu_01A09q90qw90lq917835lq9",830            "name": "get_weather",831            "input": {"location": "San Francisco, CA", "unit": "celsius"},832        },833    ]834    assert _format_message_content(content) == [{"type": "text", "text": "hello"}]835836    # Standard multi-modal inputs837    contents = [838        {"type": "image", "source_type": "url", "url": "https://..."},  # v0839        {"type": "image", "url": "https://..."},  # v1840    ]841    expected = [{"type": "image_url", "image_url": {"url": "https://..."}}]842    for content in contents:843        assert expected == _format_message_content([content])844845    contents = [846        {847            "type": "image",848            "source_type": "base64",849            "data": "<base64 data>",850            "mime_type": "image/png",851        },852        {"type": "image", "base64": "<base64 data>", "mime_type": "image/png"},853    ]854    expected = [855        {856            "type": "image_url",857            "image_url": {"url": "data:image/png;base64,<base64 data>"},858        }859    ]860    for content in contents:861        assert expected == _format_message_content([content])862863    contents = [864        {865            "type": "file",866            "source_type": "base64",867            "data": "<base64 data>",868            "mime_type": "application/pdf",869            "filename": "my_file",870        },871        {872            "type": "file",873            "base64": "<base64 data>",874            "mime_type": "application/pdf",875            "filename": "my_file",876        },877    ]878    expected = [879        {880            "type": "file",881            "file": {882                "filename": "my_file",883                "file_data": "data:application/pdf;base64,<base64 data>",884            },885        }886    ]887    for content in contents:888        assert expected == _format_message_content([content])889890    # Test warn if PDF is missing a filename and that we add a default filename891    pdf_block = {892        "type": "file",893        "base64": "<base64 data>",894        "mime_type": "application/pdf",895    }896    expected = [897        {898            "type": "file",899            "file": {900                "file_data": "data:application/pdf;base64,<base64 data>",901                "filename": "LC_AUTOGENERATED",902            },903        }904    ]905    with pytest.warns(match="filename"):906        assert expected == _format_message_content([pdf_block])907908    contents = [909        {"type": "file", "source_type": "id", "id": "file-abc123"},910        {"type": "file", "file_id": "file-abc123"},911    ]912    expected = [{"type": "file", "file": {"file_id": "file-abc123"}}]913    for content in contents:914        assert expected == _format_message_content([content])915916917class GenerateUsername(BaseModel):918    "Get a username based on someone's name and hair color."919920    name: str921    hair_color: str922923924class MakeASandwich(BaseModel):925    "Make a sandwich given a list of ingredients."926927    bread_type: str928    cheese_type: str929    condiments: list[str]930    vegetables: list[str]931932933@pytest.mark.parametrize(934    "tool_choice",935    [936        "any",937        "none",938        "auto",939        "required",940        "GenerateUsername",941        {"type": "function", "function": {"name": "MakeASandwich"}},942        False,943        None,944    ],945)946@pytest.mark.parametrize("strict", [True, False, None])947def test_bind_tools_tool_choice(tool_choice: Any, strict: bool | None) -> None:948    """Test passing in manually construct tool call message."""949    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)950    llm.bind_tools(951        tools=[GenerateUsername, MakeASandwich], tool_choice=tool_choice, strict=strict952    )953954955def test_bind_tools_response_format_defaults_strict() -> None:956    """Test that strict defaults to True when response_format is provided."""957    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)958    bound = llm.bind_tools(959        tools=[GenerateUsername],960        response_format=MakeASandwich,961    )962    tools = bound.kwargs["tools"]  # type: ignore[attr-defined]963    assert tools[0]["function"]["strict"] is True964965966def test_bind_tools_response_format_respects_strict_false() -> None:967    """Test that strict=False is respected even when response_format is provided."""968    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)969    bound = llm.bind_tools(970        tools=[GenerateUsername],971        response_format=MakeASandwich,972        strict=False,973    )974    tools = bound.kwargs["tools"]  # type: ignore[attr-defined]975    assert tools[0]["function"]["strict"] is False976977978def test_bind_tools_no_response_format_keeps_strict_none() -> None:979    """Test that strict stays None when response_format is not provided."""980    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)981    bound = llm.bind_tools(tools=[GenerateUsername])982    tools = bound.kwargs["tools"]  # type: ignore[attr-defined]983    assert "strict" not in tools[0]["function"]984985986@pytest.mark.parametrize(987    "schema", [GenerateUsername, GenerateUsername.model_json_schema()]988)989@pytest.mark.parametrize("method", ["json_schema", "function_calling", "json_mode"])990@pytest.mark.parametrize("include_raw", [True, False])991@pytest.mark.parametrize("strict", [True, False, None])992def test_with_structured_output(993    schema: type | dict[str, Any] | None,994    method: Literal["function_calling", "json_mode", "json_schema"],995    include_raw: bool,996    strict: bool | None,997) -> None:998    """Test passing in manually construct tool call message."""999    if method == "json_mode":1000        strict = None1001    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)1002    llm.with_structured_output(1003        schema, method=method, strict=strict, include_raw=include_raw1004    )100510061007def test_get_num_tokens_from_messages() -> None:1008    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)1009    messages = [1010        SystemMessage("you're a good assistant"),1011        HumanMessage("how are you"),1012        HumanMessage(1013            [1014                {"type": "text", "text": "what's in this image"},1015                {"type": "image_url", "image_url": {"url": "https://foobar.com"}},1016                {1017                    "type": "image_url",1018                    "image_url": {"url": "https://foobar.com", "detail": "low"},1019                },1020            ]1021        ),1022        AIMessage("a nice bird"),1023        AIMessage(1024            "",1025            tool_calls=[1026                ToolCall(id="foo", name="bar", args={"arg1": "arg1"}, type="tool_call")1027            ],1028        ),1029        AIMessage(1030            "",1031            additional_kwargs={1032                "function_call": {1033                    "arguments": json.dumps({"arg1": "arg1"}),1034                    "name": "fun",1035                }1036            },1037        ),1038        AIMessage(1039            "text",1040            tool_calls=[1041                ToolCall(id="foo", name="bar", args={"arg1": "arg1"}, type="tool_call")1042            ],1043        ),1044        ToolMessage("foobar", tool_call_id="foo"),1045    ]1046    expected = 431  # Updated to match token count with mocked 100x100 image10471048    # Mock _url_to_size to avoid PIL dependency in unit tests1049    with patch("langchain_openai.chat_models.base._url_to_size") as mock_url_to_size:1050        mock_url_to_size.return_value = (100, 100)  # 100x100 pixel image1051        actual = llm.get_num_tokens_from_messages(messages)10521053    assert expected == actual10541055    # Test file inputs1056    messages = [1057        HumanMessage(1058            [1059                "Summarize this document.",1060                {1061                    "type": "file",1062                    "file": {1063                        "filename": "my file",1064                        "file_data": "data:application/pdf;base64,<data>",1065                    },1066                },1067            ]1068        )1069    ]1070    actual = 01071    with pytest.warns(match="file inputs are not supported"):1072        actual = llm.get_num_tokens_from_messages(messages)1073    assert actual == 1310741075    # Test Responses1076    messages = [1077        AIMessage(1078            [1079                {1080                    "type": "function_call",1081                    "name": "multiply",1082                    "arguments": '{"x":5,"y":4}',1083                    "call_id": "call_abc123",1084                    "id": "fc_abc123",1085                    "status": "completed",1086                },1087            ],1088            tool_calls=[1089                {1090                    "type": "tool_call",1091                    "name": "multiply",1092                    "args": {"x": 5, "y": 4},1093                    "id": "call_abc123",1094                }1095            ],1096        )1097    ]1098    actual = llm.get_num_tokens_from_messages(messages)1099    assert actual110011011102class Foo(BaseModel):1103    bar: int110411051106# class FooV1(BaseModelV1):1107#     bar: int110811091110@pytest.mark.parametrize(1111    "schema",1112    [1113        Foo1114        # FooV11115    ],1116)1117def test_schema_from_with_structured_output(schema: type) -> None:1118    """Test schema from with_structured_output."""11191120    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)11211122    structured_llm = llm.with_structured_output(1123        schema, method="json_schema", strict=True1124    )11251126    expected = {1127        "properties": {"bar": {"title": "Bar", "type": "integer"}},1128        "required": ["bar"],1129        "title": schema.__name__,1130        "type": "object",1131    }1132    output_schema = cast("type[BaseModel]", structured_llm.get_output_schema())1133    actual = output_schema.model_json_schema()1134    assert actual == expected113511361137def test__create_usage_metadata() -> None:1138    usage_metadata = {1139        "completion_tokens": 15,1140        "prompt_tokens_details": None,1141        "completion_tokens_details": None,1142        "prompt_tokens": 11,1143        "total_tokens": 26,1144    }1145    result = _create_usage_metadata(usage_metadata)1146    assert result == UsageMetadata(1147        output_tokens=15,1148        input_tokens=11,1149        total_tokens=26,1150        input_token_details={},1151        output_token_details={},1152    )115311541155def test__create_usage_metadata_zero_total_tokens() -> None:1156    """Test that explicit total_tokens=0 is preserved, not replaced by sum."""1157    usage_metadata = {1158        "prompt_tokens": 10,1159        "completion_tokens": 5,1160        "total_tokens": 0,1161        "prompt_tokens_details": None,1162        "completion_tokens_details": None,1163    }1164    result = _create_usage_metadata(usage_metadata)1165    assert result["total_tokens"] == 0116611671168def test__create_usage_metadata_cache_write_tokens() -> None:1169    """`cache_write_tokens` is surfaced under the standard `cache_creation` key."""1170    usage_metadata = {1171        "completion_tokens": 15,1172        # OpenAI's `cache_write_tokens` maps to core's `cache_creation`1173        "prompt_tokens_details": {"cached_tokens": 50, "cache_write_tokens": 25},1174        "completion_tokens_details": None,1175        "prompt_tokens": 100,1176        "total_tokens": 115,1177    }1178    result = _create_usage_metadata(usage_metadata)1179    assert result["input_token_details"] == {1180        "cache_read": 50,1181        "cache_creation": 25,1182    }118311841185def test__create_usage_metadata_cache_read_only() -> None:1186    """Responses without `cache_write_tokens` emit no `cache_creation` key."""1187    usage_metadata = {1188        "completion_tokens": 15,1189        "prompt_tokens_details": {"cached_tokens": 50},1190        "completion_tokens_details": None,1191        "prompt_tokens": 100,1192        "total_tokens": 115,1193    }1194    result = _create_usage_metadata(usage_metadata)1195    assert result["input_token_details"] == {"cache_read": 50}119611971198def test__create_usage_metadata_cache_tokens_zero_retained() -> None:1199    """Explicit zero cache counts are retained (filtered on `None`, not falsiness)."""1200    usage_metadata = {1201        "completion_tokens": 15,1202        "prompt_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},1203        "completion_tokens_details": None,1204        "prompt_tokens": 100,1205        "total_tokens": 115,1206    }1207    result = _create_usage_metadata(usage_metadata)1208    assert result["input_token_details"] == {1209        "cache_read": 0,1210        "cache_creation": 0,1211    }121212131214def test__create_usage_metadata_service_tier_excludes_cache_read_tokens() -> None:1215    """Tier counts exclude cache reads but not overlapping cache writes."""1216    usage_metadata = {1217        "completion_tokens": 50,1218        "prompt_tokens_details": {1219            "cached_tokens": 256,1220            "cache_write_tokens": 3072,1221        },1222        "completion_tokens_details": {"reasoning_tokens": 10},1223        "prompt_tokens": 2304,1224        "total_tokens": 2354,1225    }1226    result = _create_usage_metadata(usage_metadata, service_tier="priority")1227    assert result["input_token_details"] == {1228        "priority_cache_read": 256,1229        "priority_cache_creation": 3072,1230        "priority": 2048,1231    }1232    assert result["output_token_details"] == {1233        "priority_reasoning": 10,1234        "priority": 40,  # 50 - 10 (reasoning)1235    }123612371238def test__create_usage_metadata_service_tier_without_detail_fields() -> None:1239    """Tier arithmetic tolerates missing cache/reasoning fields (no TypeError)."""1240    usage_metadata = {1241        "completion_tokens": 50,1242        "prompt_tokens_details": None,1243        "completion_tokens_details": None,1244        "prompt_tokens": 100,1245        "total_tokens": 150,1246    }1247    result = _create_usage_metadata(usage_metadata, service_tier="flex")1248    assert result["input_token_details"] == {"flex": 100}1249    assert result["output_token_details"] == {"flex": 50}125012511252def test__create_usage_metadata_responses() -> None:1253    response_usage_metadata = {1254        "input_tokens": 100,1255        "input_tokens_details": {"cached_tokens": 50},1256        "output_tokens": 50,1257        "output_tokens_details": {"reasoning_tokens": 10},1258        "total_tokens": 150,1259    }1260    result = _create_usage_metadata_responses(response_usage_metadata)12611262    assert result == UsageMetadata(1263        output_tokens=50,1264        input_tokens=100,1265        total_tokens=150,1266        input_token_details={"cache_read": 50},1267        output_token_details={"reasoning": 10},1268    )126912701271def test__create_usage_metadata_responses_cache_write_tokens() -> None:1272    """Responses usage maps `cache_write_tokens` to the `cache_creation` key."""1273    response_usage_metadata = {1274        "input_tokens": 100,1275        "input_tokens_details": {"cached_tokens": 50, "cache_write_tokens": 25},1276        "output_tokens": 50,1277        "output_tokens_details": {"reasoning_tokens": 10},1278        "total_tokens": 150,1279    }1280    result = _create_usage_metadata_responses(response_usage_metadata)12811282    assert result == UsageMetadata(1283        output_tokens=50,1284        input_tokens=100,1285        total_tokens=150,1286        input_token_details={"cache_read": 50, "cache_creation": 25},1287        output_token_details={"reasoning": 10},1288    )128912901291def test__create_usage_metadata_responses_service_tier_cache_write_overlap() -> None:1292    """Tier counts exclude cache reads but not overlapping cache writes."""1293    response_usage_metadata = {1294        "input_tokens": 2304,1295        "input_tokens_details": {1296            "cached_tokens": 256,1297            "cache_write_tokens": 3072,1298        },1299        "output_tokens": 50,1300        "output_tokens_details": {"reasoning_tokens": 10},1301        "total_tokens": 2354,1302    }1303    result = _create_usage_metadata_responses(1304        response_usage_metadata, service_tier="flex"1305    )1306    assert result["input_token_details"] == {1307        "flex_cache_read": 256,1308        "flex_cache_creation": 3072,1309        "flex": 2048,1310    }1311    assert result["output_token_details"] == {1312        "flex_reasoning": 10,1313        "flex": 40,  # 50 - 10 (reasoning)1314    }131513161317def test__create_usage_metadata_responses_service_tier_without_detail_fields() -> None:1318    """Tier arithmetic tolerates missing cache/reasoning fields (no TypeError)."""1319    response_usage_metadata = {1320        "input_tokens": 100,1321        "input_tokens_details": None,1322        "output_tokens": 50,1323        "output_tokens_details": None,1324        "total_tokens": 150,1325    }1326    result = _create_usage_metadata_responses(1327        response_usage_metadata, service_tier="priority"1328    )1329    assert result["input_token_details"] == {"priority": 100}1330    assert result["output_token_details"] == {"priority": 50}133113321333def test__resize_caps_dimensions_preserving_ratio() -> None:1334    """Larger side capped at 2048 then smaller at 768 keeping aspect ratio."""1335    assert _resize(2048, 4096) == (768, 1536)1336    assert _resize(4096, 2048) == (1536, 768)133713381339def test__convert_to_openai_response_format() -> None:1340    # Test response formats that aren't tool-like.1341    response_format: dict = {1342        "type": "json_schema",1343        "json_schema": {1344            "name": "math_reasoning",1345            "schema": {1346                "type": "object",1347                "properties": {1348                    "steps": {1349                        "type": "array",1350                        "items": {1351                            "type": "object",1352                            "properties": {1353                                "explanation": {"type": "string"},1354                                "output": {"type": "string"},1355                            },1356                            "required": ["explanation", "output"],1357                            "additionalProperties": False,1358                        },1359                    },1360                    "final_answer": {"type": "string"},1361                },1362                "required": ["steps", "final_answer"],1363                "additionalProperties": False,1364            },1365            "strict": True,1366        },1367    }13681369    actual = _convert_to_openai_response_format(response_format)1370    assert actual == response_format13711372    actual = _convert_to_openai_response_format(response_format["json_schema"])1373    assert actual == response_format13741375    actual = _convert_to_openai_response_format(response_format, strict=True)1376    assert actual == response_format13771378    with pytest.raises(ValueError):1379        _convert_to_openai_response_format(response_format, strict=False)138013811382@pytest.mark.parametrize("method", ["function_calling", "json_schema"])1383@pytest.mark.parametrize("strict", [True, None])1384def test_structured_output_strict(1385    method: Literal["function_calling", "json_schema"], strict: bool | None1386) -> None:1387    """Test to verify structured output with strict=True."""13881389    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)13901391    class Joke(BaseModel):1392        """Joke to tell user."""13931394        setup: str = Field(description="question to set up a joke")1395        punchline: str = Field(description="answer to resolve the joke")13961397    llm.with_structured_output(Joke, method=method, strict=strict)1398    # Schema1399    llm.with_structured_output(Joke.model_json_schema(), method=method, strict=strict)140014011402def test_nested_structured_output_strict() -> None:1403    """Test to verify structured output with strict=True for nested object."""14041405    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)14061407    class SelfEvaluation(TypedDict):1408        score: int1409        text: str14101411    class JokeWithEvaluation(TypedDict):1412        """Joke to tell user."""14131414        setup: str1415        punchline: str1416        _evaluation: SelfEvaluation14171418    llm.with_structured_output(JokeWithEvaluation, method="json_schema")141914201421def test__get_request_payload() -> None:1422    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)1423    messages: list = [1424        SystemMessage("hello"),1425        SystemMessage("bye", additional_kwargs={"__openai_role__": "developer"}),1426        SystemMessage(content=[{"type": "text", "text": "hello!"}]),1427        {"role": "human", "content": "how are you"},1428        {"role": "user", "content": [{"type": "text", "text": "feeling today"}]},1429    ]1430    expected = {1431        "messages": [1432            {"role": "system", "content": "hello"},1433            {"role": "developer", "content": "bye"},1434            {"role": "system", "content": [{"type": "text", "text": "hello!"}]},1435            {"role": "user", "content": "how are you"},1436            {"role": "user", "content": [{"type": "text", "text": "feeling today"}]},1437        ],1438        "model": OPENAI_TEST_MODEL,1439        "stream": False,1440    }1441    payload = llm._get_request_payload(messages)1442    assert payload == expected14431444    # Test we coerce to developer role for o-series models1445    llm = ChatOpenAI(model="o3")1446    payload = llm._get_request_payload(messages)1447    expected = {1448        "messages": [1449            {"role": "developer", "content": "hello"},1450            {"role": "developer", "content": "bye"},1451            {"role": "developer", "content": [{"type": "text", "text": "hello!"}]},1452            {"role": "user", "content": "how are you"},1453            {"role": "user", "content": [{"type": "text", "text": "feeling today"}]},1454        ],1455        "model": "o3",1456        "stream": False,1457    }1458    assert payload == expected14591460    # Test we ignore reasoning blocks from other providers1461    reasoning_messages: list = [1462        {1463            "role": "user",1464            "content": [1465                {"type": "reasoning_content", "reasoning_content": "reasoning..."},1466                {"type": "text", "text": "reasoned response"},1467            ],1468        },1469        {1470            "role": "user",1471            "content": [1472                {"type": "thinking", "thinking": "thinking..."},1473                {"type": "text", "text": "thoughtful response"},1474            ],1475        },1476    ]1477    expected = {1478        "messages": [1479            {1480                "role": "user",1481                "content": [{"type": "text", "text": "reasoned response"}],1482            },1483            {1484                "role": "user",1485                "content": [{"type": "text", "text": "thoughtful response"}],1486            },1487        ],1488        "model": "o3",1489        "stream": False,1490    }1491    payload = llm._get_request_payload(reasoning_messages)1492    assert payload == expected149314941495def test_sanitize_chat_completions_text_blocks() -> None:1496    messages = [1497        ToolMessage(1498            content=[{"type": "text", "text": "foo", "id": "lc_abc123"}],1499            tool_call_id="def456",1500        ),1501    ]1502    payload = ChatOpenAI(model="gpt-5.2")._get_request_payload(messages)1503    assert payload["messages"] == [1504        {1505            "content": [{"type": "text", "text": "foo"}],1506            "role": "tool",1507            "tool_call_id": "def456",1508        }1509    ]151015111512def test_init_o1() -> None:1513    with warnings.catch_warnings(record=True) as record:1514        warnings.simplefilter("error")  # Treat warnings as errors1515        ChatOpenAI(model=OPENAI_TEST_MODEL, reasoning_effort="medium")15161517    assert len(record) == 0151815191520def test_init_minimal_reasoning_effort() -> None:1521    with warnings.catch_warnings(record=True) as record:1522        warnings.simplefilter("error")1523        ChatOpenAI(model="gpt-5", reasoning_effort="minimal")15241525    assert len(record) == 0152615271528@pytest.mark.parametrize("use_responses_api", [False, True])1529@pytest.mark.parametrize("use_max_completion_tokens", [True, False])1530def test_minimal_reasoning_effort_payload(1531    use_max_completion_tokens: bool, use_responses_api: bool1532) -> None:1533    """Test that minimal reasoning effort is included in request payload."""1534    if use_max_completion_tokens:1535        kwargs = {"max_completion_tokens": 100}1536    else:1537        kwargs = {"max_tokens": 100}15381539    init_kwargs: dict[str, Any] = {1540        "model": "gpt-5",1541        "reasoning_effort": "minimal",1542        "use_responses_api": use_responses_api,1543        **kwargs,1544    }15451546    llm = ChatOpenAI(**init_kwargs)15471548    messages = [1549        {"role": "developer", "content": "respond with just 'test'"},1550        {"role": "user", "content": "hello"},1551    ]15521553    payload = llm._get_request_payload(messages, stop=None)15541555    # When using responses API, reasoning_effort becomes reasoning.effort1556    if use_responses_api:1557        assert "reasoning" in payload1558        assert payload["reasoning"] == {"effort": "minimal"}1559        # For responses API, tokens param becomes max_output_tokens1560        assert payload["max_output_tokens"] == 1001561    else:1562        # For non-responses API, reasoning_effort remains as is1563        assert payload["reasoning_effort"] == "minimal"1564        if use_max_completion_tokens:1565            assert payload["max_completion_tokens"] == 1001566        else:1567            # max_tokens gets converted to max_completion_tokens in non-responses API1568            assert payload["max_completion_tokens"] == 100156915701571@pytest.mark.parametrize("via_invoke", [False, True])1572def test_responses_api_payload_excludes_stop(via_invoke: bool) -> None:1573    """The Responses API rejects `stop`, so it must be dropped from the payload.15741575    Covers `stop` supplied both at construction time and at invoke time, since1576    they reach the payload through different code paths.1577    """1578    if via_invoke:1579        llm = ChatOpenAI(model=OPENAI_TEST_MODEL, use_responses_api=True)1580        payload = llm._get_request_payload(1581            [HumanMessage(content="Hello")], stop=["END"]1582        )1583    else:1584        llm = ChatOpenAI(  # type: ignore[call-arg]1585            model=OPENAI_TEST_MODEL, stop=["END"], use_responses_api=True1586        )1587        payload = llm._get_request_payload([HumanMessage(content="Hello")])15881589    assert "stop" not in payload159015911592def test_chat_completions_payload_includes_stop() -> None:1593    """`stop` must be preserved for the Chat Completions API, which supports it.15941595    Guards against an over-broad change dropping `stop` outside the Responses API.1596    """1597    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, stop=["END"])  # type: ignore[call-arg]15981599    payload = llm._get_request_payload([HumanMessage(content="Hello")])16001601    assert payload["stop"] == ["END"]160216031604def test_output_version_compat() -> None:1605    llm = ChatOpenAI(model="gpt-5", output_version="responses/v1")1606    assert llm._use_responses_api({}) is True160716081609def test_convert_chunk_to_generation_chunk_v1_keeps_string_content() -> None:1610    """v1 streaming keeps content as '' (not []) and stamps output_version.16111612    Covers both the usage-only (empty-choices) chunk and a content-bearing1613    chunk carrying a tool-call delta; the latter pins the per-content-chunk1614    `output_version` propagation.1615    """1616    llm = ChatOpenAI(model="gpt-4o", output_version="v1")16171618    # Empty-choices chunk (usage-only)1619    empty_chunk: dict[str, Any] = {1620        "id": "chatcmpl-test",1621        "object": "chat.completion.chunk",1622        "created": 0,1623        "model": "gpt-4o",1624        "choices": [],1625        "usage": {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8},1626    }1627    gen = llm._convert_chunk_to_generation_chunk(empty_chunk, AIMessageChunk, None)1628    assert gen is not None1629    assert gen.message.content == ""  # NOT []1630    assert gen.message.response_metadata.get("output_version") == "v1"16311632    # Content-bearing chunk with tool_call delta1633    tool_chunk: dict[str, Any] = {1634        "id": "chatcmpl-test",1635        "object": "chat.completion.chunk",1636        "created": 0,1637        "model": "gpt-4o",1638        "choices": [1639            {1640                "index": 0,1641                "delta": {1642                    "role": "assistant",1643                    "content": "",1644                    "tool_calls": [1645                        {1646                            "index": 0,1647                            "id": "call_abc",1648                            "function": {"name": "get_weather", "arguments": ""},1649                        }1650                    ],1651                },1652                "logprobs": None,1653                "finish_reason": None,1654            }1655        ],1656        "usage": None,1657    }1658    gen = llm._convert_chunk_to_generation_chunk(tool_chunk, AIMessageChunk, None)1659    assert gen is not None1660    assert isinstance(gen.message.content, str)1661    assert gen.message.response_metadata.get("output_version") == "v1"1662    assert gen.message.response_metadata.get("model_provider") == "openai"166316641665def test_v1_streaming_tool_calls_in_content_blocks() -> None:1666    """End-to-end: streaming chunks with tool calls produce correct content_blocks."""1667    stream_chunks: list[dict[str, Any]] = [1668        # Initial empty-choices chunk1669        {1670            "id": "chatcmpl-test",1671            "object": "chat.completion.chunk",1672            "created": 0,1673            "model": "gpt-4o",1674            "choices": [1675                {1676                    "index": 0,1677                    "delta": {"role": "assistant", "content": ""},1678                    "logprobs": None,1679                    "finish_reason": None,1680                }1681            ],1682            "usage": None,1683        },1684        # Text token streamed before the tool call1685        {1686            "id": "chatcmpl-test",1687            "object": "chat.completion.chunk",1688            "created": 0,1689            "model": "gpt-4o",1690            "choices": [1691                {1692                    "index": 0,1693                    "delta": {"content": "Let me check the weather."},1694                    "logprobs": None,1695                    "finish_reason": None,1696                }1697            ],1698            "usage": None,1699        },1700        # Tool call start1701        {1702            "id": "chatcmpl-test",1703            "object": "chat.completion.chunk",1704            "created": 0,1705            "model": "gpt-4o",1706            "choices": [1707                {1708                    "index": 0,1709                    "delta": {1710                        "tool_calls": [1711                            {1712                                "index": 0,1713                                "id": "call_abc",1714                                "function": {1715                                    "name": "get_weather",1716                                    "arguments": '{"loc',1717                                },1718                            }1719                        ]1720                    },1721                    "logprobs": None,1722                    "finish_reason": None,1723                }1724            ],1725            "usage": None,1726        },1727        # Tool call args continuation1728        {1729            "id": "chatcmpl-test",1730            "object": "chat.completion.chunk",1731            "created": 0,1732            "model": "gpt-4o",1733            "choices": [1734                {1735                    "index": 0,1736                    "delta": {1737                        "tool_calls": [1738                            {1739                                "index": 0,1740                                "function": {"arguments": 'ation": "SF"}'},1741                            }1742                        ]1743                    },1744                    "logprobs": None,1745                    "finish_reason": None,1746                }1747            ],1748            "usage": None,1749        },1750        # Finish1751        {1752            "id": "chatcmpl-test",1753            "object": "chat.completion.chunk",1754            "created": 0,1755            "model": "gpt-4o",1756            "choices": [1757                {1758                    "index": 0,1759                    "delta": {},1760                    "logprobs": None,1761                    "finish_reason": "tool_calls",1762                }1763            ],1764            "usage": None,1765        },1766        # Usage chunk1767        {1768            "id": "chatcmpl-test",1769            "object": "chat.completion.chunk",1770            "created": 0,1771            "model": "gpt-4o",1772            "choices": [],1773            "usage": {1774                "prompt_tokens": 10,1775                "completion_tokens": 5,1776                "total_tokens": 15,1777            },1778        },1779    ]17801781    llm = ChatOpenAI(model="gpt-4o", output_version="v1")17821783    aggregated: AIMessageChunk | None = None1784    for raw_chunk in stream_chunks:1785        gen = llm._convert_chunk_to_generation_chunk(raw_chunk, AIMessageChunk, None)1786        if gen is None:1787            continue1788        chunk = cast(AIMessageChunk, gen.message)1789        aggregated = chunk if aggregated is None else aggregated + chunk17901791    assert aggregated is not None1792    # Tool calls should be present1793    assert len(aggregated.tool_call_chunks) == 11794    assert aggregated.tool_call_chunks[0]["name"] == "get_weather"17951796    # While still a chunk, content_blocks should include both the streamed text1797    # and the in-progress tool_call_chunk (text deltas must survive alongside1798    # tool calls through the merge).1799    blocks = aggregated.content_blocks1800    block_types = {b["type"] for b in blocks}1801    assert "tool_call_chunk" in block_types1802    assert "text" in block_types18031804    # Once finalized into a non-chunk AIMessage, the in-progress tool_call_chunk1805    # resolves to a normalized v1 `tool_call` block with parsed args. This is1806    # the cross-provider view downstream consumers code against.1807    final = message_chunk_to_message(aggregated)1808    final_blocks = final.content_blocks1809    assert {"type": "text", "text": "Let me check the weather."} in final_blocks1810    assert {1811        "type": "tool_call",1812        "name": "get_weather",1813        "args": {"location": "SF"},1814        "id": "call_abc",1815    } in final_blocks181618171818def test_verbosity_parameter_payload() -> None:1819    """Test verbosity parameter is included in request payload for Responses API."""1820    llm = ChatOpenAI(model="gpt-5", verbosity="high", use_responses_api=True)18211822    messages = [{"role": "user", "content": "hello"}]1823    payload = llm._get_request_payload(messages, stop=None)18241825    assert payload["text"]["verbosity"] == "high"182618271828def test_structured_output_legacy_model() -> None:1829    class Output(TypedDict):1830        """output."""18311832        foo: str18331834    with pytest.warns(match="Cannot use method='json_schema'"):1835        llm = ChatOpenAI(model="gpt-3-legacy").with_structured_output(Output)1836    # assert tool calling was used instead of json_schema1837    assert "tools" in llm.steps[0].kwargs  # type: ignore1838    assert "response_format" not in llm.steps[0].kwargs  # type: ignore183918401841def test_structured_outputs_parser() -> None:1842    parsed_response = GenerateUsername(name="alice", hair_color="black")1843    llm_output = ChatGeneration(1844        message=AIMessage(1845            content='{"name": "alice", "hair_color": "black"}',1846            additional_kwargs={"parsed": parsed_response},1847        )1848    )1849    output_parser = RunnableLambda(1850        partial(_oai_structured_outputs_parser, schema=GenerateUsername)1851    )1852    serialized = dumps(llm_output)1853    deserialized = loads(serialized, allowed_objects=[ChatGeneration, AIMessage])1854    assert isinstance(deserialized, ChatGeneration)1855    result = output_parser.invoke(cast(AIMessage, deserialized.message))1856    assert result == parsed_response185718581859def test_create_chat_result_avoids_parsed_model_dump_warning() -> None:1860    """Chat Completions structured output must not emit a serializer warning.18611862    Built via the SDK's own `parse_chat_completion` (the path1863    `client.beta.chat.completions.parse(...)` uses) so the test exercises the real1864    `ParsedChatCompletion` typing (openai/openai-python#2872).1865    """1866    from openai.lib._parsing._completions import parse_chat_completion1867    from openai.types.chat import ChatCompletion1868    from openai.types.chat.chat_completion import Choice1869    from openai.types.chat.chat_completion_message import ChatCompletionMessage18701871    class ModelOutput(BaseModel):1872        output: str18731874    raw_response = ChatCompletion(1875        id="chatcmpl-1",1876        object="chat.completion",1877        created=0,1878        model=OPENAI_TEST_MODEL,1879        choices=[1880            Choice(1881                index=0,1882                finish_reason="stop",1883                message=ChatCompletionMessage(1884                    role="assistant", content='{"output": "Paris"}'1885                ),1886            )1887        ],1888    )1889    response = parse_chat_completion(1890        response_format=ModelOutput,1891        input_tools=openai.omit,1892        chat_completion=raw_response,1893    )18941895    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)1896    with warnings.catch_warnings(record=True) as caught_warnings:1897        warnings.simplefilter("always")1898        result = llm._create_chat_result(response)18991900    warning_messages = [str(warning.message) for warning in caught_warnings]1901    assert not any(1902        "PydanticSerializationUnexpectedValue" in message1903        for message in warning_messages1904    )1905    assert result.generations[0].message.additional_kwargs["parsed"] == ModelOutput(1906        output="Paris"1907    )190819091910@pytest.mark.skipif(1911    (PYDANTIC_VERSION.major, PYDANTIC_VERSION.minor) < (2, 8),1912    reason=(1913        "Serializing the generic `ParsedResponse` raises a `MockValSer` TypeError on "1914        "pydantic<2.8, independent of the warning under test."1915    ),1916)1917def test__construct_lc_result_from_responses_api_avoids_parsed_dump_warning() -> None:1918    """Responses API structured output must not emit serializer warnings.19191920    With `use_responses_api=True` and structured output, the SDK returns a1921    `ParsedResponse` whose output items don't match their declared field/union types.1922    Dumping it for response metadata previously emitted a spurious1923    `PydanticSerializationUnexpectedValue` warning (openai/openai-python#2872).1924    """1925    from openai.lib._parsing._responses import parse_response19261927    class DocumentScoreResult(BaseModel):1928        reasoning: str1929        relevance_score: int19301931    # Build the object the way `client.responses.parse(text_format=...)` does, so the1932    # test exercises the real `ParsedResponse` typing rather than a hand-rolled mock.1933    raw_response = Response(1934        id="resp_123",1935        created_at=1234567890,1936        model=OPENAI_TEST_MODEL,1937        object="response",1938        parallel_tool_calls=True,1939        tools=[],1940        tool_choice="auto",1941        output=[1942            ResponseOutputMessage(1943                type="message",1944                id="msg_123",1945                status="completed",1946                role="assistant",1947                content=[1948                    ResponseOutputText(1949                        type="output_text",1950                        text='{"reasoning": "relevant", "relevance_score": 1}',1951                        annotations=[],1952                    )1953                ],1954            )1955        ],1956    )1957    parsed_response = parse_response(1958        text_format=DocumentScoreResult,1959        input_tools=openai.omit,1960        response=raw_response,1961    )19621963    with warnings.catch_warnings(record=True) as caught_warnings:1964        warnings.simplefilter("always")1965        result = _construct_lc_result_from_responses_api(1966            parsed_response, schema=DocumentScoreResult1967        )19681969    warning_messages = [str(warning.message) for warning in caught_warnings]1970    assert not any(1971        "PydanticSerializationUnexpectedValue" in message1972        for message in warning_messages1973    )1974    assert result.generations[0].message.additional_kwargs[1975        "parsed"1976    ] == DocumentScoreResult(reasoning="relevant", relevance_score=1)197719781979def test_structured_outputs_parser_valid_falsy_response() -> None:1980    class LunchBox(BaseModel):1981        sandwiches: list[str]19821983        def __len__(self) -> int:1984            return len(self.sandwiches)19851986    # prepare a valid *but falsy* response object, an empty LunchBox1987    parsed_response = LunchBox(sandwiches=[])1988    assert len(parsed_response) == 01989    llm_output = AIMessage(1990        content='{"sandwiches": []}', additional_kwargs={"parsed": parsed_response}1991    )1992    output_parser = RunnableLambda(1993        partial(_oai_structured_outputs_parser, schema=LunchBox)1994    )1995    result = output_parser.invoke(llm_output)1996    assert result == parsed_response199719981999def test__construct_lc_result_from_responses_api_error_handling() -> None:2000    """Test that errors in the response are properly raised."""

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.