libs/partners/anthropic/langchain_anthropic/chat_models.py PYTHON 3,384 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 3,384.
1"""Anthropic chat models."""23from __future__ import annotations45import copy6import datetime7import hashlib8import json9import re10import warnings11from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence12from functools import cached_property13from operator import itemgetter14from typing import Any, Final, Literal, TypeGuard, cast1516import anthropic17from langchain_core.callbacks import (18    AsyncCallbackManagerForLLMRun,19    CallbackManagerForLLMRun,20)21from langchain_core.exceptions import (22    ContextOverflowError,23    ModelAPIError,24    ModelAuthenticationError,25    ModelConnectionError,26    ModelInvalidRequestError,27    ModelNotFoundError,28    ModelPermissionDeniedError,29    ModelRateLimitError,30    ModelTimeoutError,31    OutputParserException,32)33from langchain_core.language_models import (34    LanguageModelInput,35    ModelProfile,36    ModelProfileRegistry,37)38from langchain_core.language_models.chat_models import BaseChatModel, LangSmithParams39from langchain_core.messages import (40    AIMessage,41    AIMessageChunk,42    BaseMessage,43    HumanMessage,44    SystemMessage,45    ToolCall,46    ToolMessage,47    is_data_content_block,48)49from langchain_core.messages import content as types50from langchain_core.messages.ai import (51    InputTokenDetails,52    OutputTokenDetails,53    UsageMetadata,54)55from langchain_core.messages.tool import tool_call_chunk as create_tool_call_chunk56from langchain_core.output_parsers import (57    JsonOutputKeyToolsParser,58    JsonOutputParser,59    PydanticOutputParser,60    PydanticToolsParser,61)62from langchain_core.output_parsers.base import OutputParserLike63from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult64from langchain_core.runnables import Runnable, RunnableMap, RunnablePassthrough65from langchain_core.tools import BaseTool66from langchain_core.utils import from_env, get_pydantic_field_names67from langchain_core.utils._gateway import (68    GATEWAY_METADATA_RESPONSE_KEY,69    _apply_gateway_config,70    _parse_gateway_metadata,71)72from langchain_core.utils.function_calling import (73    convert_to_json_schema,74    convert_to_openai_tool,75)76from langchain_core.utils.pydantic import is_basemodel_subclass77from langchain_core.utils.utils import _build_model_kwargs78from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator79from typing_extensions import NotRequired, Self, TypedDict8081from langchain_anthropic import __version__82from langchain_anthropic._client_utils import (83    _get_default_async_httpx_client,84    _get_default_httpx_client,85)86from langchain_anthropic._compat import (87    _convert_from_v1_to_anthropic,88    _unwrap_non_standard,89)90from langchain_anthropic._sdk_compat import (91    _aparse,92    _route_unsupported_sampling_params,93)94from langchain_anthropic.data._profiles import _PROFILES95from langchain_anthropic.output_parsers import extract_tool_calls9697_message_type_lookups = {98    "human": "user",99    "ai": "assistant",100    "AIMessageChunk": "assistant",101    "HumanMessageChunk": "user",102}103104_MODEL_PROFILES = cast(ModelProfileRegistry, _PROFILES)105106_USER_AGENT: Final[str] = f"langchain-anthropic/{__version__}"107108109def _add_gateway_metadata(generation_info: dict[str, Any], raw_response: Any) -> None:110    """Add parsed LangSmith gateway metadata to `generation_info`, if present.111112    Args:113        generation_info: Generation info to mutate in place.114        raw_response: The raw provider response, or None.115    """116    headers = getattr(raw_response, "headers", None)117    if headers is None:118        return119    gateway_metadata = _parse_gateway_metadata(headers)120    if gateway_metadata is not None:121        generation_info[GATEWAY_METADATA_RESPONSE_KEY] = gateway_metadata122123124def _get_default_model_profile(model_name: str) -> ModelProfile:125    """Get the default profile for a model.126127    Args:128        model_name: The model identifier.129130    Returns:131        The model profile dictionary, or an empty dict if not found.132    """133    default = _MODEL_PROFILES.get(model_name)134    if default:135        return default.copy()136    return {}137138139_FALLBACK_MAX_OUTPUT_TOKENS: Final[int] = 4096140141142class AnthropicTool(TypedDict):143    """Anthropic tool definition for custom (user-defined) tools.144145    Custom tools use `name` and `input_schema` fields to define the tool's146    interface. These are converted from LangChain tool formats (functions, Pydantic147    models, `BaseTool` objects) via `convert_to_anthropic_tool`.148    """149150    name: str151152    input_schema: dict[str, Any]153154    description: NotRequired[str]155156    strict: NotRequired[bool]157158    cache_control: NotRequired[dict[str, str]]159160    defer_loading: NotRequired[bool]161162    input_examples: NotRequired[list[dict[str, Any]]]163164    allowed_callers: NotRequired[list[str]]165166167# ---------------------------------------------------------------------------168# Built-in Tool Support169# ---------------------------------------------------------------------------170# When Anthropic releases new built-in tools, two places may need updating:171#172# 1. _TOOL_TYPE_TO_BETA (below) - Add mapping if the tool requires a beta header.173#     Not all tools need this; only add if the API requires a beta header.174#175# 2. _is_builtin_tool() - Add the tool type prefix to _BUILTIN_TOOL_PREFIXES.176#     This ensures the tool dict is passed through to the API unchanged (instead177#     of being converted via convert_to_anthropic_tool, which may fail).178# ---------------------------------------------------------------------------179180_TOOL_TYPE_TO_BETA: dict[str, str] = {181    "web_fetch_20250910": "web-fetch-2025-09-10",182    "code_execution_20250522": "code-execution-2025-05-22",183    "mcp_toolset": "mcp-client-2025-11-20",184    "memory_20250818": "context-management-2025-06-27",185    "computer_20250124": "computer-use-2025-01-24",186    "computer_20251124": "computer-use-2025-11-24",187    "tool_search_tool_regex_20251119": "advanced-tool-use-2025-11-20",188    "tool_search_tool_bm25_20251119": "advanced-tool-use-2025-11-20",189    "advisor_20260301": "advisor-tool-2026-03-01",190}191"""Mapping of tool type to required beta header.192193Some tool types require specific beta headers to be enabled.194"""195196_BUILTIN_TOOL_PREFIXES = [197    "text_editor_",198    "computer_",199    "bash_",200    "web_search_",201    "web_fetch_",202    "code_execution_",203    "mcp_toolset",204    "memory_",205    "tool_search_",206    "advisor_",207]208209_ANTHROPIC_EXTRA_FIELDS: set[str] = {210    "allowed_callers",211    "cache_control",212    "defer_loading",213    "eager_input_streaming",214    "input_examples",215}216"""Valid Anthropic-specific extra fields"""217218219def _is_builtin_tool(tool: Any) -> TypeGuard[dict[str, Any]]:220    """Check if a tool is a built-in (server-side) Anthropic tool.221222    `tool` must be a `dict` and have a `type` key starting with one of the known223    built-in tool prefixes.224225    [Claude docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview)226    """227    if not isinstance(tool, dict):228        return False229230    tool_type = tool.get("type")231    if not tool_type or not isinstance(tool_type, str):232        return False233234    return any(tool_type.startswith(prefix) for prefix in _BUILTIN_TOOL_PREFIXES)235236237def _format_image(url: str) -> dict:238    """Convert part["image_url"]["url"] strings (OpenAI format) to Anthropic format.239240    {241        "type": "base64",242        "media_type": "image/jpeg",243        "data": "/9j/4AAQSkZJRg...",244    }245246    Or247248    {249        "type": "url",250        "url": "https://example.com/image.jpg",251    }252    """253    # Base64 encoded image254    base64_regex = r"^data:(?P<media_type>image/.+);base64,(?P<data>.+)$"255    base64_match = re.match(base64_regex, url)256257    if base64_match:258        return {259            "type": "base64",260            "media_type": base64_match.group("media_type"),261            "data": base64_match.group("data"),262        }263264    # Url265    url_regex = r"^https?://.*$"266    url_match = re.match(url_regex, url)267268    if url_match:269        return {270            "type": "url",271            "url": url,272        }273274    msg = (275        "Malformed url parameter."276        " Must be either an image URL (https://example.com/image.jpg)"277        " or base64 encoded string (data:image/png;base64,'/9j/4AAQSk'...)"278    )279    raise ValueError(280        msg,281    )282283284_TOOL_CALL_ID_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")285"""Anthropic requires `tool_use`/`tool_result` IDs to match this pattern."""286287288def _normalize_tool_call_id(tool_call_id: str | None) -> str | None:289    """Map a tool-call ID to an Anthropic-compatible form if needed.290291    Anthropic rejects `tool_use`/`tool_result` IDs that don't match292    `^[a-zA-Z0-9_-]+$`. IDs minted by other providers can violate this when a293    thread is replayed across providers (e.g. Fireworks/Kimi emits294    `functions.write_todos:0`, whose `.` and `:` are invalid). Valid IDs are295    returned unchanged; invalid ones are hashed deterministically so that a296    rewritten `tool_use.id` and its paired `tool_use_id` resolve to the same297    value, both within a request and across turns.298299    Empty and `None` IDs are passed through unchanged so that a genuinely300    malformed request surfaces as a clear error from Anthropic rather than301    being masked by a synthesized ID.302303    Args:304        tool_call_id: The tool-call ID to normalize.305306    Returns:307        The original ID if it is empty, `None`, or already valid; otherwise a308            deterministic Anthropic-compatible replacement.309    """310    if not tool_call_id or _TOOL_CALL_ID_PATTERN.match(tool_call_id):311        return tool_call_id312    digest = hashlib.sha256(tool_call_id.encode()).hexdigest()313    return f"toolu_{digest[:24]}"314315316def _normalize_block_tool_use_id(block: dict) -> dict:317    """Return `block` with its `tool_use_id` normalized, if it carries one.318319    Mirrors `_normalize_tool_call_id` for `tool_result`-style content blocks so320    that a `tool_use_id` arriving pre-structured (e.g. on a `ToolMessage` whose321    content is already a list of `tool_result` blocks) stays consistent with its322    paired, normalized `tool_use.id`. A no-op for already-valid IDs.323    """324    if "tool_use_id" in block:325        return {**block, "tool_use_id": _normalize_tool_call_id(block["tool_use_id"])}326    return block327328329def _merge_messages(330    messages: Sequence[BaseMessage],331) -> list[SystemMessage | AIMessage | HumanMessage]:332    """Merge runs of human/tool messages into single human messages with content blocks."""  # noqa: E501333    merged: list = []334    for curr in messages:335        if isinstance(curr, ToolMessage):336            if (337                isinstance(curr.content, list)338                and curr.content339                and all(340                    isinstance(block, dict) and block.get("type") == "tool_result"341                    for block in curr.content342                )343            ):344                curr = HumanMessage(curr.content)  # type: ignore[misc]345            else:346                tool_content = curr.content347                cache_ctrl = None348                # Extract cache_control from content blocks and hoist it349                # to the tool_result level.  Anthropic's API does not350                # support cache_control on tool_result content sub-blocks.351                if isinstance(tool_content, list):352                    cleaned = []353                    for block in tool_content:354                        if isinstance(block, dict) and "cache_control" in block:355                            cache_ctrl = block["cache_control"]356                            block = {357                                k: v for k, v in block.items() if k != "cache_control"358                            }359                        cleaned.append(block)360                    tool_content = cleaned361                tool_result: dict = {362                    "type": "tool_result",363                    "content": tool_content,364                    "tool_use_id": _normalize_tool_call_id(curr.tool_call_id),365                    "is_error": curr.status == "error",366                }367                if cache_ctrl:368                    tool_result["cache_control"] = cache_ctrl369                curr = HumanMessage(  # type: ignore[misc]370                    [tool_result],371                )372        last = merged[-1] if merged else None373        if any(374            all(isinstance(m, c) for m in (curr, last))375            for c in (SystemMessage, HumanMessage)376        ):377            if isinstance(cast("BaseMessage", last).content, str):378                new_content: list = [379                    {"type": "text", "text": cast("BaseMessage", last).content},380                ]381            else:382                new_content = copy.copy(cast("list", cast("BaseMessage", last).content))383            if isinstance(curr.content, str):384                new_content.append({"type": "text", "text": curr.content})385            else:386                new_content.extend(curr.content)387            merged[-1] = curr.model_copy(update={"content": new_content})388        else:389            merged.append(curr)390    return merged391392393def _format_data_content_block(block: dict) -> dict:394    """Format standard data content block to format expected by Anthropic."""395    if block["type"] == "image":396        if "url" in block:397            if block["url"].startswith("data:"):398                # Data URI399                formatted_block = {400                    "type": "image",401                    "source": _format_image(block["url"]),402                }403            else:404                formatted_block = {405                    "type": "image",406                    "source": {"type": "url", "url": block["url"]},407                }408        elif "base64" in block or block.get("source_type") == "base64":409            formatted_block = {410                "type": "image",411                "source": {412                    "type": "base64",413                    "media_type": block["mime_type"],414                    "data": block.get("base64") or block.get("data", ""),415                },416            }417        elif "file_id" in block:418            formatted_block = {419                "type": "image",420                "source": {421                    "type": "file",422                    "file_id": block["file_id"],423                },424            }425        elif block.get("source_type") == "id":426            formatted_block = {427                "type": "image",428                "source": {429                    "type": "file",430                    "file_id": block["id"],431                },432            }433        else:434            msg = (435                "Anthropic only supports 'url', 'base64', or 'id' keys for image "436                "content blocks."437            )438            raise ValueError(439                msg,440            )441442    elif block["type"] == "file":443        if "url" in block:444            formatted_block = {445                "type": "document",446                "source": {447                    "type": "url",448                    "url": block["url"],449                },450            }451        elif "base64" in block or block.get("source_type") == "base64":452            formatted_block = {453                "type": "document",454                "source": {455                    "type": "base64",456                    "media_type": block.get("mime_type") or "application/pdf",457                    "data": block.get("base64") or block.get("data", ""),458                },459            }460        elif block.get("source_type") == "text":461            formatted_block = {462                "type": "document",463                "source": {464                    "type": "text",465                    "media_type": block.get("mime_type") or "text/plain",466                    "data": block["text"],467                },468            }469        elif "file_id" in block:470            formatted_block = {471                "type": "document",472                "source": {473                    "type": "file",474                    "file_id": block["file_id"],475                },476            }477        elif block.get("source_type") == "id":478            formatted_block = {479                "type": "document",480                "source": {481                    "type": "file",482                    "file_id": block["id"],483                },484            }485        else:486            msg = (487                "Anthropic only supports 'url', 'base64', or 'id' keys for file "488                "content blocks."489            )490            raise ValueError(msg)491492    elif block["type"] == "text-plain":493        formatted_block = {494            "type": "document",495            "source": {496                "type": "text",497                "media_type": block.get("mime_type") or "text/plain",498                "data": block["text"],499            },500        }501502    else:503        msg = f"Block of type {block['type']} is not supported."504        raise ValueError(msg)505506    if formatted_block:507        for key in ["cache_control", "citations", "title", "context"]:508            if key in block:509                formatted_block[key] = block[key]510            elif (metadata := block.get("extras")) and key in metadata:511                formatted_block[key] = metadata[key]512            elif (metadata := block.get("metadata")) and key in metadata:513                # Backward compat514                formatted_block[key] = metadata[key]515516    return formatted_block517518519def _format_text_block(block: dict) -> dict:520    """Narrow a text content block to fields supported by Anthropic's API.521522    Drops LangChain-internal fields (e.g. the ``id`` minted by523    ``create_text_block``) that Anthropic rejects as extra inputs.524    """525    formatted_block = {526        k: v527        for k, v in block.items()528        if k in ("type", "text", "cache_control", "citations")529    }530    # Clean up citations to remove null file_id fields531    if formatted_block.get("citations"):532        cleaned_citations = []533        for citation in formatted_block["citations"]:534            cleaned_citation = {535                k: v for k, v in citation.items() if not (k == "file_id" and v is None)536            }537            cleaned_citations.append(cleaned_citation)538        formatted_block["citations"] = cleaned_citations539    return formatted_block540541542_TOOL_CHANGE_BLOCK_TYPES = ("tool_addition", "tool_removal")543"""Anthropic-native system content blocks that change the tool set mid-conversation."""544545_MID_CONVERSATION_TOOL_CHANGES_BETA = "mid-conversation-tool-changes-2026-07-01"546"""Beta header required to send `tool_addition` / `tool_removal` blocks."""547548_INLINE_TOOLS_BETA = "inline-tools-2026-09-15"549"""Beta header required to define a tool in a `tool_addition` block."""550551552def _is_tool_change_block(block: object) -> bool:553    """Return whether a content block changes the tool set, in either spelling."""554    return (555        isinstance(block, dict)556        and _unwrap_non_standard(block).get("type") in _TOOL_CHANGE_BLOCK_TYPES557    )558559560def _has_tool_change_block(content: object) -> bool:561    """Return whether any block in `content` changes the tool set."""562    return isinstance(content, list) and any(_is_tool_change_block(b) for b in content)563564565def _has_inline_tool_definition(content: object) -> bool:566    """Return whether `content` defines a tool in a `tool_addition` block."""567    if not isinstance(content, list):568        return False569    return any(570        isinstance(block, dict)571        and block.get("type") == "tool_addition"572        and isinstance(tool := block.get("tool"), dict)573        and tool.get("type") == "tool_definition"574        for block in content575    )576577578def _format_system_content(579    content: str | list[Any],580    *,581    model: str | None = None,582    preserve_tool_changes: bool = False,583    stacklevel: int = 3,584) -> str | list[dict]:585    """Narrow system message content to what Anthropic accepts.586587    String content is passed through unchanged; promoting it to a single-element588    block array would invalidate existing callers' prompt caches.589590    Anthropic documents a closed set of system content blocks: `text` anywhere, plus591    `tool_addition` / `tool_removal` on a mid-conversation `system` turn. Anything592    else is rejected by the API, so it is dropped with a warning rather than593    forwarded. Blocks wrapped in core's `non_standard` escape hatch are unwrapped594    first, so both spellings behave identically.595596    Args:597        content: The system message's content.598        model: The model the request targets, used only in warning text.599        preserve_tool_changes: Whether tool-change blocks should be forwarded instead600            of dropped.601        stacklevel: Frames to skip when attributing a warning, so it points at the602            caller of `_format_messages` rather than at this module. The default603            suits a direct call; a caller reached through a helper adds a frame.604605    Returns:606        Content narrowed to the blocks Anthropic accepts in this position.607    """608    if not isinstance(content, list):609        return content610611    formatted: list[dict] = []612    for raw_block in content:613        if not isinstance(raw_block, dict):614            formatted.append({"type": "text", "text": raw_block})615            continue616617        block = _unwrap_non_standard(raw_block)618        block_type = block.get("type")619        if block_type == "text":620            formatted.append(_format_text_block(block))621        elif block_type in _TOOL_CHANGE_BLOCK_TYPES:622            if preserve_tool_changes:623                formatted.append(block)624            else:625                warnings.warn(626                    f"Tool-change block {block_type!r} was dropped: it is only "627                    "valid on a `SystemMessage` sent in place, and this one was "628                    "hoisted into the top-level `system` field. Use a model whose "629                    "profile sets `mid_conversation_system_messages` and place "630                    "the message after a human or tool message, either last or "631                    f"before an AI message (model: {model!r}).",632                    UserWarning,633                    stacklevel=stacklevel,634                )635        else:636            warnings.warn(637                f"Unrecognized system content block {block_type!r} was dropped. "638                "Anthropic accepts `text` in any system message, plus "639                "`tool_addition` and `tool_removal` on a mid-conversation one.",640                UserWarning,641                stacklevel=stacklevel,642            )643    return formatted644645646def _warn_system_message_hoisted(model: str | None) -> None:647    """Warn that a non-leading system message was hoisted."""648    warnings.warn(649        "A non-leading `SystemMessage` was moved to the top-level `system` field "650        "and now applies to the entire conversation. To send it in place, use a "651        "model whose profile sets `mid_conversation_system_messages` and place "652        "it after a human or tool message, either last or before an AI message "653        f"(model: {model!r}).",654        UserWarning,655        stacklevel=3,656    )657658659def _is_server_tool_result_block(block: object) -> bool:660    """Return whether a content block is a server-side tool result."""661    return (662        isinstance(block, dict)663        and isinstance(block.get("type"), str)664        and block["type"].endswith("_tool_result")665    )666667668def _previous_turn_allows_system(previous_turn: dict | None) -> bool:669    """Return whether a system message may follow the formatted turn."""670    if previous_turn is None:671        return False672673    previous_role = previous_turn.get("role")674    previous_content = previous_turn.get("content")675    return previous_role == "user" or (676        previous_role == "assistant"677        and isinstance(previous_content, list)678        and bool(previous_content)679        and _is_server_tool_result_block(previous_content[-1])680    )681682683def _format_in_place_system_messages(684    pending_system: Sequence[BaseMessage],685    *,686    model: str | None,687) -> list[dict]:688    """Format system messages that keep their position in the message array.689690    A message whose content is narrowed away entirely is omitted: Anthropic691    rejects a `system` turn with empty content, and the dropped blocks have692    already been warned about.693694    Args:695        pending_system: System messages awaiting emission, in order.696        model: The model the request targets, used only in warning text.697698    Returns:699        Formatted `system`-role turns.700    """701    turns: list[dict] = []702    for pending in pending_system:703        content = _format_system_content(704            pending.content,705            model=model,706            preserve_tool_changes=True,707            # This helper sits between `_format_messages` and the warning site.708            stacklevel=4,709        )710        if content == []:711            continue712        turns.append({"role": "system", "content": content})713    return turns714715716def _format_messages(717    messages: Sequence[BaseMessage],718    *,719    model: str | None,720    mid_conversation_system_messages: bool = False,721) -> tuple[str | list[dict] | None, list[dict]]:722    """Format messages for Anthropic's API.723724    Args:725        messages: Messages to format.726        model: The model the request targets, used only in warning text.727        mid_conversation_system_messages: Whether a non-leading system message728            may be sent in place. When `False`, it is hoisted into `system`.729    """730    system: str | list[dict] | None = None731    formatted_messages: list[dict] = []732    toolsets: dict[str, str] = {}733    merged_messages = _merge_messages(messages)734    last_non_system_index = max(735        (i for i, m in enumerate(merged_messages) if m.type != "system"),736        default=-1,737    )738    pending_system: list[BaseMessage] = []739    for _i, message in enumerate(merged_messages):740        if message.type == "system":741            if _i == 0:742                system = _format_system_content(743                    message.content,744                    model=model,745                    preserve_tool_changes=True,746                )747                continue748            if mid_conversation_system_messages and (749                pending_system750                or _previous_turn_allows_system(751                    formatted_messages[-1] if formatted_messages else None752                )753            ):754                pending_system.append(message)755                continue756            if system is not None:757                msg = "Received multiple non-consecutive system messages."758                raise ValueError(msg)759            system = _format_system_content(message.content, model=model)760            _warn_system_message_hoisted(model)761            continue762763        role = _message_type_lookups[message.type]764        content: str | list765766        if not isinstance(message.content, str):767            # parse as dict768            if not isinstance(message.content, list):769                msg = "Anthropic message content must be str or list of dicts"770                raise ValueError(771                    msg,772                )773774            # populate content775            content = []776            for block in message.content:777                if isinstance(block, str):778                    content.append({"type": "text", "text": block})779                elif isinstance(block, dict):780                    if "type" not in block:781                        msg = "Dict content block must have a type key"782                        raise ValueError(msg)783                    if block["type"] in ("reasoning", "function_call") and (784                        not isinstance(message, AIMessage)785                        or message.response_metadata.get("model_provider")786                        != "anthropic"787                    ):788                        continue789                    if block["type"] == "image_url":790                        # convert format791                        source = _format_image(block["image_url"]["url"])792                        content.append({"type": "image", "source": source})793                    elif is_data_content_block(block):794                        content.append(_format_data_content_block(block))795                    elif block["type"] == "tool_use":796                        # If a tool_call with the same id as a tool_use content block797                        # exists, the tool_call is preferred.798                        if (799                            isinstance(message, AIMessage)800                            and (block["id"] in [tc["id"] for tc in message.tool_calls])801                            and not block.get("caller")802                        ):803                            overlapping = [804                                tc805                                for tc in message.tool_calls806                                if tc["id"] == block["id"]807                            ]808                            tool_blocks = _lc_tool_calls_to_anthropic_tool_use_blocks(809                                overlapping,810                            )811                            if toolset_name := block.get("toolset_name"):812                                for tool_block in tool_blocks:813                                    tool_block["toolset_name"] = toolset_name814                            content.extend(tool_blocks)815                        else:816                            if tool_input := block.get("input"):817                                args = tool_input818                            elif "partial_json" in block:819                                try:820                                    args = json.loads(block["partial_json"] or "{}")821                                except json.JSONDecodeError:822                                    args = {}823                            else:824                                args = {}825                            tool_use_block = _AnthropicToolUse(826                                type="tool_use",827                                name=block["name"],828                                input=args,829                                id=cast("str", _normalize_tool_call_id(block["id"])),830                            )831                            if caller := block.get("caller"):832                                tool_use_block["caller"] = caller833                            if toolset_name := block.get("toolset_name"):834                                tool_use_block["toolset_name"] = toolset_name835                            content.append(tool_use_block)836                    elif block["type"] in ("server_tool_use", "mcp_tool_use"):837                        formatted_block = {838                            k: v839                            for k, v in block.items()840                            if k841                            in (842                                "type",843                                "id",844                                "input",845                                "name",846                                "server_name",  # for mcp_tool_use847                                "cache_control",848                            )849                        }850                        # Attempt to parse streamed output851                        if block.get("input") == {} and "partial_json" in block:852                            try:853                                input_ = json.loads(block["partial_json"])854                                if input_:855                                    formatted_block["input"] = input_856                            except json.JSONDecodeError:857                                pass858                        content.append(formatted_block)859                    elif block["type"] == "text":860                        text = block.get("text", "")861                        # Only add non-empty strings for now as empty ones are not862                        # accepted.863                        # https://github.com/anthropics/anthropic-sdk-python/issues/461864                        if text.strip():865                            content.append(_format_text_block(block))866                    elif block["type"] == "thinking":867                        formatted_thinking = {868                            k: v869                            for k, v in block.items()870                            if k in ("type", "thinking", "cache_control", "signature")871                        }872                        if "signature" in formatted_thinking:873                            formatted_thinking.setdefault("thinking", "")874                        content.append(formatted_thinking)875                    elif block["type"] == "redacted_thinking":876                        content.append(877                            {878                                k: v879                                for k, v in block.items()880                                if k in ("type", "cache_control", "data")881                            },882                        )883                    elif (884                        block["type"] == "tool_result"885                        and isinstance(block.get("content"), list)886                        and any(887                            isinstance(item, dict)888                            and item.get("type") == "tool_reference"889                            for item in block["content"]890                        )891                    ):892                        # Tool search results with tool_reference blocks893                        content.append(894                            _normalize_block_tool_use_id(895                                {896                                    k: v897                                    for k, v in block.items()898                                    if k899                                    in (900                                        "type",901                                        "content",902                                        "tool_use_id",903                                        "cache_control",904                                    )905                                },906                            ),907                        )908                    elif block["type"] == "tool_search_tool_result":909                        # Omit streaming-only fields, such as `index`, from results.910                        content.append(911                            _normalize_block_tool_use_id(912                                {913                                    k: v914                                    for k, v in block.items()915                                    if k916                                    in (917                                        "type",918                                        "content",919                                        "tool_use_id",920                                        "cache_control",921                                    )922                                },923                            ),924                        )925                    elif block["type"] == "tool_result":926                        # Regular tool results that need content formatting927                        tool_content = _format_messages(928                            [HumanMessage(block["content"])],929                            model=None,930                        )[1][0]["content"]931                        content.append(932                            _normalize_block_tool_use_id(933                                {**block, "content": tool_content},934                            ),935                        )936                    elif block["type"] in (937                        "code_execution_tool_result",938                        "bash_code_execution_tool_result",939                        "text_editor_code_execution_tool_result",940                        "mcp_tool_result",941                        "web_search_tool_result",942                        "web_fetch_tool_result",943                    ):944                        content.append(945                            _normalize_block_tool_use_id(946                                {947                                    k: v948                                    for k, v in block.items()949                                    if k950                                    in (951                                        "type",952                                        "content",953                                        "tool_use_id",954                                        "is_error",  # for mcp_tool_result955                                        "cache_control",956                                        "retrieved_at",  # for web_fetch_tool_result957                                    )958                                },959                            ),960                        )961                    elif block["type"] == "advisor_tool_result":962                        content.append({k: v for k, v in block.items() if k != "index"})963                    else:964                        content.append(block)965                else:966                    msg = (967                        f"Content blocks must be str or dict, instead was: "968                        f"{type(block)}"969                    )970                    raise ValueError(971                        msg,972                    )973        else:974            content = message.content975976        if isinstance(message, AIMessage) and (977            message.tool_calls or message.invalid_tool_calls978        ):979            content = content or []980            content = (981                [{"type": "text", "text": message.content}]982                if isinstance(content, str) and content983                else content984            )985            tool_use_ids = [986                cast("dict", block)["id"]987                for block in content988                if cast("dict", block)["type"] == "tool_use"989            ]990            # `tool_use_ids` are already normalized via the branches above, so991            # compare against the normalized tool-call ID to avoid emitting a992            # duplicate `tool_use` block when the original ID was rewritten.993            missing_tool_calls = [994                tc995                for tc in message.tool_calls996                if _normalize_tool_call_id(tc["id"]) not in tool_use_ids997            ]998            cast("list", content).extend(999                _lc_tool_calls_to_anthropic_tool_use_blocks(missing_tool_calls),1000            )1001            tool_use_ids.extend(1002                cast("str", _normalize_tool_call_id(tc["id"]))1003                for tc in missing_tool_calls1004            )1005            for invalid_call in message.invalid_tool_calls:1006                tool_call_id = invalid_call.get("id")1007                tool_name = invalid_call.get("name")1008                if not tool_call_id or not tool_name:1009                    continue1010                normalized_id = _normalize_tool_call_id(tool_call_id)1011                if normalized_id in tool_use_ids:1012                    continue1013                try:1014                    args = json.loads(invalid_call.get("args") or "{}")1015                except json.JSONDecodeError:1016                    args = {}1017                cast("list", content).append(1018                    _AnthropicToolUse(1019                        type="tool_use",1020                        name=tool_name,1021                        input=args if isinstance(args, dict) else {},1022                        id=cast("str", normalized_id),1023                    )1024                )1025                tool_use_ids.append(normalized_id)10261027        if role == "assistant" and _i == last_non_system_index:1028            if isinstance(content, str):1029                content = content.rstrip()1030            elif (1031                isinstance(content, list)1032                and content1033                and isinstance(content[-1], dict)1034                and content[-1].get("type") == "text"1035            ):1036                content[-1]["text"] = content[-1]["text"].rstrip()10371038        if not content and role == "assistant" and _i < last_non_system_index:1039            # anthropic.BadRequestError: Error code: 400: all messages must have1040            # non-empty content except for the optional final assistant message1041            continue1042        if pending_system:1043            if role == "assistant":1044                formatted_messages.extend(1045                    _format_in_place_system_messages(pending_system, model=model)1046                )1047            else:1048                for pending in pending_system:1049                    if system is not None:1050                        msg = "Received multiple non-consecutive system messages."1051                        raise ValueError(msg)1052                    system = _format_system_content(pending.content, model=model)1053                    _warn_system_message_hoisted(model)1054            pending_system = []1055        if isinstance(content, list):1056            for block in content:1057                if not isinstance(block, dict):1058                    continue1059                if block.get("type") == "tool_use" and block.get("toolset_name"):1060                    toolsets[block["id"]] = block["toolset_name"]1061                elif block.get("type") == "tool_result" and (1062                    toolset_name := toolsets.get(block.get("tool_use_id", ""))1063                ):1064                    block.setdefault("toolset_name", toolset_name)1065        formatted_messages.append({"role": role, "content": content})10661067    formatted_messages.extend(1068        _format_in_place_system_messages(pending_system, model=model)1069    )1070    return system, formatted_messages107110721073def _container_id(container: Any) -> str | None:1074    """Return the container ID from either accepted `container` shape."""1075    if isinstance(container, str):1076        return container1077    if isinstance(container, dict):1078        return container.get("id")1079    return None108010811082def _collect_code_execution_tool_ids(formatted_messages: list[dict]) -> set[str]:1083    """Collect `tool_use` IDs that were called by `code_execution`.10841085    These blocks cannot have `cache_control` applied per Anthropic API1086    requirements.1087    """1088    code_execution_tool_ids: set[str] = set()10891090    for message in formatted_messages:1091        if message.get("role") != "assistant":1092            continue1093        content = message.get("content", [])1094        if not isinstance(content, list):1095            continue1096        for block in content:1097            if not isinstance(block, dict):1098                continue1099            if block.get("type") != "tool_use":1100                continue1101            caller = block.get("caller")1102            if isinstance(caller, dict):1103                caller_type = caller.get("type", "")1104                if caller_type.startswith("code_execution"):1105                    tool_id = block.get("id")1106                    if tool_id:1107                        code_execution_tool_ids.add(tool_id)11081109    return code_execution_tool_ids111011111112def _is_code_execution_related_block(1113    block: dict,1114    code_execution_tool_ids: set[str],1115) -> bool:1116    """Return whether a content block is related to `code_execution`.11171118    Returns `True` for blocks that should NOT have `cache_control` applied.1119    """1120    if not isinstance(block, dict):1121        return False11221123    block_type = block.get("type")11241125    if block_type == "tool_use":1126        caller = block.get("caller")1127        if isinstance(caller, dict):1128            caller_type = caller.get("type", "")1129            if caller_type.startswith("code_execution"):1130                return True11311132    if block_type == "tool_result":1133        tool_use_id = block.get("tool_use_id")1134        if tool_use_id and tool_use_id in code_execution_tool_ids:1135            return True11361137    return False113811391140def _reasoning_effort_levels(profile: object) -> tuple[str, ...]:1141    """Return the reasoning-effort levels declared in a model's profile, if any.11421143    Defensive against a missing/malformed profile: an absent `profile`, a1144    non-mapping value, or a missing/non-list `reasoning_effort_levels` value is1145    treated as "no levels declared" rather than raising.1146    """1147    if not isinstance(profile, Mapping):1148        return ()1149    levels = profile.get("reasoning_effort_levels")1150    if not isinstance(levels, (list, tuple)):1151        return ()1152    return tuple(levels)115311541155def _supports_forced_tool_choice(model: str) -> bool:1156    """Return whether the model accepts `tool_choice` types `any` and `tool`."""1157    return not model.startswith(1158        ("claude-fable-5-1", "claude-opus-5-5", "claude-sonnet-5-5")1159    )116011611162def _is_direct_anthropic_llm_type(llm_type: object) -> bool:1163    """Return whether an `_llm_type` reaches Claude via the direct Anthropic API.11641165    Only the direct API accepts the top-level `cache_control` request param.1166    Subclasses that route through other transports (Bedrock, future backends)1167    override `_llm_type` and must expand `cache_control` kwargs into1168    block-level breakpoints instead.11691170    Non-string `_llm_type` values return `False` rather than raising, so a1171    misbehaving subclass falls through to the safer non-direct branch.1172    """1173    return llm_type == "anthropic-chat"117411751176def _apply_cache_control_to_last_eligible_block(1177    formatted_messages: list[dict],1178    cache_control: Any,1179    code_execution_tool_ids: set[str],1180) -> bool:1181    """Place `cache_control` on the last block eligible for a breakpoint.11821183    Walks messages newest-to-oldest and, within each, blocks newest-to-oldest,1184    skipping `code_execution`-related blocks (Anthropic rejects breakpoints1185    there). String message content is promoted to a single text block so the1186    breakpoint can be attached.11871188    Returns:1189        `True` if a breakpoint was applied, `False` if every candidate was1190            `code_execution`-related (caller should warn and drop the kwarg).1191    """1192    for formatted_message in reversed(formatted_messages):1193        content = formatted_message.get("content")1194        if isinstance(content, list) and content:1195            for block in reversed(content):1196                if not isinstance(block, dict):1197                    continue1198                if _is_code_execution_related_block(block, code_execution_tool_ids):1199                    continue1200                block["cache_control"] = cache_control1201                return True1202        elif isinstance(content, str):1203            formatted_message["content"] = [1204                {1205                    "type": "text",1206                    "text": content,1207                    "cache_control": cache_control,1208                }1209            ]1210            return True1211    return False121212131214class AnthropicContextOverflowError(anthropic.BadRequestError, ContextOverflowError):1215    """BadRequestError raised when input exceeds Anthropic's context limit."""121612171218class AnthropicAuthenticationError(1219    anthropic.AuthenticationError, ModelAuthenticationError1220):1221    """Anthropic authentication error classified as a LangChain model error."""122212231224class AnthropicPermissionDeniedError(1225    anthropic.PermissionDeniedError, ModelPermissionDeniedError1226):1227    """Anthropic permission error classified as a LangChain model error."""122812291230class AnthropicInvalidRequestError(anthropic.BadRequestError, ModelInvalidRequestError):1231    """Anthropic bad-request error classified as a LangChain model error."""123212331234class AnthropicModelNotFoundError(anthropic.NotFoundError, ModelNotFoundError):1235    """Anthropic not-found error classified as a LangChain model error."""123612371238class AnthropicRateLimitError(anthropic.RateLimitError, ModelRateLimitError):1239    """Anthropic rate-limit error classified as a LangChain model error."""124012411242class AnthropicAPIError(anthropic.InternalServerError, ModelAPIError):1243    """Anthropic server error classified as a LangChain model error."""124412451246class AnthropicOverloadedError(anthropic.OverloadedError, ModelAPIError):1247    """Anthropic overloaded error (HTTP 529) classified as a LangChain model error."""124812491250class AnthropicConnectionError(anthropic.APIConnectionError, ModelConnectionError):1251    """Anthropic connection error classified as a LangChain model error."""125212531254class AnthropicTimeoutError(anthropic.APITimeoutError, ModelTimeoutError):1255    """Anthropic timeout error classified as a LangChain model error."""125612571258def _raise_if_authentication_error(e: TypeError) -> None:1259    """Re-raise anthropic SDK's missing-credentials `TypeError` with guidance."""1260    if "Could not resolve authentication method" in str(e):1261        msg = (1262            "Anthropic authentication failed: no API key or authorization "1263            "credentials were provided. Set the ANTHROPIC_API_KEY environment "1264            "variable, pass api_key=... to ChatAnthropic, or provide "1265            'credentials via default_headers={"Authorization": ...}. If you '1266            "are routing through the LangSmith gateway, set LANGSMITH_GATEWAY "1267            "and LANGSMITH_GATEWAY_API_KEY."1268        )1269        raise TypeError(msg) from e127012711272def _handle_anthropic_bad_request(e: anthropic.BadRequestError) -> None:1273    """Handle Anthropic BadRequestError."""1274    if "prompt is too long" in e.message:1275        raise AnthropicContextOverflowError(1276            message=e.message, response=e.response, body=e.body1277        ) from e1278    if ("messages: at least one message is required") in e.message:1279        message = "Received only system message(s). "1280        warnings.warn(message, stacklevel=2)1281    raise AnthropicInvalidRequestError(1282        message=e.message, response=e.response, body=e.body1283    ) from e128412851286def _handle_anthropic_api_error(e: anthropic.APIError) -> None:1287    """Re-raise an Anthropic SDK error as its LangChain-classified equivalent."""1288    if isinstance(e, anthropic.AuthenticationError):1289        raise AnthropicAuthenticationError(1290            message=e.message, response=e.response, body=e.body1291        ) from e1292    if isinstance(e, anthropic.PermissionDeniedError):1293        raise AnthropicPermissionDeniedError(1294            message=e.message, response=e.response, body=e.body1295        ) from e1296    if isinstance(e, anthropic.NotFoundError):1297        raise AnthropicModelNotFoundError(1298            message=e.message, response=e.response, body=e.body1299        ) from e1300    if isinstance(e, anthropic.RateLimitError):1301        raise AnthropicRateLimitError(1302            message=e.message, response=e.response, body=e.body1303        ) from e1304    if isinstance(e, anthropic.OverloadedError):1305        raise AnthropicOverloadedError(1306            message=e.message, response=e.response, body=e.body1307        ) from e1308    if isinstance(e, anthropic.InternalServerError):1309        raise AnthropicAPIError(1310            message=e.message, response=e.response, body=e.body1311        ) from e1312    # `APITimeoutError` subclasses `APIConnectionError`, so check it first.1313    if isinstance(e, anthropic.APITimeoutError):1314        raise AnthropicTimeoutError(e.request) from e1315    if isinstance(e, anthropic.APIConnectionError):1316        raise AnthropicConnectionError(message=e.message, request=e.request) from e1317    raise131813191320class ChatAnthropic(BaseChatModel):1321    """Anthropic (Claude) chat models.13221323    See the [LangChain docs for `ChatAnthropic`](https://docs.langchain.com/oss/python/integrations/chat/anthropic)1324    for tutorials, feature walkthroughs, and examples.13251326    See the [Claude Platform docs](https://platform.claude.com/docs/en/about-claude/models/overview)1327    for a list of the latest models, their capabilities, and pricing.13281329    Example:1330        ```python1331        # pip install -U langchain-anthropic1332        # export ANTHROPIC_API_KEY="your-api-key"13331334        from langchain_anthropic import ChatAnthropic13351336        model = ChatAnthropic(1337            model="claude-sonnet-4-5-20250929",1338            # temperature=,1339            # max_tokens=,1340            # timeout=,1341            # max_retries=,1342            # base_url="...",1343            # Refer to API reference for full list of parameters1344        )1345        ```13461347    Example: Add a tool mid-conversation1348        ```python1349        from langchain_core.messages import HumanMessage, SystemMessage1350        from langchain_anthropic import ChatAnthropic13511352        model = ChatAnthropic(model="claude-opus-5-5")1353        model.invoke(1354            [1355                HumanMessage("What time is it?"),1356                SystemMessage(1357                    [1358                        {1359                            "type": "tool_addition",1360                            "tool": {1361                                "type": "tool_definition",1362                                "definition": {1363                                    "name": "get_time",1364                                    "description": "Get the current time.",1365                                    "input_schema": {1366                                        "type": "object",1367                                        "properties": {},1368                                    },1369                                },1370                            },1371                        }1372                    ]1373                ),1374            ]1375        )1376        ```13771378    Note:1379        Any param which is not explicitly supported will be passed directly to1380        [`Anthropic.messages.create(...)`](https://platform.claude.com/docs/en/api/python/messages/create)1381        each time to the model is invoked.1382    """13831384    model_config = ConfigDict(1385        populate_by_name=True,1386    )13871388    model: str = Field(alias="model_name")1389    """Model name to use."""13901391    max_tokens: int | None = Field(default=None, alias="max_tokens_to_sample")1392    """Denotes the number of tokens to predict per generation.13931394    If not specified, this is set dynamically using the model's `max_output_tokens`1395    from its model profile.13961397    See docs on [model profiles](https://docs.langchain.com/oss/python/langchain/models#model-profiles)1398    for more information.1399    """14001401    temperature: float | None = None1402    """A non-negative float that tunes the degree of randomness in generation."""14031404    top_k: int | None = None1405    """Number of most likely tokens to consider at each step."""14061407    top_p: float | None = None1408    """Total probability mass of tokens to consider at each step."""14091410    default_request_timeout: float | None = Field(None, alias="timeout")1411    """Timeout for requests to Claude API."""14121413    # sdk default = 2: https://github.com/anthropics/anthropic-sdk-python?tab=readme-ov-file#retries1414    max_retries: int = 21415    """Number of retries allowed for requests sent to the Claude API."""14161417    stop_sequences: list[str] | None = Field(None, alias="stop")1418    """Default stop sequences."""14191420    anthropic_api_url: str | None = Field(default=None, alias="base_url")1421    """Base URL for API requests. Only specify if using a proxy or service emulator.14221423    If a value isn't passed in, will attempt to read the value first from1424    `ANTHROPIC_API_URL` and if that is not set, `ANTHROPIC_BASE_URL`.14251426    If `LANGSMITH_GATEWAY` is set, it is used as a fallback after those env vars.1427    """14281429    anthropic_api_key: SecretStr = Field(default=SecretStr(""), alias="api_key")1430    """Automatically read from env var `ANTHROPIC_API_KEY` if not provided.14311432    If `LANGSMITH_GATEWAY` is enabled and the base URL points at the gateway,1433    `LANGSMITH_GATEWAY_API_KEY` is used instead.1434    """14351436    anthropic_proxy: str | None = Field(1437        default_factory=from_env("ANTHROPIC_PROXY", default=None)1438    )1439    """Proxy to use for the Anthropic clients, will be used for every API call.14401441    If not provided, will attempt to read from the `ANTHROPIC_PROXY` environment1442    variable.1443    """14441445    default_headers: Mapping[str, str] | None = None1446    """Headers to pass to the Anthropic clients, will be used for every API call."""14471448    betas: list[str] | None = None1449    """List of beta features to enable. If specified, invocations will be routed1450    through `client.beta.messages.create`.14511452    Example: `#!python betas=["token-efficient-tools-2025-02-19"]`1453    """1454    # Can also be passed in w/ model_kwargs, but having it as a param makes better devx1455    #1456    # Precedence order:1457    # 1. Call-time kwargs (e.g., llm.invoke(..., betas=[...]))1458    # 2. model_kwargs (e.g., ChatAnthropic(model_kwargs={"betas": [...]}))1459    # 3. Direct parameter (e.g., ChatAnthropic(betas=[...]))14601461    model_kwargs: dict[str, Any] = Field(default_factory=dict)14621463    streaming: bool = False1464    """Whether to use streaming or not."""14651466    stream_usage: bool = True1467    """Whether to include usage metadata in streaming output.14681469    If `True`, additional message chunks will be generated during the stream including1470    usage metadata.1471    """14721473    thinking: dict[str, Any] | None = Field(default=None)1474    """Parameters for Claude reasoning.14751476    Examples:14771478    - `#!python {"type": "enabled", "budget_tokens": 10_000}` (pre-4.7 models)1479    - `#!python {"type": "adaptive"}` (Opus 4.6+, Opus 5, Opus 5.5, Sonnet 5)1480    - `#!python {"type": "adaptive", "display": "summarized"}` (Opus 4.7+,1481      Opus 5, Opus 5.5, Sonnet 5)1482    - `#!python {"type": "disabled"}` (Opus 5 and Sonnet 5, where adaptive1483      thinking is on by default)14841485    !!! note "Claude Opus 4.7+, Opus 5, Opus 5.5, and Sonnet 5"14861487        `budget_tokens` is removed on these models — use `{"type": "adaptive"}`1488        with `output_config.effort` to control reasoning effort. The default1489        `display` is `"omitted"`; set it to `"summarized"` to receive1490        summarized reasoning in the response. On Opus 5, disabled thinking is1491        supported only at `"high"` effort or below. On Opus 5.5, thinking1492        can't be disabled; omit `thinking` and use `output_config.effort`.1493    """14941495    output_config: dict[str, Any] | None = None1496    """Configuration options for the model's output.14971498    Supports the following keys:14991500    - `effort`: Controls how many tokens Claude uses when responding.1501      One of `"max"`, `"xhigh"`, `"high"`, `"medium"`, or `"low"`.1502    - `format`: Structured output format configuration (typically set via1503      `with_structured_output`).1504    - `task_budget`: Advisory token budget for an agentic loop (beta).1505      E.g., `#!python {"type": "tokens", "total": 128_000}`.15061507    Example:15081509    .. code-block:: python15101511        ChatAnthropic(1512            model="claude-opus-4-7",1513            output_config={1514                "effort": "xhigh",1515                "task_budget": {"type": "tokens", "total": 128_000},1516            },1517        )15181519    See Anthropic docs on1520    [extended output](https://platform.claude.com/docs/en/api/go/beta/messages/create).1521    """15221523    reasoning_effort: Literal["max", "xhigh", "high", "medium", "low"] | None = Field(1524        default=None,1525        alias="effort",1526    )1527    """Reasoning effort.15281529    Configures `output_config.effort`. If `thinking` isn't set explicitly,1530    defaults it to `{"type": "adaptive", "display": "summarized"}`. Can also1531    be passed at call time (for example,1532    `model.invoke(..., reasoning_effort="high")`).15331534    !!! note "`effort` alias"15351536        `effort` is also accepted as an alias for this field, at both1537        construction and call time. If both `effort` and `reasoning_effort` are1538        set, `effort` wins (Pydantic's alias-resolution precedence).15391540    !!! note15411542        On most models, setting `reasoning_effort` to `'high'` produces exactly1543        the same behavior as omitting the parameter altogether. On Opus 5.5 the1544        default is `'medium'`.15451546    Example: `reasoning_effort="medium"`1547    """15481549    mcp_servers: list[dict[str, Any]] | None = None1550    """List of MCP servers to use for the request.15511552    Example: `#!python mcp_servers=[{"type": "url", "url": "https://mcp.example.com/mcp",1553    "name": "example-mcp"}]`1554    """15551556    context_management: dict[str, Any] | None = None1557    """Configuration for1558    [context management](https://platform.claude.com/docs/en/build-with-claude/context-editing).1559    """15601561    container: dict[str, Any] | str | None = None1562    """Code execution container for the request.15631564    Either a container ID from a previous response, or a dict of container1565    parameters — notably1566    [skills](https://platform.claude.com/docs/en/build-with-claude/skills-guide)1567    to load into the container. Skills require a1568    [code execution](https://docs.langchain.com/oss/python/integrations/chat/anthropic#code-execution)1569    tool to be bound.15701571    ```python1572    model = ChatAnthropic(1573        model="claude-opus-5",1574        container={1575            "skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}]1576        },1577    ).bind_tools([{"type": "code_execution_20260521", "name": "code_execution"}])1578    ```15791580    Can also be passed at call time, which overrides the value set here.1581    """15821583    reuse_last_container: bool | None = None1584    """Automatically reuse container from most recent response (code execution).15851586    When using the built-in1587    [code execution tool](https://docs.langchain.com/oss/python/integrations/chat/anthropic#code-execution),1588    model responses will include container metadata. Set `reuse_last_container=True`1589    to automatically reuse the container from the most recent response for subsequent1590    invocations.1591    """15921593    inference_geo: str | None = None1594    """Controls where model inference runs. See Anthropic's1595    [data residency](https://platform.claude.com/docs/en/build-with-claude/data-residency)1596    docs for more information.1597    """15981599    user_profile_id: str | None = None1600    """User profile ID to attribute the request to.16011602    Use when acting on behalf of a party other than your organization. Setting this1603    automatically enables the required `user-profiles` beta, routing the request1604    through `client.beta.messages.create`.16051606    Can also be passed at call time, which overrides the value set here (for example,1607    `model.invoke(..., user_profile_id="uprof_...")`).1608    """16091610    @property1611    def effort(self) -> Literal["max", "xhigh", "high", "medium", "low"] | None:1612        """Alias for `reasoning_effort`."""1613        return self.reasoning_effort16141615    @property1616    def _llm_type(self) -> str:1617        """Return type of chat model."""1618        return "anthropic-chat"16191620    @property1621    def _uses_gateway(self) -> bool:1622        """Whether requests are routed through the LangSmith gateway."""1623        return self.anthropic_api_key.get_secret_value().startswith("lsv2_")16241625    @property1626    def lc_secrets(self) -> dict[str, str]:1627        """Return a mapping of secret keys to environment variables."""1628        return {1629            "anthropic_api_key": "ANTHROPIC_API_KEY",1630            "mcp_servers": "ANTHROPIC_MCP_SERVERS",1631        }16321633    @classmethod1634    def is_lc_serializable(cls) -> bool:1635        """Whether the class is serializable in langchain."""1636        return True16371638    @classmethod1639    def get_lc_namespace(cls) -> list[str]:1640        """Get the namespace of the LangChain object.16411642        Returns:1643            `["langchain", "chat_models", "anthropic"]`1644        """1645        return ["langchain", "chat_models", "anthropic"]16461647    @property1648    def _identifying_params(self) -> dict[str, Any]:1649        """Get the identifying parameters."""1650        return {1651            "model": self.model,1652            "max_tokens": self.max_tokens,1653            "temperature": self.temperature,1654            "top_k": self.top_k,1655            "top_p": self.top_p,1656            "model_kwargs": self.model_kwargs,1657            "streaming": self.streaming,1658            "max_retries": self.max_retries,1659            "default_request_timeout": self.default_request_timeout,1660            "thinking": self.thinking,1661            "output_config": self.output_config,1662        }16631664    def _get_ls_params(1665        self,1666        stop: list[str] | None = None,1667        **kwargs: Any,1668    ) -> LangSmithParams:1669        """Get standard params for tracing."""1670        params = self._get_invocation_params(stop=stop, **kwargs)1671        ls_params = LangSmithParams(1672            ls_provider="anthropic",1673            ls_model_name=params.get("model", self.model),1674            ls_model_type="chat",1675            ls_temperature=params.get("temperature", self.temperature),1676        )1677        if ls_max_tokens := params.get("max_tokens", self.max_tokens):1678            ls_params["ls_max_tokens"] = ls_max_tokens1679        if ls_stop := stop or params.get("stop", None):1680            ls_params["ls_stop"] = ls_stop1681        return ls_params16821683    @model_validator(mode="before")1684    @classmethod1685    def set_default_max_tokens(cls, values: dict[str, Any]) -> Any:1686        """Set default `max_tokens` from model profile with fallback."""1687        if values.get("max_tokens") is None:1688            model = values.get("model") or values.get("model_name")1689            profile = _get_default_model_profile(model) if model else {}1690            values["max_tokens"] = profile.get(1691                "max_output_tokens", _FALLBACK_MAX_OUTPUT_TOKENS1692            )1693        return values16941695    @model_validator(mode="before")1696    @classmethod1697    def build_extra(cls, values: dict) -> Any:1698        """Build model kwargs."""1699        all_required_field_names = get_pydantic_field_names(cls)1700        return _build_model_kwargs(values, all_required_field_names)17011702    @model_validator(mode="after")1703    def _set_anthropic_version(self) -> Self:1704        """Set package version in metadata."""1705        self._add_version("langchain-anthropic", __version__)1706        return self17071708    @model_validator(mode="before")1709    @classmethod1710    def _resolve_gateway(cls, values: Any) -> Any:1711        """Resolve the base URL and API key, applying LangSmith gateway settings.17121713        An explicit ``base_url``/``api_key`` always wins. Otherwise the base URL1714        falls back to ``ANTHROPIC_API_URL``/``ANTHROPIC_BASE_URL``, then the1715        LangSmith gateway, then the Anthropic default. The gateway key is1716        preferred only when the base URL came from the gateway; for any other1717        endpoint the provider key wins, and the gateway key is a candidate only1718        when the gateway is enabled.1719        """1720        if isinstance(values, dict):1721            _apply_gateway_config(1722                values,1723                cls,1724                base_url_field="anthropic_api_url",1725                api_key_field="anthropic_api_key",1726                provider_path="anthropic",1727                base_url_env=["ANTHROPIC_API_URL", "ANTHROPIC_BASE_URL"],1728                api_key_env="ANTHROPIC_API_KEY",1729                default_base_url="https://api.anthropic.com",1730            )1731        return values17321733    def _resolve_model_profile(self) -> ModelProfile | None:1734        profile = _get_default_model_profile(self.model) or None1735        if profile is not None and self.betas and "context-1m-2025-08-07" in self.betas:1736            profile["max_input_tokens"] = 1_000_0001737        return profile17381739    @cached_property1740    def _client_params(self) -> dict[str, Any]:1741        # Merge User-Agent with user-provided headers (user headers take precedence)1742        default_headers = {"User-Agent": _USER_AGENT}1743        if self.default_headers:1744            default_headers.update(self.default_headers)17451746        client_params: dict[str, Any] = {1747            "api_key": self.anthropic_api_key.get_secret_value(),1748            "base_url": self.anthropic_api_url,1749            "max_retries": self.max_retries,1750            "default_headers": default_headers,1751        }1752        # value <= 0 indicates the param should be ignored. None is a meaningful value1753        # for Anthropic client and treated differently than not specifying the param at1754        # all.1755        if self.default_request_timeout is None or self.default_request_timeout > 0:1756            client_params["timeout"] = self.default_request_timeout17571758        return client_params17591760    @cached_property1761    def _client(self) -> anthropic.Client:1762        client_params = self._client_params1763        http_client_params = {"base_url": client_params["base_url"]}1764        if "timeout" in client_params:1765            http_client_params["timeout"] = client_params["timeout"]1766        if self.anthropic_proxy:1767            http_client_params["anthropic_proxy"] = self.anthropic_proxy1768        http_client = _get_default_httpx_client(**http_client_params)1769        params = {1770            **client_params,1771            "http_client": http_client,1772        }1773        return anthropic.Client(**params)17741775    @cached_property1776    def _async_client(self) -> anthropic.AsyncClient:1777        client_params = self._client_params1778        http_client_params = {"base_url": client_params["base_url"]}1779        if "timeout" in client_params:1780            http_client_params["timeout"] = client_params["timeout"]1781        if self.anthropic_proxy:1782            http_client_params["anthropic_proxy"] = self.anthropic_proxy1783        http_client = _get_default_async_httpx_client(**http_client_params)1784        params = {1785            **client_params,1786            "http_client": http_client,1787        }1788        return anthropic.AsyncClient(**params)17891790    def _assert_valid_model_configuration(self, kwargs: Mapping[str, Any]) -> None:1791        """Validate resolved request configuration against model-specific invariants."""1792        request_config = {**self.model_kwargs, **kwargs}1793        thinking = request_config.get("thinking")1794        if self.thinking is not None:1795            thinking = self.thinking17961797        output_config = dict(self.output_config or {})1798        if self.reasoning_effort:1799            output_config["effort"] = self.reasoning_effort1800        request_output_config = request_config.get("output_config")1801        if isinstance(request_output_config, dict):1802            output_config.update(request_output_config)1803        effort = request_config.get("effort")1804        if effort is None:1805            effort = request_config.get("reasoning_effort")1806        if effort:1807            output_config["effort"] = effort18081809        is_fable_model = self.model.startswith("claude-fable-5")1810        is_sonnet_55 = self.model.startswith("claude-sonnet-5-5")1811        if is_fable_model or is_sonnet_55:1812            top_k = request_config.get("top_k", self.top_k)1813            top_p = request_config.get("top_p", self.top_p)1814            temperature = request_config.get("temperature", self.temperature)1815            if top_k is not None:1816                msg = f"`top_k` is not supported for {self.model}."1817                raise ValueError(msg)1818            if top_p is not None and top_p != 1:1819                msg = (1820                    f"`top_p` is not supported for {self.model} at non-default values."1821                )1822                raise ValueError(msg)1823            if temperature is not None and temperature != 1:1824                msg = (1825                    f"`temperature` is not supported for {self.model} at "1826                    "non-default values."1827                )1828                raise ValueError(msg)1829            if isinstance(thinking, Mapping) and thinking.get("type") == "disabled":1830                msg = (1831                    '`thinking={"type": "disabled"}` is not supported for '1832                    f"{self.model}; omit `thinking` to use adaptive thinking."1833                )1834                raise ValueError(msg)18351836        if (1837            (self.model.startswith("claude-opus-5") or is_fable_model or is_sonnet_55)1838            and isinstance(thinking, Mapping)1839            and thinking.get("type") == "enabled"1840        ):1841            msg = (1842                '`thinking={"type": "enabled", "budget_tokens": ...}` is not '1843                f"supported for {self.model}; use adaptive thinking and "1844                "`output_config.effort` instead."1845            )1846            raise ValueError(msg)18471848        if (1849            self.model.startswith("claude-opus-5")1850            and isinstance(thinking, Mapping)1851            and thinking.get("type") == "disabled"1852            and output_config.get("effort") in {"xhigh", "max"}1853        ):1854            msg = (1855                '`thinking={"type": "disabled"}` is not supported for '1856                f"{self.model} with "1857                f"`output_config.effort={output_config['effort']!r}`; use adaptive "1858                "thinking, omit `thinking`, or set effort to `high` or below."1859            )1860            raise ValueError(msg)18611862    def _get_request_payload(1863        self,1864        input_: LanguageModelInput,1865        *,1866        stop: list[str] | None = None,1867        **kwargs: Any,1868    ) -> dict:1869        """Get the request payload for the Anthropic API."""1870        self._assert_valid_model_configuration(kwargs)1871        messages = self._convert_input(input_).to_messages()18721873        for idx, message in enumerate(messages):1874            # Translate v1 content1875            if (1876                isinstance(message, AIMessage)1877                and message.response_metadata.get("output_version") == "v1"1878            ):1879                tcs: list[types.ToolCall] = [1880                    {1881                        "type": "tool_call",1882                        "name": tool_call["name"],1883                        "args": tool_call["args"],1884                        "id": tool_call.get("id"),1885                    }1886                    for tool_call in message.tool_calls1887                ]1888                messages[idx] = message.model_copy(1889                    update={1890                        "content": _convert_from_v1_to_anthropic(1891                            cast(list[types.ContentBlock], message.content),1892                            tcs,1893                            message.response_metadata.get("model_provider"),1894                        )1895                    }1896                )18971898        system, formatted_messages = _format_messages(1899            messages,1900            model=self.model,1901            mid_conversation_system_messages=bool(1902                (self.profile or {}).get("mid_conversation_system_messages")1903            ),1904        )1905        if isinstance(system, list) and not system:1906            # Every block was narrowed away (or the message was empty to begin1907            # with). An empty block array carries no instructions, so drop the1908            # field rather than sending it. `_format_messages` has already1909            # claimed the slot, so a second hoisted system message still errors.1910            system = None19111912        # Only the direct Anthropic API accepts top-level `cache_control`.1913        # Subclasses that route through other transports (e.g. Bedrock) expand1914        # `cache_control` kwargs into block-level breakpoints, the only form1915        # those transports accept.1916        if not _is_direct_anthropic_llm_type(getattr(self, "_llm_type", None)):1917            cache_control = kwargs.pop("cache_control", None)1918            # Empty `formatted_messages` has nothing to attach a breakpoint to;1919            # skip silently. The warning below is reserved for the surprising1920            # case where messages exist but every candidate block is ineligible.1921            if cache_control and formatted_messages:1922                code_execution_tool_ids = _collect_code_execution_tool_ids(1923                    formatted_messages1924                )1925                applied = _apply_cache_control_to_last_eligible_block(1926                    formatted_messages, cache_control, code_execution_tool_ids1927                )1928                if not applied:1929                    warnings.warn(1930                        "`cache_control` kwarg was dropped: no eligible "1931                        "content block found (all candidates are "1932                        "`code_execution`-related, which Anthropic forbids "1933                        "breakpoints on).",1934                        UserWarning,1935                        stacklevel=2,1936                    )19371938        payload = {1939            "model": self.model,1940            "max_tokens": self.max_tokens,1941            "messages": formatted_messages,1942            "temperature": self.temperature,1943            "top_k": self.top_k,1944            "top_p": self.top_p,1945            "stop_sequences": stop or self.stop_sequences,1946            "betas": self.betas,1947            "context_management": self.context_management,1948            "mcp_servers": self.mcp_servers,1949            "container": self.container,1950            "user_profile_id": self.user_profile_id,1951            "system": system,1952            **self.model_kwargs,1953            **kwargs,1954        }1955        # Captured before `self.thinking` is applied below, so a call-time1956        # `thinking` kwarg counts as "explicitly set" too.1957        thinking_explicitly_set = "thinking" in payload or self.thinking is not None1958        if self.thinking is not None:1959            payload["thinking"] = self.thinking1960        if self.inference_geo is not None:1961            payload["inference_geo"] = self.inference_geo1962        if self.model.startswith("claude-fable-5"):1963            payload.pop("temperature", None)1964            payload.pop("top_k", None)1965            payload.pop("top_p", None)19661967        # Handle output_config and effort parameter1968        # Priority: kwarg `effort`/`reasoning_effort` > kwarg `output_config`1969        # > self.reasoning_effort > self.output_config1970        output_config: dict[str, Any] = {}1971        if self.output_config:1972            output_config.update(self.output_config)1973        reasoning_effort_applied = False1974        if self.reasoning_effort:1975            output_config["effort"] = self.reasoning_effort1976            reasoning_effort_applied = True1977        payload_oc = payload.get("output_config")1978        if isinstance(payload_oc, dict):1979            output_config.update(payload_oc)19801981        # Neither `reasoning_effort` nor its `effort` alias are Anthropic API1982        # fields. Pop them so they never leak through as top-level keys.1983        effort_kwarg = payload.pop("effort", None)1984        reasoning_effort_kwarg = payload.pop("reasoning_effort", None)1985        # `effort` wins if both are set at call time, matching the1986        # construction-time alias-resolution precedence (`Field(alias="effort")`).1987        reasoning_effort_override = (1988            effort_kwarg if effort_kwarg is not None else reasoning_effort_kwarg1989        )1990        if reasoning_effort_override:1991            output_config["effort"] = reasoning_effort_override1992            reasoning_effort_applied = True19931994        if output_config:1995            payload["output_config"] = output_config19961997        # Default adaptive thinking when `reasoning_effort` is set, unless the1998        # caller explicitly provided `thinking`. Gated on `xhigh` support: only1999        # Opus 4.7+/Sonnet 5 accept the adaptive+summarized `thinking` shape —2000        # sending it to an older model (e.g. Opus 4.5, 4.6) is rejected by the

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