libs/partners/anthropic/langchain_anthropic/chat_models.py PYTHON 3,033 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 3,033.
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 _convert_from_v1_to_anthropic87from langchain_anthropic._sdk_compat import (88    _aparse,89    _route_unsupported_sampling_params,90)91from langchain_anthropic.data._profiles import _PROFILES92from langchain_anthropic.output_parsers import extract_tool_calls9394_message_type_lookups = {95    "human": "user",96    "ai": "assistant",97    "AIMessageChunk": "assistant",98    "HumanMessageChunk": "user",99}100101_MODEL_PROFILES = cast(ModelProfileRegistry, _PROFILES)102103_USER_AGENT: Final[str] = f"langchain-anthropic/{__version__}"104105106def _add_gateway_metadata(generation_info: dict[str, Any], raw_response: Any) -> None:107    """Add parsed LangSmith gateway metadata to `generation_info`, if present.108109    Args:110        generation_info: Generation info to mutate in place.111        raw_response: The raw provider response, or None.112    """113    headers = getattr(raw_response, "headers", None)114    if headers is None:115        return116    gateway_metadata = _parse_gateway_metadata(headers)117    if gateway_metadata is not None:118        generation_info[GATEWAY_METADATA_RESPONSE_KEY] = gateway_metadata119120121def _get_default_model_profile(model_name: str) -> ModelProfile:122    """Get the default profile for a model.123124    Args:125        model_name: The model identifier.126127    Returns:128        The model profile dictionary, or an empty dict if not found.129    """130    default = _MODEL_PROFILES.get(model_name)131    if default:132        return default.copy()133    return {}134135136_FALLBACK_MAX_OUTPUT_TOKENS: Final[int] = 4096137138139class AnthropicTool(TypedDict):140    """Anthropic tool definition for custom (user-defined) tools.141142    Custom tools use `name` and `input_schema` fields to define the tool's143    interface. These are converted from LangChain tool formats (functions, Pydantic144    models, `BaseTool` objects) via `convert_to_anthropic_tool`.145    """146147    name: str148149    input_schema: dict[str, Any]150151    description: NotRequired[str]152153    strict: NotRequired[bool]154155    cache_control: NotRequired[dict[str, str]]156157    defer_loading: NotRequired[bool]158159    input_examples: NotRequired[list[dict[str, Any]]]160161    allowed_callers: NotRequired[list[str]]162163164# ---------------------------------------------------------------------------165# Built-in Tool Support166# ---------------------------------------------------------------------------167# When Anthropic releases new built-in tools, two places may need updating:168#169# 1. _TOOL_TYPE_TO_BETA (below) - Add mapping if the tool requires a beta header.170#     Not all tools need this; only add if the API requires a beta header.171#172# 2. _is_builtin_tool() - Add the tool type prefix to _BUILTIN_TOOL_PREFIXES.173#     This ensures the tool dict is passed through to the API unchanged (instead174#     of being converted via convert_to_anthropic_tool, which may fail).175# ---------------------------------------------------------------------------176177_TOOL_TYPE_TO_BETA: dict[str, str] = {178    "web_fetch_20250910": "web-fetch-2025-09-10",179    "code_execution_20250522": "code-execution-2025-05-22",180    "mcp_toolset": "mcp-client-2025-11-20",181    "memory_20250818": "context-management-2025-06-27",182    "computer_20250124": "computer-use-2025-01-24",183    "computer_20251124": "computer-use-2025-11-24",184    "tool_search_tool_regex_20251119": "advanced-tool-use-2025-11-20",185    "tool_search_tool_bm25_20251119": "advanced-tool-use-2025-11-20",186    "advisor_20260301": "advisor-tool-2026-03-01",187}188"""Mapping of tool type to required beta header.189190Some tool types require specific beta headers to be enabled.191"""192193_BUILTIN_TOOL_PREFIXES = [194    "text_editor_",195    "computer_",196    "bash_",197    "web_search_",198    "web_fetch_",199    "code_execution_",200    "mcp_toolset",201    "memory_",202    "tool_search_",203    "advisor_",204]205206_ANTHROPIC_EXTRA_FIELDS: set[str] = {207    "allowed_callers",208    "cache_control",209    "defer_loading",210    "eager_input_streaming",211    "input_examples",212}213"""Valid Anthropic-specific extra fields"""214215216def _is_builtin_tool(tool: Any) -> TypeGuard[dict[str, Any]]:217    """Check if a tool is a built-in (server-side) Anthropic tool.218219    `tool` must be a `dict` and have a `type` key starting with one of the known220    built-in tool prefixes.221222    [Claude docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview)223    """224    if not isinstance(tool, dict):225        return False226227    tool_type = tool.get("type")228    if not tool_type or not isinstance(tool_type, str):229        return False230231    return any(tool_type.startswith(prefix) for prefix in _BUILTIN_TOOL_PREFIXES)232233234def _format_image(url: str) -> dict:235    """Convert part["image_url"]["url"] strings (OpenAI format) to Anthropic format.236237    {238        "type": "base64",239        "media_type": "image/jpeg",240        "data": "/9j/4AAQSkZJRg...",241    }242243    Or244245    {246        "type": "url",247        "url": "https://example.com/image.jpg",248    }249    """250    # Base64 encoded image251    base64_regex = r"^data:(?P<media_type>image/.+);base64,(?P<data>.+)$"252    base64_match = re.match(base64_regex, url)253254    if base64_match:255        return {256            "type": "base64",257            "media_type": base64_match.group("media_type"),258            "data": base64_match.group("data"),259        }260261    # Url262    url_regex = r"^https?://.*$"263    url_match = re.match(url_regex, url)264265    if url_match:266        return {267            "type": "url",268            "url": url,269        }270271    msg = (272        "Malformed url parameter."273        " Must be either an image URL (https://example.com/image.jpg)"274        " or base64 encoded string (data:image/png;base64,'/9j/4AAQSk'...)"275    )276    raise ValueError(277        msg,278    )279280281_TOOL_CALL_ID_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")282"""Anthropic requires `tool_use`/`tool_result` IDs to match this pattern."""283284285def _normalize_tool_call_id(tool_call_id: str | None) -> str | None:286    """Map a tool-call ID to an Anthropic-compatible form if needed.287288    Anthropic rejects `tool_use`/`tool_result` IDs that don't match289    `^[a-zA-Z0-9_-]+$`. IDs minted by other providers can violate this when a290    thread is replayed across providers (e.g. Fireworks/Kimi emits291    `functions.write_todos:0`, whose `.` and `:` are invalid). Valid IDs are292    returned unchanged; invalid ones are hashed deterministically so that a293    rewritten `tool_use.id` and its paired `tool_use_id` resolve to the same294    value, both within a request and across turns.295296    Empty and `None` IDs are passed through unchanged so that a genuinely297    malformed request surfaces as a clear error from Anthropic rather than298    being masked by a synthesized ID.299300    Args:301        tool_call_id: The tool-call ID to normalize.302303    Returns:304        The original ID if it is empty, `None`, or already valid; otherwise a305            deterministic Anthropic-compatible replacement.306    """307    if not tool_call_id or _TOOL_CALL_ID_PATTERN.match(tool_call_id):308        return tool_call_id309    digest = hashlib.sha256(tool_call_id.encode()).hexdigest()310    return f"toolu_{digest[:24]}"311312313def _normalize_block_tool_use_id(block: dict) -> dict:314    """Return `block` with its `tool_use_id` normalized, if it carries one.315316    Mirrors `_normalize_tool_call_id` for `tool_result`-style content blocks so317    that a `tool_use_id` arriving pre-structured (e.g. on a `ToolMessage` whose318    content is already a list of `tool_result` blocks) stays consistent with its319    paired, normalized `tool_use.id`. A no-op for already-valid IDs.320    """321    if "tool_use_id" in block:322        return {**block, "tool_use_id": _normalize_tool_call_id(block["tool_use_id"])}323    return block324325326def _merge_messages(327    messages: Sequence[BaseMessage],328) -> list[SystemMessage | AIMessage | HumanMessage]:329    """Merge runs of human/tool messages into single human messages with content blocks."""  # noqa: E501330    merged: list = []331    for curr in messages:332        if isinstance(curr, ToolMessage):333            if (334                isinstance(curr.content, list)335                and curr.content336                and all(337                    isinstance(block, dict) and block.get("type") == "tool_result"338                    for block in curr.content339                )340            ):341                curr = HumanMessage(curr.content)  # type: ignore[misc]342            else:343                tool_content = curr.content344                cache_ctrl = None345                # Extract cache_control from content blocks and hoist it346                # to the tool_result level.  Anthropic's API does not347                # support cache_control on tool_result content sub-blocks.348                if isinstance(tool_content, list):349                    cleaned = []350                    for block in tool_content:351                        if isinstance(block, dict) and "cache_control" in block:352                            cache_ctrl = block["cache_control"]353                            block = {354                                k: v for k, v in block.items() if k != "cache_control"355                            }356                        cleaned.append(block)357                    tool_content = cleaned358                tool_result: dict = {359                    "type": "tool_result",360                    "content": tool_content,361                    "tool_use_id": _normalize_tool_call_id(curr.tool_call_id),362                    "is_error": curr.status == "error",363                }364                if cache_ctrl:365                    tool_result["cache_control"] = cache_ctrl366                curr = HumanMessage(  # type: ignore[misc]367                    [tool_result],368                )369        last = merged[-1] if merged else None370        if any(371            all(isinstance(m, c) for m in (curr, last))372            for c in (SystemMessage, HumanMessage)373        ):374            if isinstance(cast("BaseMessage", last).content, str):375                new_content: list = [376                    {"type": "text", "text": cast("BaseMessage", last).content},377                ]378            else:379                new_content = copy.copy(cast("list", cast("BaseMessage", last).content))380            if isinstance(curr.content, str):381                new_content.append({"type": "text", "text": curr.content})382            else:383                new_content.extend(curr.content)384            merged[-1] = curr.model_copy(update={"content": new_content})385        else:386            merged.append(curr)387    return merged388389390def _format_data_content_block(block: dict) -> dict:391    """Format standard data content block to format expected by Anthropic."""392    if block["type"] == "image":393        if "url" in block:394            if block["url"].startswith("data:"):395                # Data URI396                formatted_block = {397                    "type": "image",398                    "source": _format_image(block["url"]),399                }400            else:401                formatted_block = {402                    "type": "image",403                    "source": {"type": "url", "url": block["url"]},404                }405        elif "base64" in block or block.get("source_type") == "base64":406            formatted_block = {407                "type": "image",408                "source": {409                    "type": "base64",410                    "media_type": block["mime_type"],411                    "data": block.get("base64") or block.get("data", ""),412                },413            }414        elif "file_id" in block:415            formatted_block = {416                "type": "image",417                "source": {418                    "type": "file",419                    "file_id": block["file_id"],420                },421            }422        elif block.get("source_type") == "id":423            formatted_block = {424                "type": "image",425                "source": {426                    "type": "file",427                    "file_id": block["id"],428                },429            }430        else:431            msg = (432                "Anthropic only supports 'url', 'base64', or 'id' keys for image "433                "content blocks."434            )435            raise ValueError(436                msg,437            )438439    elif block["type"] == "file":440        if "url" in block:441            formatted_block = {442                "type": "document",443                "source": {444                    "type": "url",445                    "url": block["url"],446                },447            }448        elif "base64" in block or block.get("source_type") == "base64":449            formatted_block = {450                "type": "document",451                "source": {452                    "type": "base64",453                    "media_type": block.get("mime_type") or "application/pdf",454                    "data": block.get("base64") or block.get("data", ""),455                },456            }457        elif block.get("source_type") == "text":458            formatted_block = {459                "type": "document",460                "source": {461                    "type": "text",462                    "media_type": block.get("mime_type") or "text/plain",463                    "data": block["text"],464                },465            }466        elif "file_id" in block:467            formatted_block = {468                "type": "document",469                "source": {470                    "type": "file",471                    "file_id": block["file_id"],472                },473            }474        elif block.get("source_type") == "id":475            formatted_block = {476                "type": "document",477                "source": {478                    "type": "file",479                    "file_id": block["id"],480                },481            }482        else:483            msg = (484                "Anthropic only supports 'url', 'base64', or 'id' keys for file "485                "content blocks."486            )487            raise ValueError(msg)488489    elif block["type"] == "text-plain":490        formatted_block = {491            "type": "document",492            "source": {493                "type": "text",494                "media_type": block.get("mime_type") or "text/plain",495                "data": block["text"],496            },497        }498499    else:500        msg = f"Block of type {block['type']} is not supported."501        raise ValueError(msg)502503    if formatted_block:504        for key in ["cache_control", "citations", "title", "context"]:505            if key in block:506                formatted_block[key] = block[key]507            elif (metadata := block.get("extras")) and key in metadata:508                formatted_block[key] = metadata[key]509            elif (metadata := block.get("metadata")) and key in metadata:510                # Backward compat511                formatted_block[key] = metadata[key]512513    return formatted_block514515516def _format_text_block(block: dict) -> dict:517    """Narrow a text content block to fields supported by Anthropic's API.518519    Drops LangChain-internal fields (e.g. the ``id`` minted by520    ``create_text_block``) that Anthropic rejects as extra inputs.521    """522    formatted_block = {523        k: v524        for k, v in block.items()525        if k in ("type", "text", "cache_control", "citations")526    }527    # Clean up citations to remove null file_id fields528    if formatted_block.get("citations"):529        cleaned_citations = []530        for citation in formatted_block["citations"]:531            cleaned_citation = {532                k: v for k, v in citation.items() if not (k == "file_id" and v is None)533            }534            cleaned_citations.append(cleaned_citation)535        formatted_block["citations"] = cleaned_citations536    return formatted_block537538539def _format_messages(540    messages: Sequence[BaseMessage],541) -> tuple[str | list[dict] | None, list[dict]]:542    """Format messages for Anthropic's API."""543    system: str | list[dict] | None = None544    formatted_messages: list[dict] = []545    merged_messages = _merge_messages(messages)546    for _i, message in enumerate(merged_messages):547        if message.type == "system":548            if system is not None:549                msg = "Received multiple non-consecutive system messages."550                raise ValueError(msg)551            if isinstance(message.content, list):552                system = [553                    (554                        (555                            _format_text_block(block)556                            if block.get("type") == "text"557                            else block558                        )559                        if isinstance(block, dict)560                        else {"type": "text", "text": block}561                    )562                    for block in message.content563                ]564            else:565                system = message.content566            continue567568        role = _message_type_lookups[message.type]569        content: str | list570571        if not isinstance(message.content, str):572            # parse as dict573            if not isinstance(message.content, list):574                msg = "Anthropic message content must be str or list of dicts"575                raise ValueError(576                    msg,577                )578579            # populate content580            content = []581            for block in message.content:582                if isinstance(block, str):583                    content.append({"type": "text", "text": block})584                elif isinstance(block, dict):585                    if "type" not in block:586                        msg = "Dict content block must have a type key"587                        raise ValueError(msg)588                    if block["type"] in ("reasoning", "function_call") and (589                        not isinstance(message, AIMessage)590                        or message.response_metadata.get("model_provider")591                        != "anthropic"592                    ):593                        continue594                    if block["type"] == "image_url":595                        # convert format596                        source = _format_image(block["image_url"]["url"])597                        content.append({"type": "image", "source": source})598                    elif is_data_content_block(block):599                        content.append(_format_data_content_block(block))600                    elif block["type"] == "tool_use":601                        # If a tool_call with the same id as a tool_use content block602                        # exists, the tool_call is preferred.603                        if (604                            isinstance(message, AIMessage)605                            and (block["id"] in [tc["id"] for tc in message.tool_calls])606                            and not block.get("caller")607                        ):608                            overlapping = [609                                tc610                                for tc in message.tool_calls611                                if tc["id"] == block["id"]612                            ]613                            content.extend(614                                _lc_tool_calls_to_anthropic_tool_use_blocks(615                                    overlapping,616                                ),617                            )618                        else:619                            if tool_input := block.get("input"):620                                args = tool_input621                            elif "partial_json" in block:622                                try:623                                    args = json.loads(block["partial_json"] or "{}")624                                except json.JSONDecodeError:625                                    args = {}626                            else:627                                args = {}628                            tool_use_block = _AnthropicToolUse(629                                type="tool_use",630                                name=block["name"],631                                input=args,632                                id=cast("str", _normalize_tool_call_id(block["id"])),633                            )634                            if caller := block.get("caller"):635                                tool_use_block["caller"] = caller636                            content.append(tool_use_block)637                    elif block["type"] in ("server_tool_use", "mcp_tool_use"):638                        formatted_block = {639                            k: v640                            for k, v in block.items()641                            if k642                            in (643                                "type",644                                "id",645                                "input",646                                "name",647                                "server_name",  # for mcp_tool_use648                                "cache_control",649                            )650                        }651                        # Attempt to parse streamed output652                        if block.get("input") == {} and "partial_json" in block:653                            try:654                                input_ = json.loads(block["partial_json"])655                                if input_:656                                    formatted_block["input"] = input_657                            except json.JSONDecodeError:658                                pass659                        content.append(formatted_block)660                    elif block["type"] == "text":661                        text = block.get("text", "")662                        # Only add non-empty strings for now as empty ones are not663                        # accepted.664                        # https://github.com/anthropics/anthropic-sdk-python/issues/461665                        if text.strip():666                            content.append(_format_text_block(block))667                    elif block["type"] == "thinking":668                        formatted_thinking = {669                            k: v670                            for k, v in block.items()671                            if k in ("type", "thinking", "cache_control", "signature")672                        }673                        if "signature" in formatted_thinking:674                            formatted_thinking.setdefault("thinking", "")675                        content.append(formatted_thinking)676                    elif block["type"] == "redacted_thinking":677                        content.append(678                            {679                                k: v680                                for k, v in block.items()681                                if k in ("type", "cache_control", "data")682                            },683                        )684                    elif (685                        block["type"] == "tool_result"686                        and isinstance(block.get("content"), list)687                        and any(688                            isinstance(item, dict)689                            and item.get("type") == "tool_reference"690                            for item in block["content"]691                        )692                    ):693                        # Tool search results with tool_reference blocks694                        content.append(695                            _normalize_block_tool_use_id(696                                {697                                    k: v698                                    for k, v in block.items()699                                    if k700                                    in (701                                        "type",702                                        "content",703                                        "tool_use_id",704                                        "cache_control",705                                    )706                                },707                            ),708                        )709                    elif block["type"] == "tool_search_tool_result":710                        # Omit streaming-only fields, such as `index`, from results.711                        content.append(712                            _normalize_block_tool_use_id(713                                {714                                    k: v715                                    for k, v in block.items()716                                    if k717                                    in (718                                        "type",719                                        "content",720                                        "tool_use_id",721                                        "cache_control",722                                    )723                                },724                            ),725                        )726                    elif block["type"] == "tool_result":727                        # Regular tool results that need content formatting728                        tool_content = _format_messages(729                            [HumanMessage(block["content"])],730                        )[1][0]["content"]731                        content.append(732                            _normalize_block_tool_use_id(733                                {**block, "content": tool_content},734                            ),735                        )736                    elif block["type"] in (737                        "code_execution_tool_result",738                        "bash_code_execution_tool_result",739                        "text_editor_code_execution_tool_result",740                        "mcp_tool_result",741                        "web_search_tool_result",742                        "web_fetch_tool_result",743                    ):744                        content.append(745                            _normalize_block_tool_use_id(746                                {747                                    k: v748                                    for k, v in block.items()749                                    if k750                                    in (751                                        "type",752                                        "content",753                                        "tool_use_id",754                                        "is_error",  # for mcp_tool_result755                                        "cache_control",756                                        "retrieved_at",  # for web_fetch_tool_result757                                    )758                                },759                            ),760                        )761                    else:762                        content.append(block)763                else:764                    msg = (765                        f"Content blocks must be str or dict, instead was: "766                        f"{type(block)}"767                    )768                    raise ValueError(769                        msg,770                    )771        else:772            content = message.content773774        # Ensure all tool_calls have a tool_use content block775        if isinstance(message, AIMessage) and message.tool_calls:776            content = content or []777            content = (778                [{"type": "text", "text": message.content}]779                if isinstance(content, str) and content780                else content781            )782            tool_use_ids = [783                cast("dict", block)["id"]784                for block in content785                if cast("dict", block)["type"] == "tool_use"786            ]787            # `tool_use_ids` are already normalized via the branches above, so788            # compare against the normalized tool-call ID to avoid emitting a789            # duplicate `tool_use` block when the original ID was rewritten.790            missing_tool_calls = [791                tc792                for tc in message.tool_calls793                if _normalize_tool_call_id(tc["id"]) not in tool_use_ids794            ]795            cast("list", content).extend(796                _lc_tool_calls_to_anthropic_tool_use_blocks(missing_tool_calls),797            )798799        if role == "assistant" and _i == len(merged_messages) - 1:800            if isinstance(content, str):801                content = content.rstrip()802            elif (803                isinstance(content, list)804                and content805                and isinstance(content[-1], dict)806                and content[-1].get("type") == "text"807            ):808                content[-1]["text"] = content[-1]["text"].rstrip()809810        if not content and role == "assistant" and _i < len(merged_messages) - 1:811            # anthropic.BadRequestError: Error code: 400: all messages must have812            # non-empty content except for the optional final assistant message813            continue814        formatted_messages.append({"role": role, "content": content})815    return system, formatted_messages816817818def _container_id(container: Any) -> str | None:819    """Return the container ID from either accepted `container` shape."""820    if isinstance(container, str):821        return container822    if isinstance(container, dict):823        return container.get("id")824    return None825826827def _collect_code_execution_tool_ids(formatted_messages: list[dict]) -> set[str]:828    """Collect `tool_use` IDs that were called by `code_execution`.829830    These blocks cannot have `cache_control` applied per Anthropic API831    requirements.832    """833    code_execution_tool_ids: set[str] = set()834835    for message in formatted_messages:836        if message.get("role") != "assistant":837            continue838        content = message.get("content", [])839        if not isinstance(content, list):840            continue841        for block in content:842            if not isinstance(block, dict):843                continue844            if block.get("type") != "tool_use":845                continue846            caller = block.get("caller")847            if isinstance(caller, dict):848                caller_type = caller.get("type", "")849                if caller_type.startswith("code_execution"):850                    tool_id = block.get("id")851                    if tool_id:852                        code_execution_tool_ids.add(tool_id)853854    return code_execution_tool_ids855856857def _is_code_execution_related_block(858    block: dict,859    code_execution_tool_ids: set[str],860) -> bool:861    """Return whether a content block is related to `code_execution`.862863    Returns `True` for blocks that should NOT have `cache_control` applied.864    """865    if not isinstance(block, dict):866        return False867868    block_type = block.get("type")869870    if block_type == "tool_use":871        caller = block.get("caller")872        if isinstance(caller, dict):873            caller_type = caller.get("type", "")874            if caller_type.startswith("code_execution"):875                return True876877    if block_type == "tool_result":878        tool_use_id = block.get("tool_use_id")879        if tool_use_id and tool_use_id in code_execution_tool_ids:880            return True881882    return False883884885def _reasoning_effort_levels(profile: object) -> tuple[str, ...]:886    """Return the reasoning-effort levels declared in a model's profile, if any.887888    Defensive against a missing/malformed profile: an absent `profile`, a889    non-mapping value, or a missing/non-list `reasoning_effort_levels` value is890    treated as "no levels declared" rather than raising.891    """892    if not isinstance(profile, Mapping):893        return ()894    levels = profile.get("reasoning_effort_levels")895    if not isinstance(levels, (list, tuple)):896        return ()897    return tuple(levels)898899900def _is_direct_anthropic_llm_type(llm_type: object) -> bool:901    """Return whether an `_llm_type` reaches Claude via the direct Anthropic API.902903    Only the direct API accepts the top-level `cache_control` request param.904    Subclasses that route through other transports (Bedrock, future backends)905    override `_llm_type` and must expand `cache_control` kwargs into906    block-level breakpoints instead.907908    Non-string `_llm_type` values return `False` rather than raising, so a909    misbehaving subclass falls through to the safer non-direct branch.910    """911    return llm_type == "anthropic-chat"912913914def _apply_cache_control_to_last_eligible_block(915    formatted_messages: list[dict],916    cache_control: Any,917    code_execution_tool_ids: set[str],918) -> bool:919    """Place `cache_control` on the last block eligible for a breakpoint.920921    Walks messages newest-to-oldest and, within each, blocks newest-to-oldest,922    skipping `code_execution`-related blocks (Anthropic rejects breakpoints923    there). String message content is promoted to a single text block so the924    breakpoint can be attached.925926    Returns:927        `True` if a breakpoint was applied, `False` if every candidate was928            `code_execution`-related (caller should warn and drop the kwarg).929    """930    for formatted_message in reversed(formatted_messages):931        content = formatted_message.get("content")932        if isinstance(content, list) and content:933            for block in reversed(content):934                if not isinstance(block, dict):935                    continue936                if _is_code_execution_related_block(block, code_execution_tool_ids):937                    continue938                block["cache_control"] = cache_control939                return True940        elif isinstance(content, str):941            formatted_message["content"] = [942                {943                    "type": "text",944                    "text": content,945                    "cache_control": cache_control,946                }947            ]948            return True949    return False950951952class AnthropicContextOverflowError(anthropic.BadRequestError, ContextOverflowError):953    """BadRequestError raised when input exceeds Anthropic's context limit."""954955956class AnthropicAuthenticationError(957    anthropic.AuthenticationError, ModelAuthenticationError958):959    """Anthropic authentication error classified as a LangChain model error."""960961962class AnthropicPermissionDeniedError(963    anthropic.PermissionDeniedError, ModelPermissionDeniedError964):965    """Anthropic permission error classified as a LangChain model error."""966967968class AnthropicInvalidRequestError(anthropic.BadRequestError, ModelInvalidRequestError):969    """Anthropic bad-request error classified as a LangChain model error."""970971972class AnthropicModelNotFoundError(anthropic.NotFoundError, ModelNotFoundError):973    """Anthropic not-found error classified as a LangChain model error."""974975976class AnthropicRateLimitError(anthropic.RateLimitError, ModelRateLimitError):977    """Anthropic rate-limit error classified as a LangChain model error."""978979980class AnthropicAPIError(anthropic.InternalServerError, ModelAPIError):981    """Anthropic server error classified as a LangChain model error."""982983984class AnthropicOverloadedError(anthropic.OverloadedError, ModelAPIError):985    """Anthropic overloaded error (HTTP 529) classified as a LangChain model error."""986987988class AnthropicConnectionError(anthropic.APIConnectionError, ModelConnectionError):989    """Anthropic connection error classified as a LangChain model error."""990991992class AnthropicTimeoutError(anthropic.APITimeoutError, ModelTimeoutError):993    """Anthropic timeout error classified as a LangChain model error."""994995996def _raise_if_authentication_error(e: TypeError) -> None:997    """Re-raise anthropic SDK's missing-credentials `TypeError` with guidance."""998    if "Could not resolve authentication method" in str(e):999        msg = (1000            "Anthropic authentication failed: no API key or authorization "1001            "credentials were provided. Set the ANTHROPIC_API_KEY environment "1002            "variable, pass api_key=... to ChatAnthropic, or provide "1003            'credentials via default_headers={"Authorization": ...}. If you '1004            "are routing through the LangSmith gateway, set LANGSMITH_GATEWAY "1005            "and LANGSMITH_GATEWAY_API_KEY."1006        )1007        raise TypeError(msg) from e100810091010def _handle_anthropic_bad_request(e: anthropic.BadRequestError) -> None:1011    """Handle Anthropic BadRequestError."""1012    if "prompt is too long" in e.message:1013        raise AnthropicContextOverflowError(1014            message=e.message, response=e.response, body=e.body1015        ) from e1016    if ("messages: at least one message is required") in e.message:1017        message = "Received only system message(s). "1018        warnings.warn(message, stacklevel=2)1019    raise AnthropicInvalidRequestError(1020        message=e.message, response=e.response, body=e.body1021    ) from e102210231024def _handle_anthropic_api_error(e: anthropic.APIError) -> None:1025    """Re-raise an Anthropic SDK error as its LangChain-classified equivalent."""1026    if isinstance(e, anthropic.AuthenticationError):1027        raise AnthropicAuthenticationError(1028            message=e.message, response=e.response, body=e.body1029        ) from e1030    if isinstance(e, anthropic.PermissionDeniedError):1031        raise AnthropicPermissionDeniedError(1032            message=e.message, response=e.response, body=e.body1033        ) from e1034    if isinstance(e, anthropic.NotFoundError):1035        raise AnthropicModelNotFoundError(1036            message=e.message, response=e.response, body=e.body1037        ) from e1038    if isinstance(e, anthropic.RateLimitError):1039        raise AnthropicRateLimitError(1040            message=e.message, response=e.response, body=e.body1041        ) from e1042    if isinstance(e, anthropic.OverloadedError):1043        raise AnthropicOverloadedError(1044            message=e.message, response=e.response, body=e.body1045        ) from e1046    if isinstance(e, anthropic.InternalServerError):1047        raise AnthropicAPIError(1048            message=e.message, response=e.response, body=e.body1049        ) from e1050    # `APITimeoutError` subclasses `APIConnectionError`, so check it first.1051    if isinstance(e, anthropic.APITimeoutError):1052        raise AnthropicTimeoutError(e.request) from e1053    if isinstance(e, anthropic.APIConnectionError):1054        raise AnthropicConnectionError(message=e.message, request=e.request) from e1055    raise105610571058class ChatAnthropic(BaseChatModel):1059    """Anthropic (Claude) chat models.10601061    See the [LangChain docs for `ChatAnthropic`](https://docs.langchain.com/oss/python/integrations/chat/anthropic)1062    for tutorials, feature walkthroughs, and examples.10631064    See the [Claude Platform docs](https://platform.claude.com/docs/en/about-claude/models/overview)1065    for a list of the latest models, their capabilities, and pricing.10661067    Example:1068        ```python1069        # pip install -U langchain-anthropic1070        # export ANTHROPIC_API_KEY="your-api-key"10711072        from langchain_anthropic import ChatAnthropic10731074        model = ChatAnthropic(1075            model="claude-sonnet-4-5-20250929",1076            # temperature=,1077            # max_tokens=,1078            # timeout=,1079            # max_retries=,1080            # base_url="...",1081            # Refer to API reference for full list of parameters1082        )1083        ```10841085    Note:1086        Any param which is not explicitly supported will be passed directly to1087        [`Anthropic.messages.create(...)`](https://platform.claude.com/docs/en/api/python/messages/create)1088        each time to the model is invoked.1089    """10901091    model_config = ConfigDict(1092        populate_by_name=True,1093    )10941095    model: str = Field(alias="model_name")1096    """Model name to use."""10971098    max_tokens: int | None = Field(default=None, alias="max_tokens_to_sample")1099    """Denotes the number of tokens to predict per generation.11001101    If not specified, this is set dynamically using the model's `max_output_tokens`1102    from its model profile.11031104    See docs on [model profiles](https://docs.langchain.com/oss/python/langchain/models#model-profiles)1105    for more information.1106    """11071108    temperature: float | None = None1109    """A non-negative float that tunes the degree of randomness in generation."""11101111    top_k: int | None = None1112    """Number of most likely tokens to consider at each step."""11131114    top_p: float | None = None1115    """Total probability mass of tokens to consider at each step."""11161117    default_request_timeout: float | None = Field(None, alias="timeout")1118    """Timeout for requests to Claude API."""11191120    # sdk default = 2: https://github.com/anthropics/anthropic-sdk-python?tab=readme-ov-file#retries1121    max_retries: int = 21122    """Number of retries allowed for requests sent to the Claude API."""11231124    stop_sequences: list[str] | None = Field(None, alias="stop")1125    """Default stop sequences."""11261127    anthropic_api_url: str | None = Field(default=None, alias="base_url")1128    """Base URL for API requests. Only specify if using a proxy or service emulator.11291130    If a value isn't passed in, will attempt to read the value first from1131    `ANTHROPIC_API_URL` and if that is not set, `ANTHROPIC_BASE_URL`.11321133    If `LANGSMITH_GATEWAY` is set, it is used as a fallback after those env vars.1134    """11351136    anthropic_api_key: SecretStr = Field(default=SecretStr(""), alias="api_key")1137    """Automatically read from env var `ANTHROPIC_API_KEY` if not provided.11381139    If `LANGSMITH_GATEWAY` is enabled and the base URL points at the gateway,1140    `LANGSMITH_GATEWAY_API_KEY` is used instead.1141    """11421143    anthropic_proxy: str | None = Field(1144        default_factory=from_env("ANTHROPIC_PROXY", default=None)1145    )1146    """Proxy to use for the Anthropic clients, will be used for every API call.11471148    If not provided, will attempt to read from the `ANTHROPIC_PROXY` environment1149    variable.1150    """11511152    default_headers: Mapping[str, str] | None = None1153    """Headers to pass to the Anthropic clients, will be used for every API call."""11541155    betas: list[str] | None = None1156    """List of beta features to enable. If specified, invocations will be routed1157    through `client.beta.messages.create`.11581159    Example: `#!python betas=["token-efficient-tools-2025-02-19"]`1160    """1161    # Can also be passed in w/ model_kwargs, but having it as a param makes better devx1162    #1163    # Precedence order:1164    # 1. Call-time kwargs (e.g., llm.invoke(..., betas=[...]))1165    # 2. model_kwargs (e.g., ChatAnthropic(model_kwargs={"betas": [...]}))1166    # 3. Direct parameter (e.g., ChatAnthropic(betas=[...]))11671168    model_kwargs: dict[str, Any] = Field(default_factory=dict)11691170    streaming: bool = False1171    """Whether to use streaming or not."""11721173    stream_usage: bool = True1174    """Whether to include usage metadata in streaming output.11751176    If `True`, additional message chunks will be generated during the stream including1177    usage metadata.1178    """11791180    thinking: dict[str, Any] | None = Field(default=None)1181    """Parameters for Claude reasoning.11821183    Examples:11841185    - `#!python {"type": "enabled", "budget_tokens": 10_000}` (pre-4.7 models)1186    - `#!python {"type": "adaptive"}` (Opus 4.6+, Opus 5, Sonnet 5)1187    - `#!python {"type": "adaptive", "display": "summarized"}` (Opus 4.7+,1188      Opus 5, Sonnet 5)1189    - `#!python {"type": "disabled"}` (Opus 5 and Sonnet 5, where adaptive1190      thinking is on by default)11911192    !!! note "Claude Opus 4.7+, Opus 5, and Sonnet 5"11931194        `budget_tokens` is removed on these models  use `{"type": "adaptive"}`1195        with `output_config.effort` to control reasoning effort. The default1196        `display` is `"omitted"`; set it to `"summarized"` to receive1197        summarized reasoning in the response. On Opus 5, disabled thinking is1198        supported only at `"high"` effort or below.1199    """12001201    output_config: dict[str, Any] | None = None1202    """Configuration options for the model's output.12031204    Supports the following keys:12051206    - `effort`: Controls how many tokens Claude uses when responding.1207      One of `"max"`, `"xhigh"`, `"high"`, `"medium"`, or `"low"`.1208    - `format`: Structured output format configuration (typically set via1209      `with_structured_output`).1210    - `task_budget`: Advisory token budget for an agentic loop (beta).1211      E.g., `#!python {"type": "tokens", "total": 128_000}`.12121213    Example:12141215    .. code-block:: python12161217        ChatAnthropic(1218            model="claude-opus-4-7",1219            output_config={1220                "effort": "xhigh",1221                "task_budget": {"type": "tokens", "total": 128_000},1222            },1223        )12241225    See Anthropic docs on1226    [extended output](https://platform.claude.com/docs/en/api/go/beta/messages/create).1227    """12281229    reasoning_effort: Literal["max", "xhigh", "high", "medium", "low"] | None = Field(1230        default=None,1231        alias="effort",1232    )1233    """Reasoning effort.12341235    Configures `output_config.effort`. If `thinking` isn't set explicitly,1236    defaults it to `{"type": "adaptive", "display": "summarized"}`. Can also1237    be passed at call time (for example,1238    `model.invoke(..., reasoning_effort="high")`).12391240    !!! note "`effort` alias"12411242        `effort` is also accepted as an alias for this field, at both1243        construction and call time. If both `effort` and `reasoning_effort` are1244        set, `effort` wins (Pydantic's alias-resolution precedence).12451246    !!! note12471248        Setting `reasoning_effort` to `'high'` produces exactly the same behavior1249        as omitting the parameter altogether.12501251    Example: `reasoning_effort="medium"`1252    """12531254    mcp_servers: list[dict[str, Any]] | None = None1255    """List of MCP servers to use for the request.12561257    Example: `#!python mcp_servers=[{"type": "url", "url": "https://mcp.example.com/mcp",1258    "name": "example-mcp"}]`1259    """12601261    context_management: dict[str, Any] | None = None1262    """Configuration for1263    [context management](https://platform.claude.com/docs/en/build-with-claude/context-editing).1264    """12651266    container: dict[str, Any] | str | None = None1267    """Code execution container for the request.12681269    Either a container ID from a previous response, or a dict of container1270    parameters  notably1271    [skills](https://platform.claude.com/docs/en/build-with-claude/skills-guide)1272    to load into the container. Skills require a1273    [code execution](https://docs.langchain.com/oss/python/integrations/chat/anthropic#code-execution)1274    tool to be bound.12751276    ```python1277    model = ChatAnthropic(1278        model="claude-opus-5",1279        container={1280            "skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}]1281        },1282    ).bind_tools([{"type": "code_execution_20260521", "name": "code_execution"}])1283    ```12841285    Can also be passed at call time, which overrides the value set here.1286    """12871288    reuse_last_container: bool | None = None1289    """Automatically reuse container from most recent response (code execution).12901291    When using the built-in1292    [code execution tool](https://docs.langchain.com/oss/python/integrations/chat/anthropic#code-execution),1293    model responses will include container metadata. Set `reuse_last_container=True`1294    to automatically reuse the container from the most recent response for subsequent1295    invocations.1296    """12971298    inference_geo: str | None = None1299    """Controls where model inference runs. See Anthropic's1300    [data residency](https://platform.claude.com/docs/en/build-with-claude/data-residency)1301    docs for more information.1302    """13031304    user_profile_id: str | None = None1305    """User profile ID to attribute the request to.13061307    Use when acting on behalf of a party other than your organization. Setting this1308    automatically enables the required `user-profiles` beta, routing the request1309    through `client.beta.messages.create`.13101311    Can also be passed at call time, which overrides the value set here (for example,1312    `model.invoke(..., user_profile_id="uprof_...")`).1313    """13141315    @property1316    def effort(self) -> Literal["max", "xhigh", "high", "medium", "low"] | None:1317        """Alias for `reasoning_effort`."""1318        return self.reasoning_effort13191320    @property1321    def _llm_type(self) -> str:1322        """Return type of chat model."""1323        return "anthropic-chat"13241325    @property1326    def _uses_gateway(self) -> bool:1327        """Whether requests are routed through the LangSmith gateway."""1328        return self.anthropic_api_key.get_secret_value().startswith("lsv2_")13291330    @property1331    def lc_secrets(self) -> dict[str, str]:1332        """Return a mapping of secret keys to environment variables."""1333        return {1334            "anthropic_api_key": "ANTHROPIC_API_KEY",1335            "mcp_servers": "ANTHROPIC_MCP_SERVERS",1336        }13371338    @classmethod1339    def is_lc_serializable(cls) -> bool:1340        """Whether the class is serializable in langchain."""1341        return True13421343    @classmethod1344    def get_lc_namespace(cls) -> list[str]:1345        """Get the namespace of the LangChain object.13461347        Returns:1348            `["langchain", "chat_models", "anthropic"]`1349        """1350        return ["langchain", "chat_models", "anthropic"]13511352    @property1353    def _identifying_params(self) -> dict[str, Any]:1354        """Get the identifying parameters."""1355        return {1356            "model": self.model,1357            "max_tokens": self.max_tokens,1358            "temperature": self.temperature,1359            "top_k": self.top_k,1360            "top_p": self.top_p,1361            "model_kwargs": self.model_kwargs,1362            "streaming": self.streaming,1363            "max_retries": self.max_retries,1364            "default_request_timeout": self.default_request_timeout,1365            "thinking": self.thinking,1366            "output_config": self.output_config,1367        }13681369    def _get_ls_params(1370        self,1371        stop: list[str] | None = None,1372        **kwargs: Any,1373    ) -> LangSmithParams:1374        """Get standard params for tracing."""1375        params = self._get_invocation_params(stop=stop, **kwargs)1376        ls_params = LangSmithParams(1377            ls_provider="anthropic",1378            ls_model_name=params.get("model", self.model),1379            ls_model_type="chat",1380            ls_temperature=params.get("temperature", self.temperature),1381        )1382        if ls_max_tokens := params.get("max_tokens", self.max_tokens):1383            ls_params["ls_max_tokens"] = ls_max_tokens1384        if ls_stop := stop or params.get("stop", None):1385            ls_params["ls_stop"] = ls_stop1386        return ls_params13871388    @model_validator(mode="before")1389    @classmethod1390    def set_default_max_tokens(cls, values: dict[str, Any]) -> Any:1391        """Set default `max_tokens` from model profile with fallback."""1392        if values.get("max_tokens") is None:1393            model = values.get("model") or values.get("model_name")1394            profile = _get_default_model_profile(model) if model else {}1395            values["max_tokens"] = profile.get(1396                "max_output_tokens", _FALLBACK_MAX_OUTPUT_TOKENS1397            )1398        return values13991400    @model_validator(mode="before")1401    @classmethod1402    def build_extra(cls, values: dict) -> Any:1403        """Build model kwargs."""1404        all_required_field_names = get_pydantic_field_names(cls)1405        return _build_model_kwargs(values, all_required_field_names)14061407    @model_validator(mode="after")1408    def _set_anthropic_version(self) -> Self:1409        """Set package version in metadata."""1410        self._add_version("langchain-anthropic", __version__)1411        return self14121413    @model_validator(mode="before")1414    @classmethod1415    def _resolve_gateway(cls, values: Any) -> Any:1416        """Resolve the base URL and API key, applying LangSmith gateway settings.14171418        An explicit ``base_url``/``api_key`` always wins. Otherwise the base URL1419        falls back to ``ANTHROPIC_API_URL``/``ANTHROPIC_BASE_URL``, then the1420        LangSmith gateway, then the Anthropic default. The gateway key is1421        preferred only when the base URL came from the gateway; for any other1422        endpoint the provider key wins, and the gateway key is a candidate only1423        when the gateway is enabled.1424        """1425        if isinstance(values, dict):1426            _apply_gateway_config(1427                values,1428                cls,1429                base_url_field="anthropic_api_url",1430                api_key_field="anthropic_api_key",1431                provider_path="anthropic",1432                base_url_env=["ANTHROPIC_API_URL", "ANTHROPIC_BASE_URL"],1433                api_key_env="ANTHROPIC_API_KEY",1434                default_base_url="https://api.anthropic.com",1435            )1436        return values14371438    def _resolve_model_profile(self) -> ModelProfile | None:1439        profile = _get_default_model_profile(self.model) or None1440        if profile is not None and self.betas and "context-1m-2025-08-07" in self.betas:1441            profile["max_input_tokens"] = 1_000_0001442        return profile14431444    @cached_property1445    def _client_params(self) -> dict[str, Any]:1446        # Merge User-Agent with user-provided headers (user headers take precedence)1447        default_headers = {"User-Agent": _USER_AGENT}1448        if self.default_headers:1449            default_headers.update(self.default_headers)14501451        client_params: dict[str, Any] = {1452            "api_key": self.anthropic_api_key.get_secret_value(),1453            "base_url": self.anthropic_api_url,1454            "max_retries": self.max_retries,1455            "default_headers": default_headers,1456        }1457        # value <= 0 indicates the param should be ignored. None is a meaningful value1458        # for Anthropic client and treated differently than not specifying the param at1459        # all.1460        if self.default_request_timeout is None or self.default_request_timeout > 0:1461            client_params["timeout"] = self.default_request_timeout14621463        return client_params14641465    @cached_property1466    def _client(self) -> anthropic.Client:1467        client_params = self._client_params1468        http_client_params = {"base_url": client_params["base_url"]}1469        if "timeout" in client_params:1470            http_client_params["timeout"] = client_params["timeout"]1471        if self.anthropic_proxy:1472            http_client_params["anthropic_proxy"] = self.anthropic_proxy1473        http_client = _get_default_httpx_client(**http_client_params)1474        params = {1475            **client_params,1476            "http_client": http_client,1477        }1478        return anthropic.Client(**params)14791480    @cached_property1481    def _async_client(self) -> anthropic.AsyncClient:1482        client_params = self._client_params1483        http_client_params = {"base_url": client_params["base_url"]}1484        if "timeout" in client_params:1485            http_client_params["timeout"] = client_params["timeout"]1486        if self.anthropic_proxy:1487            http_client_params["anthropic_proxy"] = self.anthropic_proxy1488        http_client = _get_default_async_httpx_client(**http_client_params)1489        params = {1490            **client_params,1491            "http_client": http_client,1492        }1493        return anthropic.AsyncClient(**params)14941495    def _assert_valid_model_configuration(self, kwargs: Mapping[str, Any]) -> None:1496        """Validate resolved request configuration against model-specific invariants."""1497        request_config = {**self.model_kwargs, **kwargs}1498        thinking = request_config.get("thinking")1499        if self.thinking is not None:1500            thinking = self.thinking15011502        output_config = dict(self.output_config or {})1503        if self.reasoning_effort:1504            output_config["effort"] = self.reasoning_effort1505        request_output_config = request_config.get("output_config")1506        if isinstance(request_output_config, dict):1507            output_config.update(request_output_config)1508        effort = request_config.get("effort")1509        if effort is None:1510            effort = request_config.get("reasoning_effort")1511        if effort:1512            output_config["effort"] = effort15131514        is_fable_model = self.model.startswith("claude-fable-5")1515        if is_fable_model:1516            top_k = request_config.get("top_k", self.top_k)1517            top_p = request_config.get("top_p", self.top_p)1518            temperature = request_config.get("temperature", self.temperature)1519            if top_k is not None:1520                msg = f"`top_k` is not supported for {self.model}."1521                raise ValueError(msg)1522            if top_p is not None and top_p != 1:1523                msg = (1524                    f"`top_p` is not supported for {self.model} at non-default values."1525                )1526                raise ValueError(msg)1527            if temperature is not None and temperature != 1:1528                msg = (1529                    f"`temperature` is not supported for {self.model} at "1530                    "non-default values."1531                )1532                raise ValueError(msg)1533            if isinstance(thinking, Mapping) and thinking.get("type") == "disabled":1534                msg = (1535                    '`thinking={"type": "disabled"}` is not supported for '1536                    f"{self.model}; omit `thinking` to use adaptive thinking."1537                )1538                raise ValueError(msg)15391540        if (1541            (self.model.startswith("claude-opus-5") or is_fable_model)1542            and isinstance(thinking, Mapping)1543            and thinking.get("type") == "enabled"1544        ):1545            msg = (1546                '`thinking={"type": "enabled", "budget_tokens": ...}` is not '1547                f"supported for {self.model}; use adaptive thinking and "1548                "`output_config.effort` instead."1549            )1550            raise ValueError(msg)15511552        if (1553            self.model.startswith("claude-opus-5")1554            and isinstance(thinking, Mapping)1555            and thinking.get("type") == "disabled"1556            and output_config.get("effort") in {"xhigh", "max"}1557        ):1558            msg = (1559                '`thinking={"type": "disabled"}` is not supported for '1560                f"{self.model} with "1561                f"`output_config.effort={output_config['effort']!r}`; use adaptive "1562                "thinking, omit `thinking`, or set effort to `high` or below."1563            )1564            raise ValueError(msg)15651566    def _get_request_payload(1567        self,1568        input_: LanguageModelInput,1569        *,1570        stop: list[str] | None = None,1571        **kwargs: Any,1572    ) -> dict:1573        """Get the request payload for the Anthropic API."""1574        self._assert_valid_model_configuration(kwargs)1575        messages = self._convert_input(input_).to_messages()15761577        for idx, message in enumerate(messages):1578            # Translate v1 content1579            if (1580                isinstance(message, AIMessage)1581                and message.response_metadata.get("output_version") == "v1"1582            ):1583                tcs: list[types.ToolCall] = [1584                    {1585                        "type": "tool_call",1586                        "name": tool_call["name"],1587                        "args": tool_call["args"],1588                        "id": tool_call.get("id"),1589                    }1590                    for tool_call in message.tool_calls1591                ]1592                messages[idx] = message.model_copy(1593                    update={1594                        "content": _convert_from_v1_to_anthropic(1595                            cast(list[types.ContentBlock], message.content),1596                            tcs,1597                            message.response_metadata.get("model_provider"),1598                        )1599                    }1600                )16011602        system, formatted_messages = _format_messages(messages)16031604        # Only the direct Anthropic API accepts top-level `cache_control`.1605        # Subclasses that route through other transports (e.g. Bedrock) expand1606        # `cache_control` kwargs into block-level breakpoints, the only form1607        # those transports accept.1608        if not _is_direct_anthropic_llm_type(getattr(self, "_llm_type", None)):1609            cache_control = kwargs.pop("cache_control", None)1610            # Empty `formatted_messages` has nothing to attach a breakpoint to;1611            # skip silently. The warning below is reserved for the surprising1612            # case where messages exist but every candidate block is ineligible.1613            if cache_control and formatted_messages:1614                code_execution_tool_ids = _collect_code_execution_tool_ids(1615                    formatted_messages1616                )1617                applied = _apply_cache_control_to_last_eligible_block(1618                    formatted_messages, cache_control, code_execution_tool_ids1619                )1620                if not applied:1621                    warnings.warn(1622                        "`cache_control` kwarg was dropped: no eligible "1623                        "content block found (all candidates are "1624                        "`code_execution`-related, which Anthropic forbids "1625                        "breakpoints on).",1626                        UserWarning,1627                        stacklevel=2,1628                    )16291630        payload = {1631            "model": self.model,1632            "max_tokens": self.max_tokens,1633            "messages": formatted_messages,1634            "temperature": self.temperature,1635            "top_k": self.top_k,1636            "top_p": self.top_p,1637            "stop_sequences": stop or self.stop_sequences,1638            "betas": self.betas,1639            "context_management": self.context_management,1640            "mcp_servers": self.mcp_servers,1641            "container": self.container,1642            "user_profile_id": self.user_profile_id,1643            "system": system,1644            **self.model_kwargs,1645            **kwargs,1646        }1647        # Captured before `self.thinking` is applied below, so a call-time1648        # `thinking` kwarg counts as "explicitly set" too.1649        thinking_explicitly_set = "thinking" in payload or self.thinking is not None1650        if self.thinking is not None:1651            payload["thinking"] = self.thinking1652        if self.inference_geo is not None:1653            payload["inference_geo"] = self.inference_geo1654        if self.model.startswith("claude-fable-5"):1655            payload.pop("temperature", None)1656            payload.pop("top_k", None)1657            payload.pop("top_p", None)16581659        # Handle output_config and effort parameter1660        # Priority: kwarg `effort`/`reasoning_effort` > kwarg `output_config`1661        # > self.reasoning_effort > self.output_config1662        output_config: dict[str, Any] = {}1663        if self.output_config:1664            output_config.update(self.output_config)1665        reasoning_effort_applied = False1666        if self.reasoning_effort:1667            output_config["effort"] = self.reasoning_effort1668            reasoning_effort_applied = True1669        payload_oc = payload.get("output_config")1670        if isinstance(payload_oc, dict):1671            output_config.update(payload_oc)16721673        # Neither `reasoning_effort` nor its `effort` alias are Anthropic API1674        # fields. Pop them so they never leak through as top-level keys.1675        effort_kwarg = payload.pop("effort", None)1676        reasoning_effort_kwarg = payload.pop("reasoning_effort", None)1677        # `effort` wins if both are set at call time, matching the1678        # construction-time alias-resolution precedence (`Field(alias="effort")`).1679        reasoning_effort_override = (1680            effort_kwarg if effort_kwarg is not None else reasoning_effort_kwarg1681        )1682        if reasoning_effort_override:1683            output_config["effort"] = reasoning_effort_override1684            reasoning_effort_applied = True16851686        if output_config:1687            payload["output_config"] = output_config16881689        # Default adaptive thinking when `reasoning_effort` is set, unless the1690        # caller explicitly provided `thinking`. Gated on `xhigh` support: only1691        # Opus 4.7+/Sonnet 5 accept the adaptive+summarized `thinking` shape 1692        # sending it to an older model (e.g. Opus 4.5, 4.6) is rejected by the1693        # API with "adaptive thinking is not supported on this model".1694        if (1695            reasoning_effort_applied1696            and not thinking_explicitly_set1697            and "xhigh" in _reasoning_effort_levels(self.profile)1698        ):1699            payload["thinking"] = {"type": "adaptive", "display": "summarized"}17001701        if "response_format" in payload:1702            # response_format present when using agents.create_agent's ProviderStrategy1703            # ---1704            # ProviderStrategy converts to OpenAI-style format, which passes kwargs to1705            # ChatAnthropic, ending up in our payload1706            response_format = payload.pop("response_format")1707            if (1708                isinstance(response_format, dict)1709                and response_format.get("type") == "json_schema"1710                and "schema" in response_format.get("json_schema", {})1711            ):1712                response_format = cast(dict, response_format["json_schema"]["schema"])1713            # Convert OpenAI-style response_format to Anthropic's output_config.format1714            output_config = payload.setdefault("output_config", {})1715            output_config["format"] = _convert_to_anthropic_output_config_format(1716                response_format1717            )17181719        # Handle deprecated output_format parameter for backward compatibility1720        if "output_format" in payload:1721            warnings.warn(1722                "The 'output_format' parameter is deprecated and will be removed in "1723                "langchain-anthropic 2.0.0. Use 'output_config={\"format\": ...}' "1724                "instead.",1725                DeprecationWarning,1726                stacklevel=2,1727            )1728            output_config = payload.setdefault("output_config", {})1729            output_config["format"] = payload.pop("output_format")17301731        container = payload.get("container")17321733        if self.reuse_last_container and not _container_id(container):1734            # Reuse the container from the most recent response (code execution)1735            for message in reversed(messages):1736                if (1737                    isinstance(message, AIMessage)1738                    and isinstance(1739                        last_container := message.response_metadata.get("container"),1740                        dict,1741                    )1742                    and (container_id := last_container.get("id"))1743                ):1744                    payload["container"] = (1745                        {**container, "id": container_id}1746                        if isinstance(container, dict)1747                        else container_id1748                    )1749                    break17501751        if (1752            isinstance(container, dict)1753            and container.get("skills")1754            and not any(1755                isinstance(tool, dict)1756                and str(tool.get("type", "")).startswith("code_execution")1757                for tool in (payload.get("tools") or [])1758            )1759        ):1760            warnings.warn(1761                "Skills require a code execution tool to be bound, e.g. "1762                '`bind_tools([{"type": "code_execution_20260521", '1763                '"name": "code_execution"}])`.',1764                UserWarning,1765                stacklevel=2,1766            )17671768        # Note: Beta headers are no longer required for structured outputs1769        # (output_config.format or strict tool use) as they are now generally available1770        if "tools" in payload and isinstance(payload["tools"], list):1771            # Auto-append required betas for specific tool types and input_examples1772            has_input_examples = False1773            for tool in payload["tools"]:1774                if isinstance(tool, dict):1775                    tool_type = tool.get("type")1776                    if tool_type and tool_type in _TOOL_TYPE_TO_BETA:1777                        required_beta = _TOOL_TYPE_TO_BETA[tool_type]1778                        if payload["betas"]:1779                            if required_beta not in payload["betas"]:1780                                payload["betas"] = [1781                                    *payload["betas"],1782                                    required_beta,1783                                ]1784                        else:1785                            payload["betas"] = [required_beta]1786                    # Check for input_examples1787                    if tool.get("input_examples"):1788                        has_input_examples = True17891790            # Auto-append header for input_examples1791            if has_input_examples:1792                required_beta = "advanced-tool-use-2025-11-20"1793                if payload["betas"]:1794                    if required_beta not in payload["betas"]:1795                        payload["betas"] = [*payload["betas"], required_beta]1796                else:1797                    payload["betas"] = [required_beta]17981799        # Auto-append required beta for mcp_servers1800        if payload.get("mcp_servers"):1801            required_beta = "mcp-client-2025-11-20"1802            if payload["betas"]:1803                # Append to existing betas if not already present1804                if required_beta not in payload["betas"]:1805                    payload["betas"] = [*payload["betas"], required_beta]1806            else:1807                payload["betas"] = [required_beta]18081809        # Auto-append required beta for task_budget1810        resolved_oc = payload.get("output_config")1811        if isinstance(resolved_oc, dict) and resolved_oc.get("task_budget"):1812            required_beta = "task-budgets-2026-03-13"1813            if payload.get("betas"):1814                if required_beta not in payload["betas"]:1815                    payload["betas"] = [*payload["betas"], required_beta]1816            else:1817                payload["betas"] = [required_beta]18181819        # Auto-append required beta for the `updates` thinking display mode1820        thinking = payload.get("thinking")1821        if isinstance(thinking, dict) and thinking.get("display") == "updates":1822            required_beta = "thinking-display-updates-2026-08-18"1823            if payload.get("betas"):1824                if required_beta not in payload["betas"]:1825                    payload["betas"] = [*payload["betas"], required_beta]1826            else:1827                payload["betas"] = [required_beta]18281829        # Auto-append required beta for user_profile_id1830        if payload.get("user_profile_id"):1831            required_beta = "user-profiles-2026-03-24"1832            if payload.get("betas"):1833                if required_beta not in payload["betas"]:1834                    payload["betas"] = [*payload["betas"], required_beta]1835            else:1836                payload["betas"] = [required_beta]18371838        return _route_unsupported_sampling_params(1839            {k: v for k, v in payload.items() if v is not None}1840        )18411842    def _create(self, payload: dict) -> Any:1843        try:1844            if "betas" in payload:1845                return self._client.beta.messages.with_raw_response.create(**payload)1846            return self._client.messages.with_raw_response.create(**payload)1847        except TypeError as e:1848            _raise_if_authentication_error(e)1849            raise18501851    async def _acreate(self, payload: dict) -> Any:1852        try:1853            if "betas" in payload:1854                return await self._async_client.beta.messages.with_raw_response.create(1855                    **payload1856                )1857            return await self._async_client.messages.with_raw_response.create(**payload)1858        except TypeError as e:1859            _raise_if_authentication_error(e)1860            raise18611862    def _stream(1863        self,1864        messages: list[BaseMessage],1865        stop: list[str] | None = None,1866        run_manager: CallbackManagerForLLMRun | None = None,1867        *,1868        stream_usage: bool | None = None,1869        **kwargs: Any,1870    ) -> Iterator[ChatGenerationChunk]:1871        if stream_usage is None:1872            stream_usage = self.stream_usage1873        kwargs["stream"] = True1874        payload = self._get_request_payload(messages, stop=stop, **kwargs)1875        try:1876            raw_response = self._create(payload)1877            base_generation_info: dict[str, Any] = {}1878            if self._uses_gateway:1879                _add_gateway_metadata(base_generation_info, raw_response)1880            stream = raw_response.parse()1881            coerce_content_to_string = (1882                not _tools_in_params(payload)1883                and not _documents_in_params(payload)1884                and not _thinking_in_params(payload)1885                and not _compact_in_params(payload)1886            )1887            block_start_event = None1888            is_first_chunk = True1889            for event in stream:1890                msg, block_start_event = self._make_message_chunk_from_anthropic_event(1891                    event,1892                    stream_usage=stream_usage,1893                    coerce_content_to_string=coerce_content_to_string,1894                    block_start_event=block_start_event,1895                )1896                if msg is not None:1897                    chunk = ChatGenerationChunk(1898                        message=msg,1899                        generation_info=base_generation_info1900                        if is_first_chunk1901                        else None,1902                    )1903                    is_first_chunk = False1904                    if run_manager and isinstance(msg.content, str):1905                        run_manager.on_llm_new_token(msg.content, chunk=chunk)1906                    yield chunk1907        except anthropic.BadRequestError as e:1908            _handle_anthropic_bad_request(e)1909        except anthropic.APIError as e:1910            _handle_anthropic_api_error(e)19111912    async def _astream(1913        self,1914        messages: list[BaseMessage],1915        stop: list[str] | None = None,1916        run_manager: AsyncCallbackManagerForLLMRun | None = None,1917        *,1918        stream_usage: bool | None = None,1919        **kwargs: Any,1920    ) -> AsyncIterator[ChatGenerationChunk]:1921        if stream_usage is None:1922            stream_usage = self.stream_usage1923        kwargs["stream"] = True1924        payload = self._get_request_payload(messages, stop=stop, **kwargs)1925        try:1926            raw_response = await self._acreate(payload)1927            base_generation_info: dict[str, Any] = {}1928            if self._uses_gateway:1929                _add_gateway_metadata(base_generation_info, raw_response)1930            stream = await _aparse(raw_response)1931            coerce_content_to_string = (1932                not _tools_in_params(payload)1933                and not _documents_in_params(payload)1934                and not _thinking_in_params(payload)1935                and not _compact_in_params(payload)1936            )1937            block_start_event = None1938            is_first_chunk = True1939            async for event in stream:1940                msg, block_start_event = self._make_message_chunk_from_anthropic_event(1941                    event,1942                    stream_usage=stream_usage,1943                    coerce_content_to_string=coerce_content_to_string,1944                    block_start_event=block_start_event,1945                )1946                if msg is not None:1947                    chunk = ChatGenerationChunk(1948                        message=msg,1949                        generation_info=base_generation_info1950                        if is_first_chunk1951                        else None,1952                    )1953                    is_first_chunk = False1954                    if run_manager and isinstance(msg.content, str):1955                        await run_manager.on_llm_new_token(msg.content, chunk=chunk)1956                    yield chunk1957        except anthropic.BadRequestError as e:1958            _handle_anthropic_bad_request(e)1959        except anthropic.APIError as e:1960            _handle_anthropic_api_error(e)19611962    def _make_message_chunk_from_anthropic_event(1963        self,1964        event: anthropic.types.RawMessageStreamEvent,1965        *,1966        stream_usage: bool = True,1967        coerce_content_to_string: bool,1968        block_start_event: anthropic.types.RawMessageStreamEvent | None = None,1969    ) -> tuple[AIMessageChunk | None, anthropic.types.RawMessageStreamEvent | None]:1970        """Convert Anthropic streaming event to `AIMessageChunk`.19711972        Args:1973            event: Raw streaming event from Anthropic SDK1974            stream_usage: Whether to include usage metadata in the output chunks.1975            coerce_content_to_string: Whether to convert structured content to plain1976                text strings.19771978                When `True`, only text content is preserved; when `False`, structured1979                content like tool calls and citations are maintained.1980            block_start_event: Previous content block start event, used for tracking1981                tool use blocks and maintaining context across related events.19821983        Returns:1984            Tuple with1985                - `AIMessageChunk`: Converted message chunk with appropriate content and1986                    metadata, or `None` if the event doesn't produce a chunk1987                - `RawMessageStreamEvent`: Updated `block_start_event` for tracking1988                    content blocks across sequential events, or `None` if not applicable19891990        Note:1991            Not all Anthropic events result in message chunks. Events like internal1992            state changes return `None` for the message chunk while potentially1993            updating the `block_start_event` for context tracking.1994        """1995        message_chunk: AIMessageChunk | None = None1996        # Reference: Anthropic SDK streaming implementation1997        # https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/lib/streaming/_messages.py  # noqa: E5011998        if event.type == "message_start" and stream_usage:1999            # Capture model name, but don't include usage_metadata yet2000            # as it will be properly reported in message_delta with complete info

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.