1"""OpenAI chat wrapper.23!!! warning "API scope"45 `ChatOpenAI` targets6 [official OpenAI API specifications](https://github.com/openai/openai-openapi)7 only. Non-standard response fields added by third-party providers (e.g.,8 `reasoning_content`, `reasoning_details`) are **not** extracted or9 preserved. If you are pointing `base_url` at a provider such as10 OpenRouter, vLLM, or DeepSeek, use the corresponding provider-specific11 LangChain package instead (e.g., `ChatDeepSeek`, `ChatOpenRouter`).12"""1314from __future__ import annotations1516import base6417import json18import logging19import os20import re21import ssl22import sys23import warnings24from collections.abc import (25 AsyncIterator,26 Awaitable,27 Callable,28 Iterator,29 Mapping,30 Sequence,31)32from functools import partial33from io import BytesIO34from json import JSONDecodeError35from math import ceil36from operator import itemgetter37from typing import (38 TYPE_CHECKING,39 Any,40 Literal,41 TypeAlias,42 TypeVar,43 cast,44)45from urllib.parse import urlparse4647import certifi48import openai49import tiktoken50from langchain_core.callbacks import (51 AsyncCallbackManagerForLLMRun,52 CallbackManagerForLLMRun,53)54from langchain_core.exceptions import ContextOverflowError55from langchain_core.language_models import (56 LanguageModelInput,57 ModelProfileRegistry,58)59from langchain_core.language_models.chat_models import (60 BaseChatModel,61 LangSmithParams,62)63from langchain_core.messages import (64 AIMessage,65 AIMessageChunk,66 BaseMessage,67 BaseMessageChunk,68 ChatMessage,69 ChatMessageChunk,70 FunctionMessage,71 FunctionMessageChunk,72 HumanMessage,73 HumanMessageChunk,74 InvalidToolCall,75 SystemMessage,76 SystemMessageChunk,77 ToolCall,78 ToolMessage,79 ToolMessageChunk,80 is_data_content_block,81)82from langchain_core.messages import content as types83from langchain_core.messages.ai import (84 InputTokenDetails,85 OutputTokenDetails,86 UsageMetadata,87)88from langchain_core.messages.block_translators.openai import (89 _convert_from_v03_ai_message,90 convert_to_openai_data_block,91)92from langchain_core.messages.tool import tool_call_chunk93from langchain_core.output_parsers import JsonOutputParser, PydanticOutputParser94from langchain_core.output_parsers.openai_tools import (95 JsonOutputKeyToolsParser,96 PydanticToolsParser,97 make_invalid_tool_call,98 parse_tool_call,99)100from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult101from langchain_core.runnables import (102 Runnable,103 RunnableLambda,104 RunnableMap,105 RunnablePassthrough,106)107from langchain_core.runnables.config import run_in_executor108from langchain_core.tools import BaseTool109from langchain_core.tools.base import _stringify110from langchain_core.utils import get_pydantic_field_names111from langchain_core.utils.function_calling import (112 convert_to_openai_function,113 convert_to_openai_tool,114)115from langchain_core.utils.pydantic import (116 PydanticBaseModel,117 TypeBaseModel,118 is_basemodel_subclass,119)120from langchain_core.utils.utils import _build_model_kwargs, from_env, secret_from_env121from pydantic import (122 BaseModel,123 ConfigDict,124 Field,125 SecretStr,126 ValidationError,127 field_validator,128 model_validator,129)130from pydantic.v1 import BaseModel as BaseModelV1131from typing_extensions import Self132133from langchain_openai._version import __version__134from langchain_openai.chat_models._client_utils import (135 _astream_with_chunk_timeout,136 _build_proxied_async_httpx_client,137 _build_proxied_sync_httpx_client,138 _float_env,139 _get_default_async_httpx_client,140 _get_default_httpx_client,141 _log_proxy_env_bypass_once,142 _resolve_socket_options,143 _resolve_sync_and_async_api_keys,144 _should_bypass_socket_options_for_proxy_env,145 _warn_if_proxy_env_shadowed,146)147from langchain_openai.chat_models._compat import (148 _convert_from_v1_to_chat_completions,149 _convert_from_v1_to_responses,150 _convert_to_v03_ai_message,151)152from langchain_openai.data._profiles import _PROFILES153154if TYPE_CHECKING:155 import httpx156 from langchain_core.language_models import ModelProfile157 from openai.types.responses import Response158159logger = logging.getLogger(__name__)160161# This SSL context is equivalent to the default `verify=True`.162# https://www.python-httpx.org/advanced/ssl/#configuring-client-instances163global_ssl_context = ssl.create_default_context(cafile=certifi.where())164165_ssrf_client: httpx.Client | None = None166167168def _get_ssrf_safe_client() -> httpx.Client:169 global _ssrf_client170 if _ssrf_client is None:171 from langchain_core._security._transport import ssrf_safe_client172173 _ssrf_client = ssrf_safe_client(174 verify=global_ssl_context, follow_redirects=False175 )176 return _ssrf_client177178179_MODEL_PROFILES = cast(ModelProfileRegistry, _PROFILES)180181182def _get_default_model_profile(model_name: str) -> ModelProfile:183 default = _MODEL_PROFILES.get(model_name) or {}184 return default.copy()185186187WellKnownTools = (188 "file_search",189 "web_search_preview",190 "web_search",191 "computer_use_preview",192 "code_interpreter",193 "mcp",194 "image_generation",195 "tool_search",196 "apply_patch",197)198199200def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage:201 """Convert a dictionary to a LangChain message.202203 Args:204 _dict: The dictionary.205206 Returns:207 The LangChain message.208 """209 role = _dict.get("role")210 name = _dict.get("name")211 id_ = _dict.get("id")212 if role == "user":213 return HumanMessage(content=_dict.get("content", ""), id=id_, name=name)214 if role == "assistant":215 # Fix for azure216 # Also OpenAI returns None for tool invocations217 content = _dict.get("content", "") or ""218 additional_kwargs: dict = {}219 if function_call := _dict.get("function_call"):220 additional_kwargs["function_call"] = dict(function_call)221 tool_calls = []222 invalid_tool_calls = []223 if raw_tool_calls := _dict.get("tool_calls"):224 for raw_tool_call in raw_tool_calls:225 try:226 tool_calls.append(parse_tool_call(raw_tool_call, return_id=True))227 except Exception as e:228 invalid_tool_calls.append(229 make_invalid_tool_call(raw_tool_call, str(e))230 )231 if audio := _dict.get("audio"):232 additional_kwargs["audio"] = audio233 return AIMessage(234 content=content,235 additional_kwargs=additional_kwargs,236 name=name,237 id=id_,238 tool_calls=tool_calls,239 invalid_tool_calls=invalid_tool_calls,240 )241 if role in ("system", "developer"):242 additional_kwargs = {"__openai_role__": role} if role == "developer" else {}243 return SystemMessage(244 content=_dict.get("content", ""),245 name=name,246 id=id_,247 additional_kwargs=additional_kwargs,248 )249 if role == "function":250 return FunctionMessage(251 content=_dict.get("content", ""), name=cast(str, _dict.get("name")), id=id_252 )253 if role == "tool":254 additional_kwargs = {}255 if "name" in _dict:256 additional_kwargs["name"] = _dict["name"]257 return ToolMessage(258 content=_dict.get("content", ""),259 tool_call_id=cast(str, _dict.get("tool_call_id")),260 additional_kwargs=additional_kwargs,261 name=name,262 id=id_,263 )264 return ChatMessage(content=_dict.get("content", ""), role=role, id=id_) # type: ignore[arg-type]265266267def _apply_prompt_cache_breakpoint(268 source_block: dict[str, Any], formatted_block: dict[str, Any]269) -> dict[str, Any]:270 """Apply an OpenAI prompt cache breakpoint to a formatted content block.271272 A breakpoint set directly on the block takes precedence over one nested in273 `extras`. Membership (not truthiness) decides whether to copy it, so a274 present-but-falsy value (e.g. `None`) is still preserved.275 """276 if "prompt_cache_breakpoint" in source_block:277 formatted_block["prompt_cache_breakpoint"] = source_block[278 "prompt_cache_breakpoint"279 ]280 elif isinstance(extras := source_block.get("extras"), dict) and (281 "prompt_cache_breakpoint" in extras282 ):283 formatted_block["prompt_cache_breakpoint"] = extras["prompt_cache_breakpoint"]284 return formatted_block285286287def _sanitize_chat_completions_content(content: str | list[dict]) -> str | list[dict]:288 """Sanitize content for chat/completions API.289290 For list content, filters text blocks to only keep supported keys.291 """292 if isinstance(content, list):293 sanitized = []294 for block in content:295 if (296 isinstance(block, dict)297 and block.get("type") == "text"298 and "text" in block299 ):300 sanitized_block = {"type": "text", "text": block["text"]}301 if "prompt_cache_breakpoint" in block:302 sanitized_block["prompt_cache_breakpoint"] = block[303 "prompt_cache_breakpoint"304 ]305 sanitized.append(sanitized_block)306 else:307 sanitized.append(block)308 return sanitized309 return content310311312def _format_message_content(313 content: Any,314 api: Literal["chat/completions", "responses"] = "chat/completions",315 role: str | None = None,316) -> Any:317 """Format message content."""318 if content and isinstance(content, list):319 formatted_content = []320 for block in content:321 # Remove unexpected block types322 if (323 isinstance(block, dict)324 and "type" in block325 and (326 block["type"] in ("tool_use", "thinking", "reasoning_content")327 or (328 block["type"] in ("function_call", "code_interpreter_call")329 and api == "chat/completions"330 )331 )332 ):333 continue334 if (335 isinstance(block, dict)336 and is_data_content_block(block)337 # Responses API messages handled separately in _compat (parsed into338 # image generation calls)339 and not (api == "responses" and str(role).lower().startswith("ai"))340 ):341 formatted_block = convert_to_openai_data_block(block, api=api)342 formatted_content.append(343 _apply_prompt_cache_breakpoint(block, formatted_block)344 )345 elif (346 isinstance(block, dict)347 and block.get("type") == "text"348 and "text" in block349 and isinstance(extras := block.get("extras"), dict)350 and "prompt_cache_breakpoint" in extras351 ):352 formatted_block = {"type": "text", "text": block["text"]}353 formatted_content.append(354 _apply_prompt_cache_breakpoint(block, formatted_block)355 )356 # Anthropic image blocks357 elif (358 isinstance(block, dict)359 and block.get("type") == "image"360 and (source := block.get("source"))361 and isinstance(source, dict)362 ):363 if source.get("type") == "base64" and (364 (media_type := source.get("media_type"))365 and (data := source.get("data"))366 ):367 formatted_content.append(368 {369 "type": "image_url",370 "image_url": {"url": f"data:{media_type};base64,{data}"},371 }372 )373 elif source.get("type") == "url" and (url := source.get("url")):374 formatted_content.append(375 {"type": "image_url", "image_url": {"url": url}}376 )377 else:378 continue379 else:380 formatted_content.append(block)381 else:382 formatted_content = content383384 return formatted_content385386387def _convert_message_to_dict(388 message: BaseMessage,389 api: Literal["chat/completions", "responses"] = "chat/completions",390) -> dict:391 """Convert a LangChain message to dictionary format expected by OpenAI."""392 message_dict: dict[str, Any] = {393 "content": _format_message_content(message.content, api=api, role=message.type)394 }395 if (name := message.name or message.additional_kwargs.get("name")) is not None:396 message_dict["name"] = name397398 # populate role and additional message data399 if isinstance(message, ChatMessage):400 message_dict["role"] = message.role401 elif isinstance(message, HumanMessage):402 message_dict["role"] = "user"403 elif isinstance(message, AIMessage):404 message_dict["role"] = "assistant"405 if message.tool_calls or message.invalid_tool_calls:406 message_dict["tool_calls"] = [407 _lc_tool_call_to_openai_tool_call(tc) for tc in message.tool_calls408 ] + [409 _lc_invalid_tool_call_to_openai_tool_call(tc)410 for tc in message.invalid_tool_calls411 ]412 elif "tool_calls" in message.additional_kwargs:413 message_dict["tool_calls"] = message.additional_kwargs["tool_calls"]414 tool_call_supported_props = {"id", "type", "function"}415 message_dict["tool_calls"] = [416 {k: v for k, v in tool_call.items() if k in tool_call_supported_props}417 for tool_call in message_dict["tool_calls"]418 ]419 elif "function_call" in message.additional_kwargs:420 # OpenAI raises 400 if both function_call and tool_calls are present in the421 # same message.422 message_dict["function_call"] = message.additional_kwargs["function_call"]423 else:424 pass425 # If tool calls present, content null value should be None not empty string.426 if "function_call" in message_dict or "tool_calls" in message_dict:427 message_dict["content"] = message_dict["content"] or None428429 audio: dict[str, Any] | None = None430 for block in message.content:431 if (432 isinstance(block, dict)433 and block.get("type") == "audio"434 and (id_ := block.get("id"))435 and api != "responses"436 ):437 # openai doesn't support passing the data back - only the id438 # https://platform.openai.com/docs/guides/audio/multi-turn-conversations439 audio = {"id": id_}440 if not audio and "audio" in message.additional_kwargs:441 raw_audio = message.additional_kwargs["audio"]442 audio = (443 {"id": message.additional_kwargs["audio"]["id"]}444 if "id" in raw_audio445 else raw_audio446 )447 if audio:448 message_dict["audio"] = audio449 elif isinstance(message, SystemMessage):450 message_dict["role"] = message.additional_kwargs.get(451 "__openai_role__", "system"452 )453 elif isinstance(message, FunctionMessage):454 message_dict["role"] = "function"455 elif isinstance(message, ToolMessage):456 message_dict["role"] = "tool"457 message_dict["tool_call_id"] = message.tool_call_id458 message_dict["content"] = _sanitize_chat_completions_content(459 message_dict["content"]460 )461 supported_props = {"content", "role", "tool_call_id"}462 message_dict = {k: v for k, v in message_dict.items() if k in supported_props}463 else:464 msg = f"Got unknown type {message}"465 raise TypeError(msg)466 return message_dict467468469def _convert_delta_to_message_chunk(470 _dict: Mapping[str, Any], default_class: type[BaseMessageChunk]471) -> BaseMessageChunk:472 """Convert to a LangChain message chunk."""473 id_ = _dict.get("id")474 role = cast(str, _dict.get("role"))475 content = cast(str, _dict.get("content") or "")476 additional_kwargs: dict = {}477 if _dict.get("function_call"):478 function_call = dict(_dict["function_call"])479 if "name" in function_call and function_call["name"] is None:480 function_call["name"] = ""481 additional_kwargs["function_call"] = function_call482 tool_call_chunks = []483 if raw_tool_calls := _dict.get("tool_calls"):484 try:485 tool_call_chunks = [486 tool_call_chunk(487 name=rtc["function"].get("name"),488 args=rtc["function"].get("arguments"),489 id=rtc.get("id"),490 index=rtc["index"],491 )492 for rtc in raw_tool_calls493 ]494 except KeyError:495 pass496497 if role == "user" or default_class == HumanMessageChunk:498 return HumanMessageChunk(content=content, id=id_)499 if role == "assistant" or default_class == AIMessageChunk:500 return AIMessageChunk(501 content=content,502 additional_kwargs=additional_kwargs,503 id=id_,504 tool_call_chunks=tool_call_chunks, # type: ignore[arg-type]505 )506 if role in ("system", "developer") or default_class == SystemMessageChunk:507 if role == "developer":508 additional_kwargs = {"__openai_role__": "developer"}509 else:510 additional_kwargs = {}511 return SystemMessageChunk(512 content=content, id=id_, additional_kwargs=additional_kwargs513 )514 if role == "function" or default_class == FunctionMessageChunk:515 return FunctionMessageChunk(content=content, name=_dict["name"], id=id_)516 if role == "tool" or default_class == ToolMessageChunk:517 return ToolMessageChunk(518 content=content, tool_call_id=_dict["tool_call_id"], id=id_519 )520 if role or default_class == ChatMessageChunk:521 return ChatMessageChunk(content=content, role=role, id=id_)522 return default_class(content=content, id=id_) # type: ignore[call-arg]523524525def _update_token_usage(526 overall_token_usage: int | dict, new_usage: int | dict527) -> int | dict:528 # Token usage is either ints or dictionaries529 # `reasoning_tokens` is nested inside `completion_tokens_details`530 if isinstance(new_usage, int):531 if not isinstance(overall_token_usage, int):532 msg = (533 f"Got different types for token usage: "534 f"{type(new_usage)} and {type(overall_token_usage)}"535 )536 raise ValueError(msg)537 return new_usage + overall_token_usage538 if isinstance(new_usage, dict):539 if not isinstance(overall_token_usage, dict):540 msg = (541 f"Got different types for token usage: "542 f"{type(new_usage)} and {type(overall_token_usage)}"543 )544 raise ValueError(msg)545 return {546 k: _update_token_usage(overall_token_usage.get(k, 0), v)547 for k, v in new_usage.items()548 }549 warnings.warn(f"Unexpected type for token usage: {type(new_usage)}")550 return new_usage551552553class OpenAIContextOverflowError(openai.BadRequestError, ContextOverflowError):554 """BadRequestError raised when input exceeds OpenAI's context limit."""555556557class OpenAIAPIContextOverflowError(openai.APIError, ContextOverflowError):558 """APIError raised when input exceeds OpenAI's context limit."""559560561def _handle_openai_bad_request(e: openai.BadRequestError) -> None:562 if (563 "context_length_exceeded" in str(e)564 or "Input tokens exceed the configured limit" in e.message565 or "prompt is too long" in e.message566 ):567 raise OpenAIContextOverflowError(568 message=e.message, response=e.response, body=e.body569 ) from e570 if (571 "'response_format' of type 'json_schema' is not supported with this model"572 ) in e.message:573 message = (574 "This model does not support OpenAI's structured output feature, which "575 "is the default method for `with_structured_output` as of "576 "langchain-openai==0.3. To use `with_structured_output` with this model, "577 'specify `method="function_calling"`.'578 )579 warnings.warn(message)580 raise e581 if "Invalid schema for response_format" in e.message:582 message = (583 "Invalid schema for OpenAI's structured output feature, which is the "584 "default method for `with_structured_output` as of langchain-openai==0.3. "585 'Specify `method="function_calling"` instead or update your schema. '586 "See supported schemas: "587 "https://platform.openai.com/docs/guides/structured-outputs#supported-schemas"588 )589 warnings.warn(message)590 raise e591 raise592593594def _handle_openai_api_error(e: openai.APIError) -> None:595 error_message = str(e)596 if "exceeds the context window" in error_message:597 raise OpenAIAPIContextOverflowError(598 message=e.message, request=e.request, body=e.body599 ) from e600 raise601602603_RESPONSES_API_ONLY_PREFIXES = (604 "gpt-5-pro",605 "gpt-5.2-pro",606 "gpt-5.4-pro",607 "gpt-5.5-pro",608)609610611def _model_prefers_responses_api(model_name: str | None) -> bool:612 if not model_name:613 return False614 return model_name.startswith(_RESPONSES_API_ONLY_PREFIXES) or "codex" in model_name615616617_BM = TypeVar("_BM", bound=BaseModel)618_DictOrPydanticClass: TypeAlias = dict[str, Any] | type[_BM] | type619_DictOrPydantic: TypeAlias = dict | _BM620621622class BaseChatOpenAI(BaseChatModel):623 """Base wrapper around OpenAI large language models for chat.624625 This base class targets626 [official OpenAI API specifications](https://github.com/openai/openai-openapi)627 only. Non-standard response fields added by third-party providers (e.g.,628 `reasoning_content`) are not extracted. Use a provider-specific subclass for629 full provider support.630 """631632 client: Any = Field(default=None, exclude=True)633634 async_client: Any = Field(default=None, exclude=True)635636 root_client: Any = Field(default=None, exclude=True)637638 root_async_client: Any = Field(default=None, exclude=True)639640 model_name: str = Field(default="gpt-3.5-turbo", alias="model")641 """Model name to use."""642643 temperature: float | None = None644 """What sampling temperature to use."""645646 model_kwargs: dict[str, Any] = Field(default_factory=dict)647 """Holds any model parameters valid for `create` call not explicitly specified."""648649 openai_api_key: (650 SecretStr | None | Callable[[], str] | Callable[[], Awaitable[str]]651 ) = Field(652 alias="api_key", default_factory=secret_from_env("OPENAI_API_KEY", default=None)653 )654 """API key to use.655656 Can be inferred from the `OPENAI_API_KEY` environment variable, or specified657 as a string, or sync or async callable that returns a string.658659 ??? example "Specify with environment variable"660661 ```bash662 export OPENAI_API_KEY=...663 ```664 ```python665 from langchain_openai import ChatOpenAI666667 model = ChatOpenAI(model="gpt-5-nano")668 ```669670 ??? example "Specify with a string"671672 ```python673 from langchain_openai import ChatOpenAI674675 model = ChatOpenAI(model="gpt-5-nano", api_key="...")676 ```677678 ??? example "Specify with a sync callable"679680 ```python681 from langchain_openai import ChatOpenAI682683 def get_api_key() -> str:684 # Custom logic to retrieve API key685 return "..."686687 model = ChatOpenAI(model="gpt-5-nano", api_key=get_api_key)688 ```689690 ??? example "Specify with an async callable"691692 ```python693 from langchain_openai import ChatOpenAI694695 async def get_api_key() -> str:696 # Custom async logic to retrieve API key697 return "..."698699 model = ChatOpenAI(model="gpt-5-nano", api_key=get_api_key)700 ```701 """702703 openai_api_base: str | None = Field(default=None, alias="base_url")704 """Base URL path for API requests, leave blank if not using a proxy or service emulator.705706 Resolution order (first match wins):707708 1. Explicit `base_url` (or `openai_api_base`) kwarg.709 2. Env var `OPENAI_API_BASE` (read by LangChain at init).710 3. Env var `OPENAI_BASE_URL` (read by the underlying `openai` SDK client).711712 `OPENAI_BASE_URL` is also inspected by LangChain only to decide whether to713 default-enable `stream_usage` — when set, the default is left off because many714 non-OpenAI endpoints do not support streaming token usage.715 """ # noqa: E501716717 openai_organization: str | None = Field(default=None, alias="organization")718 """Automatically inferred from env var `OPENAI_ORG_ID` if not provided."""719720 # to support explicit proxy for OpenAI721 openai_proxy: str | None = Field(722 default_factory=from_env("OPENAI_PROXY", default=None)723 )724725 request_timeout: float | tuple[float, float] | Any | None = Field(726 default=None, alias="timeout"727 )728 """Timeout for requests to OpenAI completion API.729730 Can be float, `httpx.Timeout` or `None`.731 """732733 stream_usage: bool | None = None734 """Whether to include usage metadata in streaming output.735736 If enabled, an additional message chunk will be generated during the stream737 including usage metadata.738739 This parameter is enabled unless `openai_api_base` is set or the model is740 initialized with a custom client, as many chat completions APIs do not741 support streaming token usage.742743 !!! version-added "Added in `langchain-openai` 0.3.9"744745 !!! warning "Behavior changed in `langchain-openai` 0.3.35"746747 Enabled for default base URL and client.748 """749750 max_retries: int | None = None751 """Maximum number of retries to make when generating."""752753 presence_penalty: float | None = None754 """Penalizes repeated tokens."""755756 frequency_penalty: float | None = None757 """Penalizes repeated tokens according to frequency."""758759 seed: int | None = None760 """Seed for generation"""761762 logprobs: bool | None = None763 """Whether to return logprobs."""764765 top_logprobs: int | None = None766 """Number of most likely tokens to return at each token position, each with an767 associated log probability.768769 `logprobs` must be set to true if this parameter is used.770 """771772 logit_bias: dict[int, int] | None = None773 """Modify the likelihood of specified tokens appearing in the completion."""774775 streaming: bool = False776 """Whether to stream the results or not."""777778 n: int | None = None779 """Number of chat completions to generate for each prompt."""780781 top_p: float | None = None782 """Total probability mass of tokens to consider at each step."""783784 max_tokens: int | None = Field(default=None)785 """Maximum number of tokens to generate."""786787 reasoning_effort: str | None = None788 """Constrains effort on reasoning for reasoning models.789790 For use with the Chat Completions API. Reasoning models only.791792 Currently supported values are `'minimal'`, `'low'`, `'medium'`, and793 `'high'`. Reducing reasoning effort can result in faster responses and fewer794 tokens used on reasoning in a response.795 """796797 reasoning: dict[str, Any] | None = None798 """Reasoning parameters for reasoning models. None disables reasoning.799800 For use with the Responses API.801802 ```python803 reasoning={804 "effort": None, # Default None; can be "low", "medium", or "high"805 "summary": "auto", # Can be "auto", "concise", or "detailed"806 }807 ```808809 !!! version-added "Added in `langchain-openai` 0.3.24"810 """811812 verbosity: str | None = None813 """Controls the verbosity level of responses for reasoning models.814815 For use with the Responses API.816817 Currently supported values are `'low'`, `'medium'`, and `'high'`.818819 !!! version-added "Added in `langchain-openai` 0.3.28"820 """821822 tiktoken_model_name: str | None = None823 """The model name to pass to tiktoken when using this class.824825 Tiktoken is used to count the number of tokens in documents to constrain826 them to be under a certain limit.827828 By default, when set to `None`, this will be the same as the embedding model name.829 However, there are some cases where you may want to use this `Embedding` class with830 a model name not supported by tiktoken. This can include when using Azure embeddings831 or when using one of the many model providers that expose an OpenAI-like832 API but with different models. In those cases, in order to avoid erroring833 when tiktoken is called, you can specify a model name to use here.834 """835836 default_headers: Mapping[str, str] | None = None837838 default_query: Mapping[str, object] | None = None839840 # Configure a custom httpx client. See the841 # [httpx documentation](https://www.python-httpx.org/api/#client) for more details.842 http_client: Any | None = Field(default=None, exclude=True)843 """Optional `httpx.Client`.844845 Only used for sync invocations. Must specify `http_async_client` as well if846 you'd like a custom client for async invocations.847 """848849 http_async_client: Any | None = Field(default=None, exclude=True)850 """Optional `httpx.AsyncClient`.851852 Only used for async invocations. Must specify `http_client` as well if you'd853 like a custom client for sync invocations.854 """855856 http_socket_options: Sequence[tuple[int, int, int]] | None = Field(857 default=None, exclude=True858 )859 """TCP socket options applied to the httpx transports built by this instance.860861 Defaults to a conservative TCP-keepalive + `TCP_USER_TIMEOUT` profile that862 targets a ~2-minute bound on silent connection hangs (silent mid-stream peer863 loss, gVisor/NAT idle timeouts, silent TCP black holes) on platforms that864 support the full option set. On platforms that only support a subset865 (macOS without `TCP_USER_TIMEOUT`, Windows with only `SO_KEEPALIVE`,866 minimal kernels), unsupported options are silently dropped and the bound867 degrades to whatever the remaining options + OS defaults provide — still868 better than indefinite hang.869870 Accepted values:871872 - `None` (default): use env-driven defaults. Matches the "unset" convention873 used by `http_client` elsewhere on this class.874 - `()` (empty): disable socket-option injection entirely. Inherits the OS875 defaults and restores httpx's native env-proxy auto-detection.876 - A non-empty sequence of `(level, option, value)` tuples: explicit877 override; passed verbatim to the transport (not filtered). Unsupported878 options raise `OSError` at connect time rather than being silently879 dropped — the user chose them explicitly.880881 Environment variables (only consulted when this field is `None`):882 `LANGCHAIN_OPENAI_TCP_KEEPALIVE` (set to `0` to disable entirely — the883 kill-switch), `LANGCHAIN_OPENAI_TCP_KEEPIDLE`,884 `LANGCHAIN_OPENAI_TCP_KEEPINTVL`, `LANGCHAIN_OPENAI_TCP_KEEPCNT`,885 `LANGCHAIN_OPENAI_TCP_USER_TIMEOUT_MS`.886887 Applied per side: if `http_client` is supplied, the sync path uses888 that user-owned client's socket options as-is; the async path still889 gets `http_socket_options` applied to its default builder (and890 vice-versa for `http_async_client`). Supply both to take full control.891892 !!! note "Interaction with env-proxy auto-detection"893894 When a custom `httpx` transport is active, `httpx` disables its895 native env-proxy auto-detection (`HTTP_PROXY` / `HTTPS_PROXY` /896 `ALL_PROXY` / `NO_PROXY` and macOS/Windows system proxy settings).897898 To keep the default shape safe, `ChatOpenAI` detects the899 "proxy-env-shadow" pattern and **skips the custom transport900 entirely** when **all** of the following hold:901902 - `http_socket_options` is left at its default (`None`)903 - No `http_client` or `http_async_client` supplied904 - No `openai_proxy` supplied905 - A proxy env var or system proxy is visible to httpx906907 On that specific shape, the instance falls back to pre-PR behavior908 and httpx's env-proxy auto-detection applies (a one-time `INFO` log909 records the bypass for observability).910911 If you explicitly set `http_socket_options=[...]` while a proxy912 env var is also set, no bypass — you opted into the transport, and913 a one-time `WARNING` records the shadowing. Set914 `http_socket_options=()` or `LANGCHAIN_OPENAI_TCP_KEEPALIVE=0` to915 disable transport injection explicitly, or pass a fully-configured916 `http_async_client` / `http_client` to take full control. The917 `openai_proxy` constructor kwarg is unaffected — socket options918 are applied cleanly through the proxied transport on that path.919 """920921 stream_chunk_timeout: float | None = Field(922 default_factory=lambda: _float_env(923 "LANGCHAIN_OPENAI_STREAM_CHUNK_TIMEOUT_S", 120.0924 ),925 exclude=True,926 )927 """Per-chunk wall-clock timeout (seconds) on async streaming responses.928929 Applies to async invocations only (`astream`, `ainvoke` with streaming,930 etc.). Sync streaming (`stream`) is not affected.931932 Fires between content chunks yielded by the openai SDK's streaming iterator933 (i.e., each call to `__anext__` on the response). Crucially, this is934 **not** the same as httpx's `timeout.read`:935936 - httpx's read timeout is inter-byte and gets reset every time *any* bytes937 arrive on the socket — including OpenAI's SSE keepalive comments938 (`: keepalive`) that trickle down during long model generations. A939 stream that's silent on *content* but still producing keepalives looks940 alive forever to httpx.941 - `stream_chunk_timeout` measures the gap between *parsed chunks*. The942 openai SDK's SSE parser consumes keepalive comments internally and does943 not emit them as chunks, so keepalives do *not* reset this timer. It944 fires on genuine content silence.945946 When it fires, a `StreamChunkTimeoutError`947 (subclass of `asyncio.TimeoutError`) is raised with a self-describing948 message naming this knob, the env-var override, the model, and the949 number of chunks received before the stall. A WARNING log with950 `extra={"source": "stream_chunk_timeout", "timeout_s": <value>,951 "model_name": <value>, "chunks_received": <value>}` also fires so952 aggregate logging can distinguish app-layer timeouts from953 transport-layer failures.954955 Defaults to 120s. Set to `None` or `0` to disable. Overridable via the956 `LANGCHAIN_OPENAI_STREAM_CHUNK_TIMEOUT_S` env var. Negative values957 (from either the env var or the constructor kwarg — e.g., hydrated958 from YAML/JSON configs) fall back to the default with a `WARNING` log959 rather than silently disabling the wrapper, so a misconfigured value960 still boots safely and the fallback is visible.961 """962963 stop: list[str] | str | None = Field(default=None, alias="stop_sequences")964 """Default stop sequences."""965966 extra_body: Mapping[str, Any] | None = None967 """Optional additional JSON properties to include in the request parameters968 when making requests to OpenAI compatible APIs, such as vLLM, LM Studio, or969 other providers.970971 This is the recommended way to pass custom parameters that are specific to your972 OpenAI-compatible API provider but not part of the standard OpenAI API.973974 Examples:975 - [LM Studio](https://lmstudio.ai/) TTL parameter: `extra_body={"ttl": 300}`976 - [vLLM](https://github.com/vllm-project/vllm) custom parameters:977 `extra_body={"use_beam_search": True}`978 - Any other provider-specific parameters979980 !!! warning981982 Do not use `model_kwargs` for custom parameters that are not part of the983 standard OpenAI API, as this will cause errors when making API calls. Use984 `extra_body` instead.985 """986987 include_response_headers: bool = False988 """Whether to include response headers in the output message `response_metadata`."""989990 disabled_params: dict[str, Any] | None = Field(default=None)991 """Parameters of the OpenAI client or `chat.completions` endpoint that should be992 disabled for the given model.993994 Should be specified as `{"param": None | ['val1', 'val2']}` where the key is the995 parameter and the value is either None, meaning that parameter should never be996 used, or it's a list of disabled values for the parameter.997998 For example, older models may not support the `'parallel_tool_calls'` parameter at999 all, in which case `disabled_params={"parallel_tool_calls": None}` can be passed1000 in.10011002 If a parameter is disabled then it will not be used by default in any methods, e.g.1003 in `with_structured_output`. However this does not prevent a user from directly1004 passed in the parameter during invocation.1005 """10061007 context_management: list[dict[str, Any]] | None = None1008 """Configuration for1009 [context management](https://developers.openai.com/api/docs/guides/compaction).1010 """10111012 include: list[str] | None = None1013 """Additional fields to include in generations from Responses API.10141015 Supported values:10161017 - `'file_search_call.results'`1018 - `'message.input_image.image_url'`1019 - `'computer_call_output.output.image_url'`1020 - `'reasoning.encrypted_content'`1021 - `'code_interpreter_call.outputs'`10221023 !!! version-added "Added in `langchain-openai` 0.3.24"1024 """10251026 prompt_cache_options: dict[str, Any] | None = None1027 """Options controlling OpenAI prompt cache behavior.10281029 !!! version-added "Added in `langchain-openai` 1.3.5"1030 """10311032 service_tier: str | None = None1033 """Latency tier for request.10341035 Options are `'auto'`, `'default'`, or `'flex'`.10361037 Relevant for users of OpenAI's scale tier service.1038 """10391040 store: bool | None = None1041 """If `True`, OpenAI may store response data for future use.10421043 Defaults to `True` for the Responses API and `False` for the Chat Completions API.10441045 !!! version-added "Added in `langchain-openai` 0.3.24"1046 """10471048 truncation: str | None = None1049 """Truncation strategy (Responses API).10501051 Can be `'auto'` or `'disabled'` (default).10521053 If `'auto'`, model may drop input items from the middle of the message sequence to1054 fit the context window.10551056 !!! version-added "Added in `langchain-openai` 0.3.24"1057 """10581059 use_previous_response_id: bool = False1060 """If `True`, always pass `previous_response_id` using the ID of the most recent1061 response. Responses API only.10621063 Input messages up to the most recent response will be dropped from request1064 payloads.10651066 For example, the following two are equivalent:10671068 ```python1069 model = ChatOpenAI(1070 model="...",1071 use_previous_response_id=True,1072 )1073 model.invoke(1074 [1075 HumanMessage("Hello"),1076 AIMessage("Hi there!", response_metadata={"id": "resp_123"}),1077 HumanMessage("How are you?"),1078 ]1079 )1080 ```10811082 ```python1083 model = ChatOpenAI(model="...", use_responses_api=True)1084 model.invoke([HumanMessage("How are you?")], previous_response_id="resp_123")1085 ```10861087 !!! version-added "Added in `langchain-openai` 0.3.26"1088 """10891090 use_responses_api: bool | None = None1091 """Whether to use the Responses API instead of the Chat API.10921093 If not specified then will be inferred based on invocation params.10941095 !!! version-added "Added in `langchain-openai` 0.3.9"1096 """10971098 output_version: str | None = Field(1099 default_factory=from_env("LC_OUTPUT_VERSION", default=None)1100 )1101 """Version of `AIMessage` output format to use.11021103 This field is used to roll-out new output formats for chat model `AIMessage`1104 responses in a backwards-compatible way.11051106 Supported values:11071108 - `'v0'`: `AIMessage` format as of `langchain-openai 0.3.x`.1109 - `'responses/v1'`: Formats Responses API output items into AIMessage content blocks1110 (Responses API only)1111 - `'v1'`: v1 of LangChain cross-provider standard.11121113 !!! warning "Behavior changed in `langchain-openai` 1.0.0"11141115 Default updated to `"responses/v1"`.1116 """11171118 model_config = ConfigDict(populate_by_name=True)11191120 @property1121 def model(self) -> str:1122 """Same as model_name."""1123 return self.model_name11241125 @model_validator(mode="before")1126 @classmethod1127 def build_extra(cls, values: dict[str, Any]) -> Any:1128 """Build extra kwargs from additional params that were passed in."""1129 all_required_field_names = get_pydantic_field_names(cls)1130 return _build_model_kwargs(values, all_required_field_names)11311132 @field_validator("stream_chunk_timeout", mode="after")1133 @classmethod1134 def _validate_stream_chunk_timeout(cls, value: float | None) -> float | None:1135 """Reject negative constructor values; fall back to the env-driven default.11361137 Matches the env-var path in `_float_env`: a negative value is a typo,1138 not an opt-out (`None`/`0` are the documented off switches). Configs1139 hydrated from YAML/JSON would otherwise silently disable the wrapper1140 and reintroduce the indefinite-stream hang the feature prevents.1141 """1142 if value is not None and value < 0:1143 fallback = _float_env("LANGCHAIN_OPENAI_STREAM_CHUNK_TIMEOUT_S", 120.0)1144 logger.warning(1145 "Invalid `stream_chunk_timeout=%r` (negative); "1146 "falling back to %s. Pass `None` or `0` to disable.",1147 value,1148 fallback,1149 )1150 return fallback1151 return value11521153 @model_validator(mode="before")1154 @classmethod1155 def validate_temperature(cls, values: dict[str, Any]) -> Any:1156 """Validate temperature parameter for different models.11571158 - gpt-5 models (excluding gpt-5-chat) only allow `temperature=1` or unset1159 (Defaults to 1)1160 """1161 model = values.get("model_name") or values.get("model") or ""1162 model_lower = model.lower()11631164 # For o1 models, set temperature=1 if not provided1165 if model_lower.startswith("o1") and "temperature" not in values:1166 values["temperature"] = 111671168 # For gpt-5 models, handle temperature restrictions. Temperature is supported1169 # by gpt-5-chat and gpt-5 models with reasoning_effort='none' or1170 # reasoning={'effort': 'none'}.1171 if (1172 model_lower.startswith("gpt-5")1173 and ("chat" not in model_lower)1174 and values.get("reasoning_effort") != "none"1175 and (values.get("reasoning") or {}).get("effort") != "none"1176 ):1177 temperature = values.get("temperature")1178 if temperature is not None and temperature != 1:1179 # For gpt-5 (non-chat), only temperature=1 is supported1180 # So we remove any non-defaults1181 values.pop("temperature", None)11821183 return values11841185 @model_validator(mode="after")1186 def _set_openai_chat_version(self) -> Self:1187 """Set package version in metadata.11881189 Note: Subclasses that inherit from `BaseChatOpenAI` (e.g.1190 `ChatDeepSeek`, `ChatXAI`) must use a **unique** validator name1191 (e.g. `_set_deepseek_version`) instead of overriding this one. Pydantic1192 replaces same-named `model_validator` methods rather than chaining them,1193 so reusing `_set_openai_chat_version` would silently drop the parent's1194 `langchain-openai` version entry.1195 """1196 self._add_version("langchain-openai", __version__)1197 return self11981199 @model_validator(mode="after")1200 def validate_environment(self) -> Self:1201 """Validate that api key and python package exists in environment."""1202 if self.n is not None and self.n < 1:1203 msg = "n must be at least 1."1204 raise ValueError(msg)1205 if self.n is not None and self.n > 1 and self.streaming:1206 msg = "n must be 1 when streaming."1207 raise ValueError(msg)12081209 # Check OPENAI_ORGANIZATION for backwards compatibility.1210 self.openai_organization = (1211 self.openai_organization1212 or os.getenv("OPENAI_ORG_ID")1213 or os.getenv("OPENAI_ORGANIZATION")1214 )1215 self.openai_api_base = self.openai_api_base or os.getenv("OPENAI_API_BASE")12161217 # Enable stream_usage by default if using default base URL and client1218 if (1219 all(1220 getattr(self, key, None) is None1221 for key in (1222 "stream_usage",1223 "openai_proxy",1224 "openai_api_base",1225 "base_url",1226 "client",1227 "root_client",1228 "async_client",1229 "root_async_client",1230 "http_client",1231 "http_async_client",1232 )1233 )1234 and "OPENAI_BASE_URL" not in os.environ1235 ):1236 self.stream_usage = True12371238 # Resolve API key from SecretStr or Callable1239 sync_api_key_value: str | Callable[[], str] | None = None1240 async_api_key_value: str | Callable[[], Awaitable[str]] | None = None12411242 if self.openai_api_key is not None:1243 # Because OpenAI and AsyncOpenAI clients support either sync or async1244 # callables for the API key, we need to resolve separate values here.1245 sync_api_key_value, async_api_key_value = _resolve_sync_and_async_api_keys(1246 self.openai_api_key1247 )12481249 client_params: dict = {1250 "organization": self.openai_organization,1251 "base_url": self.openai_api_base,1252 "timeout": self.request_timeout,1253 "default_headers": self.default_headers,1254 "default_query": self.default_query,1255 }1256 if self.max_retries is not None:1257 client_params["max_retries"] = self.max_retries12581259 if self.openai_proxy and (self.http_client or self.http_async_client):1260 openai_proxy = self.openai_proxy1261 http_client = self.http_client1262 http_async_client = self.http_async_client1263 msg = (1264 "Cannot specify 'openai_proxy' if one of "1265 "'http_client'/'http_async_client' is already specified. Received:\n"1266 f"{openai_proxy=}\n{http_client=}\n{http_async_client=}"1267 )1268 raise ValueError(msg)1269 if _should_bypass_socket_options_for_proxy_env(1270 http_socket_options=self.http_socket_options,1271 http_client=self.http_client,1272 http_async_client=self.http_async_client,1273 openai_proxy=self.openai_proxy,1274 ):1275 # Default-shape construction + proxy env var visible to httpx:1276 # skip the custom transport so httpx's env-proxy auto-detection1277 # still applies. Users who want kernel-level TCP tuning alongside1278 # an env proxy can opt in explicitly via `http_socket_options`.1279 resolved_socket_options: tuple[tuple[int, int, int], ...] = ()1280 _log_proxy_env_bypass_once()1281 else:1282 resolved_socket_options = _resolve_socket_options(self.http_socket_options)1283 _warn_if_proxy_env_shadowed(1284 resolved_socket_options, openai_proxy=self.openai_proxy1285 )1286 if not self.client:1287 if sync_api_key_value is None:1288 # No valid sync API key, leave client as None and raise informative1289 # error on invocation.1290 self.client = None1291 self.root_client = None1292 else:1293 if self.openai_proxy and not self.http_client:1294 self.http_client = _build_proxied_sync_httpx_client(1295 proxy=self.openai_proxy,1296 verify=global_ssl_context,1297 socket_options=resolved_socket_options,1298 )1299 sync_specific = {1300 "http_client": self.http_client1301 or _get_default_httpx_client(1302 self.openai_api_base,1303 self.request_timeout,1304 resolved_socket_options,1305 ),1306 "api_key": sync_api_key_value,1307 }1308 self.root_client = openai.OpenAI(**client_params, **sync_specific) # type: ignore[arg-type]1309 self.client = self.root_client.chat.completions1310 if not self.async_client:1311 if self.openai_proxy and not self.http_async_client:1312 self.http_async_client = _build_proxied_async_httpx_client(1313 proxy=self.openai_proxy,1314 verify=global_ssl_context,1315 socket_options=resolved_socket_options,1316 )1317 async_specific = {1318 "http_client": self.http_async_client1319 or _get_default_async_httpx_client(1320 self.openai_api_base,1321 self.request_timeout,1322 resolved_socket_options,1323 ),1324 "api_key": async_api_key_value,1325 }1326 self.root_async_client = openai.AsyncOpenAI(1327 **client_params,1328 **async_specific, # type: ignore[arg-type]1329 )1330 self.async_client = self.root_async_client.chat.completions1331 return self13321333 def _resolve_model_profile(self) -> ModelProfile | None:1334 return _get_default_model_profile(self.model_name) or None13351336 @property1337 def _default_params(self) -> dict[str, Any]:1338 """Get the default parameters for calling OpenAI API."""1339 exclude_if_none = {1340 "presence_penalty": self.presence_penalty,1341 "frequency_penalty": self.frequency_penalty,1342 "seed": self.seed,1343 "top_p": self.top_p,1344 "logprobs": self.logprobs,1345 "top_logprobs": self.top_logprobs,1346 "logit_bias": self.logit_bias,1347 "stop": self.stop or None, # Also exclude empty list for this1348 "max_tokens": self.max_tokens,1349 "extra_body": self.extra_body,1350 "n": self.n,1351 "temperature": self.temperature,1352 "reasoning_effort": self.reasoning_effort,1353 "reasoning": self.reasoning,1354 "verbosity": self.verbosity,1355 "context_management": self.context_management,1356 "include": self.include,1357 "prompt_cache_options": self.prompt_cache_options,1358 "service_tier": self.service_tier,1359 "truncation": self.truncation,1360 "store": self.store,1361 }13621363 return {1364 "model": self.model_name,1365 "stream": self.streaming,1366 **{k: v for k, v in exclude_if_none.items() if v is not None},1367 **self.model_kwargs,1368 }13691370 def _combine_llm_outputs(self, llm_outputs: list[dict | None]) -> dict:1371 overall_token_usage: dict = {}1372 system_fingerprint = None1373 for output in llm_outputs:1374 if output is None:1375 # Happens in streaming1376 continue1377 token_usage = output.get("token_usage")1378 if token_usage is not None:1379 for k, v in token_usage.items():1380 if v is None:1381 continue1382 if k in overall_token_usage:1383 overall_token_usage[k] = _update_token_usage(1384 overall_token_usage[k], v1385 )1386 else:1387 overall_token_usage[k] = v1388 if system_fingerprint is None:1389 system_fingerprint = output.get("system_fingerprint")1390 combined = {"token_usage": overall_token_usage, "model_name": self.model_name}1391 if system_fingerprint:1392 combined["system_fingerprint"] = system_fingerprint1393 return combined13941395 def _convert_chunk_to_generation_chunk(1396 self,1397 chunk: dict,1398 default_chunk_class: type,1399 base_generation_info: dict | None,1400 ) -> ChatGenerationChunk | None:1401 if chunk.get("type") == "content.delta": # From beta.chat.completions.stream1402 return None1403 token_usage = chunk.get("usage")1404 choices = (1405 chunk.get("choices", [])1406 # From beta.chat.completions.stream1407 or chunk.get("chunk", {}).get("choices", [])1408 )14091410 usage_metadata: UsageMetadata | None = (1411 _create_usage_metadata(token_usage, chunk.get("service_tier"))1412 if token_usage1413 else None1414 )1415 if len(choices) == 0:1416 # logprobs is implicitly None1417 generation_chunk = ChatGenerationChunk(1418 message=default_chunk_class(content="", usage_metadata=usage_metadata),1419 generation_info=base_generation_info,1420 )1421 # Keep content as "" (the default) rather than converting to [].1422 # Chat Completions content deltas are normalized to strings in1423 # _convert_delta_to_message_chunk. Starting with [] causes1424 # merge_content to silently drop string content (empty list is1425 # falsy, so no merge branch applies). The empty list also triggers1426 # the content_blocks isinstance(list) short-circuit, which would1427 # return [] and miss tool_call_chunks.1428 if self.output_version == "v1":1429 generation_chunk.message.response_metadata["output_version"] = "v1"14301431 return generation_chunk14321433 choice = choices[0]1434 if choice["delta"] is None:1435 return None14361437 message_chunk = _convert_delta_to_message_chunk(1438 choice["delta"], default_chunk_class1439 )1440 generation_info = {**base_generation_info} if base_generation_info else {}14411442 if finish_reason := choice.get("finish_reason"):1443 generation_info["finish_reason"] = finish_reason1444 if model_name := chunk.get("model"):1445 generation_info["model_name"] = model_name1446 if system_fingerprint := chunk.get("system_fingerprint"):1447 generation_info["system_fingerprint"] = system_fingerprint1448 if service_tier := chunk.get("service_tier"):1449 generation_info["service_tier"] = service_tier14501451 logprobs = choice.get("logprobs")1452 if logprobs:1453 generation_info["logprobs"] = logprobs14541455 if usage_metadata and isinstance(message_chunk, AIMessageChunk):1456 message_chunk.usage_metadata = usage_metadata14571458 message_chunk.response_metadata["model_provider"] = "openai"1459 # Propagate output_version so content_blocks can detect v1 mode.1460 if self.output_version == "v1":1461 message_chunk.response_metadata["output_version"] = "v1"1462 return ChatGenerationChunk(1463 message=message_chunk, generation_info=generation_info or None1464 )14651466 def _ensure_sync_client_available(self) -> None:1467 """Check that sync client is available, raise error if not."""1468 if self.client is None:1469 msg = (1470 "Sync client is not available. This happens when an async callable "1471 "was provided for the API key. Use async methods (ainvoke, astream) "1472 "instead, or provide a string or sync callable for the API key."1473 )1474 raise ValueError(msg)14751476 def _stream_responses(1477 self,1478 messages: list[BaseMessage],1479 stop: list[str] | None = None,1480 run_manager: CallbackManagerForLLMRun | None = None,1481 **kwargs: Any,1482 ) -> Iterator[ChatGenerationChunk]:1483 self._ensure_sync_client_available()1484 kwargs["stream"] = True1485 payload = self._get_request_payload(messages, stop=stop, **kwargs)1486 try:1487 if self.include_response_headers:1488 raw_context_manager = (1489 self.root_client.with_raw_response.responses.create(**payload)1490 )1491 context_manager = raw_context_manager.parse()1492 headers = {"headers": dict(raw_context_manager.headers)}1493 else:1494 context_manager = self.root_client.responses.create(**payload)1495 headers = {}1496 original_schema_obj = kwargs.get("response_format")14971498 with context_manager as response:1499 is_first_chunk = True1500 current_index = -11501 current_output_index = -11502 current_sub_index = -11503 has_reasoning = False1504 for chunk in response:1505 metadata = headers if is_first_chunk else {}1506 (1507 current_index,1508 current_output_index,1509 current_sub_index,1510 generation_chunk,1511 ) = _convert_responses_chunk_to_generation_chunk(1512 chunk,1513 current_index,1514 current_output_index,1515 current_sub_index,1516 schema=original_schema_obj,1517 metadata=metadata,1518 has_reasoning=has_reasoning,1519 output_version=self.output_version,1520 )1521 if generation_chunk:1522 if run_manager:1523 run_manager.on_llm_new_token(1524 generation_chunk.text, chunk=generation_chunk1525 )1526 is_first_chunk = False1527 if "reasoning" in generation_chunk.message.additional_kwargs:1528 has_reasoning = True1529 yield generation_chunk1530 except openai.BadRequestError as e:1531 _handle_openai_bad_request(e)1532 except openai.APIError as e:1533 _handle_openai_api_error(e)15341535 async def _astream_responses(1536 self,1537 messages: list[BaseMessage],1538 stop: list[str] | None = None,1539 run_manager: AsyncCallbackManagerForLLMRun | None = None,1540 **kwargs: Any,1541 ) -> AsyncIterator[ChatGenerationChunk]:1542 kwargs["stream"] = True1543 payload = self._get_request_payload(messages, stop=stop, **kwargs)1544 try:1545 if self.include_response_headers:1546 raw_context_manager = (1547 await self.root_async_client.with_raw_response.responses.create(1548 **payload1549 )1550 )1551 context_manager = raw_context_manager.parse()1552 headers = {"headers": dict(raw_context_manager.headers)}1553 else:1554 context_manager = await self.root_async_client.responses.create(1555 **payload1556 )1557 headers = {}1558 original_schema_obj = kwargs.get("response_format")15591560 async with context_manager as response:1561 is_first_chunk = True1562 current_index = -11563 current_output_index = -11564 current_sub_index = -11565 has_reasoning = False1566 async for chunk in _astream_with_chunk_timeout(1567 response,1568 self.stream_chunk_timeout,1569 model_name=self.model_name,1570 ):1571 metadata = headers if is_first_chunk else {}1572 (1573 current_index,1574 current_output_index,1575 current_sub_index,1576 generation_chunk,1577 ) = _convert_responses_chunk_to_generation_chunk(1578 chunk,1579 current_index,1580 current_output_index,1581 current_sub_index,1582 schema=original_schema_obj,1583 metadata=metadata,1584 has_reasoning=has_reasoning,1585 output_version=self.output_version,1586 )1587 if generation_chunk:1588 if run_manager:1589 await run_manager.on_llm_new_token(1590 generation_chunk.text, chunk=generation_chunk1591 )1592 is_first_chunk = False1593 if "reasoning" in generation_chunk.message.additional_kwargs:1594 has_reasoning = True1595 yield generation_chunk1596 except openai.BadRequestError as e:1597 _handle_openai_bad_request(e)1598 except openai.APIError as e:1599 _handle_openai_api_error(e)16001601 def _should_stream_usage(1602 self, stream_usage: bool | None = None, **kwargs: Any1603 ) -> bool:1604 """Determine whether to include usage metadata in streaming output.16051606 For backwards compatibility, we check for `stream_options` passed1607 explicitly to kwargs or in the `model_kwargs` and override `self.stream_usage`.1608 """1609 stream_usage_sources = [ # order of precedence1610 stream_usage,1611 kwargs.get("stream_options", {}).get("include_usage"),1612 self.model_kwargs.get("stream_options", {}).get("include_usage"),1613 self.stream_usage,1614 ]1615 for source in stream_usage_sources:1616 if isinstance(source, bool):1617 return source1618 return self.stream_usage or False16191620 def _stream(1621 self,1622 messages: list[BaseMessage],1623 stop: list[str] | None = None,1624 run_manager: CallbackManagerForLLMRun | None = None,1625 *,1626 stream_usage: bool | None = None,1627 **kwargs: Any,1628 ) -> Iterator[ChatGenerationChunk]:1629 self._ensure_sync_client_available()1630 kwargs["stream"] = True1631 stream_usage = self._should_stream_usage(stream_usage, **kwargs)1632 if stream_usage:1633 kwargs["stream_options"] = {"include_usage": stream_usage}1634 payload = self._get_request_payload(messages, stop=stop, **kwargs)1635 default_chunk_class: type[BaseMessageChunk] = AIMessageChunk1636 base_generation_info = {}16371638 try:1639 if "response_format" in payload:1640 if self.include_response_headers:1641 warnings.warn(1642 "Cannot currently include response headers when "1643 "response_format is specified."1644 )1645 payload.pop("stream")1646 response_stream = self.root_client.beta.chat.completions.stream(1647 **payload1648 )1649 context_manager = response_stream1650 else:1651 if self.include_response_headers:1652 raw_response = self.client.with_raw_response.create(**payload)1653 response = raw_response.parse()1654 base_generation_info = {"headers": dict(raw_response.headers)}1655 else:1656 response = self.client.create(**payload)1657 context_manager = response1658 with context_manager as response:1659 is_first_chunk = True1660 for chunk in response:1661 if not isinstance(chunk, dict):1662 chunk = chunk.model_dump()1663 generation_chunk = self._convert_chunk_to_generation_chunk(1664 chunk,1665 default_chunk_class,1666 base_generation_info if is_first_chunk else {},1667 )1668 if generation_chunk is None:1669 continue1670 default_chunk_class = generation_chunk.message.__class__1671 logprobs = (generation_chunk.generation_info or {}).get("logprobs")1672 if run_manager:1673 run_manager.on_llm_new_token(1674 generation_chunk.text,1675 chunk=generation_chunk,1676 logprobs=logprobs,1677 )1678 is_first_chunk = False1679 yield generation_chunk1680 except openai.BadRequestError as e:1681 _handle_openai_bad_request(e)1682 except openai.APIError as e:1683 _handle_openai_api_error(e)1684 if hasattr(response, "get_final_completion") and "response_format" in payload:1685 final_completion = response.get_final_completion()1686 generation_chunk = self._get_generation_chunk_from_completion(1687 final_completion1688 )1689 if run_manager:1690 run_manager.on_llm_new_token(1691 generation_chunk.text, chunk=generation_chunk1692 )1693 yield generation_chunk16941695 def _generate(1696 self,1697 messages: list[BaseMessage],1698 stop: list[str] | None = None,1699 run_manager: CallbackManagerForLLMRun | None = None,1700 **kwargs: Any,1701 ) -> ChatResult:1702 self._ensure_sync_client_available()1703 payload = self._get_request_payload(messages, stop=stop, **kwargs)1704 generation_info = None1705 raw_response = None1706 try:1707 if "response_format" in payload:1708 payload.pop("stream")1709 raw_response = (1710 self.root_client.chat.completions.with_raw_response.parse(**payload)1711 )1712 response = raw_response.parse()1713 elif self._use_responses_api(payload):1714 original_schema_obj = kwargs.get("response_format")1715 if original_schema_obj and _is_pydantic_class(original_schema_obj):1716 raw_response = self.root_client.responses.with_raw_response.parse(1717 **payload1718 )1719 else:1720 raw_response = self.root_client.responses.with_raw_response.create(1721 **payload1722 )1723 response = raw_response.parse()1724 if self.include_response_headers:1725 generation_info = {"headers": dict(raw_response.headers)}1726 return _construct_lc_result_from_responses_api(1727 response,1728 schema=original_schema_obj,1729 metadata=generation_info,1730 output_version=self.output_version,1731 )1732 else:1733 raw_response = self.client.with_raw_response.create(**payload)1734 response = raw_response.parse()1735 except openai.BadRequestError as e:1736 _handle_openai_bad_request(e)1737 except openai.APIError as e:1738 _handle_openai_api_error(e)1739 except Exception as e:1740 if raw_response is not None and hasattr(raw_response, "http_response"):1741 e.response = raw_response.http_response # type: ignore[attr-defined]1742 raise e1743 if (1744 self.include_response_headers1745 and raw_response is not None1746 and hasattr(raw_response, "headers")1747 ):1748 generation_info = {"headers": dict(raw_response.headers)}1749 return self._create_chat_result(response, generation_info)17501751 def _use_responses_api(self, payload: dict) -> bool:1752 if isinstance(self.use_responses_api, bool):1753 return self.use_responses_api1754 if (1755 self.output_version == "responses/v1"1756 or self.context_management is not None1757 or self.include is not None1758 or self.reasoning is not None1759 or self.truncation is not None1760 or self.use_previous_response_id1761 or _model_prefers_responses_api(self.model_name)1762 ):1763 return True1764 return _use_responses_api(payload)17651766 def _get_request_payload(1767 self,1768 input_: LanguageModelInput,1769 *,1770 stop: list[str] | None = None,1771 **kwargs: Any,1772 ) -> dict:1773 messages = self._convert_input(input_).to_messages()1774 if stop is not None:1775 kwargs["stop"] = stop17761777 payload = {**self._default_params, **kwargs}17781779 if self._use_responses_api(payload):1780 if self.use_previous_response_id:1781 last_messages, previous_response_id = _get_last_messages(messages)1782 payload_to_use = last_messages if previous_response_id else messages1783 if previous_response_id:1784 payload["previous_response_id"] = previous_response_id1785 payload = _construct_responses_api_payload(payload_to_use, payload)1786 else:1787 payload = _construct_responses_api_payload(messages, payload)1788 else:1789 payload["messages"] = [1790 _convert_message_to_dict(_convert_from_v1_to_chat_completions(m))1791 if isinstance(m, AIMessage)1792 else _convert_message_to_dict(m)1793 for m in messages1794 ]1795 return payload17961797 def _create_chat_result(1798 self,1799 response: dict | openai.BaseModel,1800 generation_info: dict | None = None,1801 ) -> ChatResult:1802 generations = []18031804 response_dict = (1805 response1806 if isinstance(response, dict)1807 # `parsed` may hold arbitrary Pydantic models from structured output.1808 # Exclude it from this dump and copy it from the typed response below.1809 else response.model_dump(1810 exclude={"choices": {"__all__": {"message": {"parsed"}}}},1811 warnings=False,1812 )1813 )1814 # Sometimes the AI Model calling will get error, we should raise it (this is1815 # typically followed by a null value for `choices`, which we raise for1816 # separately below).1817 if response_dict.get("error"):1818 raise ValueError(response_dict.get("error"))18191820 # Raise informative error messages for non-OpenAI chat completions APIs1821 # that return malformed responses.1822 try:1823 choices = response_dict["choices"]1824 except KeyError as e:1825 msg = f"Response missing 'choices' key: {response_dict.keys()}"1826 raise KeyError(msg) from e18271828 if choices is None:1829 # Some OpenAI-compatible APIs (e.g., vLLM) may return null choices1830 # when the response format differs or an error occurs without1831 # populating the error field. Provide a more helpful error message.1832 msg = (1833 "Received response with null value for 'choices'. "1834 "This can happen when using OpenAI-compatible APIs (e.g., vLLM) "1835 "that return a response in an unexpected format. "1836 f"Full response keys: {list(response_dict.keys())}"1837 )1838 raise TypeError(msg)18391840 token_usage = response_dict.get("usage")1841 service_tier = response_dict.get("service_tier")18421843 for res in choices:1844 message = _convert_dict_to_message(res["message"])1845 if token_usage and isinstance(message, AIMessage):1846 message.usage_metadata = _create_usage_metadata(1847 token_usage, service_tier1848 )1849 generation_info = generation_info or {}1850 generation_info["finish_reason"] = (1851 res.get("finish_reason")1852 if res.get("finish_reason") is not None1853 else generation_info.get("finish_reason")1854 )1855 if "logprobs" in res:1856 generation_info["logprobs"] = res["logprobs"]1857 gen = ChatGeneration(message=message, generation_info=generation_info)1858 generations.append(gen)1859 llm_output = {1860 "token_usage": token_usage,1861 "model_provider": "openai",1862 "model_name": response_dict.get("model", self.model_name),1863 "system_fingerprint": response_dict.get("system_fingerprint", ""),1864 }1865 if "id" in response_dict:1866 llm_output["id"] = response_dict["id"]1867 if service_tier:1868 llm_output["service_tier"] = service_tier18691870 if isinstance(response, openai.BaseModel) and getattr(1871 response, "choices", None1872 ):1873 message = response.choices[0].message # type: ignore[attr-defined]1874 if hasattr(message, "parsed"):1875 generations[0].message.additional_kwargs["parsed"] = message.parsed1876 if hasattr(message, "refusal"):1877 generations[0].message.additional_kwargs["refusal"] = message.refusal18781879 return ChatResult(generations=generations, llm_output=llm_output)18801881 async def _astream(1882 self,1883 messages: list[BaseMessage],1884 stop: list[str] | None = None,1885 run_manager: AsyncCallbackManagerForLLMRun | None = None,1886 *,1887 stream_usage: bool | None = None,1888 **kwargs: Any,1889 ) -> AsyncIterator[ChatGenerationChunk]:1890 kwargs["stream"] = True1891 stream_usage = self._should_stream_usage(stream_usage, **kwargs)1892 if stream_usage:1893 kwargs["stream_options"] = {"include_usage": stream_usage}1894 payload = self._get_request_payload(messages, stop=stop, **kwargs)1895 default_chunk_class: type[BaseMessageChunk] = AIMessageChunk1896 base_generation_info = {}18971898 try:1899 if "response_format" in payload:1900 if self.include_response_headers:1901 warnings.warn(1902 "Cannot currently include response headers when "1903 "response_format is specified."1904 )1905 payload.pop("stream")1906 response_stream = self.root_async_client.beta.chat.completions.stream(1907 **payload1908 )1909 context_manager = response_stream1910 else:1911 if self.include_response_headers:1912 raw_response = await self.async_client.with_raw_response.create(1913 **payload1914 )1915 response = raw_response.parse()1916 base_generation_info = {"headers": dict(raw_response.headers)}1917 else:1918 response = await self.async_client.create(**payload)1919 context_manager = response1920 async with context_manager as response:1921 is_first_chunk = True1922 async for chunk in _astream_with_chunk_timeout(1923 response,1924 self.stream_chunk_timeout,1925 model_name=self.model_name,1926 ):1927 if not isinstance(chunk, dict):1928 chunk = chunk.model_dump()1929 generation_chunk = self._convert_chunk_to_generation_chunk(1930 chunk,1931 default_chunk_class,1932 base_generation_info if is_first_chunk else {},1933 )1934 if generation_chunk is None:1935 continue1936 default_chunk_class = generation_chunk.message.__class__1937 logprobs = (generation_chunk.generation_info or {}).get("logprobs")1938 if run_manager:1939 await run_manager.on_llm_new_token(1940 generation_chunk.text,1941 chunk=generation_chunk,1942 logprobs=logprobs,1943 )1944 is_first_chunk = False1945 yield generation_chunk1946 except openai.BadRequestError as e:1947 _handle_openai_bad_request(e)1948 except openai.APIError as e:1949 _handle_openai_api_error(e)1950 if hasattr(response, "get_final_completion") and "response_format" in payload:1951 final_completion = await response.get_final_completion()1952 generation_chunk = self._get_generation_chunk_from_completion(1953 final_completion1954 )1955 if run_manager:1956 await run_manager.on_llm_new_token(1957 generation_chunk.text, chunk=generation_chunk1958 )1959 yield generation_chunk19601961 async def _agenerate(1962 self,1963 messages: list[BaseMessage],1964 stop: list[str] | None = None,1965 run_manager: AsyncCallbackManagerForLLMRun | None = None,1966 **kwargs: Any,1967 ) -> ChatResult:1968 payload = self._get_request_payload(messages, stop=stop, **kwargs)1969 generation_info = None1970 raw_response = None1971 try:1972 if "response_format" in payload:1973 payload.pop("stream")1974 raw_response = await self.root_async_client.chat.completions.with_raw_response.parse( # noqa: E5011975 **payload1976 )1977 response = raw_response.parse()1978 elif self._use_responses_api(payload):1979 original_schema_obj = kwargs.get("response_format")1980 if original_schema_obj and _is_pydantic_class(original_schema_obj):1981 raw_response = (1982 await self.root_async_client.responses.with_raw_response.parse(1983 **payload1984 )1985 )1986 else:1987 raw_response = (1988 await self.root_async_client.responses.with_raw_response.create(1989 **payload1990 )1991 )1992 response = raw_response.parse()1993 if self.include_response_headers:1994 generation_info = {"headers": dict(raw_response.headers)}1995 return _construct_lc_result_from_responses_api(1996 response,1997 schema=original_schema_obj,1998 metadata=generation_info,1999 output_version=self.output_version,2000 )
Findings
✓ No findings reported for this file.