libs/partners/openai/langchain_openai/chat_models/base.py PYTHON 5,525 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 5,525.
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 (55    ContextOverflowError,56    ModelAPIError,57    ModelAuthenticationError,58    ModelConnectionError,59    ModelInvalidRequestError,60    ModelNotFoundError,61    ModelPermissionDeniedError,62    ModelRateLimitError,63    ModelTimeoutError,64)65from langchain_core.language_models import (66    LanguageModelInput,67    ModelProfileRegistry,68)69from langchain_core.language_models.chat_models import (70    BaseChatModel,71    LangSmithParams,72)73from langchain_core.messages import (74    AIMessage,75    AIMessageChunk,76    BaseMessage,77    BaseMessageChunk,78    ChatMessage,79    ChatMessageChunk,80    FunctionMessage,81    FunctionMessageChunk,82    HumanMessage,83    HumanMessageChunk,84    InvalidToolCall,85    SystemMessage,86    SystemMessageChunk,87    ToolCall,88    ToolMessage,89    ToolMessageChunk,90    is_data_content_block,91)92from langchain_core.messages import content as types93from langchain_core.messages.ai import (94    InputTokenDetails,95    OutputTokenDetails,96    UsageMetadata,97)98from langchain_core.messages.block_translators.openai import (99    _convert_from_v03_ai_message,100    convert_to_openai_data_block,101)102from langchain_core.messages.tool import tool_call_chunk103from langchain_core.output_parsers import JsonOutputParser, PydanticOutputParser104from langchain_core.output_parsers.openai_tools import (105    JsonOutputKeyToolsParser,106    PydanticToolsParser,107    make_invalid_tool_call,108    parse_tool_call,109)110from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult111from langchain_core.runnables import (112    Runnable,113    RunnableLambda,114    RunnableMap,115    RunnablePassthrough,116)117from langchain_core.runnables.config import run_in_executor118from langchain_core.tools import BaseTool119from langchain_core.tools.base import _stringify120from langchain_core.utils import get_pydantic_field_names121from langchain_core.utils._gateway import (122    GATEWAY_METADATA_RESPONSE_KEY,123    _parse_gateway_metadata,124    _resolve_gateway_config,125)126from langchain_core.utils.function_calling import (127    convert_to_openai_function,128    convert_to_openai_tool,129)130from langchain_core.utils.pydantic import (131    PydanticBaseModel,132    TypeBaseModel,133    is_basemodel_subclass,134)135from langchain_core.utils.utils import LC_AUTO_PREFIX, _build_model_kwargs, from_env136from pydantic import (137    BaseModel,138    ConfigDict,139    Field,140    SecretStr,141    ValidationError,142    field_validator,143    model_validator,144)145from pydantic.v1 import BaseModel as BaseModelV1146from typing_extensions import Self147148from langchain_openai._version import __version__149from langchain_openai.chat_models._client_utils import (150    _astream_with_chunk_timeout,151    _build_proxied_async_httpx_client,152    _build_proxied_sync_httpx_client,153    _float_env,154    _get_default_async_httpx_client,155    _get_default_httpx_client,156    _log_proxy_env_bypass_once,157    _resolve_socket_options,158    _resolve_sync_and_async_api_keys,159    _should_bypass_socket_options_for_proxy_env,160    _warn_if_proxy_env_shadowed,161)162from langchain_openai.chat_models._compat import (163    _convert_from_v1_to_chat_completions,164    _convert_from_v1_to_responses,165    _convert_to_v03_ai_message,166)167from langchain_openai.data._profiles import _PROFILES168169if TYPE_CHECKING:170    import httpx171    from langchain_core.language_models import ModelProfile172    from openai.types.responses import Response173174logger = logging.getLogger(__name__)175176# This SSL context is equivalent to the default `verify=True`.177# https://www.python-httpx.org/advanced/ssl/#configuring-client-instances178global_ssl_context = ssl.create_default_context(cafile=certifi.where())179180_ssrf_client: httpx.Client | None = None181182183def _get_ssrf_safe_client() -> httpx.Client:184    global _ssrf_client185    if _ssrf_client is None:186        from langchain_core._security._transport import ssrf_safe_client187188        _ssrf_client = ssrf_safe_client(189            verify=global_ssl_context, follow_redirects=False190        )191    return _ssrf_client192193194_MODEL_PROFILES = cast(ModelProfileRegistry, _PROFILES)195196197def _get_default_model_profile(model_name: str) -> ModelProfile:198    default = _MODEL_PROFILES.get(model_name) or {}199    return default.copy()200201202WellKnownTools = (203    "file_search",204    "web_search_preview",205    "web_search",206    "computer_use_preview",207    "code_interpreter",208    "mcp",209    "image_generation",210    "tool_search",211    "apply_patch",212)213214_TOOL_EXTRAS_PASSTHROUGH = ("defer_loading", "async")215216217def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage:218    """Convert a dictionary to a LangChain message.219220    Args:221        _dict: The dictionary.222223    Returns:224        The LangChain message.225    """226    role = _dict.get("role")227    name = _dict.get("name")228    id_ = _dict.get("id")229    if role == "user":230        return HumanMessage(content=_dict.get("content", ""), id=id_, name=name)231    if role == "assistant":232        # Fix for azure233        # Also OpenAI returns None for tool invocations234        content = _dict.get("content", "") or ""235        additional_kwargs: dict = {}236        if function_call := _dict.get("function_call"):237            additional_kwargs["function_call"] = dict(function_call)238        tool_calls = []239        invalid_tool_calls = []240        if raw_tool_calls := _dict.get("tool_calls"):241            for raw_tool_call in raw_tool_calls:242                try:243                    tool_calls.append(parse_tool_call(raw_tool_call, return_id=True))244                except Exception as e:245                    invalid_tool_calls.append(246                        make_invalid_tool_call(raw_tool_call, str(e))247                    )248        if audio := _dict.get("audio"):249            additional_kwargs["audio"] = audio250        return AIMessage(251            content=content,252            additional_kwargs=additional_kwargs,253            name=name,254            id=id_,255            tool_calls=tool_calls,256            invalid_tool_calls=invalid_tool_calls,257        )258    if role in ("system", "developer"):259        additional_kwargs = {"__openai_role__": role} if role == "developer" else {}260        return SystemMessage(261            content=_dict.get("content", ""),262            name=name,263            id=id_,264            additional_kwargs=additional_kwargs,265        )266    if role == "function":267        return FunctionMessage(268            content=_dict.get("content", ""), name=cast(str, _dict.get("name")), id=id_269        )270    if role == "tool":271        additional_kwargs = {}272        if "name" in _dict:273            additional_kwargs["name"] = _dict["name"]274        return ToolMessage(275            content=_dict.get("content", ""),276            tool_call_id=cast(str, _dict.get("tool_call_id")),277            additional_kwargs=additional_kwargs,278            name=name,279            id=id_,280        )281    return ChatMessage(content=_dict.get("content", ""), role=role, id=id_)  # type: ignore[arg-type]282283284def _apply_prompt_cache_breakpoint(285    source_block: dict[str, Any], formatted_block: dict[str, Any]286) -> dict[str, Any]:287    """Apply an OpenAI prompt cache breakpoint to a formatted content block.288289    A breakpoint set directly on the block takes precedence over one nested in290    `extras`. Membership (not truthiness) decides whether to copy it, so a291    present-but-falsy value (e.g. `None`) is still preserved.292    """293    if "prompt_cache_breakpoint" in source_block:294        formatted_block["prompt_cache_breakpoint"] = source_block[295            "prompt_cache_breakpoint"296        ]297    elif isinstance(extras := source_block.get("extras"), dict) and (298        "prompt_cache_breakpoint" in extras299    ):300        formatted_block["prompt_cache_breakpoint"] = extras["prompt_cache_breakpoint"]301    return formatted_block302303304def _sanitize_chat_completions_content(content: str | list[dict]) -> str | list[dict]:305    """Sanitize content for chat/completions API.306307    For list content, filters text blocks to only keep supported keys.308    """309    if isinstance(content, list):310        sanitized = []311        for block in content:312            if (313                isinstance(block, dict)314                and block.get("type") == "text"315                and "text" in block316            ):317                sanitized_block = {"type": "text", "text": block["text"]}318                if "prompt_cache_breakpoint" in block:319                    sanitized_block["prompt_cache_breakpoint"] = block[320                        "prompt_cache_breakpoint"321                    ]322                sanitized.append(sanitized_block)323            else:324                sanitized.append(block)325        return sanitized326    return content327328329def _format_message_content(330    content: Any,331    api: Literal["chat/completions", "responses"] = "chat/completions",332    role: str | None = None,333) -> Any:334    """Format message content."""335    if content and isinstance(content, list):336        formatted_content = []337        for block in content:338            # Remove unexpected block types339            if (340                isinstance(block, dict)341                and "type" in block342                and (343                    block["type"] in ("tool_use", "thinking", "reasoning_content")344                    or (345                        block["type"] in ("function_call", "code_interpreter_call")346                        and api == "chat/completions"347                    )348                )349            ):350                continue351            if (352                isinstance(block, dict)353                and is_data_content_block(block)354                # Responses API messages handled separately in _compat (parsed into355                # image generation calls)356                and not (api == "responses" and str(role).lower().startswith("ai"))357            ):358                formatted_block = convert_to_openai_data_block(block, api=api)359                formatted_content.append(360                    _apply_prompt_cache_breakpoint(block, formatted_block)361                )362            elif (363                isinstance(block, dict)364                and block.get("type") == "text"365                and "text" in block366                and isinstance(extras := block.get("extras"), dict)367                and "prompt_cache_breakpoint" in extras368            ):369                formatted_block = {"type": "text", "text": block["text"]}370                formatted_content.append(371                    _apply_prompt_cache_breakpoint(block, formatted_block)372                )373            # Anthropic image blocks374            elif (375                isinstance(block, dict)376                and block.get("type") == "image"377                and (source := block.get("source"))378                and isinstance(source, dict)379            ):380                if source.get("type") == "base64" and (381                    (media_type := source.get("media_type"))382                    and (data := source.get("data"))383                ):384                    formatted_content.append(385                        {386                            "type": "image_url",387                            "image_url": {"url": f"data:{media_type};base64,{data}"},388                        }389                    )390                elif source.get("type") == "url" and (url := source.get("url")):391                    formatted_content.append(392                        {"type": "image_url", "image_url": {"url": url}}393                    )394                else:395                    continue396            else:397                formatted_content.append(block)398    else:399        formatted_content = content400401    return formatted_content402403404def _convert_message_to_dict(405    message: BaseMessage,406    api: Literal["chat/completions", "responses"] = "chat/completions",407) -> dict:408    """Convert a LangChain message to dictionary format expected by OpenAI."""409    message_dict: dict[str, Any] = {410        "content": _format_message_content(message.content, api=api, role=message.type)411    }412    if (name := message.name or message.additional_kwargs.get("name")) is not None:413        message_dict["name"] = name414415    # populate role and additional message data416    if isinstance(message, ChatMessage):417        message_dict["role"] = message.role418    elif isinstance(message, HumanMessage):419        message_dict["role"] = "user"420    elif isinstance(message, AIMessage):421        message_dict["role"] = "assistant"422        if message.tool_calls or message.invalid_tool_calls:423            message_dict["tool_calls"] = [424                _lc_tool_call_to_openai_tool_call(tc) for tc in message.tool_calls425            ] + [426                _lc_invalid_tool_call_to_openai_tool_call(tc)427                for tc in message.invalid_tool_calls428            ]429        elif "tool_calls" in message.additional_kwargs:430            message_dict["tool_calls"] = message.additional_kwargs["tool_calls"]431            tool_call_supported_props = {"id", "type", "function"}432            message_dict["tool_calls"] = [433                {k: v for k, v in tool_call.items() if k in tool_call_supported_props}434                for tool_call in message_dict["tool_calls"]435            ]436        elif "function_call" in message.additional_kwargs:437            # OpenAI raises 400 if both function_call and tool_calls are present in the438            # same message.439            message_dict["function_call"] = message.additional_kwargs["function_call"]440        else:441            pass442        # If tool calls present, content null value should be None not empty string.443        if "function_call" in message_dict or "tool_calls" in message_dict:444            message_dict["content"] = message_dict["content"] or None445446        audio: dict[str, Any] | None = None447        for block in message.content:448            if (449                isinstance(block, dict)450                and block.get("type") == "audio"451                and (id_ := block.get("id"))452                and api != "responses"453            ):454                # openai doesn't support passing the data back - only the id455                # https://platform.openai.com/docs/guides/audio/multi-turn-conversations456                audio = {"id": id_}457        if not audio and "audio" in message.additional_kwargs:458            raw_audio = message.additional_kwargs["audio"]459            audio = (460                {"id": message.additional_kwargs["audio"]["id"]}461                if "id" in raw_audio462                else raw_audio463            )464        if audio:465            message_dict["audio"] = audio466    elif isinstance(message, SystemMessage):467        message_dict["role"] = message.additional_kwargs.get(468            "__openai_role__", "system"469        )470    elif isinstance(message, FunctionMessage):471        message_dict["role"] = "function"472    elif isinstance(message, ToolMessage):473        message_dict["role"] = "tool"474        message_dict["tool_call_id"] = message.tool_call_id475        message_dict["content"] = _sanitize_chat_completions_content(476            message_dict["content"]477        )478        supported_props = {"content", "role", "tool_call_id"}479        message_dict = {k: v for k, v in message_dict.items() if k in supported_props}480    else:481        msg = f"Got unknown type {message}"482        raise TypeError(msg)483    return message_dict484485486def _convert_delta_to_message_chunk(487    _dict: Mapping[str, Any], default_class: type[BaseMessageChunk]488) -> BaseMessageChunk:489    """Convert to a LangChain message chunk."""490    id_ = _dict.get("id")491    role = cast(str, _dict.get("role"))492    content = cast(str, _dict.get("content") or "")493    additional_kwargs: dict = {}494    if _dict.get("function_call"):495        function_call = dict(_dict["function_call"])496        if "name" in function_call and function_call["name"] is None:497            function_call["name"] = ""498        additional_kwargs["function_call"] = function_call499    tool_call_chunks = []500    if raw_tool_calls := _dict.get("tool_calls"):501        try:502            tool_call_chunks = [503                tool_call_chunk(504                    name=rtc["function"].get("name"),505                    args=rtc["function"].get("arguments"),506                    id=rtc.get("id"),507                    index=rtc["index"],508                )509                for rtc in raw_tool_calls510            ]511        except KeyError:512            pass513514    if role == "user" or default_class == HumanMessageChunk:515        return HumanMessageChunk(content=content, id=id_)516    if role == "assistant" or default_class == AIMessageChunk:517        return AIMessageChunk(518            content=content,519            additional_kwargs=additional_kwargs,520            id=id_,521            tool_call_chunks=tool_call_chunks,  # type: ignore[arg-type]522        )523    if role in ("system", "developer") or default_class == SystemMessageChunk:524        if role == "developer":525            additional_kwargs = {"__openai_role__": "developer"}526        else:527            additional_kwargs = {}528        return SystemMessageChunk(529            content=content, id=id_, additional_kwargs=additional_kwargs530        )531    if role == "function" or default_class == FunctionMessageChunk:532        return FunctionMessageChunk(content=content, name=_dict["name"], id=id_)533    if role == "tool" or default_class == ToolMessageChunk:534        return ToolMessageChunk(535            content=content, tool_call_id=_dict["tool_call_id"], id=id_536        )537    if role or default_class == ChatMessageChunk:538        return ChatMessageChunk(content=content, role=role, id=id_)539    return default_class(content=content, id=id_)  # type: ignore[call-arg]540541542def _update_token_usage(543    overall_token_usage: int | dict, new_usage: int | dict544) -> int | dict:545    # Token usage is either ints or dictionaries546    # `reasoning_tokens` is nested inside `completion_tokens_details`547    if isinstance(new_usage, int):548        if not isinstance(overall_token_usage, int):549            msg = (550                f"Got different types for token usage: "551                f"{type(new_usage)} and {type(overall_token_usage)}"552            )553            raise ValueError(msg)554        return new_usage + overall_token_usage555    if isinstance(new_usage, dict):556        if not isinstance(overall_token_usage, dict):557            msg = (558                f"Got different types for token usage: "559                f"{type(new_usage)} and {type(overall_token_usage)}"560            )561            raise ValueError(msg)562        return {563            k: _update_token_usage(overall_token_usage.get(k, 0), v)564            for k, v in new_usage.items()565        }566    warnings.warn(f"Unexpected type for token usage: {type(new_usage)}")567    return new_usage568569570class OpenAIContextOverflowError(openai.BadRequestError, ContextOverflowError):571    """BadRequestError raised when input exceeds OpenAI's context limit."""572573574class OpenAIAPIContextOverflowError(openai.APIError, ContextOverflowError):575    """APIError raised when input exceeds OpenAI's context limit."""576577578class OpenAIAuthenticationError(openai.AuthenticationError, ModelAuthenticationError):579    """OpenAI authentication error classified as a LangChain model error."""580581582class OpenAIPermissionDeniedError(583    openai.PermissionDeniedError, ModelPermissionDeniedError584):585    """OpenAI permission error classified as a LangChain model error."""586587588class OpenAIInvalidRequestError(openai.BadRequestError, ModelInvalidRequestError):589    """OpenAI bad-request error classified as a LangChain model error."""590591592class OpenAIModelNotFoundError(openai.NotFoundError, ModelNotFoundError):593    """OpenAI not-found error classified as a LangChain model error."""594595596class OpenAIRateLimitError(openai.RateLimitError, ModelRateLimitError):597    """OpenAI rate-limit error classified as a LangChain model error."""598599600class OpenAIAPIError(openai.InternalServerError, ModelAPIError):601    """OpenAI server error classified as a LangChain model error."""602603604class OpenAIConnectionError(openai.APIConnectionError, ModelConnectionError):605    """OpenAI connection error classified as a LangChain model error."""606607608class OpenAITimeoutError(openai.APITimeoutError, ModelTimeoutError):609    """OpenAI timeout error classified as a LangChain model error."""610611612def _handle_openai_bad_request(e: openai.BadRequestError) -> None:613    if (614        "context_length_exceeded" in str(e)615        or "Input tokens exceed the configured limit" in e.message616        or "prompt is too long" in e.message617        or "ContextWindowExceededError" in e.message618    ):619        raise OpenAIContextOverflowError(620            message=e.message, response=e.response, body=e.body621        ) from e622    if (623        "'response_format' of type 'json_schema' is not supported with this model"624    ) in e.message:625        message = (626            "This model does not support OpenAI's structured output feature, which "627            "is the default method for `with_structured_output` as of "628            "langchain-openai==0.3. To use `with_structured_output` with this model, "629            'specify `method="function_calling"`.'630        )631        warnings.warn(message)632        raise OpenAIInvalidRequestError(633            message=e.message, response=e.response, body=e.body634        ) from e635    if "Invalid schema for response_format" in e.message:636        message = (637            "Invalid schema for OpenAI's structured output feature, which is the "638            "default method for `with_structured_output` as of langchain-openai==0.3. "639            'Specify `method="function_calling"` instead or update your schema. '640            "See supported schemas: "641            "https://platform.openai.com/docs/guides/structured-outputs#supported-schemas"642        )643        warnings.warn(message)644    raise OpenAIInvalidRequestError(645        message=e.message, response=e.response, body=e.body646    ) from e647648649def _handle_openai_api_error(e: openai.APIError) -> None:650    error_message = str(e)651    if "exceeds the context window" in error_message:652        raise OpenAIAPIContextOverflowError(653            message=e.message, request=e.request, body=e.body654        ) from e655    if isinstance(e, openai.AuthenticationError):656        raise OpenAIAuthenticationError(657            message=e.message, response=e.response, body=e.body658        ) from e659    if isinstance(e, openai.PermissionDeniedError):660        raise OpenAIPermissionDeniedError(661            message=e.message, response=e.response, body=e.body662        ) from e663    if isinstance(e, openai.NotFoundError):664        raise OpenAIModelNotFoundError(665            message=e.message, response=e.response, body=e.body666        ) from e667    if isinstance(e, openai.RateLimitError):668        raise OpenAIRateLimitError(669            message=e.message, response=e.response, body=e.body670        ) from e671    if isinstance(e, openai.InternalServerError):672        raise OpenAIAPIError(message=e.message, response=e.response, body=e.body) from e673    if isinstance(e, openai.APITimeoutError):674        raise OpenAITimeoutError(e.request) from e675    if isinstance(e, openai.APIConnectionError):676        raise OpenAIConnectionError(message=e.message, request=e.request) from e677    raise678679680def _add_gateway_metadata(generation_info: dict[str, Any], raw_response: Any) -> None:681    """Add parsed LangSmith gateway metadata to `generation_info`, if present.682683    Args:684        generation_info: Generation info to mutate in place.685        raw_response: The raw provider response, or None.686    """687    headers = getattr(raw_response, "headers", None)688    if headers is None:689        return690    gateway_metadata = _parse_gateway_metadata(headers)691    if gateway_metadata is not None:692        generation_info[GATEWAY_METADATA_RESPONSE_KEY] = gateway_metadata693694695_RESPONSES_API_ONLY_PREFIXES = (696    "gpt-5-pro",697    "gpt-5.2-pro",698    "gpt-5.4-pro",699    "gpt-5.5-pro",700    "gpt-5.6-sol",701)702703704def _model_prefers_responses_api(model_name: str | None) -> bool:705    if not model_name:706        return False707    return model_name.startswith(_RESPONSES_API_ONLY_PREFIXES) or "codex" in model_name708709710_BM = TypeVar("_BM", bound=BaseModel)711_DictOrPydanticClass: TypeAlias = dict[str, Any] | type[_BM] | type712_DictOrPydantic: TypeAlias = dict | _BM713714715class BaseChatOpenAI(BaseChatModel):716    """Base wrapper around OpenAI large language models for chat.717718    This base class targets719    [official OpenAI API specifications](https://github.com/openai/openai-openapi)720    only. Non-standard response fields added by third-party providers (e.g.,721    `reasoning_content`) are not extracted. Use a provider-specific subclass for722    full provider support.723    """724725    client: Any = Field(default=None, exclude=True)726727    async_client: Any = Field(default=None, exclude=True)728729    root_client: Any = Field(default=None, exclude=True)730731    root_async_client: Any = Field(default=None, exclude=True)732733    model_name: str = Field(default="gpt-3.5-turbo", alias="model")734    """Model name to use."""735736    temperature: float | None = None737    """What sampling temperature to use."""738739    model_kwargs: dict[str, Any] = Field(default_factory=dict)740    """Holds any model parameters valid for `create` call not explicitly specified."""741742    openai_api_key: (743        SecretStr | None | Callable[[], str] | Callable[[], Awaitable[str]]744    ) = Field(alias="api_key", default=None)745    """API key to use.746747    Can be inferred from the `OPENAI_API_KEY` environment variable, or specified748    as a string, or sync or async callable that returns a string.749750    ??? example "Specify with environment variable"751752        ```bash753        export OPENAI_API_KEY=...754        ```755        ```python756        from langchain_openai import ChatOpenAI757758        model = ChatOpenAI(model="gpt-5-nano")759        ```760761    ??? example "Specify with a string"762763        ```python764        from langchain_openai import ChatOpenAI765766        model = ChatOpenAI(model="gpt-5-nano", api_key="...")767        ```768769    ??? example "Specify with a sync callable"770771        ```python772        from langchain_openai import ChatOpenAI773774        def get_api_key() -> str:775            # Custom logic to retrieve API key776            return "..."777778        model = ChatOpenAI(model="gpt-5-nano", api_key=get_api_key)779        ```780781    ??? example "Specify with an async callable"782783        ```python784        from langchain_openai import ChatOpenAI785786        async def get_api_key() -> str:787            # Custom async logic to retrieve API key788            return "..."789790        model = ChatOpenAI(model="gpt-5-nano", api_key=get_api_key)791        ```792    """793794    openai_api_base: str | None = Field(default=None, alias="base_url")795    """Base URL path for API requests, leave blank if not using a proxy or service emulator.796797    Resolution order (first match wins):798799    1. Explicit `base_url` (or `openai_api_base`) kwarg.800    2. Env var `OPENAI_API_BASE` (read by LangChain at init).801    3. Env var `OPENAI_BASE_URL` (read by the underlying `openai` SDK client).802803    `OPENAI_BASE_URL` is also inspected by LangChain only to decide whether to804    default-enable `stream_usage`  when set, the default is left off because many805    non-OpenAI endpoints do not support streaming token usage.806    """  # noqa: E501807808    openai_organization: str | None = Field(default=None, alias="organization")809    """Automatically inferred from env var `OPENAI_ORG_ID` if not provided."""810811    # to support explicit proxy for OpenAI812    openai_proxy: str | None = Field(813        default_factory=from_env("OPENAI_PROXY", default=None)814    )815816    request_timeout: float | tuple[float, float] | Any | None = Field(817        default=None, alias="timeout"818    )819    """Timeout for requests to OpenAI completion API.820821    Can be float, `httpx.Timeout` or `None`.822    """823824    stream_usage: bool | None = None825    """Whether to include usage metadata in streaming output.826827    If enabled, an additional message chunk will be generated during the stream828    including usage metadata.829830    This parameter is enabled unless `openai_api_base` is set or the model is831    initialized with a custom client, as many chat completions APIs do not832    support streaming token usage.833834    !!! version-added "Added in `langchain-openai` 0.3.9"835836    !!! warning "Behavior changed in `langchain-openai` 0.3.35"837838        Enabled for default base URL and client.839    """840841    max_retries: int | None = None842    """Maximum number of retries to make when generating."""843844    presence_penalty: float | None = None845    """Penalizes repeated tokens."""846847    frequency_penalty: float | None = None848    """Penalizes repeated tokens according to frequency."""849850    seed: int | None = None851    """Seed for generation"""852853    logprobs: bool | None = None854    """Whether to return logprobs."""855856    top_logprobs: int | None = None857    """Number of most likely tokens to return at each token position, each with an858    associated log probability.859860    `logprobs` must be set to true if this parameter is used.861    """862863    logit_bias: dict[int, int] | None = None864    """Modify the likelihood of specified tokens appearing in the completion."""865866    streaming: bool = False867    """Whether to stream the results or not."""868869    n: int | None = None870    """Number of chat completions to generate for each prompt."""871872    top_p: float | None = None873    """Total probability mass of tokens to consider at each step."""874875    max_tokens: int | None = Field(default=None)876    """Maximum number of tokens to generate."""877878    reasoning_effort: str | None = None879    """Constrains effort on reasoning for reasoning models.880881    For use with the Chat Completions API. Reasoning models only.882883    Currently supported values are `'minimal'`, `'low'`, `'medium'`, and884    `'high'`. Reducing reasoning effort can result in faster responses and fewer885    tokens used on reasoning in a response.886887    !!! note "Changing reasoning effort mid-conversation"888889        Changing this value part-way through a conversation changes a request-level890        parameter, which invalidates the cached prompt prefix.891892        Models that support it (currently GPT-6) can instead carry the new effort893        in a `configuration_update` item attached to the message that should start894        using it:895896        ```python897        HumanMessage(898            [899                {"type": "configuration_update", "reasoning": {"effort": "high"}},900                {"type": "text", "text": "Analyze the failure modes."},901            ]902        )903        ```904905        The new effort applies from that message onward, until another update906        overrides it.907    """908909    reasoning: dict[str, Any] | None = None910    """Reasoning parameters for reasoning models. None disables reasoning.911912    For use with the Responses API.913914    ```python915    reasoning={916        "effort": None,  # Default None; can be "low", "medium", or "high"917        "summary": "auto",  # Can be "auto", "concise", or "detailed"918    }919    ```920921    !!! version-added "Added in `langchain-openai` 0.3.24"922    """923924    verbosity: str | None = None925    """Controls the verbosity level of responses for reasoning models.926927    For use with the Responses API.928929    Currently supported values are `'low'`, `'medium'`, and `'high'`.930931    !!! version-added "Added in `langchain-openai` 0.3.28"932    """933934    tiktoken_model_name: str | None = None935    """The model name to pass to tiktoken when using this class.936937    Tiktoken is used to count the number of tokens in documents to constrain938    them to be under a certain limit.939940    By default, when set to `None`, this will be the same as the embedding model name.941    However, there are some cases where you may want to use this `Embedding` class with942    a model name not supported by tiktoken. This can include when using Azure embeddings943    or when using one of the many model providers that expose an OpenAI-like944    API but with different models. In those cases, in order to avoid erroring945    when tiktoken is called, you can specify a model name to use here.946    """947948    default_headers: Mapping[str, str] | None = None949950    default_query: Mapping[str, object] | None = None951952    # Configure a custom httpx client. See the953    # [httpx documentation](https://www.python-httpx.org/api/#client) for more details.954    http_client: Any | None = Field(default=None, exclude=True)955    """Optional `httpx.Client`.956957    Only used for sync invocations. Must specify `http_async_client` as well if958    you'd like a custom client for async invocations.959    """960961    http_async_client: Any | None = Field(default=None, exclude=True)962    """Optional `httpx.AsyncClient`.963964    Only used for async invocations. Must specify `http_client` as well if you'd965    like a custom client for sync invocations.966    """967968    http_socket_options: Sequence[tuple[int, int, int]] | None = Field(969        default=None, exclude=True970    )971    """TCP socket options applied to the httpx transports built by this instance.972973    Defaults to a conservative TCP-keepalive + `TCP_USER_TIMEOUT` profile that974    targets a ~2-minute bound on silent connection hangs (silent mid-stream peer975    loss, gVisor/NAT idle timeouts, silent TCP black holes) on platforms that976    support the full option set. On platforms that only support a subset977    (macOS without `TCP_USER_TIMEOUT`, Windows with only `SO_KEEPALIVE`,978    minimal kernels), unsupported options are silently dropped and the bound979    degrades to whatever the remaining options + OS defaults provide  still980    better than indefinite hang.981982    Accepted values:983984    - `None` (default): use env-driven defaults. Matches the "unset" convention985        used by `http_client` elsewhere on this class.986    - `()` (empty): disable socket-option injection entirely. Inherits the OS987        defaults and restores httpx's native env-proxy auto-detection.988    - A non-empty sequence of `(level, option, value)` tuples: explicit989        override; passed verbatim to the transport (not filtered). Unsupported990        options raise `OSError` at connect time rather than being silently991        dropped  the user chose them explicitly.992993    Environment variables (only consulted when this field is `None`):994    `LANGCHAIN_OPENAI_TCP_KEEPALIVE` (set to `0` to disable entirely  the995    kill-switch), `LANGCHAIN_OPENAI_TCP_KEEPIDLE`,996    `LANGCHAIN_OPENAI_TCP_KEEPINTVL`, `LANGCHAIN_OPENAI_TCP_KEEPCNT`,997    `LANGCHAIN_OPENAI_TCP_USER_TIMEOUT_MS`.998999    Applied per side: if `http_client` is supplied, the sync path uses1000    that user-owned client's socket options as-is; the async path still1001    gets `http_socket_options` applied to its default builder (and1002    vice-versa for `http_async_client`). Supply both to take full control.10031004    !!! note "Interaction with env-proxy auto-detection"10051006        When a custom `httpx` transport is active, `httpx` disables its1007        native env-proxy auto-detection (`HTTP_PROXY` / `HTTPS_PROXY` /1008        `ALL_PROXY` / `NO_PROXY` and macOS/Windows system proxy settings).10091010        To keep the default shape safe, `ChatOpenAI` detects the1011        "proxy-env-shadow" pattern and **skips the custom transport1012        entirely** when **all** of the following hold:10131014        - `http_socket_options` is left at its default (`None`)1015        - No `http_client` or `http_async_client` supplied1016        - No `openai_proxy` supplied1017        - A proxy env var or system proxy is visible to httpx10181019        On that specific shape, the instance falls back to pre-PR behavior1020        and httpx's env-proxy auto-detection applies (a one-time `INFO` log1021        records the bypass for observability).10221023        If you explicitly set `http_socket_options=[...]` while a proxy1024        env var is also set, no bypass  you opted into the transport, and1025        a one-time `WARNING` records the shadowing. Set1026        `http_socket_options=()` or `LANGCHAIN_OPENAI_TCP_KEEPALIVE=0` to1027        disable transport injection explicitly, or pass a fully-configured1028        `http_async_client` / `http_client` to take full control. The1029        `openai_proxy` constructor kwarg is unaffected  socket options1030        are applied cleanly through the proxied transport on that path.1031    """10321033    stream_chunk_timeout: float | None = Field(1034        default_factory=lambda: _float_env(1035            "LANGCHAIN_OPENAI_STREAM_CHUNK_TIMEOUT_S", 120.01036        ),1037        exclude=True,1038    )1039    """Per-chunk wall-clock timeout (seconds) on async streaming responses.10401041    Applies to async invocations only (`astream`, `ainvoke` with streaming,1042    etc.). Sync streaming (`stream`) is not affected.10431044    Fires between content chunks yielded by the openai SDK's streaming iterator1045    (i.e., each call to `__anext__` on the response). Crucially, this is1046    **not** the same as httpx's `timeout.read`:10471048    - httpx's read timeout is inter-byte and gets reset every time *any* bytes1049        arrive on the socket  including OpenAI's SSE keepalive comments1050        (`: keepalive`) that trickle down during long model generations. A1051        stream that's silent on *content* but still producing keepalives looks1052        alive forever to httpx.1053    - `stream_chunk_timeout` measures the gap between *parsed chunks*. The1054        openai SDK's SSE parser consumes keepalive comments internally and does1055        not emit them as chunks, so keepalives do *not* reset this timer. It1056        fires on genuine content silence.10571058    When it fires, a `StreamChunkTimeoutError`1059    (subclass of `asyncio.TimeoutError`) is raised with a self-describing1060    message naming this knob, the env-var override, the model, and the1061    number of chunks received before the stall. A WARNING log with1062    `extra={"source": "stream_chunk_timeout", "timeout_s": <value>,1063    "model_name": <value>, "chunks_received": <value>}` also fires so1064    aggregate logging can distinguish app-layer timeouts from1065    transport-layer failures.10661067    Defaults to 120s. Set to `None` or `0` to disable. Overridable via the1068    `LANGCHAIN_OPENAI_STREAM_CHUNK_TIMEOUT_S` env var. Negative values1069    (from either the env var or the constructor kwarg  e.g., hydrated1070    from YAML/JSON configs) fall back to the default with a `WARNING` log1071    rather than silently disabling the wrapper, so a misconfigured value1072    still boots safely and the fallback is visible.1073    """10741075    stop: list[str] | str | None = Field(default=None, alias="stop_sequences")1076    """Default stop sequences."""10771078    extra_body: Mapping[str, Any] | None = None1079    """Optional additional JSON properties to include in the request parameters1080    when making requests to OpenAI compatible APIs, such as vLLM, LM Studio, or1081    other providers.10821083    This is the recommended way to pass custom parameters that are specific to your1084    OpenAI-compatible API provider but not part of the standard OpenAI API.10851086    Examples:1087    - [LM Studio](https://lmstudio.ai/) TTL parameter: `extra_body={"ttl": 300}`1088    - [vLLM](https://github.com/vllm-project/vllm) custom parameters:1089        `extra_body={"use_beam_search": True}`1090    - Any other provider-specific parameters10911092    !!! warning10931094        Do not use `model_kwargs` for custom parameters that are not part of the1095        standard OpenAI API, as this will cause errors when making API calls. Use1096        `extra_body` instead.1097    """10981099    include_response_headers: bool = False1100    """Whether to include response headers in the output message `response_metadata`.11011102    Note: some inference providers return additional metadata (such as served model1103    names) in the response headers. Enable to capture these metadata.1104    """11051106    disabled_params: dict[str, Any] | None = Field(default=None)1107    """Parameters of the OpenAI client or `chat.completions` endpoint that should be1108    disabled for the given model.11091110    Should be specified as `{"param": None | ['val1', 'val2']}` where the key is the1111    parameter and the value is either None, meaning that parameter should never be1112    used, or it's a list of disabled values for the parameter.11131114    For example, older models may not support the `'parallel_tool_calls'` parameter at1115    all, in which case `disabled_params={"parallel_tool_calls": None}` can be passed1116    in.11171118    If a parameter is disabled then it will not be used by default in any methods, e.g.1119    in `with_structured_output`. However this does not prevent a user from directly1120    passed in the parameter during invocation.1121    """11221123    context_management: list[dict[str, Any]] | None = None1124    """Configuration for1125    [context management](https://developers.openai.com/api/docs/guides/compaction).1126    """11271128    include: list[str] | None = None1129    """Additional fields to include in generations from Responses API.11301131    Supported values:11321133    - `'file_search_call.results'`1134    - `'message.input_image.image_url'`1135    - `'computer_call_output.output.image_url'`1136    - `'reasoning.encrypted_content'`1137    - `'code_interpreter_call.outputs'`11381139    !!! version-added "Added in `langchain-openai` 0.3.24"1140    """11411142    prompt_cache_options: dict[str, Any] | None = None1143    """Options controlling OpenAI prompt cache behavior.11441145    !!! version-added "Added in `langchain-openai` 1.3.5"1146    """11471148    service_tier: str | None = None1149    """Latency tier for request.11501151    Options are `'auto'`, `'default'`, or `'flex'`.11521153    Relevant for users of OpenAI's scale tier service.1154    """11551156    store: bool | None = None1157    """If `True`, OpenAI may store response data for future use.11581159    Defaults to `True` for the Responses API and `False` for the Chat Completions API.11601161    !!! version-added "Added in `langchain-openai` 0.3.24"1162    """11631164    truncation: str | None = None1165    """Truncation strategy (Responses API).11661167    Can be `'auto'` or `'disabled'` (default).11681169    If `'auto'`, model may drop input items from the middle of the message sequence to1170    fit the context window.11711172    !!! version-added "Added in `langchain-openai` 0.3.24"1173    """11741175    use_previous_response_id: bool = False1176    """If `True`, always pass `previous_response_id` using the ID of the most recent1177    response. Responses API only.11781179    Input messages up to the most recent response will be dropped from request1180    payloads.11811182    For example, the following two are equivalent:11831184    ```python1185    model = ChatOpenAI(1186        model="...",1187        use_previous_response_id=True,1188    )1189    model.invoke(1190        [1191            HumanMessage("Hello"),1192            AIMessage("Hi there!", response_metadata={"id": "resp_123"}),1193            HumanMessage("How are you?"),1194        ]1195    )1196    ```11971198    ```python1199    model = ChatOpenAI(model="...", use_responses_api=True)1200    model.invoke([HumanMessage("How are you?")], previous_response_id="resp_123")1201    ```12021203    !!! version-added "Added in `langchain-openai` 0.3.26"1204    """12051206    use_responses_api: bool | None = None1207    """Whether to use the Responses API instead of the Chat API.12081209    If not specified then will be inferred based on invocation params.12101211    !!! version-added "Added in `langchain-openai` 0.3.9"1212    """12131214    output_version: str | None = Field(1215        default_factory=from_env("LC_OUTPUT_VERSION", default=None)1216    )1217    """Version of `AIMessage` output format to use.12181219    This field is used to roll-out new output formats for chat model `AIMessage`1220    responses in a backwards-compatible way.12211222    Supported values:12231224    - `'v0'`: `AIMessage` format as of `langchain-openai 0.3.x`.1225    - `'responses/v1'`: Formats Responses API output items into AIMessage content blocks1226        (Responses API only)1227    - `'v1'`: v1 of LangChain cross-provider standard.12281229    !!! warning "Behavior changed in `langchain-openai` 1.0.0"12301231        Default updated to `"responses/v1"`.1232    """12331234    model_config = ConfigDict(populate_by_name=True)12351236    @property1237    def _uses_gateway(self) -> bool:1238        """Whether requests are routed through the LangSmith gateway.12391240        Detected from the resolved API key: LangSmith keys (used to authenticate1241        to the gateway) carry the `lsv2_` prefix. Callable keys cannot be1242        inspected without invoking them, so they are treated as non-gateway.1243        """1244        api_key = self.openai_api_key1245        if isinstance(api_key, SecretStr):1246            return api_key.get_secret_value().startswith("lsv2_")1247        return False12481249    @property1250    def model(self) -> str:1251        """Same as model_name."""1252        return self.model_name12531254    @model_validator(mode="before")1255    @classmethod1256    def build_extra(cls, values: dict[str, Any]) -> Any:1257        """Build extra kwargs from additional params that were passed in."""1258        all_required_field_names = get_pydantic_field_names(cls)1259        return _build_model_kwargs(values, all_required_field_names)12601261    @field_validator("stream_chunk_timeout", mode="after")1262    @classmethod1263    def _validate_stream_chunk_timeout(cls, value: float | None) -> float | None:1264        """Reject negative constructor values; fall back to the env-driven default.12651266        Matches the env-var path in `_float_env`: a negative value is a typo,1267        not an opt-out (`None`/`0` are the documented off switches). Configs1268        hydrated from YAML/JSON would otherwise silently disable the wrapper1269        and reintroduce the indefinite-stream hang the feature prevents.1270        """1271        if value is not None and value < 0:1272            fallback = _float_env("LANGCHAIN_OPENAI_STREAM_CHUNK_TIMEOUT_S", 120.0)1273            logger.warning(1274                "Invalid `stream_chunk_timeout=%r` (negative); "1275                "falling back to %s. Pass `None` or `0` to disable.",1276                value,1277                fallback,1278            )1279            return fallback1280        return value12811282    @model_validator(mode="before")1283    @classmethod1284    def validate_temperature(cls, values: dict[str, Any]) -> Any:1285        """Validate temperature parameter for different models.12861287        - gpt-5 models (excluding gpt-5-chat) only allow `temperature=1` or unset1288            (Defaults to 1)1289        """1290        model = values.get("model_name") or values.get("model") or ""1291        model_lower = model.lower()12921293        # For o1 models, set temperature=1 if not provided1294        if model_lower.startswith("o1") and "temperature" not in values:1295            values["temperature"] = 112961297        # For gpt-5 models, handle temperature restrictions. Temperature is supported1298        # by gpt-5-chat and gpt-5 models with reasoning_effort='none' or1299        # reasoning={'effort': 'none'}.1300        if (1301            model_lower.startswith("gpt-5")1302            and ("chat" not in model_lower)1303            and values.get("reasoning_effort") != "none"1304            and (values.get("reasoning") or {}).get("effort") != "none"1305        ):1306            temperature = values.get("temperature")1307            if temperature is not None and temperature != 1:1308                # For gpt-5 (non-chat), only temperature=1 is supported1309                # So we remove any non-defaults1310                values.pop("temperature", None)13111312        return values13131314    @model_validator(mode="after")1315    def _set_openai_chat_version(self) -> Self:1316        """Set package version in metadata.13171318        Note: Subclasses that inherit from `BaseChatOpenAI` (e.g.1319        `ChatDeepSeek`, `ChatXAI`) must use a **unique** validator name1320        (e.g. `_set_deepseek_version`) instead of overriding this one. Pydantic1321        replaces same-named `model_validator` methods rather than chaining them,1322        so reusing `_set_openai_chat_version` would silently drop the parent's1323        `langchain-openai` version entry.1324        """1325        self._add_version("langchain-openai", __version__)1326        return self13271328    @model_validator(mode="after")1329    def validate_environment(self) -> Self:1330        """Validate that api key and python package exists in environment."""1331        if self.n is not None and self.n < 1:1332            msg = "n must be at least 1."1333            raise ValueError(msg)1334        if self.n is not None and self.n > 1 and self.streaming:1335            msg = "n must be 1 when streaming."1336            raise ValueError(msg)13371338        # Check OPENAI_ORGANIZATION for backwards compatibility.1339        self.openai_organization = (1340            self.openai_organization1341            or os.getenv("OPENAI_ORG_ID")1342            or os.getenv("OPENAI_ORGANIZATION")1343        )1344        # Resolve base URL and API key, applying LangSmith gateway settings.1345        _gateway_config = _resolve_gateway_config(1346            base_url=self.openai_api_base,1347            api_key=self.openai_api_key,1348            provider_path="openai/v1",1349            base_url_env="OPENAI_API_BASE",1350            api_key_env="OPENAI_API_KEY",1351        )1352        self.openai_api_base = _gateway_config.base_url1353        self.openai_api_key = _gateway_config.api_key1354        _base_url_from_gateway = _gateway_config.base_url_from_gateway13551356        # Enable stream_usage by default if using default base URL and client,1357        # or when the base URL was set by the LangSmith gateway (which proxies1358        # to OpenAI and supports streaming token usage).1359        if all(1360            getattr(self, key, None) is None1361            for key in (1362                "stream_usage",1363                "openai_proxy",1364                "client",1365                "root_client",1366                "async_client",1367                "root_async_client",1368                "http_client",1369                "http_async_client",1370            )1371        ) and (1372            _base_url_from_gateway1373            or (self.openai_api_base is None and "OPENAI_BASE_URL" not in os.environ)1374        ):1375            self.stream_usage = True13761377        # Resolve API key from SecretStr or Callable1378        sync_api_key_value: str | Callable[[], str] | None = None1379        async_api_key_value: str | Callable[[], Awaitable[str]] | None = None13801381        if self.openai_api_key is not None:1382            # Because OpenAI and AsyncOpenAI clients support either sync or async1383            # callables for the API key, we need to resolve separate values here.1384            sync_api_key_value, async_api_key_value = _resolve_sync_and_async_api_keys(1385                self.openai_api_key1386            )13871388        client_params: dict = {1389            "organization": self.openai_organization,1390            "base_url": self.openai_api_base,1391            "timeout": self.request_timeout,1392            "default_headers": self.default_headers,1393            "default_query": self.default_query,1394        }1395        if self.max_retries is not None:1396            client_params["max_retries"] = self.max_retries13971398        if self.openai_proxy and (self.http_client or self.http_async_client):1399            openai_proxy = self.openai_proxy1400            http_client = self.http_client1401            http_async_client = self.http_async_client1402            msg = (1403                "Cannot specify 'openai_proxy' if one of "1404                "'http_client'/'http_async_client' is already specified. Received:\n"1405                f"{openai_proxy=}\n{http_client=}\n{http_async_client=}"1406            )1407            raise ValueError(msg)1408        if _should_bypass_socket_options_for_proxy_env(1409            http_socket_options=self.http_socket_options,1410            http_client=self.http_client,1411            http_async_client=self.http_async_client,1412            openai_proxy=self.openai_proxy,1413        ):1414            # Default-shape construction + proxy env var visible to httpx:1415            # skip the custom transport so httpx's env-proxy auto-detection1416            # still applies. Users who want kernel-level TCP tuning alongside1417            # an env proxy can opt in explicitly via `http_socket_options`.1418            resolved_socket_options: tuple[tuple[int, int, int], ...] = ()1419            _log_proxy_env_bypass_once()1420        else:1421            resolved_socket_options = _resolve_socket_options(self.http_socket_options)1422            _warn_if_proxy_env_shadowed(1423                resolved_socket_options, openai_proxy=self.openai_proxy1424            )1425        if not self.client:1426            if sync_api_key_value is None:1427                # No valid sync API key, leave client as None and raise informative1428                # error on invocation.1429                self.client = None1430                self.root_client = None1431            else:1432                if self.openai_proxy and not self.http_client:1433                    self.http_client = _build_proxied_sync_httpx_client(1434                        proxy=self.openai_proxy,1435                        verify=global_ssl_context,1436                        socket_options=resolved_socket_options,1437                    )1438                sync_specific = {1439                    "http_client": self.http_client1440                    or _get_default_httpx_client(1441                        self.openai_api_base,1442                        self.request_timeout,1443                        resolved_socket_options,1444                    ),1445                    "api_key": sync_api_key_value,1446                }1447                self.root_client = openai.OpenAI(**client_params, **sync_specific)  # type: ignore[arg-type]1448                self.client = self.root_client.chat.completions1449        if not self.async_client:1450            if self.openai_proxy and not self.http_async_client:1451                self.http_async_client = _build_proxied_async_httpx_client(1452                    proxy=self.openai_proxy,1453                    verify=global_ssl_context,1454                    socket_options=resolved_socket_options,1455                )1456            async_specific = {1457                "http_client": self.http_async_client1458                or _get_default_async_httpx_client(1459                    self.openai_api_base,1460                    self.request_timeout,1461                    resolved_socket_options,1462                ),1463                "api_key": async_api_key_value,1464            }1465            self.root_async_client = openai.AsyncOpenAI(1466                **client_params,1467                **async_specific,  # type: ignore[arg-type]1468            )1469            self.async_client = self.root_async_client.chat.completions1470        return self14711472    def _resolve_model_profile(self) -> ModelProfile | None:1473        return _get_default_model_profile(self.model_name) or None14741475    @property1476    def _default_params(self) -> dict[str, Any]:1477        """Get the default parameters for calling OpenAI API."""1478        exclude_if_none = {1479            "presence_penalty": self.presence_penalty,1480            "frequency_penalty": self.frequency_penalty,1481            "seed": self.seed,1482            "top_p": self.top_p,1483            "logprobs": self.logprobs,1484            "top_logprobs": self.top_logprobs,1485            "logit_bias": self.logit_bias,1486            "stop": self.stop or None,  # Also exclude empty list for this1487            "max_tokens": self.max_tokens,1488            "extra_body": self.extra_body,1489            "n": self.n,1490            "temperature": self.temperature,1491            "reasoning_effort": self.reasoning_effort,1492            "reasoning": self.reasoning,1493            "verbosity": self.verbosity,1494            "context_management": self.context_management,1495            "include": self.include,1496            "prompt_cache_options": self.prompt_cache_options,1497            "service_tier": self.service_tier,1498            "truncation": self.truncation,1499            "store": self.store,1500        }15011502        return {1503            "model": self.model_name,1504            "stream": self.streaming,1505            **{k: v for k, v in exclude_if_none.items() if v is not None},1506            **self.model_kwargs,1507        }15081509    def _combine_llm_outputs(self, llm_outputs: list[dict | None]) -> dict:1510        overall_token_usage: dict = {}1511        system_fingerprint = None1512        for output in llm_outputs:1513            if output is None:1514                # Happens in streaming1515                continue1516            token_usage = output.get("token_usage")1517            if token_usage is not None:1518                for k, v in token_usage.items():1519                    if v is None:1520                        continue1521                    if k in overall_token_usage:1522                        overall_token_usage[k] = _update_token_usage(1523                            overall_token_usage[k], v1524                        )1525                    else:1526                        overall_token_usage[k] = v1527            if system_fingerprint is None:1528                system_fingerprint = output.get("system_fingerprint")1529        combined = {"token_usage": overall_token_usage, "model_name": self.model_name}1530        if system_fingerprint:1531            combined["system_fingerprint"] = system_fingerprint1532        return combined15331534    def _convert_chunk_to_generation_chunk(1535        self,1536        chunk: dict,1537        default_chunk_class: type,1538        base_generation_info: dict | None,1539    ) -> ChatGenerationChunk | None:1540        if chunk.get("type") == "content.delta":  # From beta.chat.completions.stream1541            return None1542        token_usage = chunk.get("usage")1543        choices = (1544            chunk.get("choices", [])1545            # From beta.chat.completions.stream1546            or chunk.get("chunk", {}).get("choices", [])1547        )15481549        usage_metadata: UsageMetadata | None = (1550            _create_usage_metadata(token_usage, chunk.get("service_tier"))1551            if token_usage1552            else None1553        )1554        if len(choices) == 0:1555            # logprobs is implicitly None1556            generation_chunk = ChatGenerationChunk(1557                message=default_chunk_class(content="", usage_metadata=usage_metadata),1558                generation_info=base_generation_info,1559            )1560            # Keep content as "" (the default) rather than converting to [].1561            # Chat Completions content deltas are normalized to strings in1562            # _convert_delta_to_message_chunk. Starting with [] causes1563            # merge_content to silently drop string content (empty list is1564            # falsy, so no merge branch applies). The empty list also triggers1565            # the content_blocks isinstance(list) short-circuit, which would1566            # return [] and miss tool_call_chunks.1567            if self.output_version == "v1":1568                generation_chunk.message.response_metadata["output_version"] = "v1"15691570            return generation_chunk15711572        choice = choices[0]1573        if choice["delta"] is None:1574            return None15751576        message_chunk = _convert_delta_to_message_chunk(1577            choice["delta"], default_chunk_class1578        )1579        generation_info = {**base_generation_info} if base_generation_info else {}15801581        if finish_reason := choice.get("finish_reason"):1582            generation_info["finish_reason"] = finish_reason1583            if model_name := chunk.get("model"):1584                generation_info["model_name"] = model_name1585            if system_fingerprint := chunk.get("system_fingerprint"):1586                generation_info["system_fingerprint"] = system_fingerprint1587            if service_tier := chunk.get("service_tier"):1588                generation_info["service_tier"] = service_tier15891590        logprobs = choice.get("logprobs")1591        if logprobs:1592            generation_info["logprobs"] = logprobs15931594        if usage_metadata and isinstance(message_chunk, AIMessageChunk):1595            message_chunk.usage_metadata = usage_metadata15961597        message_chunk.response_metadata["model_provider"] = "openai"1598        # Propagate output_version so content_blocks can detect v1 mode.1599        if self.output_version == "v1":1600            message_chunk.response_metadata["output_version"] = "v1"1601        return ChatGenerationChunk(1602            message=message_chunk, generation_info=generation_info or None1603        )16041605    def _ensure_sync_client_available(self) -> None:1606        """Check that sync client is available, raise error if not."""1607        if self.client is None:1608            msg = (1609                "Sync client is not available. This happens when an async callable "1610                "was provided for the API key. Use async methods (ainvoke, astream) "1611                "instead, or provide a string or sync callable for the API key."1612            )1613            raise ValueError(msg)16141615    def _stream_responses(1616        self,1617        messages: list[BaseMessage],1618        stop: list[str] | None = None,1619        run_manager: CallbackManagerForLLMRun | None = None,1620        **kwargs: Any,1621    ) -> Iterator[ChatGenerationChunk]:1622        self._ensure_sync_client_available()1623        kwargs["stream"] = True1624        payload = self._get_request_payload(messages, stop=stop, **kwargs)1625        headers: dict = {}1626        base_generation_info: dict = {}1627        try:1628            if self.include_response_headers or self._uses_gateway:1629                raw_context_manager = (1630                    self.root_client.with_raw_response.responses.create(**payload)1631                )1632                context_manager = raw_context_manager.parse()1633                if self.include_response_headers:1634                    headers = {"headers": dict(raw_context_manager.headers)}1635                _add_gateway_metadata(base_generation_info, raw_context_manager)1636            else:1637                context_manager = self.root_client.responses.create(**payload)1638            original_schema_obj = kwargs.get("response_format")16391640            with context_manager as response:1641                is_first_chunk = True1642                current_index = -11643                current_output_index = -11644                current_sub_index = -11645                has_reasoning = False1646                for chunk in response:1647                    metadata = headers if is_first_chunk else {}1648                    (1649                        current_index,1650                        current_output_index,1651                        current_sub_index,1652                        generation_chunk,1653                    ) = _convert_responses_chunk_to_generation_chunk(1654                        chunk,1655                        current_index,1656                        current_output_index,1657                        current_sub_index,1658                        schema=original_schema_obj,1659                        metadata=metadata,1660                        has_reasoning=has_reasoning,1661                        output_version=self.output_version,1662                    )1663                    if generation_chunk:1664                        if is_first_chunk and base_generation_info:1665                            generation_chunk.generation_info = {1666                                **base_generation_info,1667                                **(generation_chunk.generation_info or {}),1668                            }1669                        if run_manager:1670                            run_manager.on_llm_new_token(1671                                generation_chunk.text, chunk=generation_chunk1672                            )1673                        is_first_chunk = False1674                        if "reasoning" in generation_chunk.message.additional_kwargs:1675                            has_reasoning = True1676                        yield generation_chunk1677        except openai.BadRequestError as e:1678            _handle_openai_bad_request(e)1679        except openai.APIError as e:1680            _handle_openai_api_error(e)16811682    async def _astream_responses(1683        self,1684        messages: list[BaseMessage],1685        stop: list[str] | None = None,1686        run_manager: AsyncCallbackManagerForLLMRun | None = None,1687        **kwargs: Any,1688    ) -> AsyncIterator[ChatGenerationChunk]:1689        kwargs["stream"] = True1690        payload = self._get_request_payload(messages, stop=stop, **kwargs)1691        headers: dict = {}1692        base_generation_info: dict = {}1693        try:1694            if self.include_response_headers or self._uses_gateway:1695                raw_context_manager = (1696                    await self.root_async_client.with_raw_response.responses.create(1697                        **payload1698                    )1699                )1700                context_manager = raw_context_manager.parse()1701                if self.include_response_headers:1702                    headers = {"headers": dict(raw_context_manager.headers)}1703                _add_gateway_metadata(base_generation_info, raw_context_manager)1704            else:1705                context_manager = await self.root_async_client.responses.create(1706                    **payload1707                )1708            original_schema_obj = kwargs.get("response_format")17091710            async with context_manager as response:1711                is_first_chunk = True1712                current_index = -11713                current_output_index = -11714                current_sub_index = -11715                has_reasoning = False1716                async for chunk in _astream_with_chunk_timeout(1717                    response,1718                    self.stream_chunk_timeout,1719                    model_name=self.model_name,1720                ):1721                    metadata = headers if is_first_chunk else {}1722                    (1723                        current_index,1724                        current_output_index,1725                        current_sub_index,1726                        generation_chunk,1727                    ) = _convert_responses_chunk_to_generation_chunk(1728                        chunk,1729                        current_index,1730                        current_output_index,1731                        current_sub_index,1732                        schema=original_schema_obj,1733                        metadata=metadata,1734                        has_reasoning=has_reasoning,1735                        output_version=self.output_version,1736                    )1737                    if generation_chunk:1738                        if is_first_chunk and base_generation_info:1739                            generation_chunk.generation_info = {1740                                **base_generation_info,1741                                **(generation_chunk.generation_info or {}),1742                            }1743                        if run_manager:1744                            await run_manager.on_llm_new_token(1745                                generation_chunk.text, chunk=generation_chunk1746                            )1747                        is_first_chunk = False1748                        if "reasoning" in generation_chunk.message.additional_kwargs:1749                            has_reasoning = True1750                        yield generation_chunk1751        except openai.BadRequestError as e:1752            _handle_openai_bad_request(e)1753        except openai.APIError as e:1754            _handle_openai_api_error(e)17551756    def _should_stream_usage(1757        self, stream_usage: bool | None = None, **kwargs: Any1758    ) -> bool:1759        """Determine whether to include usage metadata in streaming output.17601761        For backwards compatibility, we check for `stream_options` passed1762        explicitly to kwargs or in the `model_kwargs` and override `self.stream_usage`.1763        """1764        stream_usage_sources = [  # order of precedence1765            stream_usage,1766            kwargs.get("stream_options", {}).get("include_usage"),1767            self.model_kwargs.get("stream_options", {}).get("include_usage"),1768            self.stream_usage,1769        ]1770        for source in stream_usage_sources:1771            if isinstance(source, bool):1772                return source1773        return self.stream_usage or False17741775    def _stream(1776        self,1777        messages: list[BaseMessage],1778        stop: list[str] | None = None,1779        run_manager: CallbackManagerForLLMRun | None = None,1780        *,1781        stream_usage: bool | None = None,1782        **kwargs: Any,1783    ) -> Iterator[ChatGenerationChunk]:1784        self._ensure_sync_client_available()1785        kwargs["stream"] = True1786        stream_usage = self._should_stream_usage(stream_usage, **kwargs)1787        if stream_usage:1788            kwargs["stream_options"] = {"include_usage": stream_usage}1789        payload = self._get_request_payload(messages, stop=stop, **kwargs)1790        default_chunk_class: type[BaseMessageChunk] = AIMessageChunk1791        base_generation_info = {}17921793        try:1794            if "response_format" in payload:1795                if self.include_response_headers:1796                    warnings.warn(1797                        "Cannot currently include response headers when "1798                        "response_format is specified."1799                    )1800                payload.pop("stream")1801                response_stream = self.root_client.beta.chat.completions.stream(1802                    **payload1803                )1804                context_manager = response_stream1805            else:1806                if self.include_response_headers or self._uses_gateway:1807                    raw_response = self.client.with_raw_response.create(**payload)1808                    response = raw_response.parse()1809                    if self.include_response_headers:1810                        base_generation_info = {"headers": dict(raw_response.headers)}1811                    _add_gateway_metadata(base_generation_info, raw_response)1812                else:1813                    response = self.client.create(**payload)1814                context_manager = response1815            with context_manager as response:1816                is_first_chunk = True1817                for chunk in response:1818                    if not isinstance(chunk, dict):1819                        chunk = chunk.model_dump()1820                    generation_chunk = self._convert_chunk_to_generation_chunk(1821                        chunk,1822                        default_chunk_class,1823                        base_generation_info if is_first_chunk else {},1824                    )1825                    if generation_chunk is None:1826                        continue1827                    default_chunk_class = generation_chunk.message.__class__1828                    logprobs = (generation_chunk.generation_info or {}).get("logprobs")1829                    if run_manager:1830                        run_manager.on_llm_new_token(1831                            generation_chunk.text,1832                            chunk=generation_chunk,1833                            logprobs=logprobs,1834                        )1835                    is_first_chunk = False1836                    yield generation_chunk1837        except openai.BadRequestError as e:1838            _handle_openai_bad_request(e)1839        except openai.APIError as e:1840            _handle_openai_api_error(e)1841        if hasattr(response, "get_final_completion") and "response_format" in payload:1842            final_completion = response.get_final_completion()1843            generation_chunk = self._get_generation_chunk_from_completion(1844                final_completion1845            )1846            if run_manager:1847                run_manager.on_llm_new_token(1848                    generation_chunk.text, chunk=generation_chunk1849                )1850            yield generation_chunk18511852    def _generate(1853        self,1854        messages: list[BaseMessage],1855        stop: list[str] | None = None,1856        run_manager: CallbackManagerForLLMRun | None = None,1857        **kwargs: Any,1858    ) -> ChatResult:1859        self._ensure_sync_client_available()1860        payload = self._get_request_payload(messages, stop=stop, **kwargs)1861        generation_info = None1862        raw_response = None1863        try:1864            if "response_format" in payload:1865                payload.pop("stream")1866                raw_response = (1867                    self.root_client.chat.completions.with_raw_response.parse(**payload)1868                )1869                response = raw_response.parse()1870            elif self._use_responses_api(payload):1871                original_schema_obj = kwargs.get("response_format")1872                if original_schema_obj and _is_pydantic_class(original_schema_obj):1873                    raw_response = self.root_client.responses.with_raw_response.parse(1874                        **payload1875                    )1876                else:1877                    raw_response = self.root_client.responses.with_raw_response.create(1878                        **payload1879                    )1880                response = raw_response.parse()1881                if self.include_response_headers:1882                    generation_info = {"headers": dict(raw_response.headers)}1883                generation_info = generation_info or {}1884                _add_gateway_metadata(generation_info, raw_response)1885                # Gateway metadata belongs on `generation_info`, not the message1886                # `response_metadata` that `metadata` populates.1887                gateway_metadata = generation_info.pop(1888                    GATEWAY_METADATA_RESPONSE_KEY, None1889                )1890                result = _construct_lc_result_from_responses_api(1891                    response,1892                    schema=original_schema_obj,1893                    metadata=generation_info,1894                    output_version=self.output_version,1895                )1896                if gateway_metadata is not None:1897                    for generation in result.generations:1898                        generation.generation_info = generation.generation_info or {}1899                        generation.generation_info[GATEWAY_METADATA_RESPONSE_KEY] = (1900                            gateway_metadata1901                        )1902                return result1903            else:1904                raw_response = self.client.with_raw_response.create(**payload)1905                response = raw_response.parse()1906        except openai.BadRequestError as e:1907            _handle_openai_bad_request(e)1908        except openai.APIError as e:1909            _handle_openai_api_error(e)1910        except Exception as e:1911            if raw_response is not None and hasattr(raw_response, "http_response"):1912                e.response = raw_response.http_response  # type: ignore[attr-defined]1913            raise e1914        if (1915            self.include_response_headers1916            and raw_response is not None1917            and hasattr(raw_response, "headers")1918        ):1919            generation_info = {"headers": dict(raw_response.headers)}1920        generation_info = generation_info or {}1921        _add_gateway_metadata(generation_info, raw_response)1922        return self._create_chat_result(response, generation_info)19231924    def _use_responses_api(self, payload: dict) -> bool:1925        if isinstance(self.use_responses_api, bool):1926            return self.use_responses_api1927        if (1928            self.output_version == "responses/v1"1929            or self.context_management is not None1930            or self.include is not None1931            or self.reasoning is not None1932            or self.truncation is not None1933            or self.use_previous_response_id1934            or _model_prefers_responses_api(self.model_name)1935            or (1936                (self.model_name or "").lower().startswith("gpt-6")1937                and payload.get("tools")1938            )1939        ):1940            return True1941        return _use_responses_api(payload)19421943    def _get_request_payload(1944        self,1945        input_: LanguageModelInput,1946        *,1947        stop: list[str] | None = None,1948        **kwargs: Any,1949    ) -> dict:1950        messages = self._convert_input(input_).to_messages()1951        if stop is not None:1952            kwargs["stop"] = stop19531954        payload = {**self._default_params, **kwargs}19551956        if self._use_responses_api(payload):1957            if self.use_previous_response_id:1958                last_messages, previous_response_id = _get_last_messages(messages)1959                payload_to_use = last_messages if previous_response_id else messages1960                if previous_response_id:1961                    payload["previous_response_id"] = previous_response_id1962                payload = _construct_responses_api_payload(payload_to_use, payload)1963            else:1964                payload = _construct_responses_api_payload(messages, payload)1965        else:1966            payload["messages"] = [1967                _convert_message_to_dict(_convert_from_v1_to_chat_completions(m))1968                if isinstance(m, AIMessage)1969                else _convert_message_to_dict(m)1970                for m in messages1971            ]1972        return payload19731974    def _create_chat_result(1975        self,1976        response: dict | openai.BaseModel,1977        generation_info: dict | None = None,1978    ) -> ChatResult:1979        generations = []19801981        if not isinstance(response, dict | openai.BaseModel):1982            # `parse()` yields a `str` when the endpoint returns a non-JSON body,1983            # e.g. an HTML error page served after a redirect.1984            preview = repr(response)1985            if len(preview) > 200:1986                preview = f"{preview[:200]}..."1987            msg = (1988                "Unexpected response type from OpenAI-compatible endpoint. "1989                "Expected a dict or openai.BaseModel, got "1990                f"{type(response).__name__}: {preview}"1991            )1992            raise ValueError(msg)19931994        response_dict = (1995            response1996            if isinstance(response, dict)1997            # `parsed` may hold arbitrary Pydantic models from structured output.1998            # Exclude it from this dump and copy it from the typed response below.1999            else response.model_dump(2000                exclude={"choices": {"__all__": {"message": {"parsed"}}}},

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.