libs/partners/openai/tests/unit_tests/chat_models/test_base.py PYTHON 5,524 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 5,524.
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 httpx214import openai15import pytest16from langchain_core.exceptions import (17    ContextOverflowError,18    ModelAPIError,19    ModelAuthenticationError,20    ModelConnectionError,21    ModelError,22    ModelInvalidRequestError,23    ModelNotFoundError,24    ModelPermissionDeniedError,25    ModelRateLimitError,26    ModelTimeoutError,27)28from langchain_core.load import dumps, loads29from langchain_core.messages import (30    AIMessage,31    AIMessageChunk,32    BaseMessage,33    FunctionMessage,34    HumanMessage,35    InvalidToolCall,36    SystemMessage,37    ToolCall,38    ToolMessage,39    message_chunk_to_message,40)41from langchain_core.messages import content as types42from langchain_core.messages.ai import UsageMetadata43from langchain_core.messages.block_translators.openai import (44    _convert_from_v03_ai_message,45)46from langchain_core.outputs import ChatGeneration, ChatResult47from langchain_core.runnables import RunnableLambda48from langchain_core.runnables.base import RunnableBinding, RunnableSequence49from langchain_core.tracers.base import BaseTracer50from langchain_core.tracers.schemas import Run51from langchain_core.utils._gateway import GATEWAY_METADATA_RESPONSE_KEY52from langchain_core.utils.pydantic import PYDANTIC_VERSION53from openai.types.responses import (54    ResponseApplyPatchToolCall,55    ResponseApplyPatchToolCallOutput,56    ResponseOutputMessage,57    ResponseReasoningItem,58    ResponseTextDeltaEvent,59)60from openai.types.responses.response import IncompleteDetails, Response61from openai.types.responses.response_apply_patch_tool_call import OperationCreateFile62from openai.types.responses.response_error import ResponseError63from openai.types.responses.response_file_search_tool_call import (64    ResponseFileSearchToolCall,65    Result,66)67from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall68from openai.types.responses.response_function_web_search import (69    ActionSearch,70    ResponseFunctionWebSearch,71)72from openai.types.responses.response_output_refusal import ResponseOutputRefusal73from openai.types.responses.response_output_text import ResponseOutputText74from openai.types.responses.response_reasoning_item import Summary75from openai.types.responses.response_usage import (76    InputTokensDetails,77    OutputTokensDetails,78    ResponseUsage,79)80from pydantic import BaseModel, Field, SecretStr81from typing_extensions import Self, TypedDict8283from langchain_openai import ChatOpenAI84from langchain_openai.chat_models._compat import (85    _FUNCTION_CALL_IDS_MAP_KEY,86    _convert_from_v1_to_chat_completions,87    _convert_from_v1_to_responses,88    _convert_to_v03_ai_message,89)90from langchain_openai.chat_models.base import (91    OpenAIRefusalError,92    _construct_lc_result_from_responses_api,93    _construct_responses_api_input,94    _convert_dict_to_message,95    _convert_message_to_dict,96    _convert_responses_chunk_to_generation_chunk,97    _convert_to_openai_response_format,98    _create_usage_metadata,99    _create_usage_metadata_responses,100    _format_message_content,101    _get_last_messages,102    _make_computer_call_output_from_message,103    _model_prefers_responses_api,104    _oai_structured_outputs_parser,105    _resize,106)107108OPENAI_TEST_MODEL = "gpt-5.5"109OPENAI_TEMPERATURE_CAPABLE_TEST_MODEL = "gpt-4o-mini"110111112def test_openai_model_param() -> None:113    llm = ChatOpenAI(model="foo")114    assert llm.model_name == "foo"115    assert llm.model == "foo"116    llm = ChatOpenAI(model_name="foo")  # type: ignore[call-arg]117    assert llm.model_name == "foo"118    assert llm.model == "foo"119120    llm = ChatOpenAI(max_tokens=10)  # type: ignore[call-arg]121    assert llm.max_tokens == 10122    llm = ChatOpenAI(max_completion_tokens=10)123    assert llm.max_tokens == 10124125126@pytest.mark.parametrize("async_api", [True, False])127def test_streaming_attribute_should_stream(async_api: bool) -> None:128    llm = ChatOpenAI(model="foo", streaming=True)129    assert llm._should_stream(async_api=async_api)130131132def test_openai_client_caching() -> None:133    """Test that the OpenAI client is cached."""134    llm1 = ChatOpenAI(model=OPENAI_TEST_MODEL)135    llm2 = ChatOpenAI(model=OPENAI_TEST_MODEL)136    assert llm1.root_client._client is llm2.root_client._client137138    llm3 = ChatOpenAI(model=OPENAI_TEST_MODEL, base_url="foo")139    assert llm1.root_client._client is not llm3.root_client._client140141    llm4 = ChatOpenAI(model=OPENAI_TEST_MODEL, timeout=None)142    assert llm1.root_client._client is llm4.root_client._client143144    llm5 = ChatOpenAI(model=OPENAI_TEST_MODEL, timeout=3)145    assert llm1.root_client._client is not llm5.root_client._client146147    llm6 = ChatOpenAI(148        model=OPENAI_TEST_MODEL, timeout=httpx.Timeout(timeout=60.0, connect=5.0)149    )150    assert llm1.root_client._client is not llm6.root_client._client151152    llm7 = ChatOpenAI(model=OPENAI_TEST_MODEL, timeout=(5, 1))153    assert llm1.root_client._client is not llm7.root_client._client154155156def test_profile() -> None:157    model = ChatOpenAI(model="gpt-5.2-pro")158    assert model.profile159    assert not model.profile["structured_output"]160161    model = ChatOpenAI(model="gpt-5")162    assert model.profile163    assert model.profile["structured_output"]164    assert model.profile["tool_calling"]165166    # Test overwriting a field167    model.profile["tool_calling"] = False168    assert not model.profile["tool_calling"]169170    # Test we didn't mutate171    model = ChatOpenAI(model="gpt-5")172    assert model.profile173    assert model.profile["tool_calling"]174175    # Test passing in profile176    model = ChatOpenAI(model="gpt-5", profile={"tool_calling": False})177    assert model.profile == {"tool_calling": False}178179    # Test overrides for gpt-5 input tokens180    model = ChatOpenAI(model="gpt-5")181    assert model.profile["max_input_tokens"] == 272_000182183184def test_gpt_5_reasoning_effort_levels() -> None:185    """GPT-5 and GPT-5.1 predate `xhigh`, and GPT-5 supports `minimal`."""186    for model_name in ("gpt-5", "gpt-5-mini", "gpt-5-nano"):187        model = ChatOpenAI(model=model_name)188        assert model.profile189        assert model.profile["reasoning_effort_levels"] == [190            "minimal",191            "low",192            "medium",193            "high",194        ]195196    model = ChatOpenAI(model="gpt-5.1")197    assert model.profile198    assert model.profile["reasoning_effort_levels"] == ["none", "low", "medium", "high"]199200201def test_gpt_5_3_chat_latest_profile_has_no_reasoning_effort() -> None:202    model = ChatOpenAI(model="gpt-5.3-chat-latest")203204    assert model.profile205    assert model.profile["reasoning_output"] is False206    assert "reasoning_effort_levels" not in model.profile207208209def test_gpt_6_astra_reasoning_effort_levels() -> None:210    model = ChatOpenAI(model="gpt-6-astra")211212    assert model.profile213    assert model.profile["reasoning_effort_levels"] == [214        "low",215        "medium",216        "high",217        "xhigh",218        "max",219    ]220    assert "reasoning_effort_default" not in model.profile221222223def test_function_message_dict_to_function_message() -> None:224    content = json.dumps({"result": "Example #1"})225    name = "test_function"226    result = _convert_dict_to_message(227        {"role": "function", "name": name, "content": content}228    )229    assert isinstance(result, FunctionMessage)230    assert result.name == name231    assert result.content == content232233234def test__convert_dict_to_message_human() -> None:235    message = {"role": "user", "content": "foo"}236    result = _convert_dict_to_message(message)237    expected_output = HumanMessage(content="foo")238    assert result == expected_output239    assert _convert_message_to_dict(expected_output) == message240241242def test__convert_dict_to_message_human_with_name() -> None:243    message = {"role": "user", "content": "foo", "name": "test"}244    result = _convert_dict_to_message(message)245    expected_output = HumanMessage(content="foo", name="test")246    assert result == expected_output247    assert _convert_message_to_dict(expected_output) == message248249250def test__convert_dict_to_message_ai() -> None:251    message = {"role": "assistant", "content": "foo"}252    result = _convert_dict_to_message(message)253    expected_output = AIMessage(content="foo")254    assert result == expected_output255    assert _convert_message_to_dict(expected_output) == message256257258def test__convert_dict_to_message_ai_with_name() -> None:259    message = {"role": "assistant", "content": "foo", "name": "test"}260    result = _convert_dict_to_message(message)261    expected_output = AIMessage(content="foo", name="test")262    assert result == expected_output263    assert _convert_message_to_dict(expected_output) == message264265266def test__convert_dict_to_message_system() -> None:267    message = {"role": "system", "content": "foo"}268    result = _convert_dict_to_message(message)269    expected_output = SystemMessage(content="foo")270    assert result == expected_output271    assert _convert_message_to_dict(expected_output) == message272273274def test__convert_dict_to_message_developer() -> None:275    message = {"role": "developer", "content": "foo"}276    result = _convert_dict_to_message(message)277    expected_output = SystemMessage(278        content="foo", additional_kwargs={"__openai_role__": "developer"}279    )280    assert result == expected_output281    assert _convert_message_to_dict(expected_output) == message282283284def test__convert_dict_to_message_system_with_name() -> None:285    message = {"role": "system", "content": "foo", "name": "test"}286    result = _convert_dict_to_message(message)287    expected_output = SystemMessage(content="foo", name="test")288    assert result == expected_output289    assert _convert_message_to_dict(expected_output) == message290291292def test__convert_dict_to_message_tool() -> None:293    message = {"role": "tool", "content": "foo", "tool_call_id": "bar"}294    result = _convert_dict_to_message(message)295    expected_output = ToolMessage(content="foo", tool_call_id="bar")296    assert result == expected_output297    assert _convert_message_to_dict(expected_output) == message298299300def test__convert_dict_to_message_tool_call() -> None:301    raw_tool_call = {302        "id": "call_wm0JY6CdwOMZ4eTxHWUThDNz",303        "function": {304            "arguments": '{"name": "Sally", "hair_color": "green"}',305            "name": "GenerateUsername",306        },307        "type": "function",308    }309    message = {"role": "assistant", "content": None, "tool_calls": [raw_tool_call]}310    result = _convert_dict_to_message(message)311    expected_output = AIMessage(312        content="",313        tool_calls=[314            ToolCall(315                name="GenerateUsername",316                args={"name": "Sally", "hair_color": "green"},317                id="call_wm0JY6CdwOMZ4eTxHWUThDNz",318                type="tool_call",319            )320        ],321    )322    assert result == expected_output323    assert _convert_message_to_dict(expected_output) == message324325    # Test malformed tool call326    raw_tool_calls: list = [327        {328            "id": "call_wm0JY6CdwOMZ4eTxHWUThDNz",329            "function": {"arguments": "oops", "name": "GenerateUsername"},330            "type": "function",331        },332        {333            "id": "call_abc123",334            "function": {335                "arguments": '{"name": "Sally", "hair_color": "green"}',336                "name": "GenerateUsername",337            },338            "type": "function",339        },340    ]341    raw_tool_calls = sorted(raw_tool_calls, key=lambda x: x["id"])342    message = {"role": "assistant", "content": None, "tool_calls": raw_tool_calls}343    result = _convert_dict_to_message(message)344    expected_output = AIMessage(345        content="",346        invalid_tool_calls=[347            InvalidToolCall(348                name="GenerateUsername",349                args="oops",350                id="call_wm0JY6CdwOMZ4eTxHWUThDNz",351                error=(352                    "Function GenerateUsername arguments:\n\noops\n\nare not "353                    "valid JSON. Received JSONDecodeError Expecting value: line 1 "354                    "column 1 (char 0)\nFor troubleshooting, visit: https://docs"355                    ".langchain.com/oss/python/langchain/errors/OUTPUT_PARSING_FAILURE "356                ),357                type="invalid_tool_call",358            )359        ],360        tool_calls=[361            ToolCall(362                name="GenerateUsername",363                args={"name": "Sally", "hair_color": "green"},364                id="call_abc123",365                type="tool_call",366            )367        ],368    )369    assert result == expected_output370    reverted_message_dict = _convert_message_to_dict(expected_output)371    reverted_message_dict["tool_calls"] = sorted(372        reverted_message_dict["tool_calls"], key=lambda x: x["id"]373    )374    assert reverted_message_dict == message375376377class MockAsyncContextManager:378    def __init__(self, chunk_list: list) -> None:379        self.current_chunk = 0380        self.chunk_list = chunk_list381        self.chunk_num = len(chunk_list)382383    async def __aenter__(self) -> Self:384        return self385386    async def __aexit__(387        self,388        exc_type: type[BaseException] | None,389        exc: BaseException | None,390        tb: TracebackType | None,391    ) -> None:392        pass393394    def __aiter__(self) -> MockAsyncContextManager:395        return self396397    async def __anext__(self) -> dict:398        if self.current_chunk < self.chunk_num:399            chunk = self.chunk_list[self.current_chunk]400            self.current_chunk += 1401            return chunk402        raise StopAsyncIteration403404405class MockSyncContextManager:406    def __init__(self, chunk_list: list) -> None:407        self.current_chunk = 0408        self.chunk_list = chunk_list409        self.chunk_num = len(chunk_list)410411    def __enter__(self) -> Self:412        return self413414    def __exit__(415        self,416        exc_type: type[BaseException] | None,417        exc: BaseException | None,418        tb: TracebackType | None,419    ) -> None:420        pass421422    def __iter__(self) -> MockSyncContextManager:423        return self424425    def __next__(self) -> dict:426        if self.current_chunk < self.chunk_num:427            chunk = self.chunk_list[self.current_chunk]428            self.current_chunk += 1429            return chunk430        raise StopIteration431432433GLM4_STREAM_META = """{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u4eba\u5de5\u667a\u80fd"}}]}434{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u52a9\u624b"}}]}435{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":","}}]}436{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u4f60\u53ef\u4ee5"}}]}437{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u53eb\u6211"}}]}438{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"AI"}}]}439{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u52a9\u624b"}}]}440{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"。"}}]}441{"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}}442[DONE]"""  # noqa: E501443444445@pytest.fixture446def mock_glm4_completion() -> list:447    list_chunk_data = GLM4_STREAM_META.split("\n")448    result_list = []449    for msg in list_chunk_data:450        if msg != "[DONE]":451            result_list.append(json.loads(msg))452453    return result_list454455456async def test_glm4_astream(mock_glm4_completion: list) -> None:457    llm_name = "glm-4"458    llm = ChatOpenAI(model=llm_name, stream_usage=True)459    mock_client = AsyncMock()460461    async def mock_create(*args: Any, **kwargs: Any) -> MockAsyncContextManager:462        return MockAsyncContextManager(mock_glm4_completion)463464    mock_client.create = mock_create465    usage_chunk = mock_glm4_completion[-1]466467    usage_metadata: UsageMetadata | None = None468    with patch.object(llm, "async_client", mock_client):469        async for chunk in llm.astream("你的名字叫什么?只回答名字"):470            assert isinstance(chunk, AIMessageChunk)471            if chunk.usage_metadata is not None:472                usage_metadata = chunk.usage_metadata473474    assert usage_metadata is not None475476    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]477    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]478    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]479480481def test_glm4_stream(mock_glm4_completion: list) -> None:482    llm_name = "glm-4"483    llm = ChatOpenAI(model=llm_name, stream_usage=True)484    mock_client = MagicMock()485486    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:487        return MockSyncContextManager(mock_glm4_completion)488489    mock_client.create = mock_create490    usage_chunk = mock_glm4_completion[-1]491492    usage_metadata: UsageMetadata | None = None493    with patch.object(llm, "client", mock_client):494        for chunk in llm.stream("你的名字叫什么?只回答名字"):495            assert isinstance(chunk, AIMessageChunk)496            if chunk.usage_metadata is not None:497                usage_metadata = chunk.usage_metadata498499    assert usage_metadata is not None500501    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]502    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]503    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]504505506DEEPSEEK_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}507{"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}508{"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}509{"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}510{"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}511{"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}512{"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}513{"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}514{"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}515{"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}516{"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}517{"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}518{"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}519{"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}520{"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}521{"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}522{"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}}523[DONE]"""  # noqa: E501524525526@pytest.fixture527def mock_deepseek_completion() -> list[dict]:528    list_chunk_data = DEEPSEEK_STREAM_DATA.split("\n")529    result_list = []530    for msg in list_chunk_data:531        if msg != "[DONE]":532            result_list.append(json.loads(msg))533534    return result_list535536537async def test_deepseek_astream(mock_deepseek_completion: list) -> None:538    llm_name = "deepseek-chat"539    llm = ChatOpenAI(model=llm_name, stream_usage=True)540    mock_client = AsyncMock()541542    async def mock_create(*args: Any, **kwargs: Any) -> MockAsyncContextManager:543        return MockAsyncContextManager(mock_deepseek_completion)544545    mock_client.create = mock_create546    usage_chunk = mock_deepseek_completion[-1]547    usage_metadata: UsageMetadata | None = None548    with patch.object(llm, "async_client", mock_client):549        async for chunk in llm.astream("你的名字叫什么?只回答名字"):550            assert isinstance(chunk, AIMessageChunk)551            if chunk.usage_metadata is not None:552                usage_metadata = chunk.usage_metadata553554    assert usage_metadata is not None555556    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]557    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]558    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]559560561def test_deepseek_stream(mock_deepseek_completion: list) -> None:562    llm_name = "deepseek-chat"563    llm = ChatOpenAI(model=llm_name, stream_usage=True)564    mock_client = MagicMock()565566    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:567        return MockSyncContextManager(mock_deepseek_completion)568569    mock_client.create = mock_create570    usage_chunk = mock_deepseek_completion[-1]571    usage_metadata: UsageMetadata | None = None572    with patch.object(llm, "client", mock_client):573        for chunk in llm.stream("你的名字叫什么?只回答名字"):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"]583584585OPENAI_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}586{"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}587{"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}588{"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}589{"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}590{"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}}591[DONE]"""  # noqa: E501592593594@pytest.fixture595def mock_openai_completion() -> list[dict]:596    list_chunk_data = OPENAI_STREAM_DATA.split("\n")597    result_list = []598    for msg in list_chunk_data:599        if msg != "[DONE]":600            result_list.append(json.loads(msg))601602    return result_list603604605async def test_openai_astream(mock_openai_completion: list) -> None:606    llm_name = OPENAI_TEST_MODEL607    llm = ChatOpenAI(model=llm_name, stream_usage=True)608    assert llm.stream_usage609    mock_client = AsyncMock()610611    async def mock_create(*args: Any, **kwargs: Any) -> MockAsyncContextManager:612        return MockAsyncContextManager(mock_openai_completion)613614    mock_client.create = mock_create615    usage_chunk = mock_openai_completion[-1]616    usage_metadata: UsageMetadata | None = None617    with patch.object(llm, "async_client", mock_client):618        async for chunk in llm.astream("你的名字叫什么?只回答名字"):619            assert isinstance(chunk, AIMessageChunk)620            if chunk.usage_metadata is not None:621                usage_metadata = chunk.usage_metadata622623    assert usage_metadata is not None624625    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]626    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]627    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]628629630def test_openai_stream(mock_openai_completion: list) -> None:631    llm_name = OPENAI_TEST_MODEL632    llm = ChatOpenAI(model=llm_name, stream_usage=True)633    assert llm.stream_usage634    mock_client = MagicMock()635636    call_kwargs = []637638    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:639        call_kwargs.append(kwargs)640        return MockSyncContextManager(mock_openai_completion)641642    mock_client.create = mock_create643    usage_chunk = mock_openai_completion[-1]644    usage_metadata: UsageMetadata | None = None645    with patch.object(llm, "client", mock_client):646        for chunk in llm.stream("你的名字叫什么?只回答名字"):647            assert isinstance(chunk, AIMessageChunk)648            if chunk.usage_metadata is not None:649                usage_metadata = chunk.usage_metadata650651    assert call_kwargs[-1]["stream_options"] == {"include_usage": True}652    assert usage_metadata is not None653    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]654    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]655    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]656657    # Verify no streaming outside of default base URL or clients658    for param, value in {659        "stream_usage": False,660        "openai_proxy": "http://localhost:7890",661        "openai_api_base": "https://example.com/v1",662        "base_url": "https://example.com/v1",663        "client": mock_client,664        "root_client": mock_client,665        "async_client": mock_client,666        "root_async_client": mock_client,667        "http_client": httpx.Client(),668        "http_async_client": httpx.AsyncClient(),669    }.items():670        llm = ChatOpenAI(model=llm_name, **{param: value})  # type: ignore[arg-type]671        assert not llm.stream_usage672        with patch.object(llm, "client", mock_client):673            _ = list(llm.stream("..."))674        assert "stream_options" not in call_kwargs[-1]675676677def test_openai_stream_events_v3_lifecycle(mock_openai_completion: list) -> None:678    """`stream_events(version="v3")` on chat completions emits a valid lifecycle."""679    from langchain_tests.utils.stream_lifecycle import assert_valid_event_stream680681    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)682    mock_client = MagicMock()683684    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:685        return MockSyncContextManager(mock_openai_completion)686687    mock_client.create = mock_create688    with patch.object(llm, "client", mock_client):689        events = list(llm.stream_events("你的名字叫什么?只回答名字", version="v3"))690691    assert_valid_event_stream(events)692    # At minimum, a text block with the accumulated answer.693    finishes = [e for e in events if e["event"] == "content-block-finish"]694    assert len(finishes) >= 1695    text_finishes = [f for f in finishes if f["content"]["type"] == "text"]696    assert len(text_finishes) == 1697698699@pytest.fixture700def mock_completion() -> dict:701    return {702        "id": "chatcmpl-7fcZavknQda3SQ",703        "object": "chat.completion",704        "created": 1689989000,705        "model": OPENAI_TEST_MODEL,706        "choices": [707            {708                "index": 0,709                "message": {"role": "assistant", "content": "Bar Baz", "name": "Erick"},710                "finish_reason": "stop",711            }712        ],713    }714715716@pytest.fixture717def mock_client(mock_completion: dict) -> MagicMock:718    rtn = MagicMock()719720    mock_create = MagicMock()721722    mock_resp = MagicMock()723    mock_resp.headers = {"content-type": "application/json"}724    mock_resp.parse.return_value = mock_completion725    mock_create.return_value = mock_resp726727    rtn.with_raw_response.create = mock_create728    rtn.create.return_value = mock_completion729    return rtn730731732@pytest.fixture733def mock_async_client(mock_completion: dict) -> AsyncMock:734    rtn = AsyncMock()735736    mock_create = AsyncMock()737    mock_resp = MagicMock()738    mock_resp.parse.return_value = mock_completion739    mock_create.return_value = mock_resp740741    rtn.with_raw_response.create = mock_create742    rtn.create.return_value = mock_completion743    return rtn744745746def test_openai_invoke(mock_client: MagicMock) -> None:747    llm = ChatOpenAI()748749    with patch.object(llm, "client", mock_client):750        res = llm.invoke("bar")751        assert res.content == "Bar Baz"752753        # headers are not in response_metadata if include_response_headers not set754        assert "headers" not in res.response_metadata755    assert mock_client.with_raw_response.create.called756757758async def test_openai_ainvoke(mock_async_client: AsyncMock) -> None:759    llm = ChatOpenAI()760761    with patch.object(llm, "async_client", mock_async_client):762        res = await llm.ainvoke("bar")763        assert res.content == "Bar Baz"764765        # headers are not in response_metadata if include_response_headers not set766        assert "headers" not in res.response_metadata767    assert mock_async_client.with_raw_response.create.called768769770class _GatewayMetadataTracer(BaseTracer):771    """Captures gateway metadata promoted onto completed LLM runs."""772773    def __init__(self) -> None:774        super().__init__()775        self.gateway_metadata: dict | None = None776777    def _persist_run(self, run: Run) -> None:778        """No-op; runs are inspected as they complete."""779780    def _on_llm_end(self, run: Run) -> None:781        metadata = run.extra.get("metadata", {})782        if "ls_gateway_info" in metadata:783            self.gateway_metadata = metadata["ls_gateway_info"]784785786_GATEWAY_METADATA_HEADERS = httpx2.Headers(787    {"x-langsmith-gateway-metadata": '{"provider": "openai"}'}788)789790_RESPONSES_API_COMPLETION = Response(791    id="resp_123",792    created_at=1234567890,793    model=OPENAI_TEST_MODEL,794    object="response",795    parallel_tool_calls=True,796    tools=[],797    tool_choice="auto",798    output=[799        ResponseOutputMessage(800            type="message",801            id="msg_123",802            content=[803                ResponseOutputText(type="output_text", text="Bar Baz", annotations=[])804            ],805            role="assistant",806            status="completed",807        )808    ],809)810811_RESPONSES_API_STREAM = [812    ResponseTextDeltaEvent(813        content_index=0,814        delta="Bar Baz",815        item_id="msg_123",816        output_index=0,817        sequence_number=0,818        logprobs=[],819        type="response.output_text.delta",820    ),821]822823824@pytest.mark.parametrize("use_responses_api", [False, True])825def test_openai_invoke_surfaces_gateway_metadata(826    mock_completion: dict, *, use_responses_api: bool827) -> None:828    """Gateway metadata header is surfaced on `generation_info`, not the message."""829    llm = ChatOpenAI(use_responses_api=use_responses_api)830    mock_client = MagicMock()831    mock_resp = MagicMock()832    mock_resp.headers = _GATEWAY_METADATA_HEADERS833    if use_responses_api:834        mock_resp.parse.return_value = _RESPONSES_API_COMPLETION835        mock_client.responses.with_raw_response.create.return_value = mock_resp836        client_attr = "root_client"837    else:838        mock_resp.parse.return_value = mock_completion839        mock_client.with_raw_response.create.return_value = mock_resp840        client_attr = "client"841842    tracer = _GatewayMetadataTracer()843    with patch.object(llm, client_attr, mock_client):844        res = llm.invoke("bar", config={"callbacks": [tracer]})845846    # Gateway metadata reaches the tracer via `generation_info`...847    assert tracer.gateway_metadata == {"provider": "openai"}848    # ...but is kept off the user-facing message `response_metadata`.849    assert GATEWAY_METADATA_RESPONSE_KEY not in res.response_metadata850851852@pytest.mark.parametrize("use_responses_api", [False, True])853def test_openai_stream_surfaces_gateway_metadata(854    mock_openai_completion: list, *, use_responses_api: bool855) -> None:856    """Gateway metadata reaches the tracer for a gateway-routed stream."""857    # A LangSmith API key signals gateway routing, so streaming fetches raw858    # headers.859    llm = ChatOpenAI(860        model=OPENAI_TEST_MODEL,861        api_key="lsv2_pt_example",  # type: ignore[arg-type]862        use_responses_api=use_responses_api,863    )864    mock_client = MagicMock()865    mock_resp = MagicMock()866    mock_resp.headers = _GATEWAY_METADATA_HEADERS867    if use_responses_api:868        mock_resp.parse.return_value = MockSyncContextManager(_RESPONSES_API_STREAM)869        mock_client.with_raw_response.responses.create.return_value = mock_resp870        mock_client.responses.create.return_value = MockSyncContextManager(871            _RESPONSES_API_STREAM872        )873        client_attr = "root_client"874    else:875        mock_resp.parse.return_value = MockSyncContextManager(mock_openai_completion)876        mock_client.with_raw_response.create.return_value = mock_resp877        client_attr = "client"878879    tracer = _GatewayMetadataTracer()880    with patch.object(llm, client_attr, mock_client):881        for chunk in llm.stream("what is your name?", config={"callbacks": [tracer]}):882            # Gateway metadata is kept off the user-facing chunk metadata.883            assert GATEWAY_METADATA_RESPONSE_KEY not in chunk.response_metadata884885    assert tracer.gateway_metadata == {"provider": "openai"}886887888@pytest.mark.parametrize("use_responses_api", [False, True])889async def test_openai_astream_surfaces_gateway_metadata(890    mock_openai_completion: list, *, use_responses_api: bool891) -> None:892    """Gateway metadata reaches the tracer for a gateway-routed async stream."""893    llm = ChatOpenAI(894        model=OPENAI_TEST_MODEL,895        api_key="lsv2_pt_example",  # type: ignore[arg-type]896        use_responses_api=use_responses_api,897    )898    mock_client = AsyncMock()899    mock_resp = MagicMock()900    mock_resp.headers = _GATEWAY_METADATA_HEADERS901    if use_responses_api:902        mock_resp.parse.return_value = MockAsyncContextManager(_RESPONSES_API_STREAM)903        mock_client.with_raw_response.responses.create.return_value = mock_resp904        mock_client.responses.create.return_value = MockAsyncContextManager(905            _RESPONSES_API_STREAM906        )907        client_attr = "root_async_client"908    else:909        mock_resp.parse.return_value = MockAsyncContextManager(mock_openai_completion)910        mock_client.with_raw_response.create.return_value = mock_resp911        client_attr = "async_client"912913    tracer = _GatewayMetadataTracer()914    with patch.object(llm, client_attr, mock_client):915        async for chunk in llm.astream(916            "what is your name?", config={"callbacks": [tracer]}917        ):918            # Gateway metadata is kept off the user-facing chunk metadata.919            assert GATEWAY_METADATA_RESPONSE_KEY not in chunk.response_metadata920921    assert tracer.gateway_metadata == {"provider": "openai"}922923924@pytest.mark.parametrize(925    "model",926    [927        OPENAI_TEST_MODEL,928        "gpt-5-nano",929        "o3",930        "gpt-5.2",931    ],932)933def test__get_encoding_model(model: str) -> None:934    ChatOpenAI(model=model)._get_encoding_model()935936937def test_openai_invoke_name(mock_client: MagicMock) -> None:938    llm = ChatOpenAI()939940    with patch.object(llm, "client", mock_client):941        messages = [HumanMessage(content="Foo", name="Katie")]942        res = llm.invoke(messages)943        call_args, call_kwargs = mock_client.with_raw_response.create.call_args944        assert len(call_args) == 0  # no positional args945        call_messages = call_kwargs["messages"]946        assert len(call_messages) == 1947        assert call_messages[0]["role"] == "user"948        assert call_messages[0]["content"] == "Foo"949        assert call_messages[0]["name"] == "Katie"950951        # check return type has name952        assert res.content == "Bar Baz"953        assert res.name == "Erick"954955956def test_function_calls_with_tool_calls(mock_client: MagicMock) -> None:957    # Test that we ignore function calls if tool_calls are present958    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)959    tool_call_message = AIMessage(960        content="",961        additional_kwargs={962            "function_call": {963                "name": "get_weather",964                "arguments": '{"location": "Boston"}',965            }966        },967        tool_calls=[968            {969                "name": "get_weather",970                "args": {"location": "Boston"},971                "id": "abc123",972                "type": "tool_call",973            }974        ],975    )976    messages = [977        HumanMessage("What's the weather in Boston?"),978        tool_call_message,979        ToolMessage(content="It's sunny.", name="get_weather", tool_call_id="abc123"),980    ]981    with patch.object(llm, "client", mock_client):982        _ = llm.invoke(messages)983        _, call_kwargs = mock_client.with_raw_response.create.call_args984        call_messages = call_kwargs["messages"]985        tool_call_message_payload = call_messages[1]986        assert "tool_calls" in tool_call_message_payload987        assert "function_call" not in tool_call_message_payload988989    # Test we don't ignore function calls if tool_calls are not present990    cast(AIMessage, messages[1]).tool_calls = []991    with patch.object(llm, "client", mock_client):992        _ = llm.invoke(messages)993        _, call_kwargs = mock_client.with_raw_response.create.call_args994        call_messages = call_kwargs["messages"]995        tool_call_message_payload = call_messages[1]996        assert "function_call" in tool_call_message_payload997        assert "tool_calls" not in tool_call_message_payload9989991000def test_custom_token_counting() -> None:1001    def token_encoder(text: str) -> list[int]:1002        return [1, 2, 3]10031004    llm = ChatOpenAI(custom_get_token_ids=token_encoder)1005    assert llm.get_token_ids("foo") == [1, 2, 3]100610071008def test_format_message_content() -> None:1009    content: Any = "hello"1010    assert content == _format_message_content(content)10111012    content = None1013    assert content == _format_message_content(content)10141015    content = []1016    assert content == _format_message_content(content)10171018    content = [1019        {"type": "text", "text": "What is in this image?"},1020        {"type": "image_url", "image_url": {"url": "url.com"}},1021    ]1022    assert content == _format_message_content(content)10231024    content = [1025        {"type": "text", "text": "hello"},1026        {1027            "type": "tool_use",1028            "id": "toolu_01A09q90qw90lq917835lq9",1029            "name": "get_weather",1030            "input": {"location": "San Francisco, CA", "unit": "celsius"},1031        },1032    ]1033    assert _format_message_content(content) == [{"type": "text", "text": "hello"}]10341035    # Standard multi-modal inputs1036    contents = [1037        {"type": "image", "source_type": "url", "url": "https://..."},  # v01038        {"type": "image", "url": "https://..."},  # v11039    ]1040    expected = [{"type": "image_url", "image_url": {"url": "https://..."}}]1041    for content in contents:1042        assert expected == _format_message_content([content])10431044    contents = [1045        {1046            "type": "image",1047            "source_type": "base64",1048            "data": "<base64 data>",1049            "mime_type": "image/png",1050        },1051        {"type": "image", "base64": "<base64 data>", "mime_type": "image/png"},1052    ]1053    expected = [1054        {1055            "type": "image_url",1056            "image_url": {"url": "data:image/png;base64,<base64 data>"},1057        }1058    ]1059    for content in contents:1060        assert expected == _format_message_content([content])10611062    contents = [1063        {1064            "type": "file",1065            "source_type": "base64",1066            "data": "<base64 data>",1067            "mime_type": "application/pdf",1068            "filename": "my_file",1069        },1070        {1071            "type": "file",1072            "base64": "<base64 data>",1073            "mime_type": "application/pdf",1074            "filename": "my_file",1075        },1076    ]1077    expected = [1078        {1079            "type": "file",1080            "file": {1081                "filename": "my_file",1082                "file_data": "data:application/pdf;base64,<base64 data>",1083            },1084        }1085    ]1086    for content in contents:1087        assert expected == _format_message_content([content])10881089    # Test warn if PDF is missing a filename and that we add a default filename1090    pdf_block = {1091        "type": "file",1092        "base64": "<base64 data>",1093        "mime_type": "application/pdf",1094    }1095    expected = [1096        {1097            "type": "file",1098            "file": {1099                "file_data": "data:application/pdf;base64,<base64 data>",1100                "filename": "LC_AUTOGENERATED",1101            },1102        }1103    ]1104    with pytest.warns(match="filename"):1105        assert expected == _format_message_content([pdf_block])11061107    contents = [1108        {"type": "file", "source_type": "id", "id": "file-abc123"},1109        {"type": "file", "file_id": "file-abc123"},1110    ]1111    expected = [{"type": "file", "file": {"file_id": "file-abc123"}}]1112    for content in contents:1113        assert expected == _format_message_content([content])111411151116class GenerateUsername(BaseModel):1117    "Get a username based on someone's name and hair color."11181119    name: str1120    hair_color: str112111221123class MakeASandwich(BaseModel):1124    "Make a sandwich given a list of ingredients."11251126    bread_type: str1127    cheese_type: str1128    condiments: list[str]1129    vegetables: list[str]113011311132@pytest.mark.parametrize(1133    "tool_choice",1134    [1135        "any",1136        "none",1137        "auto",1138        "required",1139        "GenerateUsername",1140        {"type": "function", "function": {"name": "MakeASandwich"}},1141        False,1142        None,1143    ],1144)1145@pytest.mark.parametrize("strict", [True, False, None])1146def test_bind_tools_tool_choice(tool_choice: Any, strict: bool | None) -> None:1147    """Test passing in manually construct tool call message."""1148    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)1149    llm.bind_tools(1150        tools=[GenerateUsername, MakeASandwich], tool_choice=tool_choice, strict=strict1151    )115211531154def test_bind_tools_response_format_defaults_strict() -> None:1155    """Test that strict defaults to True when response_format is provided."""1156    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)1157    bound = llm.bind_tools(1158        tools=[GenerateUsername],1159        response_format=MakeASandwich,1160    )1161    tools = bound.kwargs["tools"]  # type: ignore[attr-defined]1162    assert tools[0]["function"]["strict"] is True116311641165def test_bind_tools_response_format_respects_strict_false() -> None:1166    """Test that strict=False is respected even when response_format is provided."""1167    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)1168    bound = llm.bind_tools(1169        tools=[GenerateUsername],1170        response_format=MakeASandwich,1171        strict=False,1172    )1173    tools = bound.kwargs["tools"]  # type: ignore[attr-defined]1174    assert tools[0]["function"]["strict"] is False117511761177def test_bind_tools_no_response_format_keeps_strict_none() -> None:1178    """Test that strict stays None when response_format is not provided."""1179    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)1180    bound = llm.bind_tools(tools=[GenerateUsername])1181    tools = bound.kwargs["tools"]  # type: ignore[attr-defined]1182    assert "strict" not in tools[0]["function"]118311841185@pytest.mark.parametrize(1186    "schema", [GenerateUsername, GenerateUsername.model_json_schema()]1187)1188@pytest.mark.parametrize("method", ["json_schema", "function_calling", "json_mode"])1189@pytest.mark.parametrize("include_raw", [True, False])1190@pytest.mark.parametrize("strict", [True, False, None])1191def test_with_structured_output(1192    schema: type | dict[str, Any] | None,1193    method: Literal["function_calling", "json_mode", "json_schema"],1194    include_raw: bool,1195    strict: bool | None,1196) -> None:1197    """Test passing in manually construct tool call message."""1198    if method == "json_mode":1199        strict = None1200    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)1201    llm.with_structured_output(1202        schema, method=method, strict=strict, include_raw=include_raw1203    )120412051206def test_get_num_tokens_from_messages() -> None:1207    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)1208    messages = [1209        SystemMessage("you're a good assistant"),1210        HumanMessage("how are you"),1211        HumanMessage(1212            [1213                {"type": "text", "text": "what's in this image"},1214                {"type": "image_url", "image_url": {"url": "https://foobar.com"}},1215                {1216                    "type": "image_url",1217                    "image_url": {"url": "https://foobar.com", "detail": "low"},1218                },1219            ]1220        ),1221        AIMessage("a nice bird"),1222        AIMessage(1223            "",1224            tool_calls=[1225                ToolCall(id="foo", name="bar", args={"arg1": "arg1"}, type="tool_call")1226            ],1227        ),1228        AIMessage(1229            "",1230            additional_kwargs={1231                "function_call": {1232                    "arguments": json.dumps({"arg1": "arg1"}),1233                    "name": "fun",1234                }1235            },1236        ),1237        AIMessage(1238            "text",1239            tool_calls=[1240                ToolCall(id="foo", name="bar", args={"arg1": "arg1"}, type="tool_call")1241            ],1242        ),1243        ToolMessage("foobar", tool_call_id="foo"),1244    ]1245    expected = 431  # Updated to match token count with mocked 100x100 image12461247    # Mock _url_to_size to avoid PIL dependency in unit tests1248    with patch("langchain_openai.chat_models.base._url_to_size") as mock_url_to_size:1249        mock_url_to_size.return_value = (100, 100)  # 100x100 pixel image1250        actual = llm.get_num_tokens_from_messages(messages)12511252    assert expected == actual12531254    # Test file inputs1255    messages = [1256        HumanMessage(1257            [1258                "Summarize this document.",1259                {1260                    "type": "file",1261                    "file": {1262                        "filename": "my file",1263                        "file_data": "data:application/pdf;base64,<data>",1264                    },1265                },1266            ]1267        )1268    ]1269    actual = 01270    with pytest.warns(match="file inputs are not supported"):1271        actual = llm.get_num_tokens_from_messages(messages)1272    assert actual == 1312731274    # Test Responses1275    messages = [1276        AIMessage(1277            [1278                {1279                    "type": "function_call",1280                    "name": "multiply",1281                    "arguments": '{"x":5,"y":4}',1282                    "call_id": "call_abc123",1283                    "id": "fc_abc123",1284                    "status": "completed",1285                },1286            ],1287            tool_calls=[1288                {1289                    "type": "tool_call",1290                    "name": "multiply",1291                    "args": {"x": 5, "y": 4},1292                    "id": "call_abc123",1293                }1294            ],1295        )1296    ]1297    actual = llm.get_num_tokens_from_messages(messages)1298    assert actual129913001301@pytest.mark.parametrize(1302    "model", ["o1", "o1-preview", "o1-mini", "o3", "o3-mini", "o4-mini"]1303)1304def test_get_num_tokens_from_messages_o_series(model: str) -> None:1305    """o-series models use the same message token format as gpt-4/gpt-5.13061307    Regression test: these raised NotImplementedError.1308    """1309    llm = ChatOpenAI(model=model)1310    messages = [1311        SystemMessage("you're a good assistant"),1312        HumanMessage("how are you"),1313    ]1314    actual = llm.get_num_tokens_from_messages(messages)1315    expected = ChatOpenAI(model=OPENAI_TEST_MODEL).get_num_tokens_from_messages(1316        messages1317    )1318    assert actual == expected131913201321def test_get_num_tokens_from_messages_gpt_6() -> None:1322    llm = ChatOpenAI(model="gpt-6-astra")1323    messages = [HumanMessage("how are you")]13241325    assert llm._get_encoding_model()[1].name == "o200k_base"1326    assert llm.get_num_tokens_from_messages(messages) > 0132713281329class Foo(BaseModel):1330    bar: int133113321333# class FooV1(BaseModelV1):1334#     bar: int133513361337@pytest.mark.parametrize(1338    "schema",1339    [1340        Foo1341        # FooV11342    ],1343)1344def test_schema_from_with_structured_output(schema: type) -> None:1345    """Test schema from with_structured_output."""13461347    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)13481349    structured_llm = llm.with_structured_output(1350        schema, method="json_schema", strict=True1351    )13521353    expected = {1354        "properties": {"bar": {"title": "Bar", "type": "integer"}},1355        "required": ["bar"],1356        "title": schema.__name__,1357        "type": "object",1358    }1359    output_schema = cast("type[BaseModel]", structured_llm.get_output_schema())1360    actual = output_schema.model_json_schema()1361    assert actual == expected136213631364def test__create_usage_metadata() -> None:1365    usage_metadata = {1366        "completion_tokens": 15,1367        "prompt_tokens_details": None,1368        "completion_tokens_details": None,1369        "prompt_tokens": 11,1370        "total_tokens": 26,1371    }1372    result = _create_usage_metadata(usage_metadata)1373    assert result == UsageMetadata(1374        output_tokens=15,1375        input_tokens=11,1376        total_tokens=26,1377        input_token_details={},1378        output_token_details={},1379    )138013811382def test__create_usage_metadata_zero_total_tokens() -> None:1383    """Test that explicit total_tokens=0 is preserved, not replaced by sum."""1384    usage_metadata = {1385        "prompt_tokens": 10,1386        "completion_tokens": 5,1387        "total_tokens": 0,1388        "prompt_tokens_details": None,1389        "completion_tokens_details": None,1390    }1391    result = _create_usage_metadata(usage_metadata)1392    assert result["total_tokens"] == 0139313941395def test__create_usage_metadata_cache_write_tokens() -> None:1396    """`cache_write_tokens` is surfaced under the standard `cache_creation` key."""1397    usage_metadata = {1398        "completion_tokens": 15,1399        # OpenAI's `cache_write_tokens` maps to core's `cache_creation`1400        "prompt_tokens_details": {"cached_tokens": 50, "cache_write_tokens": 25},1401        "completion_tokens_details": None,1402        "prompt_tokens": 100,1403        "total_tokens": 115,1404    }1405    result = _create_usage_metadata(usage_metadata)1406    assert result["input_token_details"] == {1407        "cache_read": 50,1408        "cache_creation": 25,1409    }141014111412def test__create_usage_metadata_cache_read_only() -> None:1413    """Responses without `cache_write_tokens` emit no `cache_creation` key."""1414    usage_metadata = {1415        "completion_tokens": 15,1416        "prompt_tokens_details": {"cached_tokens": 50},1417        "completion_tokens_details": None,1418        "prompt_tokens": 100,1419        "total_tokens": 115,1420    }1421    result = _create_usage_metadata(usage_metadata)1422    assert result["input_token_details"] == {"cache_read": 50}142314241425def test__create_usage_metadata_cache_tokens_zero_retained() -> None:1426    """Explicit zero cache counts are retained (filtered on `None`, not falsiness)."""1427    usage_metadata = {1428        "completion_tokens": 15,1429        "prompt_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},1430        "completion_tokens_details": None,1431        "prompt_tokens": 100,1432        "total_tokens": 115,1433    }1434    result = _create_usage_metadata(usage_metadata)1435    assert result["input_token_details"] == {1436        "cache_read": 0,1437        "cache_creation": 0,1438    }143914401441def test__create_usage_metadata_service_tier_excludes_cache_read_tokens() -> None:1442    """Tier counts exclude cache reads but not overlapping cache writes."""1443    usage_metadata = {1444        "completion_tokens": 50,1445        "prompt_tokens_details": {1446            "cached_tokens": 256,1447            "cache_write_tokens": 3072,1448        },1449        "completion_tokens_details": {"reasoning_tokens": 10},1450        "prompt_tokens": 2304,1451        "total_tokens": 2354,1452    }1453    result = _create_usage_metadata(usage_metadata, service_tier="priority")1454    assert result["input_token_details"] == {1455        "priority_cache_read": 256,1456        "priority_cache_creation": 3072,1457        "priority": 2048,1458    }1459    assert result["output_token_details"] == {1460        "priority_reasoning": 10,1461        "priority": 40,  # 50 - 10 (reasoning)1462    }146314641465def test__create_usage_metadata_service_tier_without_detail_fields() -> None:1466    """Tier arithmetic tolerates missing cache/reasoning fields (no TypeError)."""1467    usage_metadata = {1468        "completion_tokens": 50,1469        "prompt_tokens_details": None,1470        "completion_tokens_details": None,1471        "prompt_tokens": 100,1472        "total_tokens": 150,1473    }1474    result = _create_usage_metadata(usage_metadata, service_tier="flex")1475    assert result["input_token_details"] == {"flex": 100}1476    assert result["output_token_details"] == {"flex": 50}147714781479def test__create_usage_metadata_responses() -> None:1480    response_usage_metadata = {1481        "input_tokens": 100,1482        "input_tokens_details": {"cached_tokens": 50},1483        "output_tokens": 50,1484        "output_tokens_details": {"reasoning_tokens": 10},1485        "total_tokens": 150,1486    }1487    result = _create_usage_metadata_responses(response_usage_metadata)14881489    assert result == UsageMetadata(1490        output_tokens=50,1491        input_tokens=100,1492        total_tokens=150,1493        input_token_details={"cache_read": 50},1494        output_token_details={"reasoning": 10},1495    )149614971498def test__create_usage_metadata_responses_cache_write_tokens() -> None:1499    """Responses usage maps `cache_write_tokens` to the `cache_creation` key."""1500    response_usage_metadata = {1501        "input_tokens": 100,1502        "input_tokens_details": {"cached_tokens": 50, "cache_write_tokens": 25},1503        "output_tokens": 50,1504        "output_tokens_details": {"reasoning_tokens": 10},1505        "total_tokens": 150,1506    }1507    result = _create_usage_metadata_responses(response_usage_metadata)15081509    assert result == UsageMetadata(1510        output_tokens=50,1511        input_tokens=100,1512        total_tokens=150,1513        input_token_details={"cache_read": 50, "cache_creation": 25},1514        output_token_details={"reasoning": 10},1515    )151615171518def test__create_usage_metadata_responses_service_tier_cache_write_overlap() -> None:1519    """Tier counts exclude cache reads but not overlapping cache writes."""1520    response_usage_metadata = {1521        "input_tokens": 2304,1522        "input_tokens_details": {1523            "cached_tokens": 256,1524            "cache_write_tokens": 3072,1525        },1526        "output_tokens": 50,1527        "output_tokens_details": {"reasoning_tokens": 10},1528        "total_tokens": 2354,1529    }1530    result = _create_usage_metadata_responses(1531        response_usage_metadata, service_tier="flex"1532    )1533    assert result["input_token_details"] == {1534        "flex_cache_read": 256,1535        "flex_cache_creation": 3072,1536        "flex": 2048,1537    }1538    assert result["output_token_details"] == {1539        "flex_reasoning": 10,1540        "flex": 40,  # 50 - 10 (reasoning)1541    }154215431544def test__create_usage_metadata_responses_service_tier_without_detail_fields() -> None:1545    """Tier arithmetic tolerates missing cache/reasoning fields (no TypeError)."""1546    response_usage_metadata = {1547        "input_tokens": 100,1548        "input_tokens_details": None,1549        "output_tokens": 50,1550        "output_tokens_details": None,1551        "total_tokens": 150,1552    }1553    result = _create_usage_metadata_responses(1554        response_usage_metadata, service_tier="priority"1555    )1556    assert result["input_token_details"] == {"priority": 100}1557    assert result["output_token_details"] == {"priority": 50}155815591560def test__resize_caps_dimensions_preserving_ratio() -> None:1561    """Larger side capped at 2048 then smaller at 768 keeping aspect ratio."""1562    assert _resize(2048, 4096) == (768, 1536)1563    assert _resize(4096, 2048) == (1536, 768)156415651566def test__convert_to_openai_response_format() -> None:1567    # Test response formats that aren't tool-like.1568    response_format: dict = {1569        "type": "json_schema",1570        "json_schema": {1571            "name": "math_reasoning",1572            "schema": {1573                "type": "object",1574                "properties": {1575                    "steps": {1576                        "type": "array",1577                        "items": {1578                            "type": "object",1579                            "properties": {1580                                "explanation": {"type": "string"},1581                                "output": {"type": "string"},1582                            },1583                            "required": ["explanation", "output"],1584                            "additionalProperties": False,1585                        },1586                    },1587                    "final_answer": {"type": "string"},1588                },1589                "required": ["steps", "final_answer"],1590                "additionalProperties": False,1591            },1592            "strict": True,1593        },1594    }15951596    actual = _convert_to_openai_response_format(response_format)1597    assert actual == response_format15981599    actual = _convert_to_openai_response_format(response_format["json_schema"])1600    assert actual == response_format16011602    actual = _convert_to_openai_response_format(response_format, strict=True)1603    assert actual == response_format16041605    with pytest.raises(ValueError):1606        _convert_to_openai_response_format(response_format, strict=False)160716081609@pytest.mark.parametrize("method", ["function_calling", "json_schema"])1610@pytest.mark.parametrize("strict", [True, None])1611def test_structured_output_strict(1612    method: Literal["function_calling", "json_schema"], strict: bool | None1613) -> None:1614    """Test to verify structured output with strict=True."""16151616    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)16171618    class Joke(BaseModel):1619        """Joke to tell user."""16201621        setup: str = Field(description="question to set up a joke")1622        punchline: str = Field(description="answer to resolve the joke")16231624    llm.with_structured_output(Joke, method=method, strict=strict)1625    # Schema1626    llm.with_structured_output(Joke.model_json_schema(), method=method, strict=strict)162716281629def test_nested_structured_output_strict() -> None:1630    """Test to verify structured output with strict=True for nested object."""16311632    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)16331634    class SelfEvaluation(TypedDict):1635        score: int1636        text: str16371638    class JokeWithEvaluation(TypedDict):1639        """Joke to tell user."""16401641        setup: str1642        punchline: str1643        _evaluation: SelfEvaluation16441645    llm.with_structured_output(JokeWithEvaluation, method="json_schema")164616471648def test__get_request_payload() -> None:1649    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)1650    messages: list = [1651        SystemMessage("hello"),1652        SystemMessage("bye", additional_kwargs={"__openai_role__": "developer"}),1653        SystemMessage(content=[{"type": "text", "text": "hello!"}]),1654        {"role": "human", "content": "how are you"},1655        {"role": "user", "content": [{"type": "text", "text": "feeling today"}]},1656    ]1657    expected = {1658        "messages": [1659            {"role": "system", "content": "hello"},1660            {"role": "developer", "content": "bye"},1661            {"role": "system", "content": [{"type": "text", "text": "hello!"}]},1662            {"role": "user", "content": "how are you"},1663            {"role": "user", "content": [{"type": "text", "text": "feeling today"}]},1664        ],1665        "model": OPENAI_TEST_MODEL,1666        "stream": False,1667    }1668    payload = llm._get_request_payload(messages)1669    assert payload == expected16701671    # Test we coerce to developer role for o-series models1672    llm = ChatOpenAI(model="o3")1673    payload = llm._get_request_payload(messages)1674    expected = {1675        "messages": [1676            {"role": "developer", "content": "hello"},1677            {"role": "developer", "content": "bye"},1678            {"role": "developer", "content": [{"type": "text", "text": "hello!"}]},1679            {"role": "user", "content": "how are you"},1680            {"role": "user", "content": [{"type": "text", "text": "feeling today"}]},1681        ],1682        "model": "o3",1683        "stream": False,1684    }1685    assert payload == expected16861687    # Test we ignore reasoning blocks from other providers1688    reasoning_messages: list = [1689        {1690            "role": "user",1691            "content": [1692                {"type": "reasoning_content", "reasoning_content": "reasoning..."},1693                {"type": "text", "text": "reasoned response"},1694            ],1695        },1696        {1697            "role": "user",1698            "content": [1699                {"type": "thinking", "thinking": "thinking..."},1700                {"type": "text", "text": "thoughtful response"},1701            ],1702        },1703    ]1704    expected = {1705        "messages": [1706            {1707                "role": "user",1708                "content": [{"type": "text", "text": "reasoned response"}],1709            },1710            {1711                "role": "user",1712                "content": [{"type": "text", "text": "thoughtful response"}],1713            },1714        ],1715        "model": "o3",1716        "stream": False,1717    }1718    payload = llm._get_request_payload(reasoning_messages)1719    assert payload == expected172017211722def test_sanitize_chat_completions_text_blocks() -> None:1723    messages = [1724        ToolMessage(1725            content=[{"type": "text", "text": "foo", "id": "lc_abc123"}],1726            tool_call_id="def456",1727        ),1728    ]1729    payload = ChatOpenAI(model="gpt-5.2")._get_request_payload(messages)1730    assert payload["messages"] == [1731        {1732            "content": [{"type": "text", "text": "foo"}],1733            "role": "tool",1734            "tool_call_id": "def456",1735        }1736    ]173717381739def test_init_o1() -> None:1740    with warnings.catch_warnings(record=True) as record:1741        warnings.simplefilter("error")  # Treat warnings as errors1742        ChatOpenAI(model=OPENAI_TEST_MODEL, reasoning_effort="medium")17431744    assert len(record) == 0174517461747def test_init_minimal_reasoning_effort() -> None:1748    with warnings.catch_warnings(record=True) as record:1749        warnings.simplefilter("error")1750        ChatOpenAI(model="gpt-5", reasoning_effort="minimal")17511752    assert len(record) == 0175317541755@pytest.mark.parametrize("use_responses_api", [False, True])1756@pytest.mark.parametrize("use_max_completion_tokens", [True, False])1757def test_minimal_reasoning_effort_payload(1758    use_max_completion_tokens: bool, use_responses_api: bool1759) -> None:1760    """Test that minimal reasoning effort is included in request payload."""1761    if use_max_completion_tokens:1762        kwargs = {"max_completion_tokens": 100}1763    else:1764        kwargs = {"max_tokens": 100}17651766    init_kwargs: dict[str, Any] = {1767        "model": "gpt-5",1768        "reasoning_effort": "minimal",1769        "use_responses_api": use_responses_api,1770        **kwargs,1771    }17721773    llm = ChatOpenAI(**init_kwargs)17741775    messages = [1776        {"role": "developer", "content": "respond with just 'test'"},1777        {"role": "user", "content": "hello"},1778    ]17791780    payload = llm._get_request_payload(messages, stop=None)17811782    # When using responses API, reasoning_effort becomes reasoning.effort1783    if use_responses_api:1784        assert "reasoning" in payload1785        assert payload["reasoning"] == {"effort": "minimal"}1786        # For responses API, tokens param becomes max_output_tokens1787        assert payload["max_output_tokens"] == 1001788    else:1789        # For non-responses API, reasoning_effort remains as is1790        assert payload["reasoning_effort"] == "minimal"1791        if use_max_completion_tokens:1792            assert payload["max_completion_tokens"] == 1001793        else:1794            # max_tokens gets converted to max_completion_tokens in non-responses API1795            assert payload["max_completion_tokens"] == 100179617971798@pytest.mark.parametrize("via_invoke", [False, True])1799def test_responses_api_payload_excludes_stop(via_invoke: bool) -> None:1800    """The Responses API rejects `stop`, so it must be dropped from the payload.18011802    Covers `stop` supplied both at construction time and at invoke time, since1803    they reach the payload through different code paths.1804    """1805    if via_invoke:1806        llm = ChatOpenAI(model=OPENAI_TEST_MODEL, use_responses_api=True)1807        payload = llm._get_request_payload(1808            [HumanMessage(content="Hello")], stop=["END"]1809        )1810    else:1811        llm = ChatOpenAI(  # type: ignore[call-arg]1812            model=OPENAI_TEST_MODEL, stop=["END"], use_responses_api=True1813        )1814        payload = llm._get_request_payload([HumanMessage(content="Hello")])18151816    assert "stop" not in payload181718181819def test_chat_completions_payload_includes_stop() -> None:1820    """`stop` must be preserved for the Chat Completions API, which supports it.18211822    Guards against an over-broad change dropping `stop` outside the Responses API.1823    """1824    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, stop=["END"])  # type: ignore[call-arg]18251826    payload = llm._get_request_payload([HumanMessage(content="Hello")])18271828    assert payload["stop"] == ["END"]182918301831def test_output_version_compat() -> None:1832    llm = ChatOpenAI(model="gpt-5", output_version="responses/v1")1833    assert llm._use_responses_api({}) is True183418351836def test_convert_chunk_to_generation_chunk_v1_keeps_string_content() -> None:1837    """v1 streaming keeps content as '' (not []) and stamps output_version.18381839    Covers both the usage-only (empty-choices) chunk and a content-bearing1840    chunk carrying a tool-call delta; the latter pins the per-content-chunk1841    `output_version` propagation.1842    """1843    llm = ChatOpenAI(model="gpt-4o", output_version="v1")18441845    # Empty-choices chunk (usage-only)1846    empty_chunk: dict[str, Any] = {1847        "id": "chatcmpl-test",1848        "object": "chat.completion.chunk",1849        "created": 0,1850        "model": "gpt-4o",1851        "choices": [],1852        "usage": {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8},1853    }1854    gen = llm._convert_chunk_to_generation_chunk(empty_chunk, AIMessageChunk, None)1855    assert gen is not None1856    assert gen.message.content == ""  # NOT []1857    assert gen.message.response_metadata.get("output_version") == "v1"18581859    # Content-bearing chunk with tool_call delta1860    tool_chunk: dict[str, Any] = {1861        "id": "chatcmpl-test",1862        "object": "chat.completion.chunk",1863        "created": 0,1864        "model": "gpt-4o",1865        "choices": [1866            {1867                "index": 0,1868                "delta": {1869                    "role": "assistant",1870                    "content": "",1871                    "tool_calls": [1872                        {1873                            "index": 0,1874                            "id": "call_abc",1875                            "function": {"name": "get_weather", "arguments": ""},1876                        }1877                    ],1878                },1879                "logprobs": None,1880                "finish_reason": None,1881            }1882        ],1883        "usage": None,1884    }1885    gen = llm._convert_chunk_to_generation_chunk(tool_chunk, AIMessageChunk, None)1886    assert gen is not None1887    assert isinstance(gen.message.content, str)1888    assert gen.message.response_metadata.get("output_version") == "v1"1889    assert gen.message.response_metadata.get("model_provider") == "openai"189018911892def test_v1_streaming_tool_calls_in_content_blocks() -> None:1893    """End-to-end: streaming chunks with tool calls produce correct content_blocks."""1894    stream_chunks: list[dict[str, Any]] = [1895        # Initial empty-choices chunk1896        {1897            "id": "chatcmpl-test",1898            "object": "chat.completion.chunk",1899            "created": 0,1900            "model": "gpt-4o",1901            "choices": [1902                {1903                    "index": 0,1904                    "delta": {"role": "assistant", "content": ""},1905                    "logprobs": None,1906                    "finish_reason": None,1907                }1908            ],1909            "usage": None,1910        },1911        # Text token streamed before the tool call1912        {1913            "id": "chatcmpl-test",1914            "object": "chat.completion.chunk",1915            "created": 0,1916            "model": "gpt-4o",1917            "choices": [1918                {1919                    "index": 0,1920                    "delta": {"content": "Let me check the weather."},1921                    "logprobs": None,1922                    "finish_reason": None,1923                }1924            ],1925            "usage": None,1926        },1927        # Tool call start1928        {1929            "id": "chatcmpl-test",1930            "object": "chat.completion.chunk",1931            "created": 0,1932            "model": "gpt-4o",1933            "choices": [1934                {1935                    "index": 0,1936                    "delta": {1937                        "tool_calls": [1938                            {1939                                "index": 0,1940                                "id": "call_abc",1941                                "function": {1942                                    "name": "get_weather",1943                                    "arguments": '{"loc',1944                                },1945                            }1946                        ]1947                    },1948                    "logprobs": None,1949                    "finish_reason": None,1950                }1951            ],1952            "usage": None,1953        },1954        # Tool call args continuation1955        {1956            "id": "chatcmpl-test",1957            "object": "chat.completion.chunk",1958            "created": 0,1959            "model": "gpt-4o",1960            "choices": [1961                {1962                    "index": 0,1963                    "delta": {1964                        "tool_calls": [1965                            {1966                                "index": 0,1967                                "function": {"arguments": 'ation": "SF"}'},1968                            }1969                        ]1970                    },1971                    "logprobs": None,1972                    "finish_reason": None,1973                }1974            ],1975            "usage": None,1976        },1977        # Finish1978        {1979            "id": "chatcmpl-test",1980            "object": "chat.completion.chunk",1981            "created": 0,1982            "model": "gpt-4o",1983            "choices": [1984                {1985                    "index": 0,1986                    "delta": {},1987                    "logprobs": None,1988                    "finish_reason": "tool_calls",1989                }1990            ],1991            "usage": None,1992        },1993        # Usage chunk1994        {1995            "id": "chatcmpl-test",1996            "object": "chat.completion.chunk",1997            "created": 0,1998            "model": "gpt-4o",1999            "choices": [],2000            "usage": {

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.