libs/partners/openai/langchain_openai/chat_models/base.py PYTHON 5,249 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 5,249.
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.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.