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.