libs/partners/openai/langchain_openai/chat_models/base.py PYTHON 5,684 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 5,684.
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    _unwrap_non_standard,167)168from langchain_openai.data._profiles import _PROFILES169170if TYPE_CHECKING:171    import httpx172    from langchain_core.language_models import ModelProfile173    from openai.types.responses import Response174175logger = logging.getLogger(__name__)176177# This SSL context is equivalent to the default `verify=True`.178# https://www.python-httpx.org/advanced/ssl/#configuring-client-instances179global_ssl_context = ssl.create_default_context(cafile=certifi.where())180181_ssrf_client: httpx.Client | None = None182183184def _get_ssrf_safe_client() -> httpx.Client:185    global _ssrf_client186    if _ssrf_client is None:187        from langchain_core._security._transport import ssrf_safe_client188189        _ssrf_client = ssrf_safe_client(190            verify=global_ssl_context, follow_redirects=False191        )192    return _ssrf_client193194195_MODEL_PROFILES = cast(ModelProfileRegistry, _PROFILES)196197198def _get_default_model_profile(model_name: str) -> ModelProfile:199    default = _MODEL_PROFILES.get(model_name) or {}200    return default.copy()201202203WellKnownTools = (204    "file_search",205    "web_search_preview",206    "web_search",207    "computer_use_preview",208    "code_interpreter",209    "mcp",210    "image_generation",211    "tool_search",212    "apply_patch",213)214215_TOOL_EXTRAS_PASSTHROUGH = ("defer_loading", "async")216217218def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage:219    """Convert a dictionary to a LangChain message.220221    Args:222        _dict: The dictionary.223224    Returns:225        The LangChain message.226    """227    role = _dict.get("role")228    name = _dict.get("name")229    id_ = _dict.get("id")230    if role == "user":231        return HumanMessage(content=_dict.get("content", ""), id=id_, name=name)232    if role == "assistant":233        # Fix for azure234        # Also OpenAI returns None for tool invocations235        content = _dict.get("content", "") or ""236        additional_kwargs: dict = {}237        if function_call := _dict.get("function_call"):238            additional_kwargs["function_call"] = dict(function_call)239        tool_calls = []240        invalid_tool_calls = []241        if raw_tool_calls := _dict.get("tool_calls"):242            for raw_tool_call in raw_tool_calls:243                try:244                    tool_calls.append(parse_tool_call(raw_tool_call, return_id=True))245                except Exception as e:246                    invalid_tool_calls.append(247                        make_invalid_tool_call(raw_tool_call, str(e))248                    )249        if audio := _dict.get("audio"):250            additional_kwargs["audio"] = audio251        return AIMessage(252            content=content,253            additional_kwargs=additional_kwargs,254            name=name,255            id=id_,256            tool_calls=tool_calls,257            invalid_tool_calls=invalid_tool_calls,258        )259    if role in ("system", "developer"):260        additional_kwargs = {"__openai_role__": role} if role == "developer" else {}261        return SystemMessage(262            content=_dict.get("content", ""),263            name=name,264            id=id_,265            additional_kwargs=additional_kwargs,266        )267    if role == "function":268        return FunctionMessage(269            content=_dict.get("content", ""), name=cast(str, _dict.get("name")), id=id_270        )271    if role == "tool":272        additional_kwargs = {}273        if "name" in _dict:274            additional_kwargs["name"] = _dict["name"]275        return ToolMessage(276            content=_dict.get("content", ""),277            tool_call_id=cast(str, _dict.get("tool_call_id")),278            additional_kwargs=additional_kwargs,279            name=name,280            id=id_,281        )282    return ChatMessage(content=_dict.get("content", ""), role=role, id=id_)  # type: ignore[arg-type]283284285def _apply_prompt_cache_breakpoint(286    source_block: dict[str, Any], formatted_block: dict[str, Any]287) -> dict[str, Any]:288    """Apply an OpenAI prompt cache breakpoint to a formatted content block.289290    A breakpoint set directly on the block takes precedence over one nested in291    `extras`. Membership (not truthiness) decides whether to copy it, so a292    present-but-falsy value (e.g. `None`) is still preserved.293    """294    if "prompt_cache_breakpoint" in source_block:295        formatted_block["prompt_cache_breakpoint"] = source_block[296            "prompt_cache_breakpoint"297        ]298    elif isinstance(extras := source_block.get("extras"), dict) and (299        "prompt_cache_breakpoint" in extras300    ):301        formatted_block["prompt_cache_breakpoint"] = extras["prompt_cache_breakpoint"]302    return formatted_block303304305def _sanitize_chat_completions_content(content: str | list[dict]) -> str | list[dict]:306    """Sanitize content for chat/completions API.307308    For list content, filters text blocks to only keep supported keys.309    """310    if isinstance(content, list):311        sanitized = []312        for block in content:313            if (314                isinstance(block, dict)315                and block.get("type") == "text"316                and "text" in block317            ):318                sanitized_block = {"type": "text", "text": block["text"]}319                if "prompt_cache_breakpoint" in block:320                    sanitized_block["prompt_cache_breakpoint"] = block[321                        "prompt_cache_breakpoint"322                    ]323                sanitized.append(sanitized_block)324            else:325                sanitized.append(block)326        return sanitized327    return content328329330_ADDITIONAL_TOOLS_BLOCK_TYPE = "additional_tools"331"""Responses API input item that adds tools partway through a conversation."""332333334def _is_ai_role(role: str | None) -> bool:335    """Return whether a message's role is the assistant's.336337    Assistant content is replayed model output, not a caller's instruction, so it is338    exempt from the placement checks a caller's own blocks are held to.339    """340    return str(role).lower().startswith("ai")341342343def _is_system_role(role: str | None) -> bool:344    """Return whether a message's role carries provider instructions.345346    `SystemMessage` reports `"system"` whether or not it is later emitted with347    OpenAI's `developer` role, so one check covers both spellings.348    """349    return role in ("system", "developer")350351352def _raise_if_additional_tools(content: Any, reason: str) -> None:353    """Reject an `additional_tools` block that cannot work where it was placed.354355    The block only reaches the wire as a Responses top-level input item carried on356    a system message. Anywhere else it is this provider's own block type in a357    position this provider forbids, which the error taxonomy makes loud rather than358    silent: nothing routes a request to the Responses API based on message content,359    so a silent drop would make the broken case the default outcome.360361    Args:362        content: The message's content.363        reason: Sentence explaining why this placement cannot work, and how to fix364            it. Appended to the error.365366    Raises:367        ValueError: If an `additional_tools` block is present, in either spelling.368    """369    if not isinstance(content, list):370        return371    for raw_block in content:372        if (373            isinstance(raw_block, dict)374            and _unwrap_non_standard(raw_block).get("type")375            == _ADDITIONAL_TOOLS_BLOCK_TYPE376        ):377            msg = f"`additional_tools` {reason}"378            raise ValueError(msg)379380381def _format_message_content(382    content: Any,383    api: Literal["chat/completions", "responses"] = "chat/completions",384    role: str | None = None,385) -> Any:386    """Format message content."""387    if _is_ai_role(role):388        # Replayed assistant output; `additional_tools` is also an output item, so389        # an echoed one must survive a round trip rather than abort the request.390        pass391    elif not _is_system_role(role):392        _raise_if_additional_tools(393            content,394            "must be carried on a `SystemMessage`. OpenAI restricts the input item "395            'to `role: "developer"`, so it cannot be sent on any other message.',396        )397    elif api == "chat/completions":398        _raise_if_additional_tools(399            content,400            "requires the Responses API and cannot be sent via Chat Completions. "401            "Set `use_responses_api=True`.",402        )403    if content and isinstance(content, list):404        formatted_content = []405        for block in content:406            # Remove unexpected block types407            if (408                isinstance(block, dict)409                and "type" in block410                and (411                    block["type"] in ("tool_use", "thinking", "reasoning_content")412                    or (413                        block["type"] in ("function_call", "code_interpreter_call")414                        and api == "chat/completions"415                    )416                )417            ):418                continue419            if (420                isinstance(block, dict)421                and is_data_content_block(block)422                # Responses API messages handled separately in _compat (parsed into423                # image generation calls)424                and not (api == "responses" and str(role).lower().startswith("ai"))425            ):426                formatted_block = convert_to_openai_data_block(block, api=api)427                formatted_content.append(428                    _apply_prompt_cache_breakpoint(block, formatted_block)429                )430            elif (431                isinstance(block, dict)432                and block.get("type") == "text"433                and "text" in block434                and isinstance(extras := block.get("extras"), dict)435                and "prompt_cache_breakpoint" in extras436            ):437                formatted_block = {"type": "text", "text": block["text"]}438                formatted_content.append(439                    _apply_prompt_cache_breakpoint(block, formatted_block)440                )441            # Anthropic image blocks442            elif (443                isinstance(block, dict)444                and block.get("type") == "image"445                and (source := block.get("source"))446                and isinstance(source, dict)447            ):448                if source.get("type") == "base64" and (449                    (media_type := source.get("media_type"))450                    and (data := source.get("data"))451                ):452                    formatted_content.append(453                        {454                            "type": "image_url",455                            "image_url": {"url": f"data:{media_type};base64,{data}"},456                        }457                    )458                elif source.get("type") == "url" and (url := source.get("url")):459                    formatted_content.append(460                        {"type": "image_url", "image_url": {"url": url}}461                    )462                else:463                    continue464            else:465                formatted_content.append(block)466    else:467        formatted_content = content468469    return formatted_content470471472def _convert_message_to_dict(473    message: BaseMessage,474    api: Literal["chat/completions", "responses"] = "chat/completions",475) -> dict:476    """Convert a LangChain message to dictionary format expected by OpenAI."""477    message_dict: dict[str, Any] = {478        "content": _format_message_content(message.content, api=api, role=message.type)479    }480    if (name := message.name or message.additional_kwargs.get("name")) is not None:481        message_dict["name"] = name482483    # populate role and additional message data484    if isinstance(message, ChatMessage):485        message_dict["role"] = message.role486    elif isinstance(message, HumanMessage):487        message_dict["role"] = "user"488    elif isinstance(message, AIMessage):489        message_dict["role"] = "assistant"490        if message.tool_calls or message.invalid_tool_calls:491            message_dict["tool_calls"] = [492                _lc_tool_call_to_openai_tool_call(tc) for tc in message.tool_calls493            ] + [494                _lc_invalid_tool_call_to_openai_tool_call(tc)495                for tc in message.invalid_tool_calls496            ]497        elif "tool_calls" in message.additional_kwargs:498            message_dict["tool_calls"] = message.additional_kwargs["tool_calls"]499            tool_call_supported_props = {"id", "type", "function"}500            message_dict["tool_calls"] = [501                {k: v for k, v in tool_call.items() if k in tool_call_supported_props}502                for tool_call in message_dict["tool_calls"]503            ]504        elif "function_call" in message.additional_kwargs:505            # OpenAI raises 400 if both function_call and tool_calls are present in the506            # same message.507            message_dict["function_call"] = message.additional_kwargs["function_call"]508        else:509            pass510        # If tool calls present, content null value should be None not empty string.511        if "function_call" in message_dict or "tool_calls" in message_dict:512            message_dict["content"] = message_dict["content"] or None513514        audio: dict[str, Any] | None = None515        for block in message.content:516            if (517                isinstance(block, dict)518                and block.get("type") == "audio"519                and (id_ := block.get("id"))520                and api != "responses"521            ):522                # openai doesn't support passing the data back - only the id523                # https://platform.openai.com/docs/guides/audio/multi-turn-conversations524                audio = {"id": id_}525        if not audio and "audio" in message.additional_kwargs:526            raw_audio = message.additional_kwargs["audio"]527            audio = (528                {"id": message.additional_kwargs["audio"]["id"]}529                if "id" in raw_audio530                else raw_audio531            )532        if audio:533            message_dict["audio"] = audio534    elif isinstance(message, SystemMessage):535        message_dict["role"] = message.additional_kwargs.get(536            "__openai_role__", "system"537        )538    elif isinstance(message, FunctionMessage):539        message_dict["role"] = "function"540    elif isinstance(message, ToolMessage):541        message_dict["role"] = "tool"542        message_dict["tool_call_id"] = message.tool_call_id543        message_dict["content"] = _sanitize_chat_completions_content(544            message_dict["content"]545        )546        supported_props = {"content", "role", "tool_call_id"}547        message_dict = {k: v for k, v in message_dict.items() if k in supported_props}548    else:549        msg = f"Got unknown type {message}"550        raise TypeError(msg)551    return message_dict552553554def _convert_delta_to_message_chunk(555    _dict: Mapping[str, Any], default_class: type[BaseMessageChunk]556) -> BaseMessageChunk:557    """Convert to a LangChain message chunk."""558    id_ = _dict.get("id")559    role = cast(str, _dict.get("role"))560    content = cast(str, _dict.get("content") or "")561    additional_kwargs: dict = {}562    if _dict.get("function_call"):563        function_call = dict(_dict["function_call"])564        if "name" in function_call and function_call["name"] is None:565            function_call["name"] = ""566        additional_kwargs["function_call"] = function_call567    tool_call_chunks = []568    if raw_tool_calls := _dict.get("tool_calls"):569        try:570            tool_call_chunks = [571                tool_call_chunk(572                    name=rtc["function"].get("name"),573                    args=rtc["function"].get("arguments"),574                    id=rtc.get("id"),575                    index=rtc["index"],576                )577                for rtc in raw_tool_calls578            ]579        except KeyError:580            pass581582    if role == "user" or default_class == HumanMessageChunk:583        return HumanMessageChunk(content=content, id=id_)584    if role == "assistant" or default_class == AIMessageChunk:585        return AIMessageChunk(586            content=content,587            additional_kwargs=additional_kwargs,588            id=id_,589            tool_call_chunks=tool_call_chunks,  # type: ignore[arg-type]590        )591    if role in ("system", "developer") or default_class == SystemMessageChunk:592        if role == "developer":593            additional_kwargs = {"__openai_role__": "developer"}594        else:595            additional_kwargs = {}596        return SystemMessageChunk(597            content=content, id=id_, additional_kwargs=additional_kwargs598        )599    if role == "function" or default_class == FunctionMessageChunk:600        return FunctionMessageChunk(content=content, name=_dict["name"], id=id_)601    if role == "tool" or default_class == ToolMessageChunk:602        return ToolMessageChunk(603            content=content, tool_call_id=_dict["tool_call_id"], id=id_604        )605    if role or default_class == ChatMessageChunk:606        return ChatMessageChunk(content=content, role=role, id=id_)607    return default_class(content=content, id=id_)  # type: ignore[call-arg]608609610def _update_token_usage(611    overall_token_usage: int | dict, new_usage: int | dict612) -> int | dict:613    # Token usage is either ints or dictionaries614    # `reasoning_tokens` is nested inside `completion_tokens_details`615    if isinstance(new_usage, int):616        if not isinstance(overall_token_usage, int):617            msg = (618                f"Got different types for token usage: "619                f"{type(new_usage)} and {type(overall_token_usage)}"620            )621            raise ValueError(msg)622        return new_usage + overall_token_usage623    if isinstance(new_usage, dict):624        if not isinstance(overall_token_usage, dict):625            msg = (626                f"Got different types for token usage: "627                f"{type(new_usage)} and {type(overall_token_usage)}"628            )629            raise ValueError(msg)630        return {631            k: _update_token_usage(overall_token_usage.get(k, 0), v)632            for k, v in new_usage.items()633        }634    warnings.warn(f"Unexpected type for token usage: {type(new_usage)}")635    return new_usage636637638class OpenAIContextOverflowError(openai.BadRequestError, ContextOverflowError):639    """BadRequestError raised when input exceeds OpenAI's context limit."""640641642class OpenAIAPIContextOverflowError(openai.APIError, ContextOverflowError):643    """APIError raised when input exceeds OpenAI's context limit."""644645646class OpenAIAuthenticationError(openai.AuthenticationError, ModelAuthenticationError):647    """OpenAI authentication error classified as a LangChain model error."""648649650class OpenAIPermissionDeniedError(651    openai.PermissionDeniedError, ModelPermissionDeniedError652):653    """OpenAI permission error classified as a LangChain model error."""654655656class OpenAIInvalidRequestError(openai.BadRequestError, ModelInvalidRequestError):657    """OpenAI bad-request error classified as a LangChain model error."""658659660class OpenAIModelNotFoundError(openai.NotFoundError, ModelNotFoundError):661    """OpenAI not-found error classified as a LangChain model error."""662663664class OpenAIRateLimitError(openai.RateLimitError, ModelRateLimitError):665    """OpenAI rate-limit error classified as a LangChain model error."""666667668class OpenAIAPIError(openai.InternalServerError, ModelAPIError):669    """OpenAI server error classified as a LangChain model error."""670671672class OpenAIConnectionError(openai.APIConnectionError, ModelConnectionError):673    """OpenAI connection error classified as a LangChain model error."""674675676class OpenAITimeoutError(openai.APITimeoutError, ModelTimeoutError):677    """OpenAI timeout error classified as a LangChain model error."""678679680def _handle_openai_bad_request(e: openai.BadRequestError) -> None:681    if (682        "context_length_exceeded" in str(e)683        or "Input tokens exceed the configured limit" in e.message684        or "prompt is too long" in e.message685        or "ContextWindowExceededError" in e.message686    ):687        raise OpenAIContextOverflowError(688            message=e.message, response=e.response, body=e.body689        ) from e690    if (691        "'response_format' of type 'json_schema' is not supported with this model"692    ) in e.message:693        message = (694            "This model does not support OpenAI's structured output feature, which "695            "is the default method for `with_structured_output` as of "696            "langchain-openai==0.3. To use `with_structured_output` with this model, "697            'specify `method="function_calling"`.'698        )699        warnings.warn(message)700        raise OpenAIInvalidRequestError(701            message=e.message, response=e.response, body=e.body702        ) from e703    if "Invalid schema for response_format" in e.message:704        message = (705            "Invalid schema for OpenAI's structured output feature, which is the "706            "default method for `with_structured_output` as of langchain-openai==0.3. "707            'Specify `method="function_calling"` instead or update your schema. '708            "See supported schemas: "709            "https://platform.openai.com/docs/guides/structured-outputs#supported-schemas"710        )711        warnings.warn(message)712    raise OpenAIInvalidRequestError(713        message=e.message, response=e.response, body=e.body714    ) from e715716717def _handle_openai_api_error(e: openai.APIError) -> None:718    error_message = str(e)719    if "exceeds the context window" in error_message:720        raise OpenAIAPIContextOverflowError(721            message=e.message, request=e.request, body=e.body722        ) from e723    if isinstance(e, openai.AuthenticationError):724        raise OpenAIAuthenticationError(725            message=e.message, response=e.response, body=e.body726        ) from e727    if isinstance(e, openai.PermissionDeniedError):728        raise OpenAIPermissionDeniedError(729            message=e.message, response=e.response, body=e.body730        ) from e731    if isinstance(e, openai.NotFoundError):732        raise OpenAIModelNotFoundError(733            message=e.message, response=e.response, body=e.body734        ) from e735    if isinstance(e, openai.RateLimitError):736        raise OpenAIRateLimitError(737            message=e.message, response=e.response, body=e.body738        ) from e739    if isinstance(e, openai.InternalServerError):740        raise OpenAIAPIError(message=e.message, response=e.response, body=e.body) from e741    if isinstance(e, openai.APITimeoutError):742        raise OpenAITimeoutError(e.request) from e743    if isinstance(e, openai.APIConnectionError):744        raise OpenAIConnectionError(message=e.message, request=e.request) from e745    raise746747748def _add_gateway_metadata(generation_info: dict[str, Any], raw_response: Any) -> None:749    """Add parsed LangSmith gateway metadata to `generation_info`, if present.750751    Args:752        generation_info: Generation info to mutate in place.753        raw_response: The raw provider response, or None.754    """755    headers = getattr(raw_response, "headers", None)756    if headers is None:757        return758    gateway_metadata = _parse_gateway_metadata(headers)759    if gateway_metadata is not None:760        generation_info[GATEWAY_METADATA_RESPONSE_KEY] = gateway_metadata761762763_RESPONSES_API_ONLY_PREFIXES = (764    "gpt-5-pro",765    "gpt-5.2-pro",766    "gpt-5.4-pro",767    "gpt-5.5-pro",768    "gpt-5.6-sol",769)770771772def _model_prefers_responses_api(model_name: str | None) -> bool:773    if not model_name:774        return False775    return model_name.startswith(_RESPONSES_API_ONLY_PREFIXES) or "codex" in model_name776777778_BM = TypeVar("_BM", bound=BaseModel)779_DictOrPydanticClass: TypeAlias = dict[str, Any] | type[_BM] | type780_DictOrPydantic: TypeAlias = dict | _BM781782783class BaseChatOpenAI(BaseChatModel):784    """Base wrapper around OpenAI large language models for chat.785786    This base class targets787    [official OpenAI API specifications](https://github.com/openai/openai-openapi)788    only. Non-standard response fields added by third-party providers (e.g.,789    `reasoning_content`) are not extracted. Use a provider-specific subclass for790    full provider support.791    """792793    client: Any = Field(default=None, exclude=True)794795    async_client: Any = Field(default=None, exclude=True)796797    root_client: Any = Field(default=None, exclude=True)798799    root_async_client: Any = Field(default=None, exclude=True)800801    model_name: str = Field(default="gpt-3.5-turbo", alias="model")802    """Model name to use."""803804    temperature: float | None = None805    """What sampling temperature to use."""806807    model_kwargs: dict[str, Any] = Field(default_factory=dict)808    """Holds any model parameters valid for `create` call not explicitly specified."""809810    openai_api_key: (811        SecretStr | None | Callable[[], str] | Callable[[], Awaitable[str]]812    ) = Field(alias="api_key", default=None)813    """API key to use.814815    Can be inferred from the `OPENAI_API_KEY` environment variable, or specified816    as a string, or sync or async callable that returns a string.817818    ??? example "Specify with environment variable"819820        ```bash821        export OPENAI_API_KEY=...822        ```823        ```python824        from langchain_openai import ChatOpenAI825826        model = ChatOpenAI(model="gpt-5-nano")827        ```828829    ??? example "Specify with a string"830831        ```python832        from langchain_openai import ChatOpenAI833834        model = ChatOpenAI(model="gpt-5-nano", api_key="...")835        ```836837    ??? example "Specify with a sync callable"838839        ```python840        from langchain_openai import ChatOpenAI841842        def get_api_key() -> str:843            # Custom logic to retrieve API key844            return "..."845846        model = ChatOpenAI(model="gpt-5-nano", api_key=get_api_key)847        ```848849    ??? example "Specify with an async callable"850851        ```python852        from langchain_openai import ChatOpenAI853854        async def get_api_key() -> str:855            # Custom async logic to retrieve API key856            return "..."857858        model = ChatOpenAI(model="gpt-5-nano", api_key=get_api_key)859        ```860    """861862    openai_api_base: str | None = Field(default=None, alias="base_url")863    """Base URL path for API requests, leave blank if not using a proxy or service emulator.864865    Resolution order (first match wins):866867    1. Explicit `base_url` (or `openai_api_base`) kwarg.868    2. Env var `OPENAI_API_BASE` (read by LangChain at init).869    3. Env var `OPENAI_BASE_URL` (read by the underlying `openai` SDK client).870871    `OPENAI_BASE_URL` is also inspected by LangChain only to decide whether to872    default-enable `stream_usage` — when set, the default is left off because many873    non-OpenAI endpoints do not support streaming token usage.874    """  # noqa: E501875876    openai_organization: str | None = Field(default=None, alias="organization")877    """Automatically inferred from env var `OPENAI_ORG_ID` if not provided."""878879    # to support explicit proxy for OpenAI880    openai_proxy: str | None = Field(881        default_factory=from_env("OPENAI_PROXY", default=None)882    )883884    request_timeout: float | tuple[float, float] | Any | None = Field(885        default=None, alias="timeout"886    )887    """Timeout for requests to OpenAI completion API.888889    Can be float, `httpx.Timeout` or `None`.890    """891892    stream_usage: bool | None = None893    """Whether to include usage metadata in streaming output.894895    If enabled, an additional message chunk will be generated during the stream896    including usage metadata.897898    This parameter is enabled unless `openai_api_base` is set or the model is899    initialized with a custom client, as many chat completions APIs do not900    support streaming token usage.901902    !!! version-added "Added in `langchain-openai` 0.3.9"903904    !!! warning "Behavior changed in `langchain-openai` 0.3.35"905906        Enabled for default base URL and client.907    """908909    max_retries: int | None = None910    """Maximum number of retries to make when generating."""911912    presence_penalty: float | None = None913    """Penalizes repeated tokens."""914915    frequency_penalty: float | None = None916    """Penalizes repeated tokens according to frequency."""917918    seed: int | None = None919    """Seed for generation"""920921    logprobs: bool | None = None922    """Whether to return logprobs."""923924    top_logprobs: int | None = None925    """Number of most likely tokens to return at each token position, each with an926    associated log probability.927928    `logprobs` must be set to true if this parameter is used.929    """930931    logit_bias: dict[int, int] | None = None932    """Modify the likelihood of specified tokens appearing in the completion."""933934    streaming: bool = False935    """Whether to stream the results or not."""936937    n: int | None = None938    """Number of chat completions to generate for each prompt."""939940    top_p: float | None = None941    """Total probability mass of tokens to consider at each step."""942943    max_tokens: int | None = Field(default=None)944    """Maximum number of tokens to generate."""945946    reasoning_effort: str | None = None947    """Constrains effort on reasoning for reasoning models.948949    For use with the Chat Completions API. Reasoning models only.950951    Currently supported values are `'minimal'`, `'low'`, `'medium'`, and952    `'high'`. Reducing reasoning effort can result in faster responses and fewer953    tokens used on reasoning in a response.954955    !!! note "Changing reasoning effort mid-conversation"956957        Changing this value part-way through a conversation changes a request-level958        parameter, which invalidates the cached prompt prefix.959960        Models that support it (currently GPT-6) can instead carry the new effort961        in a `configuration_update` item attached to the message that should start962        using it:963964        ```python965        HumanMessage(966            [967                {"type": "configuration_update", "reasoning": {"effort": "high"}},968                {"type": "text", "text": "Analyze the failure modes."},969            ]970        )971        ```972973        The new effort applies from that message onward, until another update974        overrides it.975    """976977    reasoning: dict[str, Any] | None = None978    """Reasoning parameters for reasoning models. None disables reasoning.979980    For use with the Responses API.981982    ```python983    reasoning={984        "effort": None,  # Default None; can be "low", "medium", or "high"985        "summary": "auto",  # Can be "auto", "concise", or "detailed"986    }987    ```988989    !!! version-added "Added in `langchain-openai` 0.3.24"990    """991992    verbosity: str | None = None993    """Controls the verbosity level of responses for reasoning models.994995    For use with the Responses API.996997    Currently supported values are `'low'`, `'medium'`, and `'high'`.998999    !!! version-added "Added in `langchain-openai` 0.3.28"1000    """10011002    tiktoken_model_name: str | None = None1003    """The model name to pass to tiktoken when using this class.10041005    Tiktoken is used to count the number of tokens in documents to constrain1006    them to be under a certain limit.10071008    By default, when set to `None`, this will be the same as the embedding model name.1009    However, there are some cases where you may want to use this `Embedding` class with1010    a model name not supported by tiktoken. This can include when using Azure embeddings1011    or when using one of the many model providers that expose an OpenAI-like1012    API but with different models. In those cases, in order to avoid erroring1013    when tiktoken is called, you can specify a model name to use here.1014    """10151016    default_headers: Mapping[str, str] | None = None10171018    default_query: Mapping[str, object] | None = None10191020    # Configure a custom httpx client. See the1021    # [httpx documentation](https://www.python-httpx.org/api/#client) for more details.1022    http_client: Any | None = Field(default=None, exclude=True)1023    """Optional `httpx.Client`.10241025    Only used for sync invocations. Must specify `http_async_client` as well if1026    you'd like a custom client for async invocations.1027    """10281029    http_async_client: Any | None = Field(default=None, exclude=True)1030    """Optional `httpx.AsyncClient`.10311032    Only used for async invocations. Must specify `http_client` as well if you'd1033    like a custom client for sync invocations.1034    """10351036    http_socket_options: Sequence[tuple[int, int, int]] | None = Field(1037        default=None, exclude=True1038    )1039    """TCP socket options applied to the httpx transports built by this instance.10401041    Defaults to a conservative TCP-keepalive + `TCP_USER_TIMEOUT` profile that1042    targets a ~2-minute bound on silent connection hangs (silent mid-stream peer1043    loss, gVisor/NAT idle timeouts, silent TCP black holes) on platforms that1044    support the full option set. On platforms that only support a subset1045    (macOS without `TCP_USER_TIMEOUT`, Windows with only `SO_KEEPALIVE`,1046    minimal kernels), unsupported options are silently dropped and the bound1047    degrades to whatever the remaining options + OS defaults provide — still1048    better than indefinite hang.10491050    Accepted values:10511052    - `None` (default): use env-driven defaults. Matches the "unset" convention1053        used by `http_client` elsewhere on this class.1054    - `()` (empty): disable socket-option injection entirely. Inherits the OS1055        defaults and restores httpx's native env-proxy auto-detection.1056    - A non-empty sequence of `(level, option, value)` tuples: explicit1057        override; passed verbatim to the transport (not filtered). Unsupported1058        options raise `OSError` at connect time rather than being silently1059        dropped — the user chose them explicitly.10601061    Environment variables (only consulted when this field is `None`):1062    `LANGCHAIN_OPENAI_TCP_KEEPALIVE` (set to `0` to disable entirely — the1063    kill-switch), `LANGCHAIN_OPENAI_TCP_KEEPIDLE`,1064    `LANGCHAIN_OPENAI_TCP_KEEPINTVL`, `LANGCHAIN_OPENAI_TCP_KEEPCNT`,1065    `LANGCHAIN_OPENAI_TCP_USER_TIMEOUT_MS`.10661067    Applied per side: if `http_client` is supplied, the sync path uses1068    that user-owned client's socket options as-is; the async path still1069    gets `http_socket_options` applied to its default builder (and1070    vice-versa for `http_async_client`). Supply both to take full control.10711072    !!! note "Interaction with env-proxy auto-detection"10731074        When a custom `httpx` transport is active, `httpx` disables its1075        native env-proxy auto-detection (`HTTP_PROXY` / `HTTPS_PROXY` /1076        `ALL_PROXY` / `NO_PROXY` and macOS/Windows system proxy settings).10771078        To keep the default shape safe, `ChatOpenAI` detects the1079        "proxy-env-shadow" pattern and **skips the custom transport1080        entirely** when **all** of the following hold:10811082        - `http_socket_options` is left at its default (`None`)1083        - No `http_client` or `http_async_client` supplied1084        - No `openai_proxy` supplied1085        - A proxy env var or system proxy is visible to httpx10861087        On that specific shape, the instance falls back to pre-PR behavior1088        and httpx's env-proxy auto-detection applies (a one-time `INFO` log1089        records the bypass for observability).10901091        If you explicitly set `http_socket_options=[...]` while a proxy1092        env var is also set, no bypass — you opted into the transport, and1093        a one-time `WARNING` records the shadowing. Set1094        `http_socket_options=()` or `LANGCHAIN_OPENAI_TCP_KEEPALIVE=0` to1095        disable transport injection explicitly, or pass a fully-configured1096        `http_async_client` / `http_client` to take full control. The1097        `openai_proxy` constructor kwarg is unaffected — socket options1098        are applied cleanly through the proxied transport on that path.1099    """11001101    stream_chunk_timeout: float | None = Field(1102        default_factory=lambda: _float_env(1103            "LANGCHAIN_OPENAI_STREAM_CHUNK_TIMEOUT_S", 120.01104        ),1105        exclude=True,1106    )1107    """Per-chunk wall-clock timeout (seconds) on async streaming responses.11081109    Applies to async invocations only (`astream`, `ainvoke` with streaming,1110    etc.). Sync streaming (`stream`) is not affected.11111112    Fires between content chunks yielded by the openai SDK's streaming iterator1113    (i.e., each call to `__anext__` on the response). Crucially, this is1114    **not** the same as httpx's `timeout.read`:11151116    - httpx's read timeout is inter-byte and gets reset every time *any* bytes1117        arrive on the socket — including OpenAI's SSE keepalive comments1118        (`: keepalive`) that trickle down during long model generations. A1119        stream that's silent on *content* but still producing keepalives looks1120        alive forever to httpx.1121    - `stream_chunk_timeout` measures the gap between *parsed chunks*. The1122        openai SDK's SSE parser consumes keepalive comments internally and does1123        not emit them as chunks, so keepalives do *not* reset this timer. It1124        fires on genuine content silence.11251126    When it fires, a `StreamChunkTimeoutError`1127    (subclass of `asyncio.TimeoutError`) is raised with a self-describing1128    message naming this knob, the env-var override, the model, and the1129    number of chunks received before the stall. A WARNING log with1130    `extra={"source": "stream_chunk_timeout", "timeout_s": <value>,1131    "model_name": <value>, "chunks_received": <value>}` also fires so1132    aggregate logging can distinguish app-layer timeouts from1133    transport-layer failures.11341135    Defaults to 120s. Set to `None` or `0` to disable. Overridable via the1136    `LANGCHAIN_OPENAI_STREAM_CHUNK_TIMEOUT_S` env var. Negative values1137    (from either the env var or the constructor kwarg — e.g., hydrated1138    from YAML/JSON configs) fall back to the default with a `WARNING` log1139    rather than silently disabling the wrapper, so a misconfigured value1140    still boots safely and the fallback is visible.1141    """11421143    stop: list[str] | str | None = Field(default=None, alias="stop_sequences")1144    """Default stop sequences."""11451146    extra_body: Mapping[str, Any] | None = None1147    """Optional additional JSON properties to include in the request parameters1148    when making requests to OpenAI compatible APIs, such as vLLM, LM Studio, or1149    other providers.11501151    This is the recommended way to pass custom parameters that are specific to your1152    OpenAI-compatible API provider but not part of the standard OpenAI API.11531154    Examples:1155    - [LM Studio](https://lmstudio.ai/) TTL parameter: `extra_body={"ttl": 300}`1156    - [vLLM](https://github.com/vllm-project/vllm) custom parameters:1157        `extra_body={"use_beam_search": True}`1158    - Any other provider-specific parameters11591160    !!! warning11611162        Do not use `model_kwargs` for custom parameters that are not part of the1163        standard OpenAI API, as this will cause errors when making API calls. Use1164        `extra_body` instead.1165    """11661167    include_response_headers: bool = False1168    """Whether to include response headers in the output message `response_metadata`.11691170    Note: some inference providers return additional metadata (such as served model1171    names) in the response headers. Enable to capture these metadata.1172    """11731174    disabled_params: dict[str, Any] | None = Field(default=None)1175    """Parameters of the OpenAI client or `chat.completions` endpoint that should be1176    disabled for the given model.11771178    Should be specified as `{"param": None | ['val1', 'val2']}` where the key is the1179    parameter and the value is either None, meaning that parameter should never be1180    used, or it's a list of disabled values for the parameter.11811182    For example, older models may not support the `'parallel_tool_calls'` parameter at1183    all, in which case `disabled_params={"parallel_tool_calls": None}` can be passed1184    in.11851186    If a parameter is disabled then it will not be used by default in any methods, e.g.1187    in `with_structured_output`. However this does not prevent a user from directly1188    passed in the parameter during invocation.1189    """11901191    context_management: list[dict[str, Any]] | None = None1192    """Configuration for1193    [context management](https://developers.openai.com/api/docs/guides/compaction).1194    """11951196    include: list[str] | None = None1197    """Additional fields to include in generations from Responses API.11981199    Supported values:12001201    - `'file_search_call.results'`1202    - `'message.input_image.image_url'`1203    - `'computer_call_output.output.image_url'`1204    - `'reasoning.encrypted_content'`1205    - `'code_interpreter_call.outputs'`12061207    !!! version-added "Added in `langchain-openai` 0.3.24"1208    """12091210    prompt_cache_options: dict[str, Any] | None = None1211    """Options controlling OpenAI prompt cache behavior.12121213    !!! version-added "Added in `langchain-openai` 1.3.5"1214    """12151216    service_tier: str | None = None1217    """Latency tier for request.12181219    Options are `'auto'`, `'default'`, or `'flex'`.12201221    Relevant for users of OpenAI's scale tier service.1222    """12231224    store: bool | None = None1225    """If `True`, OpenAI may store response data for future use.12261227    Defaults to `True` for the Responses API and `False` for the Chat Completions API.12281229    !!! version-added "Added in `langchain-openai` 0.3.24"1230    """12311232    truncation: str | None = None1233    """Truncation strategy (Responses API).12341235    Can be `'auto'` or `'disabled'` (default).12361237    If `'auto'`, model may drop input items from the middle of the message sequence to1238    fit the context window.12391240    !!! version-added "Added in `langchain-openai` 0.3.24"1241    """12421243    use_previous_response_id: bool = False1244    """If `True`, always pass `previous_response_id` using the ID of the most recent1245    response. Responses API only.12461247    Input messages up to the most recent response will be dropped from request1248    payloads.12491250    For example, the following two are equivalent:12511252    ```python1253    model = ChatOpenAI(1254        model="...",1255        use_previous_response_id=True,1256    )1257    model.invoke(1258        [1259            HumanMessage("Hello"),1260            AIMessage("Hi there!", response_metadata={"id": "resp_123"}),1261            HumanMessage("How are you?"),1262        ]1263    )1264    ```12651266    ```python1267    model = ChatOpenAI(model="...", use_responses_api=True)1268    model.invoke([HumanMessage("How are you?")], previous_response_id="resp_123")1269    ```12701271    !!! version-added "Added in `langchain-openai` 0.3.26"1272    """12731274    use_responses_api: bool | None = None1275    """Whether to use the Responses API instead of the Chat API.12761277    If not specified, set to `True` when instance settings require the Responses API,12781279    !!! version-added "Added in `langchain-openai` 0.3.9"1280    """12811282    output_version: str | None = Field(1283        default_factory=from_env("LC_OUTPUT_VERSION", default=None)1284    )1285    """Version of `AIMessage` output format to use.12861287    This field is used to roll-out new output formats for chat model `AIMessage`1288    responses in a backwards-compatible way.12891290    Supported values:12911292    - `'v0'`: `AIMessage` format as of `langchain-openai 0.3.x`.1293    - `'responses/v1'`: Formats Responses API output items into AIMessage content blocks1294        (Responses API only)1295    - `'v1'`: v1 of LangChain cross-provider standard.12961297    !!! warning "Behavior changed in `langchain-openai` 1.0.0"12981299        Default updated to `"responses/v1"`.1300    """13011302    model_config = ConfigDict(populate_by_name=True)13031304    @property1305    def _uses_gateway(self) -> bool:1306        """Whether requests are routed through the LangSmith gateway.13071308        Detected from the resolved API key: LangSmith keys (used to authenticate1309        to the gateway) carry the `lsv2_` prefix. Callable keys cannot be1310        inspected without invoking them, so they are treated as non-gateway.1311        """1312        api_key = self.openai_api_key1313        if isinstance(api_key, SecretStr):1314            return api_key.get_secret_value().startswith("lsv2_")1315        return False13161317    @property1318    def model(self) -> str:1319        """Same as model_name."""1320        return self.model_name13211322    @model_validator(mode="before")1323    @classmethod1324    def build_extra(cls, values: dict[str, Any]) -> Any:1325        """Build extra kwargs from additional params that were passed in."""1326        all_required_field_names = get_pydantic_field_names(cls)1327        return _build_model_kwargs(values, all_required_field_names)13281329    @field_validator("stream_chunk_timeout", mode="after")1330    @classmethod1331    def _validate_stream_chunk_timeout(cls, value: float | None) -> float | None:1332        """Reject negative constructor values; fall back to the env-driven default.13331334        Matches the env-var path in `_float_env`: a negative value is a typo,1335        not an opt-out (`None`/`0` are the documented off switches). Configs1336        hydrated from YAML/JSON would otherwise silently disable the wrapper1337        and reintroduce the indefinite-stream hang the feature prevents.1338        """1339        if value is not None and value < 0:1340            fallback = _float_env("LANGCHAIN_OPENAI_STREAM_CHUNK_TIMEOUT_S", 120.0)1341            logger.warning(1342                "Invalid `stream_chunk_timeout=%r` (negative); "1343                "falling back to %s. Pass `None` or `0` to disable.",1344                value,1345                fallback,1346            )1347            return fallback1348        return value13491350    @model_validator(mode="before")1351    @classmethod1352    def validate_temperature(cls, values: dict[str, Any]) -> Any:1353        """Validate temperature parameter for different models.13541355        - gpt-5 models (excluding gpt-5-chat) only allow `temperature=1` or unset1356            (Defaults to 1)1357        """1358        model = values.get("model_name") or values.get("model") or ""1359        model_lower = model.lower()13601361        # For o1 models, set temperature=1 if not provided1362        if model_lower.startswith("o1") and "temperature" not in values:1363            values["temperature"] = 113641365        # For gpt-5 models, handle temperature restrictions. Temperature is supported1366        # by gpt-5-chat and gpt-5 models with reasoning_effort='none' or1367        # reasoning={'effort': 'none'}.1368        if (1369            model_lower.startswith("gpt-5")1370            and ("chat" not in model_lower)1371            and values.get("reasoning_effort") != "none"1372            and (values.get("reasoning") or {}).get("effort") != "none"1373        ):1374            temperature = values.get("temperature")1375            if temperature is not None and temperature != 1:1376                # For gpt-5 (non-chat), only temperature=1 is supported1377                # So we remove any non-defaults1378                values.pop("temperature", None)13791380        return values13811382    @model_validator(mode="after")1383    def _set_openai_chat_version(self) -> Self:1384        """Set package version in metadata.13851386        Note: Subclasses that inherit from `BaseChatOpenAI` (e.g.1387        `ChatDeepSeek`, `ChatXAI`) must use a **unique** validator name1388        (e.g. `_set_deepseek_version`) instead of overriding this one. Pydantic1389        replaces same-named `model_validator` methods rather than chaining them,1390        so reusing `_set_openai_chat_version` would silently drop the parent's1391        `langchain-openai` version entry.1392        """1393        self._add_version("langchain-openai", __version__)1394        return self13951396    @model_validator(mode="after")1397    def _infer_use_responses_api(self) -> Self:1398        """Expose unconditional instance-level Responses API routing."""1399        if self.use_responses_api is None and self._use_responses_api({}):1400            self.use_responses_api = True1401        return self14021403    @model_validator(mode="after")1404    def validate_environment(self) -> Self:1405        """Validate that api key and python package exists in environment."""1406        if self.n is not None and self.n < 1:1407            msg = "n must be at least 1."1408            raise ValueError(msg)1409        if self.n is not None and self.n > 1 and self.streaming:1410            msg = "n must be 1 when streaming."1411            raise ValueError(msg)14121413        # Check OPENAI_ORGANIZATION for backwards compatibility.1414        self.openai_organization = (1415            self.openai_organization1416            or os.getenv("OPENAI_ORG_ID")1417            or os.getenv("OPENAI_ORGANIZATION")1418        )1419        # Resolve base URL and API key, applying LangSmith gateway settings.1420        _gateway_config = _resolve_gateway_config(1421            base_url=self.openai_api_base,1422            api_key=self.openai_api_key,1423            provider_path="openai/v1",1424            base_url_env="OPENAI_API_BASE",1425            api_key_env="OPENAI_API_KEY",1426        )1427        self.openai_api_base = _gateway_config.base_url1428        self.openai_api_key = _gateway_config.api_key1429        _base_url_from_gateway = _gateway_config.base_url_from_gateway14301431        # Enable stream_usage by default if using default base URL and client,1432        # or when the base URL was set by the LangSmith gateway (which proxies1433        # to OpenAI and supports streaming token usage).1434        if all(1435            getattr(self, key, None) is None1436            for key in (1437                "stream_usage",1438                "openai_proxy",1439                "client",1440                "root_client",1441                "async_client",1442                "root_async_client",1443                "http_client",1444                "http_async_client",1445            )1446        ) and (1447            _base_url_from_gateway1448            or (self.openai_api_base is None and "OPENAI_BASE_URL" not in os.environ)1449        ):1450            self.stream_usage = True14511452        # Resolve API key from SecretStr or Callable1453        sync_api_key_value: str | Callable[[], str] | None = None1454        async_api_key_value: str | Callable[[], Awaitable[str]] | None = None14551456        if self.openai_api_key is not None:1457            # Because OpenAI and AsyncOpenAI clients support either sync or async1458            # callables for the API key, we need to resolve separate values here.1459            sync_api_key_value, async_api_key_value = _resolve_sync_and_async_api_keys(1460                self.openai_api_key1461            )14621463        client_params: dict = {1464            "organization": self.openai_organization,1465            "base_url": self.openai_api_base,1466            "timeout": self.request_timeout,1467            "default_headers": self.default_headers,1468            "default_query": self.default_query,1469        }1470        if self.max_retries is not None:1471            client_params["max_retries"] = self.max_retries14721473        if self.openai_proxy and (self.http_client or self.http_async_client):1474            openai_proxy = self.openai_proxy1475            http_client = self.http_client1476            http_async_client = self.http_async_client1477            msg = (1478                "Cannot specify 'openai_proxy' if one of "1479                "'http_client'/'http_async_client' is already specified. Received:\n"1480                f"{openai_proxy=}\n{http_client=}\n{http_async_client=}"1481            )1482            raise ValueError(msg)1483        if _should_bypass_socket_options_for_proxy_env(1484            http_socket_options=self.http_socket_options,1485            http_client=self.http_client,1486            http_async_client=self.http_async_client,1487            openai_proxy=self.openai_proxy,1488        ):1489            # Default-shape construction + proxy env var visible to httpx:1490            # skip the custom transport so httpx's env-proxy auto-detection1491            # still applies. Users who want kernel-level TCP tuning alongside1492            # an env proxy can opt in explicitly via `http_socket_options`.1493            resolved_socket_options: tuple[tuple[int, int, int], ...] = ()1494            _log_proxy_env_bypass_once()1495        else:1496            resolved_socket_options = _resolve_socket_options(self.http_socket_options)1497            _warn_if_proxy_env_shadowed(1498                resolved_socket_options, openai_proxy=self.openai_proxy1499            )1500        if not self.client:1501            if sync_api_key_value is None:1502                # No valid sync API key, leave client as None and raise informative1503                # error on invocation.1504                self.client = None1505                self.root_client = None1506            else:1507                if self.openai_proxy and not self.http_client:1508                    self.http_client = _build_proxied_sync_httpx_client(1509                        proxy=self.openai_proxy,1510                        verify=global_ssl_context,1511                        socket_options=resolved_socket_options,1512                    )1513                sync_specific = {1514                    "http_client": self.http_client1515                    or _get_default_httpx_client(1516                        self.openai_api_base,1517                        self.request_timeout,1518                        resolved_socket_options,1519                    ),1520                    "api_key": sync_api_key_value,1521                }1522                self.root_client = openai.OpenAI(**client_params, **sync_specific)  # type: ignore[arg-type]1523                self.client = self.root_client.chat.completions1524        if not self.async_client:1525            if self.openai_proxy and not self.http_async_client:1526                self.http_async_client = _build_proxied_async_httpx_client(1527                    proxy=self.openai_proxy,1528                    verify=global_ssl_context,1529                    socket_options=resolved_socket_options,1530                )1531            async_specific = {1532                "http_client": self.http_async_client1533                or _get_default_async_httpx_client(1534                    self.openai_api_base,1535                    self.request_timeout,1536                    resolved_socket_options,1537                ),1538                "api_key": async_api_key_value,1539            }1540            self.root_async_client = openai.AsyncOpenAI(1541                **client_params,1542                **async_specific,  # type: ignore[arg-type]1543            )1544            self.async_client = self.root_async_client.chat.completions1545        return self15461547    def _resolve_model_profile(self) -> ModelProfile | None:1548        profile = _get_default_model_profile(self.model_name) or None1549        if (1550            profile is not None1551            and profile.get("mid_conversation_tools")1552            and not self._use_responses_api({})1553        ):1554            # Only the Responses API carries an `additional_tools` input item.1555            # Routing that depends on the request can't be claimed up front.1556            profile["mid_conversation_tools"] = False1557        return profile15581559    @property1560    def _default_params(self) -> dict[str, Any]:1561        """Get the default parameters for calling OpenAI API."""1562        exclude_if_none = {1563            "presence_penalty": self.presence_penalty,1564            "frequency_penalty": self.frequency_penalty,1565            "seed": self.seed,1566            "top_p": self.top_p,1567            "logprobs": self.logprobs,1568            "top_logprobs": self.top_logprobs,1569            "logit_bias": self.logit_bias,1570            "stop": self.stop or None,  # Also exclude empty list for this1571            "max_tokens": self.max_tokens,1572            "extra_body": self.extra_body,1573            "n": self.n,1574            "temperature": self.temperature,1575            "reasoning_effort": self.reasoning_effort,1576            "reasoning": self.reasoning,1577            "verbosity": self.verbosity,1578            "context_management": self.context_management,1579            "include": self.include,1580            "prompt_cache_options": self.prompt_cache_options,1581            "service_tier": self.service_tier,1582            "truncation": self.truncation,1583            "store": self.store,1584        }15851586        return {1587            "model": self.model_name,1588            "stream": self.streaming,1589            **{k: v for k, v in exclude_if_none.items() if v is not None},1590            **self.model_kwargs,1591        }15921593    def _combine_llm_outputs(self, llm_outputs: list[dict | None]) -> dict:1594        overall_token_usage: dict = {}1595        system_fingerprint = None1596        for output in llm_outputs:1597            if output is None:1598                # Happens in streaming1599                continue1600            token_usage = output.get("token_usage")1601            if token_usage is not None:1602                for k, v in token_usage.items():1603                    if v is None:1604                        continue1605                    if k in overall_token_usage:1606                        overall_token_usage[k] = _update_token_usage(1607                            overall_token_usage[k], v1608                        )1609                    else:1610                        overall_token_usage[k] = v1611            if system_fingerprint is None:1612                system_fingerprint = output.get("system_fingerprint")1613        combined = {"token_usage": overall_token_usage, "model_name": self.model_name}1614        if system_fingerprint:1615            combined["system_fingerprint"] = system_fingerprint1616        return combined16171618    def _convert_chunk_to_generation_chunk(1619        self,1620        chunk: dict,1621        default_chunk_class: type,1622        base_generation_info: dict | None,1623    ) -> ChatGenerationChunk | None:1624        if chunk.get("type") == "content.delta":  # From beta.chat.completions.stream1625            return None1626        token_usage = chunk.get("usage")1627        choices = (1628            chunk.get("choices", [])1629            # From beta.chat.completions.stream1630            or chunk.get("chunk", {}).get("choices", [])1631        )16321633        usage_metadata: UsageMetadata | None = (1634            _create_usage_metadata(token_usage, chunk.get("service_tier"))1635            if token_usage1636            else None1637        )1638        if len(choices) == 0:1639            # logprobs is implicitly None1640            generation_chunk = ChatGenerationChunk(1641                message=default_chunk_class(content="", usage_metadata=usage_metadata),1642                generation_info=base_generation_info,1643            )1644            # Keep content as "" (the default) rather than converting to [].1645            # Chat Completions content deltas are normalized to strings in1646            # _convert_delta_to_message_chunk. Starting with [] causes1647            # merge_content to silently drop string content (empty list is1648            # falsy, so no merge branch applies). The empty list also triggers1649            # the content_blocks isinstance(list) short-circuit, which would1650            # return [] and miss tool_call_chunks.1651            if self.output_version == "v1":1652                generation_chunk.message.response_metadata["output_version"] = "v1"16531654            return generation_chunk16551656        choice = choices[0]1657        if choice["delta"] is None:1658            return None16591660        message_chunk = _convert_delta_to_message_chunk(1661            choice["delta"], default_chunk_class1662        )1663        generation_info = {**base_generation_info} if base_generation_info else {}16641665        if finish_reason := choice.get("finish_reason"):1666            generation_info["finish_reason"] = finish_reason1667            if model_name := chunk.get("model"):1668                generation_info["model_name"] = model_name1669            if system_fingerprint := chunk.get("system_fingerprint"):1670                generation_info["system_fingerprint"] = system_fingerprint1671            if service_tier := chunk.get("service_tier"):1672                generation_info["service_tier"] = service_tier16731674        logprobs = choice.get("logprobs")1675        if logprobs:1676            generation_info["logprobs"] = logprobs16771678        if usage_metadata and isinstance(message_chunk, AIMessageChunk):1679            message_chunk.usage_metadata = usage_metadata16801681        message_chunk.response_metadata["model_provider"] = "openai"1682        # Propagate output_version so content_blocks can detect v1 mode.1683        if self.output_version == "v1":1684            message_chunk.response_metadata["output_version"] = "v1"1685        return ChatGenerationChunk(1686            message=message_chunk, generation_info=generation_info or None1687        )16881689    def _ensure_sync_client_available(self) -> None:1690        """Check that sync client is available, raise error if not."""1691        if self.client is None:1692            msg = (1693                "Sync client is not available. This happens when an async callable "1694                "was provided for the API key. Use async methods (ainvoke, astream) "1695                "instead, or provide a string or sync callable for the API key."1696            )1697            raise ValueError(msg)16981699    def _stream_responses(1700        self,1701        messages: list[BaseMessage],1702        stop: list[str] | None = None,1703        run_manager: CallbackManagerForLLMRun | None = None,1704        **kwargs: Any,1705    ) -> Iterator[ChatGenerationChunk]:1706        self._ensure_sync_client_available()1707        kwargs["stream"] = True1708        payload = self._get_request_payload(messages, stop=stop, **kwargs)1709        headers: dict = {}1710        base_generation_info: dict = {}1711        try:1712            if self.include_response_headers or self._uses_gateway:1713                raw_context_manager = (1714                    self.root_client.with_raw_response.responses.create(**payload)1715                )1716                context_manager = raw_context_manager.parse()1717                if self.include_response_headers:1718                    headers = {"headers": dict(raw_context_manager.headers)}1719                _add_gateway_metadata(base_generation_info, raw_context_manager)1720            else:1721                context_manager = self.root_client.responses.create(**payload)1722            original_schema_obj = kwargs.get("response_format")17231724            with context_manager as response:1725                is_first_chunk = True1726                current_index = -11727                current_output_index = -11728                current_sub_index = -11729                has_reasoning = False1730                item_output_indices: dict[str, int] = {}1731                for chunk in response:1732                    metadata = headers if is_first_chunk else {}1733                    (1734                        current_index,1735                        current_output_index,1736                        current_sub_index,1737                        generation_chunk,1738                    ) = _convert_responses_chunk_to_generation_chunk(1739                        chunk,1740                        current_index,1741                        current_output_index,1742                        current_sub_index,1743                        schema=original_schema_obj,1744                        metadata=metadata,1745                        has_reasoning=has_reasoning,1746                        output_version=self.output_version,1747                        item_output_indices=item_output_indices,1748                    )1749                    if generation_chunk:1750                        if is_first_chunk and base_generation_info:1751                            generation_chunk.generation_info = {1752                                **base_generation_info,1753                                **(generation_chunk.generation_info or {}),1754                            }1755                        if run_manager:1756                            run_manager.on_llm_new_token(1757                                generation_chunk.text, chunk=generation_chunk1758                            )1759                        is_first_chunk = False1760                        if "reasoning" in generation_chunk.message.additional_kwargs:1761                            has_reasoning = True1762                        yield generation_chunk1763        except openai.BadRequestError as e:1764            _handle_openai_bad_request(e)1765        except openai.APIError as e:1766            _handle_openai_api_error(e)17671768    async def _astream_responses(1769        self,1770        messages: list[BaseMessage],1771        stop: list[str] | None = None,1772        run_manager: AsyncCallbackManagerForLLMRun | None = None,1773        **kwargs: Any,1774    ) -> AsyncIterator[ChatGenerationChunk]:1775        kwargs["stream"] = True1776        payload = self._get_request_payload(messages, stop=stop, **kwargs)1777        headers: dict = {}1778        base_generation_info: dict = {}1779        try:1780            if self.include_response_headers or self._uses_gateway:1781                raw_context_manager = (1782                    await self.root_async_client.with_raw_response.responses.create(1783                        **payload1784                    )1785                )1786                context_manager = raw_context_manager.parse()1787                if self.include_response_headers:1788                    headers = {"headers": dict(raw_context_manager.headers)}1789                _add_gateway_metadata(base_generation_info, raw_context_manager)1790            else:1791                context_manager = await self.root_async_client.responses.create(1792                    **payload1793                )1794            original_schema_obj = kwargs.get("response_format")17951796            async with context_manager as response:1797                is_first_chunk = True1798                current_index = -11799                current_output_index = -11800                current_sub_index = -11801                has_reasoning = False1802                item_output_indices: dict[str, int] = {}1803                async for chunk in _astream_with_chunk_timeout(1804                    response,1805                    self.stream_chunk_timeout,1806                    model_name=self.model_name,1807                ):1808                    metadata = headers if is_first_chunk else {}1809                    (1810                        current_index,1811                        current_output_index,1812                        current_sub_index,1813                        generation_chunk,1814                    ) = _convert_responses_chunk_to_generation_chunk(1815                        chunk,1816                        current_index,1817                        current_output_index,1818                        current_sub_index,1819                        schema=original_schema_obj,1820                        metadata=metadata,1821                        has_reasoning=has_reasoning,1822                        output_version=self.output_version,1823                        item_output_indices=item_output_indices,1824                    )1825                    if generation_chunk:1826                        if is_first_chunk and base_generation_info:1827                            generation_chunk.generation_info = {1828                                **base_generation_info,1829                                **(generation_chunk.generation_info or {}),1830                            }1831                        if run_manager:1832                            await run_manager.on_llm_new_token(1833                                generation_chunk.text, chunk=generation_chunk1834                            )1835                        is_first_chunk = False1836                        if "reasoning" in generation_chunk.message.additional_kwargs:1837                            has_reasoning = True1838                        yield generation_chunk1839        except openai.BadRequestError as e:1840            _handle_openai_bad_request(e)1841        except openai.APIError as e:1842            _handle_openai_api_error(e)18431844    def _should_stream_usage(1845        self, stream_usage: bool | None = None, **kwargs: Any1846    ) -> bool:1847        """Determine whether to include usage metadata in streaming output.18481849        For backwards compatibility, we check for `stream_options` passed1850        explicitly to kwargs or in the `model_kwargs` and override `self.stream_usage`.1851        """1852        stream_usage_sources = [  # order of precedence1853            stream_usage,1854            kwargs.get("stream_options", {}).get("include_usage"),1855            self.model_kwargs.get("stream_options", {}).get("include_usage"),1856            self.stream_usage,1857        ]1858        for source in stream_usage_sources:1859            if isinstance(source, bool):1860                return source1861        return self.stream_usage or False18621863    def _stream(1864        self,1865        messages: list[BaseMessage],1866        stop: list[str] | None = None,1867        run_manager: CallbackManagerForLLMRun | None = None,1868        *,1869        stream_usage: bool | None = None,1870        **kwargs: Any,1871    ) -> Iterator[ChatGenerationChunk]:1872        self._ensure_sync_client_available()1873        kwargs["stream"] = True1874        stream_usage = self._should_stream_usage(stream_usage, **kwargs)1875        if stream_usage:1876            kwargs["stream_options"] = {"include_usage": stream_usage}1877        payload = self._get_request_payload(messages, stop=stop, **kwargs)1878        default_chunk_class: type[BaseMessageChunk] = AIMessageChunk1879        base_generation_info = {}18801881        try:1882            if "response_format" in payload:1883                if self.include_response_headers:1884                    warnings.warn(1885                        "Cannot currently include response headers when "1886                        "response_format is specified."1887                    )1888                payload.pop("stream")1889                response_stream = self.root_client.beta.chat.completions.stream(1890                    **payload1891                )1892                context_manager = response_stream1893            else:1894                if self.include_response_headers or self._uses_gateway:1895                    raw_response = self.client.with_raw_response.create(**payload)1896                    response = raw_response.parse()1897                    if self.include_response_headers:1898                        base_generation_info = {"headers": dict(raw_response.headers)}1899                    _add_gateway_metadata(base_generation_info, raw_response)1900                else:1901                    response = self.client.create(**payload)1902                context_manager = response1903            with context_manager as response:1904                is_first_chunk = True1905                for chunk in response:1906                    if not isinstance(chunk, dict):1907                        chunk = chunk.model_dump()1908                    generation_chunk = self._convert_chunk_to_generation_chunk(1909                        chunk,1910                        default_chunk_class,1911                        base_generation_info if is_first_chunk else {},1912                    )1913                    if generation_chunk is None:1914                        continue1915                    default_chunk_class = generation_chunk.message.__class__1916                    logprobs = (generation_chunk.generation_info or {}).get("logprobs")1917                    if run_manager:1918                        run_manager.on_llm_new_token(1919                            generation_chunk.text,1920                            chunk=generation_chunk,1921                            logprobs=logprobs,1922                        )1923                    is_first_chunk = False1924                    yield generation_chunk1925        except openai.BadRequestError as e:1926            _handle_openai_bad_request(e)1927        except openai.APIError as e:1928            _handle_openai_api_error(e)1929        if hasattr(response, "get_final_completion") and "response_format" in payload:1930            final_completion = response.get_final_completion()1931            generation_chunk = self._get_generation_chunk_from_completion(1932                final_completion1933            )1934            if run_manager:1935                run_manager.on_llm_new_token(1936                    generation_chunk.text, chunk=generation_chunk1937                )1938            yield generation_chunk19391940    def _generate(1941        self,1942        messages: list[BaseMessage],1943        stop: list[str] | None = None,1944        run_manager: CallbackManagerForLLMRun | None = None,1945        **kwargs: Any,1946    ) -> ChatResult:1947        self._ensure_sync_client_available()1948        payload = self._get_request_payload(messages, stop=stop, **kwargs)1949        generation_info = None1950        raw_response = None1951        try:1952            if "response_format" in payload:1953                payload.pop("stream")1954                raw_response = (1955                    self.root_client.chat.completions.with_raw_response.parse(**payload)1956                )1957                response = raw_response.parse()1958            elif self._use_responses_api(payload):1959                original_schema_obj = kwargs.get("response_format")1960                if original_schema_obj and _is_pydantic_class(original_schema_obj):1961                    raw_response = self.root_client.responses.with_raw_response.parse(1962                        **payload1963                    )1964                else:1965                    raw_response = self.root_client.responses.with_raw_response.create(1966                        **payload1967                    )1968                response = raw_response.parse()1969                if self.include_response_headers:1970                    generation_info = {"headers": dict(raw_response.headers)}1971                generation_info = generation_info or {}1972                _add_gateway_metadata(generation_info, raw_response)1973                # Gateway metadata belongs on `generation_info`, not the message1974                # `response_metadata` that `metadata` populates.1975                gateway_metadata = generation_info.pop(1976                    GATEWAY_METADATA_RESPONSE_KEY, None1977                )1978                result = _construct_lc_result_from_responses_api(1979                    response,1980                    schema=original_schema_obj,1981                    metadata=generation_info,1982                    output_version=self.output_version,1983                )1984                if gateway_metadata is not None:1985                    for generation in result.generations:1986                        generation.generation_info = generation.generation_info or {}1987                        generation.generation_info[GATEWAY_METADATA_RESPONSE_KEY] = (1988                            gateway_metadata1989                        )1990                return result1991            else:1992                raw_response = self.client.with_raw_response.create(**payload)1993                response = raw_response.parse()1994        except openai.BadRequestError as e:1995            _handle_openai_bad_request(e)1996        except openai.APIError as e:1997            _handle_openai_api_error(e)1998        except Exception as e:1999            if raw_response is not None and hasattr(raw_response, "http_response"):2000                e.response = raw_response.http_response  # type: ignore[attr-defined]

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.

Generated by searchcode.com · powered by code_get_file + code_get_findings