libs/partners/openai/tests/unit_tests/chat_models/test_base.py PYTHON 5,828 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 5,828.
1"""Test OpenAI Chat API wrapper."""23from __future__ import annotations45import copy6import json7import warnings8from functools import partial9from types import TracebackType10from typing import Any, Literal, cast11from unittest.mock import AsyncMock, MagicMock, patch1213import httpx14import httpx215import openai16import pytest17from langchain_core.exceptions import (18    ContextOverflowError,19    ModelAPIError,20    ModelAuthenticationError,21    ModelConnectionError,22    ModelError,23    ModelInvalidRequestError,24    ModelNotFoundError,25    ModelPermissionDeniedError,26    ModelRateLimitError,27    ModelTimeoutError,28)29from langchain_core.load import dumps, loads30from langchain_core.messages import (31    AIMessage,32    AIMessageChunk,33    BaseMessage,34    FunctionMessage,35    HumanMessage,36    InvalidToolCall,37    SystemMessage,38    ToolCall,39    ToolMessage,40    message_chunk_to_message,41)42from langchain_core.messages import content as types43from langchain_core.messages.ai import UsageMetadata44from langchain_core.messages.block_translators.openai import (45    _convert_from_v03_ai_message,46)47from langchain_core.outputs import ChatGeneration, ChatResult48from langchain_core.runnables import RunnableLambda49from langchain_core.runnables.base import RunnableBinding, RunnableSequence50from langchain_core.tracers.base import BaseTracer51from langchain_core.tracers.schemas import Run52from langchain_core.utils._gateway import GATEWAY_METADATA_RESPONSE_KEY53from langchain_core.utils.pydantic import PYDANTIC_VERSION54from openai.types.responses import (55    ResponseApplyPatchToolCall,56    ResponseApplyPatchToolCallOutput,57    ResponseOutputMessage,58    ResponseReasoningItem,59    ResponseTextDeltaEvent,60)61from openai.types.responses.response import IncompleteDetails, Response62from openai.types.responses.response_apply_patch_tool_call import OperationCreateFile63from openai.types.responses.response_error import ResponseError64from openai.types.responses.response_file_search_tool_call import (65    ResponseFileSearchToolCall,66    Result,67)68from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall69from openai.types.responses.response_function_web_search import (70    ActionSearch,71    ResponseFunctionWebSearch,72)73from openai.types.responses.response_output_refusal import ResponseOutputRefusal74from openai.types.responses.response_output_text import ResponseOutputText75from openai.types.responses.response_reasoning_item import Summary76from openai.types.responses.response_usage import (77    InputTokensDetails,78    OutputTokensDetails,79    ResponseUsage,80)81from pydantic import BaseModel, Field, SecretStr82from typing_extensions import Self, TypedDict8384from langchain_openai import ChatOpenAI85from langchain_openai.chat_models._compat import (86    _FUNCTION_CALL_IDS_MAP_KEY,87    _convert_from_v1_to_chat_completions,88    _convert_from_v1_to_responses,89    _convert_to_v03_ai_message,90)91from langchain_openai.chat_models.base import (92    OpenAIRefusalError,93    _construct_lc_result_from_responses_api,94    _construct_responses_api_input,95    _convert_dict_to_message,96    _convert_message_to_dict,97    _convert_responses_chunk_to_generation_chunk,98    _convert_to_openai_response_format,99    _create_usage_metadata,100    _create_usage_metadata_responses,101    _format_message_content,102    _get_last_messages,103    _make_computer_call_output_from_message,104    _model_prefers_responses_api,105    _oai_structured_outputs_parser,106    _resize,107)108109OPENAI_TEST_MODEL = "gpt-5.5"110OPENAI_TEMPERATURE_CAPABLE_TEST_MODEL = "gpt-4o-mini"111112113def test_openai_model_param() -> None:114    llm = ChatOpenAI(model="foo")115    assert llm.model_name == "foo"116    assert llm.model == "foo"117    llm = ChatOpenAI(model_name="foo")  # type: ignore[call-arg]118    assert llm.model_name == "foo"119    assert llm.model == "foo"120121    llm = ChatOpenAI(max_tokens=10)  # type: ignore[call-arg]122    assert llm.max_tokens == 10123    llm = ChatOpenAI(max_completion_tokens=10)124    assert llm.max_tokens == 10125126127@pytest.mark.parametrize("async_api", [True, False])128def test_streaming_attribute_should_stream(async_api: bool) -> None:129    llm = ChatOpenAI(model="foo", streaming=True)130    assert llm._should_stream(async_api=async_api)131132133def test_openai_client_caching() -> None:134    """Test that the OpenAI client is cached."""135    llm1 = ChatOpenAI(model=OPENAI_TEST_MODEL)136    llm2 = ChatOpenAI(model=OPENAI_TEST_MODEL)137    assert llm1.root_client._client is llm2.root_client._client138139    llm3 = ChatOpenAI(model=OPENAI_TEST_MODEL, base_url="foo")140    assert llm1.root_client._client is not llm3.root_client._client141142    llm4 = ChatOpenAI(model=OPENAI_TEST_MODEL, timeout=None)143    assert llm1.root_client._client is llm4.root_client._client144145    llm5 = ChatOpenAI(model=OPENAI_TEST_MODEL, timeout=3)146    assert llm1.root_client._client is not llm5.root_client._client147148    llm6 = ChatOpenAI(149        model=OPENAI_TEST_MODEL, timeout=httpx.Timeout(timeout=60.0, connect=5.0)150    )151    assert llm1.root_client._client is not llm6.root_client._client152153    llm7 = ChatOpenAI(model=OPENAI_TEST_MODEL, timeout=(5, 1))154    assert llm1.root_client._client is not llm7.root_client._client155156157def test_profile() -> None:158    model = ChatOpenAI(model="gpt-5.2-pro")159    assert model.profile160    assert not model.profile["structured_output"]161162    model = ChatOpenAI(model="gpt-5")163    assert model.profile164    assert model.profile["structured_output"]165    assert model.profile["tool_calling"]166167    # Test overwriting a field168    model.profile["tool_calling"] = False169    assert not model.profile["tool_calling"]170171    # Test we didn't mutate172    model = ChatOpenAI(model="gpt-5")173    assert model.profile174    assert model.profile["tool_calling"]175176    # Test passing in profile177    model = ChatOpenAI(model="gpt-5", profile={"tool_calling": False})178    assert model.profile == {"tool_calling": False}179180    # Test overrides for gpt-5 input tokens181    model = ChatOpenAI(model="gpt-5")182    assert model.profile["max_input_tokens"] == 272_000183184185def test_gpt_5_reasoning_effort_levels() -> None:186    """GPT-5 and GPT-5.1 predate `xhigh`, and GPT-5 supports `minimal`."""187    for model_name in ("gpt-5", "gpt-5-mini", "gpt-5-nano"):188        model = ChatOpenAI(model=model_name)189        assert model.profile190        assert model.profile["reasoning_effort_levels"] == [191            "minimal",192            "low",193            "medium",194            "high",195        ]196197    model = ChatOpenAI(model="gpt-5.1")198    assert model.profile199    assert model.profile["reasoning_effort_levels"] == ["none", "low", "medium", "high"]200201202def test_gpt_5_3_chat_latest_profile_has_no_reasoning_effort() -> None:203    model = ChatOpenAI(model="gpt-5.3-chat-latest")204205    assert model.profile206    assert model.profile["reasoning_output"] is False207    assert "reasoning_effort_levels" not in model.profile208209210def test_gpt_6_astra_reasoning_effort_levels() -> None:211    model = ChatOpenAI(model="gpt-6-astra")212213    assert model.profile214    assert model.profile["reasoning_effort_levels"] == [215        "low",216        "medium",217        "high",218        "xhigh",219        "max",220    ]221    assert "reasoning_effort_default" not in model.profile222223224@pytest.mark.parametrize(225    ("kwargs", "expected"),226    [227        ({"use_responses_api": True}, True),228        # Routes every request to the Responses API.229        ({"reasoning": {"effort": "low"}}, True),230        ({"use_responses_api": False}, False),231        # Routing is decided per request, so it can't be claimed up front.232        ({}, False),233    ],234)235def test_profile_mid_conversation_tools_follow_responses_api(236    kwargs: dict[str, Any], expected: bool237) -> None:238    """Only the Responses API carries an `additional_tools` input item."""239    model = ChatOpenAI(model="gpt-5.6", **kwargs)240241    assert model.profile242    assert model.profile["mid_conversation_tools"] is expected243    assert model.profile["mid_conversation_system_messages"] is True244245246def test_function_message_dict_to_function_message() -> None:247    content = json.dumps({"result": "Example #1"})248    name = "test_function"249    result = _convert_dict_to_message(250        {"role": "function", "name": name, "content": content}251    )252    assert isinstance(result, FunctionMessage)253    assert result.name == name254    assert result.content == content255256257def test__convert_dict_to_message_human() -> None:258    message = {"role": "user", "content": "foo"}259    result = _convert_dict_to_message(message)260    expected_output = HumanMessage(content="foo")261    assert result == expected_output262    assert _convert_message_to_dict(expected_output) == message263264265def test__convert_dict_to_message_human_with_name() -> None:266    message = {"role": "user", "content": "foo", "name": "test"}267    result = _convert_dict_to_message(message)268    expected_output = HumanMessage(content="foo", name="test")269    assert result == expected_output270    assert _convert_message_to_dict(expected_output) == message271272273def test__convert_dict_to_message_ai() -> None:274    message = {"role": "assistant", "content": "foo"}275    result = _convert_dict_to_message(message)276    expected_output = AIMessage(content="foo")277    assert result == expected_output278    assert _convert_message_to_dict(expected_output) == message279280281def test__convert_dict_to_message_ai_with_name() -> None:282    message = {"role": "assistant", "content": "foo", "name": "test"}283    result = _convert_dict_to_message(message)284    expected_output = AIMessage(content="foo", name="test")285    assert result == expected_output286    assert _convert_message_to_dict(expected_output) == message287288289def test__convert_dict_to_message_system() -> None:290    message = {"role": "system", "content": "foo"}291    result = _convert_dict_to_message(message)292    expected_output = SystemMessage(content="foo")293    assert result == expected_output294    assert _convert_message_to_dict(expected_output) == message295296297def test__convert_dict_to_message_developer() -> None:298    message = {"role": "developer", "content": "foo"}299    result = _convert_dict_to_message(message)300    expected_output = SystemMessage(301        content="foo", additional_kwargs={"__openai_role__": "developer"}302    )303    assert result == expected_output304    assert _convert_message_to_dict(expected_output) == message305306307def test__convert_dict_to_message_system_with_name() -> None:308    message = {"role": "system", "content": "foo", "name": "test"}309    result = _convert_dict_to_message(message)310    expected_output = SystemMessage(content="foo", name="test")311    assert result == expected_output312    assert _convert_message_to_dict(expected_output) == message313314315def test__convert_dict_to_message_tool() -> None:316    message = {"role": "tool", "content": "foo", "tool_call_id": "bar"}317    result = _convert_dict_to_message(message)318    expected_output = ToolMessage(content="foo", tool_call_id="bar")319    assert result == expected_output320    assert _convert_message_to_dict(expected_output) == message321322323def test__convert_dict_to_message_tool_call() -> None:324    raw_tool_call = {325        "id": "call_wm0JY6CdwOMZ4eTxHWUThDNz",326        "function": {327            "arguments": '{"name": "Sally", "hair_color": "green"}',328            "name": "GenerateUsername",329        },330        "type": "function",331    }332    message = {"role": "assistant", "content": None, "tool_calls": [raw_tool_call]}333    result = _convert_dict_to_message(message)334    expected_output = AIMessage(335        content="",336        tool_calls=[337            ToolCall(338                name="GenerateUsername",339                args={"name": "Sally", "hair_color": "green"},340                id="call_wm0JY6CdwOMZ4eTxHWUThDNz",341                type="tool_call",342            )343        ],344    )345    assert result == expected_output346    assert _convert_message_to_dict(expected_output) == message347348    # Test malformed tool call349    raw_tool_calls: list = [350        {351            "id": "call_wm0JY6CdwOMZ4eTxHWUThDNz",352            "function": {"arguments": "oops", "name": "GenerateUsername"},353            "type": "function",354        },355        {356            "id": "call_abc123",357            "function": {358                "arguments": '{"name": "Sally", "hair_color": "green"}',359                "name": "GenerateUsername",360            },361            "type": "function",362        },363    ]364    raw_tool_calls = sorted(raw_tool_calls, key=lambda x: x["id"])365    message = {"role": "assistant", "content": None, "tool_calls": raw_tool_calls}366    result = _convert_dict_to_message(message)367    expected_output = AIMessage(368        content="",369        invalid_tool_calls=[370            InvalidToolCall(371                name="GenerateUsername",372                args="oops",373                id="call_wm0JY6CdwOMZ4eTxHWUThDNz",374                error=(375                    "Function GenerateUsername arguments:\n\noops\n\nare not "376                    "valid JSON. Received JSONDecodeError Expecting value: line 1 "377                    "column 1 (char 0)\nFor troubleshooting, visit: https://docs"378                    ".langchain.com/oss/python/langchain/errors/OUTPUT_PARSING_FAILURE "379                ),380                type="invalid_tool_call",381            )382        ],383        tool_calls=[384            ToolCall(385                name="GenerateUsername",386                args={"name": "Sally", "hair_color": "green"},387                id="call_abc123",388                type="tool_call",389            )390        ],391    )392    assert result == expected_output393    reverted_message_dict = _convert_message_to_dict(expected_output)394    reverted_message_dict["tool_calls"] = sorted(395        reverted_message_dict["tool_calls"], key=lambda x: x["id"]396    )397    assert reverted_message_dict == message398399400class MockAsyncContextManager:401    def __init__(self, chunk_list: list) -> None:402        self.current_chunk = 0403        self.chunk_list = chunk_list404        self.chunk_num = len(chunk_list)405406    async def __aenter__(self) -> Self:407        return self408409    async def __aexit__(410        self,411        exc_type: type[BaseException] | None,412        exc: BaseException | None,413        tb: TracebackType | None,414    ) -> None:415        pass416417    def __aiter__(self) -> MockAsyncContextManager:418        return self419420    async def __anext__(self) -> dict:421        if self.current_chunk < self.chunk_num:422            chunk = self.chunk_list[self.current_chunk]423            self.current_chunk += 1424            return chunk425        raise StopAsyncIteration426427428class MockSyncContextManager:429    def __init__(self, chunk_list: list) -> None:430        self.current_chunk = 0431        self.chunk_list = chunk_list432        self.chunk_num = len(chunk_list)433434    def __enter__(self) -> Self:435        return self436437    def __exit__(438        self,439        exc_type: type[BaseException] | None,440        exc: BaseException | None,441        tb: TracebackType | None,442    ) -> None:443        pass444445    def __iter__(self) -> MockSyncContextManager:446        return self447448    def __next__(self) -> dict:449        if self.current_chunk < self.chunk_num:450            chunk = self.chunk_list[self.current_chunk]451            self.current_chunk += 1452            return chunk453        raise StopIteration454455456GLM4_STREAM_META = """{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u4eba\u5de5\u667a\u80fd"}}]}457{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u52a9\u624b"}}]}458{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":","}}]}459{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u4f60\u53ef\u4ee5"}}]}460{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u53eb\u6211"}}]}461{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"AI"}}]}462{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u52a9\u624b"}}]}463{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"。"}}]}464{"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}}465[DONE]"""  # noqa: E501466467468@pytest.fixture469def mock_glm4_completion() -> list:470    list_chunk_data = GLM4_STREAM_META.split("\n")471    result_list = []472    for msg in list_chunk_data:473        if msg != "[DONE]":474            result_list.append(json.loads(msg))475476    return result_list477478479async def test_glm4_astream(mock_glm4_completion: list) -> None:480    llm_name = "glm-4"481    llm = ChatOpenAI(model=llm_name, stream_usage=True)482    mock_client = AsyncMock()483484    async def mock_create(*args: Any, **kwargs: Any) -> MockAsyncContextManager:485        return MockAsyncContextManager(mock_glm4_completion)486487    mock_client.create = mock_create488    usage_chunk = mock_glm4_completion[-1]489490    usage_metadata: UsageMetadata | None = None491    with patch.object(llm, "async_client", mock_client):492        async for chunk in llm.astream("你的名字叫什么?只回答名字"):493            assert isinstance(chunk, AIMessageChunk)494            if chunk.usage_metadata is not None:495                usage_metadata = chunk.usage_metadata496497    assert usage_metadata is not None498499    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]500    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]501    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]502503504def test_glm4_stream(mock_glm4_completion: list) -> None:505    llm_name = "glm-4"506    llm = ChatOpenAI(model=llm_name, stream_usage=True)507    mock_client = MagicMock()508509    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:510        return MockSyncContextManager(mock_glm4_completion)511512    mock_client.create = mock_create513    usage_chunk = mock_glm4_completion[-1]514515    usage_metadata: UsageMetadata | None = None516    with patch.object(llm, "client", mock_client):517        for chunk in llm.stream("你的名字叫什么?只回答名字"):518            assert isinstance(chunk, AIMessageChunk)519            if chunk.usage_metadata is not None:520                usage_metadata = chunk.usage_metadata521522    assert usage_metadata is not None523524    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]525    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]526    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]527528529DEEPSEEK_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}530{"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}531{"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}532{"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}533{"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}534{"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}535{"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}536{"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}537{"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}538{"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}539{"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}540{"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}541{"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}542{"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}543{"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}544{"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}545{"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}}546[DONE]"""  # noqa: E501547548549@pytest.fixture550def mock_deepseek_completion() -> list[dict]:551    list_chunk_data = DEEPSEEK_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_deepseek_astream(mock_deepseek_completion: list) -> None:561    llm_name = "deepseek-chat"562    llm = ChatOpenAI(model=llm_name, stream_usage=True)563    mock_client = AsyncMock()564565    async def mock_create(*args: Any, **kwargs: Any) -> MockAsyncContextManager:566        return MockAsyncContextManager(mock_deepseek_completion)567568    mock_client.create = mock_create569    usage_chunk = mock_deepseek_completion[-1]570    usage_metadata: UsageMetadata | None = None571    with patch.object(llm, "async_client", mock_client):572        async for chunk in llm.astream("你的名字叫什么?只回答名字"):573            assert isinstance(chunk, AIMessageChunk)574            if chunk.usage_metadata is not None:575                usage_metadata = chunk.usage_metadata576577    assert usage_metadata is not None578579    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]580    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]581    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]582583584def test_deepseek_stream(mock_deepseek_completion: list) -> None:585    llm_name = "deepseek-chat"586    llm = ChatOpenAI(model=llm_name, stream_usage=True)587    mock_client = MagicMock()588589    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:590        return MockSyncContextManager(mock_deepseek_completion)591592    mock_client.create = mock_create593    usage_chunk = mock_deepseek_completion[-1]594    usage_metadata: UsageMetadata | None = None595    with patch.object(llm, "client", mock_client):596        for chunk in llm.stream("你的名字叫什么?只回答名字"):597            assert isinstance(chunk, AIMessageChunk)598            if chunk.usage_metadata is not None:599                usage_metadata = chunk.usage_metadata600601    assert usage_metadata is not None602603    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]604    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]605    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]606607608OPENAI_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}609{"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}610{"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}611{"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}612{"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}613{"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}}614[DONE]"""  # noqa: E501615616617@pytest.fixture618def mock_openai_completion() -> list[dict]:619    list_chunk_data = OPENAI_STREAM_DATA.split("\n")620    result_list = []621    for msg in list_chunk_data:622        if msg != "[DONE]":623            result_list.append(json.loads(msg))624625    return result_list626627628async def test_openai_astream(mock_openai_completion: list) -> None:629    llm_name = OPENAI_TEST_MODEL630    llm = ChatOpenAI(model=llm_name, stream_usage=True)631    assert llm.stream_usage632    mock_client = AsyncMock()633634    async def mock_create(*args: Any, **kwargs: Any) -> MockAsyncContextManager:635        return MockAsyncContextManager(mock_openai_completion)636637    mock_client.create = mock_create638    usage_chunk = mock_openai_completion[-1]639    usage_metadata: UsageMetadata | None = None640    with patch.object(llm, "async_client", mock_client):641        async for chunk in llm.astream("你的名字叫什么?只回答名字"):642            assert isinstance(chunk, AIMessageChunk)643            if chunk.usage_metadata is not None:644                usage_metadata = chunk.usage_metadata645646    assert usage_metadata is not None647648    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]649    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]650    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]651652653def test_openai_stream(mock_openai_completion: list) -> None:654    llm_name = OPENAI_TEST_MODEL655    llm = ChatOpenAI(model=llm_name, stream_usage=True)656    assert llm.stream_usage657    mock_client = MagicMock()658659    call_kwargs = []660661    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:662        call_kwargs.append(kwargs)663        return MockSyncContextManager(mock_openai_completion)664665    mock_client.create = mock_create666    usage_chunk = mock_openai_completion[-1]667    usage_metadata: UsageMetadata | None = None668    with patch.object(llm, "client", mock_client):669        for chunk in llm.stream("你的名字叫什么?只回答名字"):670            assert isinstance(chunk, AIMessageChunk)671            if chunk.usage_metadata is not None:672                usage_metadata = chunk.usage_metadata673674    assert call_kwargs[-1]["stream_options"] == {"include_usage": True}675    assert usage_metadata is not None676    assert usage_metadata["input_tokens"] == usage_chunk["usage"]["prompt_tokens"]677    assert usage_metadata["output_tokens"] == usage_chunk["usage"]["completion_tokens"]678    assert usage_metadata["total_tokens"] == usage_chunk["usage"]["total_tokens"]679680    # Verify no streaming outside of default base URL or clients681    for param, value in {682        "stream_usage": False,683        "openai_proxy": "http://localhost:7890",684        "openai_api_base": "https://example.com/v1",685        "base_url": "https://example.com/v1",686        "client": mock_client,687        "root_client": mock_client,688        "async_client": mock_client,689        "root_async_client": mock_client,690        "http_client": httpx.Client(),691        "http_async_client": httpx.AsyncClient(),692    }.items():693        llm = ChatOpenAI(model=llm_name, **{param: value})  # type: ignore[arg-type]694        assert not llm.stream_usage695        with patch.object(llm, "client", mock_client):696            _ = list(llm.stream("..."))697        assert "stream_options" not in call_kwargs[-1]698699700def test_openai_stream_events_v3_lifecycle(mock_openai_completion: list) -> None:701    """`stream_events(version="v3")` on chat completions emits a valid lifecycle."""702    from langchain_tests.utils.stream_lifecycle import assert_valid_event_stream703704    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)705    mock_client = MagicMock()706707    def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:708        return MockSyncContextManager(mock_openai_completion)709710    mock_client.create = mock_create711    with patch.object(llm, "client", mock_client):712        events = list(llm.stream_events("你的名字叫什么?只回答名字", version="v3"))713714    assert_valid_event_stream(events)715    # At minimum, a text block with the accumulated answer.716    finishes = [e for e in events if e["event"] == "content-block-finish"]717    assert len(finishes) >= 1718    text_finishes = [f for f in finishes if f["content"]["type"] == "text"]719    assert len(text_finishes) == 1720721722@pytest.fixture723def mock_completion() -> dict:724    return {725        "id": "chatcmpl-7fcZavknQda3SQ",726        "object": "chat.completion",727        "created": 1689989000,728        "model": OPENAI_TEST_MODEL,729        "choices": [730            {731                "index": 0,732                "message": {"role": "assistant", "content": "Bar Baz", "name": "Erick"},733                "finish_reason": "stop",734            }735        ],736    }737738739@pytest.fixture740def mock_client(mock_completion: dict) -> MagicMock:741    rtn = MagicMock()742743    mock_create = MagicMock()744745    mock_resp = MagicMock()746    mock_resp.headers = {"content-type": "application/json"}747    mock_resp.parse.return_value = mock_completion748    mock_create.return_value = mock_resp749750    rtn.with_raw_response.create = mock_create751    rtn.create.return_value = mock_completion752    return rtn753754755@pytest.fixture756def mock_async_client(mock_completion: dict) -> AsyncMock:757    rtn = AsyncMock()758759    mock_create = AsyncMock()760    mock_resp = MagicMock()761    mock_resp.parse.return_value = mock_completion762    mock_create.return_value = mock_resp763764    rtn.with_raw_response.create = mock_create765    rtn.create.return_value = mock_completion766    return rtn767768769def test_openai_invoke(mock_client: MagicMock) -> None:770    llm = ChatOpenAI()771772    with patch.object(llm, "client", mock_client):773        res = llm.invoke("bar")774        assert res.content == "Bar Baz"775776        # headers are not in response_metadata if include_response_headers not set777        assert "headers" not in res.response_metadata778    assert mock_client.with_raw_response.create.called779780781async def test_openai_ainvoke(mock_async_client: AsyncMock) -> None:782    llm = ChatOpenAI()783784    with patch.object(llm, "async_client", mock_async_client):785        res = await llm.ainvoke("bar")786        assert res.content == "Bar Baz"787788        # headers are not in response_metadata if include_response_headers not set789        assert "headers" not in res.response_metadata790    assert mock_async_client.with_raw_response.create.called791792793class _GatewayMetadataTracer(BaseTracer):794    """Captures gateway metadata promoted onto completed LLM runs."""795796    def __init__(self) -> None:797        super().__init__()798        self.gateway_metadata: dict | None = None799800    def _persist_run(self, run: Run) -> None:801        """No-op; runs are inspected as they complete."""802803    def _on_llm_end(self, run: Run) -> None:804        metadata = run.extra.get("metadata", {})805        if "ls_gateway_info" in metadata:806            self.gateway_metadata = metadata["ls_gateway_info"]807808809_GATEWAY_METADATA_HEADERS = httpx2.Headers(810    {"x-langsmith-gateway-metadata": '{"provider": "openai"}'}811)812813_RESPONSES_API_COMPLETION = Response(814    id="resp_123",815    created_at=1234567890,816    model=OPENAI_TEST_MODEL,817    object="response",818    parallel_tool_calls=True,819    tools=[],820    tool_choice="auto",821    output=[822        ResponseOutputMessage(823            type="message",824            id="msg_123",825            content=[826                ResponseOutputText(type="output_text", text="Bar Baz", annotations=[])827            ],828            role="assistant",829            status="completed",830        )831    ],832)833834_RESPONSES_API_STREAM = [835    ResponseTextDeltaEvent(836        content_index=0,837        delta="Bar Baz",838        item_id="msg_123",839        output_index=0,840        sequence_number=0,841        logprobs=[],842        type="response.output_text.delta",843    ),844]845846847@pytest.mark.parametrize("use_responses_api", [False, True])848def test_openai_invoke_surfaces_gateway_metadata(849    mock_completion: dict, *, use_responses_api: bool850) -> None:851    """Gateway metadata header is surfaced on `generation_info`, not the message."""852    llm = ChatOpenAI(use_responses_api=use_responses_api)853    mock_client = MagicMock()854    mock_resp = MagicMock()855    mock_resp.headers = _GATEWAY_METADATA_HEADERS856    if use_responses_api:857        mock_resp.parse.return_value = _RESPONSES_API_COMPLETION858        mock_client.responses.with_raw_response.create.return_value = mock_resp859        client_attr = "root_client"860    else:861        mock_resp.parse.return_value = mock_completion862        mock_client.with_raw_response.create.return_value = mock_resp863        client_attr = "client"864865    tracer = _GatewayMetadataTracer()866    with patch.object(llm, client_attr, mock_client):867        res = llm.invoke("bar", config={"callbacks": [tracer]})868869    # Gateway metadata reaches the tracer via `generation_info`...870    assert tracer.gateway_metadata == {"provider": "openai"}871    # ...but is kept off the user-facing message `response_metadata`.872    assert GATEWAY_METADATA_RESPONSE_KEY not in res.response_metadata873874875@pytest.mark.parametrize("use_responses_api", [False, True])876def test_openai_stream_surfaces_gateway_metadata(877    mock_openai_completion: list, *, use_responses_api: bool878) -> None:879    """Gateway metadata reaches the tracer for a gateway-routed stream."""880    # A LangSmith API key signals gateway routing, so streaming fetches raw881    # headers.882    llm = ChatOpenAI(883        model=OPENAI_TEST_MODEL,884        api_key="lsv2_pt_example",  # type: ignore[arg-type]885        use_responses_api=use_responses_api,886    )887    mock_client = MagicMock()888    mock_resp = MagicMock()889    mock_resp.headers = _GATEWAY_METADATA_HEADERS890    if use_responses_api:891        mock_resp.parse.return_value = MockSyncContextManager(_RESPONSES_API_STREAM)892        mock_client.with_raw_response.responses.create.return_value = mock_resp893        mock_client.responses.create.return_value = MockSyncContextManager(894            _RESPONSES_API_STREAM895        )896        client_attr = "root_client"897    else:898        mock_resp.parse.return_value = MockSyncContextManager(mock_openai_completion)899        mock_client.with_raw_response.create.return_value = mock_resp900        client_attr = "client"901902    tracer = _GatewayMetadataTracer()903    with patch.object(llm, client_attr, mock_client):904        for chunk in llm.stream("what is your name?", config={"callbacks": [tracer]}):905            # Gateway metadata is kept off the user-facing chunk metadata.906            assert GATEWAY_METADATA_RESPONSE_KEY not in chunk.response_metadata907908    assert tracer.gateway_metadata == {"provider": "openai"}909910911@pytest.mark.parametrize("use_responses_api", [False, True])912async def test_openai_astream_surfaces_gateway_metadata(913    mock_openai_completion: list, *, use_responses_api: bool914) -> None:915    """Gateway metadata reaches the tracer for a gateway-routed async stream."""916    llm = ChatOpenAI(917        model=OPENAI_TEST_MODEL,918        api_key="lsv2_pt_example",  # type: ignore[arg-type]919        use_responses_api=use_responses_api,920    )921    mock_client = AsyncMock()922    mock_resp = MagicMock()923    mock_resp.headers = _GATEWAY_METADATA_HEADERS924    if use_responses_api:925        mock_resp.parse.return_value = MockAsyncContextManager(_RESPONSES_API_STREAM)926        mock_client.with_raw_response.responses.create.return_value = mock_resp927        mock_client.responses.create.return_value = MockAsyncContextManager(928            _RESPONSES_API_STREAM929        )930        client_attr = "root_async_client"931    else:932        mock_resp.parse.return_value = MockAsyncContextManager(mock_openai_completion)933        mock_client.with_raw_response.create.return_value = mock_resp934        client_attr = "async_client"935936    tracer = _GatewayMetadataTracer()937    with patch.object(llm, client_attr, mock_client):938        async for chunk in llm.astream(939            "what is your name?", config={"callbacks": [tracer]}940        ):941            # Gateway metadata is kept off the user-facing chunk metadata.942            assert GATEWAY_METADATA_RESPONSE_KEY not in chunk.response_metadata943944    assert tracer.gateway_metadata == {"provider": "openai"}945946947@pytest.mark.parametrize(948    "model",949    [950        OPENAI_TEST_MODEL,951        "gpt-5-nano",952        "o3",953        "gpt-5.2",954    ],955)956def test__get_encoding_model(model: str) -> None:957    ChatOpenAI(model=model)._get_encoding_model()958959960def test_openai_invoke_name(mock_client: MagicMock) -> None:961    llm = ChatOpenAI()962963    with patch.object(llm, "client", mock_client):964        messages = [HumanMessage(content="Foo", name="Katie")]965        res = llm.invoke(messages)966        call_args, call_kwargs = mock_client.with_raw_response.create.call_args967        assert len(call_args) == 0  # no positional args968        call_messages = call_kwargs["messages"]969        assert len(call_messages) == 1970        assert call_messages[0]["role"] == "user"971        assert call_messages[0]["content"] == "Foo"972        assert call_messages[0]["name"] == "Katie"973974        # check return type has name975        assert res.content == "Bar Baz"976        assert res.name == "Erick"977978979def test_function_calls_with_tool_calls(mock_client: MagicMock) -> None:980    # Test that we ignore function calls if tool_calls are present981    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)982    tool_call_message = AIMessage(983        content="",984        additional_kwargs={985            "function_call": {986                "name": "get_weather",987                "arguments": '{"location": "Boston"}',988            }989        },990        tool_calls=[991            {992                "name": "get_weather",993                "args": {"location": "Boston"},994                "id": "abc123",995                "type": "tool_call",996            }997        ],998    )999    messages = [1000        HumanMessage("What's the weather in Boston?"),1001        tool_call_message,1002        ToolMessage(content="It's sunny.", name="get_weather", tool_call_id="abc123"),1003    ]1004    with patch.object(llm, "client", mock_client):1005        _ = llm.invoke(messages)1006        _, call_kwargs = mock_client.with_raw_response.create.call_args1007        call_messages = call_kwargs["messages"]1008        tool_call_message_payload = call_messages[1]1009        assert "tool_calls" in tool_call_message_payload1010        assert "function_call" not in tool_call_message_payload10111012    # Test we don't ignore function calls if tool_calls are not present1013    cast(AIMessage, messages[1]).tool_calls = []1014    with patch.object(llm, "client", mock_client):1015        _ = llm.invoke(messages)1016        _, call_kwargs = mock_client.with_raw_response.create.call_args1017        call_messages = call_kwargs["messages"]1018        tool_call_message_payload = call_messages[1]1019        assert "function_call" in tool_call_message_payload1020        assert "tool_calls" not in tool_call_message_payload102110221023def test_custom_token_counting() -> None:1024    def token_encoder(text: str) -> list[int]:1025        return [1, 2, 3]10261027    llm = ChatOpenAI(custom_get_token_ids=token_encoder)1028    assert llm.get_token_ids("foo") == [1, 2, 3]102910301031def test_format_message_content() -> None:1032    content: Any = "hello"1033    assert content == _format_message_content(content)10341035    content = None1036    assert content == _format_message_content(content)10371038    content = []1039    assert content == _format_message_content(content)10401041    content = [1042        {"type": "text", "text": "What is in this image?"},1043        {"type": "image_url", "image_url": {"url": "url.com"}},1044    ]1045    assert content == _format_message_content(content)10461047    content = [1048        {"type": "text", "text": "hello"},1049        {1050            "type": "tool_use",1051            "id": "toolu_01A09q90qw90lq917835lq9",1052            "name": "get_weather",1053            "input": {"location": "San Francisco, CA", "unit": "celsius"},1054        },1055    ]1056    assert _format_message_content(content) == [{"type": "text", "text": "hello"}]10571058    # Standard multi-modal inputs1059    contents = [1060        {"type": "image", "source_type": "url", "url": "https://..."},  # v01061        {"type": "image", "url": "https://..."},  # v11062    ]1063    expected = [{"type": "image_url", "image_url": {"url": "https://..."}}]1064    for content in contents:1065        assert expected == _format_message_content([content])10661067    contents = [1068        {1069            "type": "image",1070            "source_type": "base64",1071            "data": "<base64 data>",1072            "mime_type": "image/png",1073        },1074        {"type": "image", "base64": "<base64 data>", "mime_type": "image/png"},1075    ]1076    expected = [1077        {1078            "type": "image_url",1079            "image_url": {"url": "data:image/png;base64,<base64 data>"},1080        }1081    ]1082    for content in contents:1083        assert expected == _format_message_content([content])10841085    contents = [1086        {1087            "type": "file",1088            "source_type": "base64",1089            "data": "<base64 data>",1090            "mime_type": "application/pdf",1091            "filename": "my_file",1092        },1093        {1094            "type": "file",1095            "base64": "<base64 data>",1096            "mime_type": "application/pdf",1097            "filename": "my_file",1098        },1099    ]1100    expected = [1101        {1102            "type": "file",1103            "file": {1104                "filename": "my_file",1105                "file_data": "data:application/pdf;base64,<base64 data>",1106            },1107        }1108    ]1109    for content in contents:1110        assert expected == _format_message_content([content])11111112    # Test warn if PDF is missing a filename and that we add a default filename1113    pdf_block = {1114        "type": "file",1115        "base64": "<base64 data>",1116        "mime_type": "application/pdf",1117    }1118    expected = [1119        {1120            "type": "file",1121            "file": {1122                "file_data": "data:application/pdf;base64,<base64 data>",1123                "filename": "LC_AUTOGENERATED",1124            },1125        }1126    ]1127    with pytest.warns(match="filename"):1128        assert expected == _format_message_content([pdf_block])11291130    contents = [1131        {"type": "file", "source_type": "id", "id": "file-abc123"},1132        {"type": "file", "file_id": "file-abc123"},1133    ]1134    expected = [{"type": "file", "file": {"file_id": "file-abc123"}}]1135    for content in contents:1136        assert expected == _format_message_content([content])113711381139class GenerateUsername(BaseModel):1140    "Get a username based on someone's name and hair color."11411142    name: str1143    hair_color: str114411451146class MakeASandwich(BaseModel):1147    "Make a sandwich given a list of ingredients."11481149    bread_type: str1150    cheese_type: str1151    condiments: list[str]1152    vegetables: list[str]115311541155@pytest.mark.parametrize(1156    "tool_choice",1157    [1158        "any",1159        "none",1160        "auto",1161        "required",1162        "GenerateUsername",1163        {"type": "function", "function": {"name": "MakeASandwich"}},1164        False,1165        None,1166    ],1167)1168@pytest.mark.parametrize("strict", [True, False, None])1169def test_bind_tools_tool_choice(tool_choice: Any, strict: bool | None) -> None:1170    """Test passing in manually construct tool call message."""1171    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)1172    llm.bind_tools(1173        tools=[GenerateUsername, MakeASandwich], tool_choice=tool_choice, strict=strict1174    )117511761177def test_bind_tools_response_format_defaults_strict() -> None:1178    """Test that strict defaults to True when response_format is provided."""1179    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)1180    bound = llm.bind_tools(1181        tools=[GenerateUsername],1182        response_format=MakeASandwich,1183    )1184    tools = bound.kwargs["tools"]  # type: ignore[attr-defined]1185    assert tools[0]["function"]["strict"] is True118611871188def test_bind_tools_response_format_respects_strict_false() -> None:1189    """Test that strict=False is respected even when response_format is provided."""1190    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)1191    bound = llm.bind_tools(1192        tools=[GenerateUsername],1193        response_format=MakeASandwich,1194        strict=False,1195    )1196    tools = bound.kwargs["tools"]  # type: ignore[attr-defined]1197    assert tools[0]["function"]["strict"] is False119811991200def test_bind_tools_no_response_format_keeps_strict_none() -> None:1201    """Test that strict stays None when response_format is not provided."""1202    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)1203    bound = llm.bind_tools(tools=[GenerateUsername])1204    tools = bound.kwargs["tools"]  # type: ignore[attr-defined]1205    assert "strict" not in tools[0]["function"]120612071208@pytest.mark.parametrize(1209    "schema", [GenerateUsername, GenerateUsername.model_json_schema()]1210)1211@pytest.mark.parametrize("method", ["json_schema", "function_calling", "json_mode"])1212@pytest.mark.parametrize("include_raw", [True, False])1213@pytest.mark.parametrize("strict", [True, False, None])1214def test_with_structured_output(1215    schema: type | dict[str, Any] | None,1216    method: Literal["function_calling", "json_mode", "json_schema"],1217    include_raw: bool,1218    strict: bool | None,1219) -> None:1220    """Test passing in manually construct tool call message."""1221    if method == "json_mode":1222        strict = None1223    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, temperature=0)1224    llm.with_structured_output(1225        schema, method=method, strict=strict, include_raw=include_raw1226    )122712281229def test_get_num_tokens_from_messages() -> None:1230    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)1231    messages = [1232        SystemMessage("you're a good assistant"),1233        HumanMessage("how are you"),1234        HumanMessage(1235            [1236                {"type": "text", "text": "what's in this image"},1237                {"type": "image_url", "image_url": {"url": "https://foobar.com"}},1238                {1239                    "type": "image_url",1240                    "image_url": {"url": "https://foobar.com", "detail": "low"},1241                },1242            ]1243        ),1244        AIMessage("a nice bird"),1245        AIMessage(1246            "",1247            tool_calls=[1248                ToolCall(id="foo", name="bar", args={"arg1": "arg1"}, type="tool_call")1249            ],1250        ),1251        AIMessage(1252            "",1253            additional_kwargs={1254                "function_call": {1255                    "arguments": json.dumps({"arg1": "arg1"}),1256                    "name": "fun",1257                }1258            },1259        ),1260        AIMessage(1261            "text",1262            tool_calls=[1263                ToolCall(id="foo", name="bar", args={"arg1": "arg1"}, type="tool_call")1264            ],1265        ),1266        ToolMessage("foobar", tool_call_id="foo"),1267    ]1268    expected = 431  # Updated to match token count with mocked 100x100 image12691270    # Mock _url_to_size to avoid PIL dependency in unit tests1271    with patch("langchain_openai.chat_models.base._url_to_size") as mock_url_to_size:1272        mock_url_to_size.return_value = (100, 100)  # 100x100 pixel image1273        actual = llm.get_num_tokens_from_messages(messages)12741275    assert expected == actual12761277    # Test file inputs1278    messages = [1279        HumanMessage(1280            [1281                "Summarize this document.",1282                {1283                    "type": "file",1284                    "file": {1285                        "filename": "my file",1286                        "file_data": "data:application/pdf;base64,<data>",1287                    },1288                },1289            ]1290        )1291    ]1292    actual = 01293    with pytest.warns(match="file inputs are not supported"):1294        actual = llm.get_num_tokens_from_messages(messages)1295    assert actual == 1312961297    # Test Responses1298    messages = [1299        AIMessage(1300            [1301                {1302                    "type": "function_call",1303                    "name": "multiply",1304                    "arguments": '{"x":5,"y":4}',1305                    "call_id": "call_abc123",1306                    "id": "fc_abc123",1307                    "status": "completed",1308                },1309            ],1310            tool_calls=[1311                {1312                    "type": "tool_call",1313                    "name": "multiply",1314                    "args": {"x": 5, "y": 4},1315                    "id": "call_abc123",1316                }1317            ],1318        )1319    ]1320    actual = llm.get_num_tokens_from_messages(messages)1321    assert actual132213231324@pytest.mark.parametrize(1325    "model", ["o1", "o1-preview", "o1-mini", "o3", "o3-mini", "o4-mini"]1326)1327def test_get_num_tokens_from_messages_o_series(model: str) -> None:1328    """o-series models use the same message token format as gpt-4/gpt-5.13291330    Regression test: these raised NotImplementedError.1331    """1332    llm = ChatOpenAI(model=model)1333    messages = [1334        SystemMessage("you're a good assistant"),1335        HumanMessage("how are you"),1336    ]1337    actual = llm.get_num_tokens_from_messages(messages)1338    expected = ChatOpenAI(model=OPENAI_TEST_MODEL).get_num_tokens_from_messages(1339        messages1340    )1341    assert actual == expected134213431344def test_get_num_tokens_from_messages_gpt_6() -> None:1345    llm = ChatOpenAI(model="gpt-6-astra")1346    messages = [HumanMessage("how are you")]13471348    assert llm._get_encoding_model()[1].name == "o200k_base"1349    assert llm.get_num_tokens_from_messages(messages) > 0135013511352class Foo(BaseModel):1353    bar: int135413551356# class FooV1(BaseModelV1):1357#     bar: int135813591360@pytest.mark.parametrize(1361    "schema",1362    [1363        Foo1364        # FooV11365    ],1366)1367def test_schema_from_with_structured_output(schema: type) -> None:1368    """Test schema from with_structured_output."""13691370    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)13711372    structured_llm = llm.with_structured_output(1373        schema, method="json_schema", strict=True1374    )13751376    expected = {1377        "properties": {"bar": {"title": "Bar", "type": "integer"}},1378        "required": ["bar"],1379        "title": schema.__name__,1380        "type": "object",1381    }1382    output_schema = cast("type[BaseModel]", structured_llm.get_output_schema())1383    actual = output_schema.model_json_schema()1384    assert actual == expected138513861387def test__create_usage_metadata() -> None:1388    usage_metadata = {1389        "completion_tokens": 15,1390        "prompt_tokens_details": None,1391        "completion_tokens_details": None,1392        "prompt_tokens": 11,1393        "total_tokens": 26,1394    }1395    result = _create_usage_metadata(usage_metadata)1396    assert result == UsageMetadata(1397        output_tokens=15,1398        input_tokens=11,1399        total_tokens=26,1400        input_token_details={},1401        output_token_details={},1402    )140314041405def test__create_usage_metadata_zero_total_tokens() -> None:1406    """Test that explicit total_tokens=0 is preserved, not replaced by sum."""1407    usage_metadata = {1408        "prompt_tokens": 10,1409        "completion_tokens": 5,1410        "total_tokens": 0,1411        "prompt_tokens_details": None,1412        "completion_tokens_details": None,1413    }1414    result = _create_usage_metadata(usage_metadata)1415    assert result["total_tokens"] == 0141614171418def test__create_usage_metadata_cache_write_tokens() -> None:1419    """`cache_write_tokens` is surfaced under the standard `cache_creation` key."""1420    usage_metadata = {1421        "completion_tokens": 15,1422        # OpenAI's `cache_write_tokens` maps to core's `cache_creation`1423        "prompt_tokens_details": {"cached_tokens": 50, "cache_write_tokens": 25},1424        "completion_tokens_details": None,1425        "prompt_tokens": 100,1426        "total_tokens": 115,1427    }1428    result = _create_usage_metadata(usage_metadata)1429    assert result["input_token_details"] == {1430        "cache_read": 50,1431        "cache_creation": 25,1432    }143314341435def test__create_usage_metadata_cache_read_only() -> None:1436    """Responses without `cache_write_tokens` emit no `cache_creation` key."""1437    usage_metadata = {1438        "completion_tokens": 15,1439        "prompt_tokens_details": {"cached_tokens": 50},1440        "completion_tokens_details": None,1441        "prompt_tokens": 100,1442        "total_tokens": 115,1443    }1444    result = _create_usage_metadata(usage_metadata)1445    assert result["input_token_details"] == {"cache_read": 50}144614471448def test__create_usage_metadata_cache_tokens_zero_retained() -> None:1449    """Explicit zero cache counts are retained (filtered on `None`, not falsiness)."""1450    usage_metadata = {1451        "completion_tokens": 15,1452        "prompt_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},1453        "completion_tokens_details": None,1454        "prompt_tokens": 100,1455        "total_tokens": 115,1456    }1457    result = _create_usage_metadata(usage_metadata)1458    assert result["input_token_details"] == {1459        "cache_read": 0,1460        "cache_creation": 0,1461    }146214631464def test__create_usage_metadata_service_tier_excludes_cache_read_tokens() -> None:1465    """Tier counts exclude cache reads but not overlapping cache writes."""1466    usage_metadata = {1467        "completion_tokens": 50,1468        "prompt_tokens_details": {1469            "cached_tokens": 256,1470            "cache_write_tokens": 3072,1471        },1472        "completion_tokens_details": {"reasoning_tokens": 10},1473        "prompt_tokens": 2304,1474        "total_tokens": 2354,1475    }1476    result = _create_usage_metadata(usage_metadata, service_tier="priority")1477    assert result["input_token_details"] == {1478        "priority_cache_read": 256,1479        "priority_cache_creation": 3072,1480        "priority": 2048,1481    }1482    assert result["output_token_details"] == {1483        "priority_reasoning": 10,1484        "priority": 40,  # 50 - 10 (reasoning)1485    }148614871488def test__create_usage_metadata_service_tier_without_detail_fields() -> None:1489    """Tier arithmetic tolerates missing cache/reasoning fields (no TypeError)."""1490    usage_metadata = {1491        "completion_tokens": 50,1492        "prompt_tokens_details": None,1493        "completion_tokens_details": None,1494        "prompt_tokens": 100,1495        "total_tokens": 150,1496    }1497    result = _create_usage_metadata(usage_metadata, service_tier="flex")1498    assert result["input_token_details"] == {"flex": 100}1499    assert result["output_token_details"] == {"flex": 50}150015011502def test__create_usage_metadata_responses() -> None:1503    response_usage_metadata = {1504        "input_tokens": 100,1505        "input_tokens_details": {"cached_tokens": 50},1506        "output_tokens": 50,1507        "output_tokens_details": {"reasoning_tokens": 10},1508        "total_tokens": 150,1509    }1510    result = _create_usage_metadata_responses(response_usage_metadata)15111512    assert result == UsageMetadata(1513        output_tokens=50,1514        input_tokens=100,1515        total_tokens=150,1516        input_token_details={"cache_read": 50},1517        output_token_details={"reasoning": 10},1518    )151915201521def test__create_usage_metadata_responses_cache_write_tokens() -> None:1522    """Responses usage maps `cache_write_tokens` to the `cache_creation` key."""1523    response_usage_metadata = {1524        "input_tokens": 100,1525        "input_tokens_details": {"cached_tokens": 50, "cache_write_tokens": 25},1526        "output_tokens": 50,1527        "output_tokens_details": {"reasoning_tokens": 10},1528        "total_tokens": 150,1529    }1530    result = _create_usage_metadata_responses(response_usage_metadata)15311532    assert result == UsageMetadata(1533        output_tokens=50,1534        input_tokens=100,1535        total_tokens=150,1536        input_token_details={"cache_read": 50, "cache_creation": 25},1537        output_token_details={"reasoning": 10},1538    )153915401541def test__create_usage_metadata_responses_service_tier_cache_write_overlap() -> None:1542    """Tier counts exclude cache reads but not overlapping cache writes."""1543    response_usage_metadata = {1544        "input_tokens": 2304,1545        "input_tokens_details": {1546            "cached_tokens": 256,1547            "cache_write_tokens": 3072,1548        },1549        "output_tokens": 50,1550        "output_tokens_details": {"reasoning_tokens": 10},1551        "total_tokens": 2354,1552    }1553    result = _create_usage_metadata_responses(1554        response_usage_metadata, service_tier="flex"1555    )1556    assert result["input_token_details"] == {1557        "flex_cache_read": 256,1558        "flex_cache_creation": 3072,1559        "flex": 2048,1560    }1561    assert result["output_token_details"] == {1562        "flex_reasoning": 10,1563        "flex": 40,  # 50 - 10 (reasoning)1564    }156515661567def test__create_usage_metadata_responses_service_tier_without_detail_fields() -> None:1568    """Tier arithmetic tolerates missing cache/reasoning fields (no TypeError)."""1569    response_usage_metadata = {1570        "input_tokens": 100,1571        "input_tokens_details": None,1572        "output_tokens": 50,1573        "output_tokens_details": None,1574        "total_tokens": 150,1575    }1576    result = _create_usage_metadata_responses(1577        response_usage_metadata, service_tier="priority"1578    )1579    assert result["input_token_details"] == {"priority": 100}1580    assert result["output_token_details"] == {"priority": 50}158115821583def test__resize_caps_dimensions_preserving_ratio() -> None:1584    """Larger side capped at 2048 then smaller at 768 keeping aspect ratio."""1585    assert _resize(2048, 4096) == (768, 1536)1586    assert _resize(4096, 2048) == (1536, 768)158715881589def test__convert_to_openai_response_format() -> None:1590    # Test response formats that aren't tool-like.1591    response_format: dict = {1592        "type": "json_schema",1593        "json_schema": {1594            "name": "math_reasoning",1595            "schema": {1596                "type": "object",1597                "properties": {1598                    "steps": {1599                        "type": "array",1600                        "items": {1601                            "type": "object",1602                            "properties": {1603                                "explanation": {"type": "string"},1604                                "output": {"type": "string"},1605                            },1606                            "required": ["explanation", "output"],1607                            "additionalProperties": False,1608                        },1609                    },1610                    "final_answer": {"type": "string"},1611                },1612                "required": ["steps", "final_answer"],1613                "additionalProperties": False,1614            },1615            "strict": True,1616        },1617    }16181619    actual = _convert_to_openai_response_format(response_format)1620    assert actual == response_format16211622    actual = _convert_to_openai_response_format(response_format["json_schema"])1623    assert actual == response_format16241625    actual = _convert_to_openai_response_format(response_format, strict=True)1626    assert actual == response_format16271628    with pytest.raises(ValueError):1629        _convert_to_openai_response_format(response_format, strict=False)163016311632@pytest.mark.parametrize("method", ["function_calling", "json_schema"])1633@pytest.mark.parametrize("strict", [True, None])1634def test_structured_output_strict(1635    method: Literal["function_calling", "json_schema"], strict: bool | None1636) -> None:1637    """Test to verify structured output with strict=True."""16381639    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)16401641    class Joke(BaseModel):1642        """Joke to tell user."""16431644        setup: str = Field(description="question to set up a joke")1645        punchline: str = Field(description="answer to resolve the joke")16461647    llm.with_structured_output(Joke, method=method, strict=strict)1648    # Schema1649    llm.with_structured_output(Joke.model_json_schema(), method=method, strict=strict)165016511652def test_nested_structured_output_strict() -> None:1653    """Test to verify structured output with strict=True for nested object."""16541655    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)16561657    class SelfEvaluation(TypedDict):1658        score: int1659        text: str16601661    class JokeWithEvaluation(TypedDict):1662        """Joke to tell user."""16631664        setup: str1665        punchline: str1666        _evaluation: SelfEvaluation16671668    llm.with_structured_output(JokeWithEvaluation, method="json_schema")166916701671def test__get_request_payload() -> None:1672    llm = ChatOpenAI(model=OPENAI_TEST_MODEL)1673    messages: list = [1674        SystemMessage("hello"),1675        SystemMessage("bye", additional_kwargs={"__openai_role__": "developer"}),1676        SystemMessage(content=[{"type": "text", "text": "hello!"}]),1677        {"role": "human", "content": "how are you"},1678        {"role": "user", "content": [{"type": "text", "text": "feeling today"}]},1679    ]1680    expected = {1681        "messages": [1682            {"role": "system", "content": "hello"},1683            {"role": "developer", "content": "bye"},1684            {"role": "system", "content": [{"type": "text", "text": "hello!"}]},1685            {"role": "user", "content": "how are you"},1686            {"role": "user", "content": [{"type": "text", "text": "feeling today"}]},1687        ],1688        "model": OPENAI_TEST_MODEL,1689        "stream": False,1690    }1691    payload = llm._get_request_payload(messages)1692    assert payload == expected16931694    # Test we coerce to developer role for o-series models1695    llm = ChatOpenAI(model="o3")1696    payload = llm._get_request_payload(messages)1697    expected = {1698        "messages": [1699            {"role": "developer", "content": "hello"},1700            {"role": "developer", "content": "bye"},1701            {"role": "developer", "content": [{"type": "text", "text": "hello!"}]},1702            {"role": "user", "content": "how are you"},1703            {"role": "user", "content": [{"type": "text", "text": "feeling today"}]},1704        ],1705        "model": "o3",1706        "stream": False,1707    }1708    assert payload == expected17091710    # Test we ignore reasoning blocks from other providers1711    reasoning_messages: list = [1712        {1713            "role": "user",1714            "content": [1715                {"type": "reasoning_content", "reasoning_content": "reasoning..."},1716                {"type": "text", "text": "reasoned response"},1717            ],1718        },1719        {1720            "role": "user",1721            "content": [1722                {"type": "thinking", "thinking": "thinking..."},1723                {"type": "text", "text": "thoughtful response"},1724            ],1725        },1726    ]1727    expected = {1728        "messages": [1729            {1730                "role": "user",1731                "content": [{"type": "text", "text": "reasoned response"}],1732            },1733            {1734                "role": "user",1735                "content": [{"type": "text", "text": "thoughtful response"}],1736            },1737        ],1738        "model": "o3",1739        "stream": False,1740    }1741    payload = llm._get_request_payload(reasoning_messages)1742    assert payload == expected174317441745def test_sanitize_chat_completions_text_blocks() -> None:1746    messages = [1747        ToolMessage(1748            content=[{"type": "text", "text": "foo", "id": "lc_abc123"}],1749            tool_call_id="def456",1750        ),1751    ]1752    payload = ChatOpenAI(model="gpt-5.2")._get_request_payload(messages)1753    assert payload["messages"] == [1754        {1755            "content": [{"type": "text", "text": "foo"}],1756            "role": "tool",1757            "tool_call_id": "def456",1758        }1759    ]176017611762def test_init_o1() -> None:1763    with warnings.catch_warnings(record=True) as record:1764        warnings.simplefilter("error")  # Treat warnings as errors1765        ChatOpenAI(model=OPENAI_TEST_MODEL, reasoning_effort="medium")17661767    assert len(record) == 0176817691770def test_init_minimal_reasoning_effort() -> None:1771    with warnings.catch_warnings(record=True) as record:1772        warnings.simplefilter("error")1773        ChatOpenAI(model="gpt-5", reasoning_effort="minimal")17741775    assert len(record) == 0177617771778@pytest.mark.parametrize("use_responses_api", [False, True])1779@pytest.mark.parametrize("use_max_completion_tokens", [True, False])1780def test_minimal_reasoning_effort_payload(1781    use_max_completion_tokens: bool, use_responses_api: bool1782) -> None:1783    """Test that minimal reasoning effort is included in request payload."""1784    if use_max_completion_tokens:1785        kwargs = {"max_completion_tokens": 100}1786    else:1787        kwargs = {"max_tokens": 100}17881789    init_kwargs: dict[str, Any] = {1790        "model": "gpt-5",1791        "reasoning_effort": "minimal",1792        "use_responses_api": use_responses_api,1793        **kwargs,1794    }17951796    llm = ChatOpenAI(**init_kwargs)17971798    messages = [1799        {"role": "developer", "content": "respond with just 'test'"},1800        {"role": "user", "content": "hello"},1801    ]18021803    payload = llm._get_request_payload(messages, stop=None)18041805    # When using responses API, reasoning_effort becomes reasoning.effort1806    if use_responses_api:1807        assert "reasoning" in payload1808        assert payload["reasoning"] == {"effort": "minimal"}1809        # For responses API, tokens param becomes max_output_tokens1810        assert payload["max_output_tokens"] == 1001811    else:1812        # For non-responses API, reasoning_effort remains as is1813        assert payload["reasoning_effort"] == "minimal"1814        if use_max_completion_tokens:1815            assert payload["max_completion_tokens"] == 1001816        else:1817            # max_tokens gets converted to max_completion_tokens in non-responses API1818            assert payload["max_completion_tokens"] == 100181918201821@pytest.mark.parametrize("via_invoke", [False, True])1822def test_responses_api_payload_excludes_stop(via_invoke: bool) -> None:1823    """The Responses API rejects `stop`, so it must be dropped from the payload.18241825    Covers `stop` supplied both at construction time and at invoke time, since1826    they reach the payload through different code paths.1827    """1828    if via_invoke:1829        llm = ChatOpenAI(model=OPENAI_TEST_MODEL, use_responses_api=True)1830        payload = llm._get_request_payload(1831            [HumanMessage(content="Hello")], stop=["END"]1832        )1833    else:1834        llm = ChatOpenAI(  # type: ignore[call-arg]1835            model=OPENAI_TEST_MODEL, stop=["END"], use_responses_api=True1836        )1837        payload = llm._get_request_payload([HumanMessage(content="Hello")])18381839    assert "stop" not in payload184018411842def test_chat_completions_payload_includes_stop() -> None:1843    """`stop` must be preserved for the Chat Completions API, which supports it.18441845    Guards against an over-broad change dropping `stop` outside the Responses API.1846    """1847    llm = ChatOpenAI(model=OPENAI_TEST_MODEL, stop=["END"])  # type: ignore[call-arg]18481849    payload = llm._get_request_payload([HumanMessage(content="Hello")])18501851    assert payload["stop"] == ["END"]185218531854def test_output_version_compat() -> None:1855    llm = ChatOpenAI(model="gpt-5", output_version="responses/v1")1856    assert llm._use_responses_api({}) is True185718581859def test_convert_chunk_to_generation_chunk_v1_keeps_string_content() -> None:1860    """v1 streaming keeps content as '' (not []) and stamps output_version.18611862    Covers both the usage-only (empty-choices) chunk and a content-bearing1863    chunk carrying a tool-call delta; the latter pins the per-content-chunk1864    `output_version` propagation.1865    """1866    llm = ChatOpenAI(model="gpt-4o", output_version="v1")18671868    # Empty-choices chunk (usage-only)1869    empty_chunk: dict[str, Any] = {1870        "id": "chatcmpl-test",1871        "object": "chat.completion.chunk",1872        "created": 0,1873        "model": "gpt-4o",1874        "choices": [],1875        "usage": {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8},1876    }1877    gen = llm._convert_chunk_to_generation_chunk(empty_chunk, AIMessageChunk, None)1878    assert gen is not None1879    assert gen.message.content == ""  # NOT []1880    assert gen.message.response_metadata.get("output_version") == "v1"18811882    # Content-bearing chunk with tool_call delta1883    tool_chunk: dict[str, Any] = {1884        "id": "chatcmpl-test",1885        "object": "chat.completion.chunk",1886        "created": 0,1887        "model": "gpt-4o",1888        "choices": [1889            {1890                "index": 0,1891                "delta": {1892                    "role": "assistant",1893                    "content": "",1894                    "tool_calls": [1895                        {1896                            "index": 0,1897                            "id": "call_abc",1898                            "function": {"name": "get_weather", "arguments": ""},1899                        }1900                    ],1901                },1902                "logprobs": None,1903                "finish_reason": None,1904            }1905        ],1906        "usage": None,1907    }1908    gen = llm._convert_chunk_to_generation_chunk(tool_chunk, AIMessageChunk, None)1909    assert gen is not None1910    assert isinstance(gen.message.content, str)1911    assert gen.message.response_metadata.get("output_version") == "v1"1912    assert gen.message.response_metadata.get("model_provider") == "openai"191319141915def test_v1_streaming_tool_calls_in_content_blocks() -> None:1916    """End-to-end: streaming chunks with tool calls produce correct content_blocks."""1917    stream_chunks: list[dict[str, Any]] = [1918        # Initial empty-choices chunk1919        {1920            "id": "chatcmpl-test",1921            "object": "chat.completion.chunk",1922            "created": 0,1923            "model": "gpt-4o",1924            "choices": [1925                {1926                    "index": 0,1927                    "delta": {"role": "assistant", "content": ""},1928                    "logprobs": None,1929                    "finish_reason": None,1930                }1931            ],1932            "usage": None,1933        },1934        # Text token streamed before the tool call1935        {1936            "id": "chatcmpl-test",1937            "object": "chat.completion.chunk",1938            "created": 0,1939            "model": "gpt-4o",1940            "choices": [1941                {1942                    "index": 0,1943                    "delta": {"content": "Let me check the weather."},1944                    "logprobs": None,1945                    "finish_reason": None,1946                }1947            ],1948            "usage": None,1949        },1950        # Tool call start1951        {1952            "id": "chatcmpl-test",1953            "object": "chat.completion.chunk",1954            "created": 0,1955            "model": "gpt-4o",1956            "choices": [1957                {1958                    "index": 0,1959                    "delta": {1960                        "tool_calls": [1961                            {1962                                "index": 0,1963                                "id": "call_abc",1964                                "function": {1965                                    "name": "get_weather",1966                                    "arguments": '{"loc',1967                                },1968                            }1969                        ]1970                    },1971                    "logprobs": None,1972                    "finish_reason": None,1973                }1974            ],1975            "usage": None,1976        },1977        # Tool call args continuation1978        {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                        "tool_calls": [1988                            {1989                                "index": 0,1990                                "function": {"arguments": 'ation": "SF"}'},1991                            }1992                        ]1993                    },1994                    "logprobs": None,1995                    "finish_reason": None,1996                }1997            ],1998            "usage": None,1999        },2000        # Finish

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.

Generated by searchcode.com · powered by code_get_file + code_get_findings