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