libs/partners/anthropic/langchain_anthropic/chat_models.py PYTHON 2,406 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 2,406.
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, cast1516import anthropic17from langchain_core.callbacks import (18    AsyncCallbackManagerForLLMRun,19    CallbackManagerForLLMRun,20)21from langchain_core.exceptions import ContextOverflowError, OutputParserException22from langchain_core.language_models import (23    LanguageModelInput,24    ModelProfile,25    ModelProfileRegistry,26)27from langchain_core.language_models.chat_models import BaseChatModel, LangSmithParams28from langchain_core.messages import (29    AIMessage,30    AIMessageChunk,31    BaseMessage,32    HumanMessage,33    SystemMessage,34    ToolCall,35    ToolMessage,36    is_data_content_block,37)38from langchain_core.messages import content as types39from langchain_core.messages.ai import InputTokenDetails, UsageMetadata40from langchain_core.messages.tool import tool_call_chunk as create_tool_call_chunk41from langchain_core.output_parsers import (42    JsonOutputKeyToolsParser,43    JsonOutputParser,44    PydanticOutputParser,45    PydanticToolsParser,46)47from langchain_core.output_parsers.base import OutputParserLike48from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult49from langchain_core.runnables import Runnable, RunnableMap, RunnablePassthrough50from langchain_core.tools import BaseTool51from langchain_core.utils import from_env, get_pydantic_field_names, secret_from_env52from langchain_core.utils.function_calling import (53    convert_to_json_schema,54    convert_to_openai_tool,55)56from langchain_core.utils.pydantic import is_basemodel_subclass57from langchain_core.utils.utils import _build_model_kwargs58from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator59from typing_extensions import NotRequired, Self, TypedDict6061from langchain_anthropic import __version__62from langchain_anthropic._client_utils import (63    _get_default_async_httpx_client,64    _get_default_httpx_client,65)66from langchain_anthropic._compat import _convert_from_v1_to_anthropic67from langchain_anthropic.data._profiles import _PROFILES68from langchain_anthropic.output_parsers import extract_tool_calls6970_message_type_lookups = {71    "human": "user",72    "ai": "assistant",73    "AIMessageChunk": "assistant",74    "HumanMessageChunk": "user",75}7677_MODEL_PROFILES = cast(ModelProfileRegistry, _PROFILES)7879_USER_AGENT: Final[str] = f"langchain-anthropic/{__version__}"808182def _get_default_model_profile(model_name: str) -> ModelProfile:83    """Get the default profile for a model.8485    Args:86        model_name: The model identifier.8788    Returns:89        The model profile dictionary, or an empty dict if not found.90    """91    default = _MODEL_PROFILES.get(model_name)92    if default:93        return default.copy()94    return {}959697_FALLBACK_MAX_OUTPUT_TOKENS: Final[int] = 40969899100class AnthropicTool(TypedDict):101    """Anthropic tool definition for custom (user-defined) tools.102103    Custom tools use `name` and `input_schema` fields to define the tool's104    interface. These are converted from LangChain tool formats (functions, Pydantic105    models, `BaseTool` objects) via `convert_to_anthropic_tool`.106    """107108    name: str109110    input_schema: dict[str, Any]111112    description: NotRequired[str]113114    strict: NotRequired[bool]115116    cache_control: NotRequired[dict[str, str]]117118    defer_loading: NotRequired[bool]119120    input_examples: NotRequired[list[dict[str, Any]]]121122    allowed_callers: NotRequired[list[str]]123124125# ---------------------------------------------------------------------------126# Built-in Tool Support127# ---------------------------------------------------------------------------128# When Anthropic releases new built-in tools, two places may need updating:129#130# 1. _TOOL_TYPE_TO_BETA (below) - Add mapping if the tool requires a beta header.131#     Not all tools need this; only add if the API requires a beta header.132#133# 2. _is_builtin_tool() - Add the tool type prefix to _BUILTIN_TOOL_PREFIXES.134#     This ensures the tool dict is passed through to the API unchanged (instead135#     of being converted via convert_to_anthropic_tool, which may fail).136# ---------------------------------------------------------------------------137138_TOOL_TYPE_TO_BETA: dict[str, str] = {139    "web_fetch_20250910": "web-fetch-2025-09-10",140    "code_execution_20250522": "code-execution-2025-05-22",141    "code_execution_20250825": "code-execution-2025-08-25",142    "mcp_toolset": "mcp-client-2025-11-20",143    "memory_20250818": "context-management-2025-06-27",144    "computer_20250124": "computer-use-2025-01-24",145    "computer_20251124": "computer-use-2025-11-24",146    "tool_search_tool_regex_20251119": "advanced-tool-use-2025-11-20",147    "tool_search_tool_bm25_20251119": "advanced-tool-use-2025-11-20",148}149"""Mapping of tool type to required beta header.150151Some tool types require specific beta headers to be enabled.152"""153154_BUILTIN_TOOL_PREFIXES = [155    "text_editor_",156    "computer_",157    "bash_",158    "web_search_",159    "web_fetch_",160    "code_execution_",161    "mcp_toolset",162    "memory_",163    "tool_search_",164    "advisor_",165]166167_ANTHROPIC_EXTRA_FIELDS: set[str] = {168    "allowed_callers",169    "cache_control",170    "defer_loading",171    "eager_input_streaming",172    "input_examples",173}174"""Valid Anthropic-specific extra fields"""175176177def _is_builtin_tool(tool: Any) -> bool:178    """Check if a tool is a built-in (server-side) Anthropic tool.179180    `tool` must be a `dict` and have a `type` key starting with one of the known181    built-in tool prefixes.182183    [Claude docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview)184    """185    if not isinstance(tool, dict):186        return False187188    tool_type = tool.get("type")189    if not tool_type or not isinstance(tool_type, str):190        return False191192    return any(tool_type.startswith(prefix) for prefix in _BUILTIN_TOOL_PREFIXES)193194195def _format_image(url: str) -> dict:196    """Convert part["image_url"]["url"] strings (OpenAI format) to Anthropic format.197198    {199        "type": "base64",200        "media_type": "image/jpeg",201        "data": "/9j/4AAQSkZJRg...",202    }203204    Or205206    {207        "type": "url",208        "url": "https://example.com/image.jpg",209    }210    """211    # Base64 encoded image212    base64_regex = r"^data:(?P<media_type>image/.+);base64,(?P<data>.+)$"213    base64_match = re.match(base64_regex, url)214215    if base64_match:216        return {217            "type": "base64",218            "media_type": base64_match.group("media_type"),219            "data": base64_match.group("data"),220        }221222    # Url223    url_regex = r"^https?://.*$"224    url_match = re.match(url_regex, url)225226    if url_match:227        return {228            "type": "url",229            "url": url,230        }231232    msg = (233        "Malformed url parameter."234        " Must be either an image URL (https://example.com/image.jpg)"235        " or base64 encoded string (data:image/png;base64,'/9j/4AAQSk'...)"236    )237    raise ValueError(238        msg,239    )240241242_TOOL_CALL_ID_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")243"""Anthropic requires `tool_use`/`tool_result` IDs to match this pattern."""244245246def _normalize_tool_call_id(tool_call_id: str | None) -> str | None:247    """Map a tool-call ID to an Anthropic-compatible form if needed.248249    Anthropic rejects `tool_use`/`tool_result` IDs that don't match250    `^[a-zA-Z0-9_-]+$`. IDs minted by other providers can violate this when a251    thread is replayed across providers (e.g. Fireworks/Kimi emits252    `functions.write_todos:0`, whose `.` and `:` are invalid). Valid IDs are253    returned unchanged; invalid ones are hashed deterministically so that a254    rewritten `tool_use.id` and its paired `tool_use_id` resolve to the same255    value, both within a request and across turns.256257    Empty and `None` IDs are passed through unchanged so that a genuinely258    malformed request surfaces as a clear error from Anthropic rather than259    being masked by a synthesized ID.260261    Args:262        tool_call_id: The tool-call ID to normalize.263264    Returns:265        The original ID if it is empty, `None`, or already valid; otherwise a266            deterministic Anthropic-compatible replacement.267    """268    if not tool_call_id or _TOOL_CALL_ID_PATTERN.match(tool_call_id):269        return tool_call_id270    digest = hashlib.sha256(tool_call_id.encode()).hexdigest()271    return f"toolu_{digest[:24]}"272273274def _normalize_block_tool_use_id(block: dict) -> dict:275    """Return `block` with its `tool_use_id` normalized, if it carries one.276277    Mirrors `_normalize_tool_call_id` for `tool_result`-style content blocks so278    that a `tool_use_id` arriving pre-structured (e.g. on a `ToolMessage` whose279    content is already a list of `tool_result` blocks) stays consistent with its280    paired, normalized `tool_use.id`. A no-op for already-valid IDs.281    """282    if "tool_use_id" in block:283        return {**block, "tool_use_id": _normalize_tool_call_id(block["tool_use_id"])}284    return block285286287def _merge_messages(288    messages: Sequence[BaseMessage],289) -> list[SystemMessage | AIMessage | HumanMessage]:290    """Merge runs of human/tool messages into single human messages with content blocks."""  # noqa: E501291    merged: list = []292    for curr in messages:293        if isinstance(curr, ToolMessage):294            if (295                isinstance(curr.content, list)296                and curr.content297                and all(298                    isinstance(block, dict) and block.get("type") == "tool_result"299                    for block in curr.content300                )301            ):302                curr = HumanMessage(curr.content)  # type: ignore[misc]303            else:304                tool_content = curr.content305                cache_ctrl = None306                # Extract cache_control from content blocks and hoist it307                # to the tool_result level.  Anthropic's API does not308                # support cache_control on tool_result content sub-blocks.309                if isinstance(tool_content, list):310                    cleaned = []311                    for block in tool_content:312                        if isinstance(block, dict) and "cache_control" in block:313                            cache_ctrl = block["cache_control"]314                            block = {315                                k: v for k, v in block.items() if k != "cache_control"316                            }317                        cleaned.append(block)318                    tool_content = cleaned319                tool_result: dict = {320                    "type": "tool_result",321                    "content": tool_content,322                    "tool_use_id": _normalize_tool_call_id(curr.tool_call_id),323                    "is_error": curr.status == "error",324                }325                if cache_ctrl:326                    tool_result["cache_control"] = cache_ctrl327                curr = HumanMessage(  # type: ignore[misc]328                    [tool_result],329                )330        last = merged[-1] if merged else None331        if any(332            all(isinstance(m, c) for m in (curr, last))333            for c in (SystemMessage, HumanMessage)334        ):335            if isinstance(cast("BaseMessage", last).content, str):336                new_content: list = [337                    {"type": "text", "text": cast("BaseMessage", last).content},338                ]339            else:340                new_content = copy.copy(cast("list", cast("BaseMessage", last).content))341            if isinstance(curr.content, str):342                new_content.append({"type": "text", "text": curr.content})343            else:344                new_content.extend(curr.content)345            merged[-1] = curr.model_copy(update={"content": new_content})346        else:347            merged.append(curr)348    return merged349350351def _format_data_content_block(block: dict) -> dict:352    """Format standard data content block to format expected by Anthropic."""353    if block["type"] == "image":354        if "url" in block:355            if block["url"].startswith("data:"):356                # Data URI357                formatted_block = {358                    "type": "image",359                    "source": _format_image(block["url"]),360                }361            else:362                formatted_block = {363                    "type": "image",364                    "source": {"type": "url", "url": block["url"]},365                }366        elif "base64" in block or block.get("source_type") == "base64":367            formatted_block = {368                "type": "image",369                "source": {370                    "type": "base64",371                    "media_type": block["mime_type"],372                    "data": block.get("base64") or block.get("data", ""),373                },374            }375        elif "file_id" in block:376            formatted_block = {377                "type": "image",378                "source": {379                    "type": "file",380                    "file_id": block["file_id"],381                },382            }383        elif block.get("source_type") == "id":384            formatted_block = {385                "type": "image",386                "source": {387                    "type": "file",388                    "file_id": block["id"],389                },390            }391        else:392            msg = (393                "Anthropic only supports 'url', 'base64', or 'id' keys for image "394                "content blocks."395            )396            raise ValueError(397                msg,398            )399400    elif block["type"] == "file":401        if "url" in block:402            formatted_block = {403                "type": "document",404                "source": {405                    "type": "url",406                    "url": block["url"],407                },408            }409        elif "base64" in block or block.get("source_type") == "base64":410            formatted_block = {411                "type": "document",412                "source": {413                    "type": "base64",414                    "media_type": block.get("mime_type") or "application/pdf",415                    "data": block.get("base64") or block.get("data", ""),416                },417            }418        elif block.get("source_type") == "text":419            formatted_block = {420                "type": "document",421                "source": {422                    "type": "text",423                    "media_type": block.get("mime_type") or "text/plain",424                    "data": block["text"],425                },426            }427        elif "file_id" in block:428            formatted_block = {429                "type": "document",430                "source": {431                    "type": "file",432                    "file_id": block["file_id"],433                },434            }435        elif block.get("source_type") == "id":436            formatted_block = {437                "type": "document",438                "source": {439                    "type": "file",440                    "file_id": block["id"],441                },442            }443        else:444            msg = (445                "Anthropic only supports 'url', 'base64', or 'id' keys for file "446                "content blocks."447            )448            raise ValueError(msg)449450    elif block["type"] == "text-plain":451        formatted_block = {452            "type": "document",453            "source": {454                "type": "text",455                "media_type": block.get("mime_type") or "text/plain",456                "data": block["text"],457            },458        }459460    else:461        msg = f"Block of type {block['type']} is not supported."462        raise ValueError(msg)463464    if formatted_block:465        for key in ["cache_control", "citations", "title", "context"]:466            if key in block:467                formatted_block[key] = block[key]468            elif (metadata := block.get("extras")) and key in metadata:469                formatted_block[key] = metadata[key]470            elif (metadata := block.get("metadata")) and key in metadata:471                # Backward compat472                formatted_block[key] = metadata[key]473474    return formatted_block475476477def _format_messages(478    messages: Sequence[BaseMessage],479) -> tuple[str | list[dict] | None, list[dict]]:480    """Format messages for Anthropic's API."""481    system: str | list[dict] | None = None482    formatted_messages: list[dict] = []483    merged_messages = _merge_messages(messages)484    for _i, message in enumerate(merged_messages):485        if message.type == "system":486            if system is not None:487                msg = "Received multiple non-consecutive system messages."488                raise ValueError(msg)489            if isinstance(message.content, list):490                system = [491                    (492                        block493                        if isinstance(block, dict)494                        else {"type": "text", "text": block}495                    )496                    for block in message.content497                ]498            else:499                system = message.content500            continue501502        role = _message_type_lookups[message.type]503        content: str | list504505        if not isinstance(message.content, str):506            # parse as dict507            if not isinstance(message.content, list):508                msg = "Anthropic message content must be str or list of dicts"509                raise ValueError(510                    msg,511                )512513            # populate content514            content = []515            for block in message.content:516                if isinstance(block, str):517                    content.append({"type": "text", "text": block})518                elif isinstance(block, dict):519                    if "type" not in block:520                        msg = "Dict content block must have a type key"521                        raise ValueError(msg)522                    if block["type"] in ("reasoning", "function_call") and (523                        not isinstance(message, AIMessage)524                        or message.response_metadata.get("model_provider")525                        != "anthropic"526                    ):527                        continue528                    if block["type"] == "image_url":529                        # convert format530                        source = _format_image(block["image_url"]["url"])531                        content.append({"type": "image", "source": source})532                    elif is_data_content_block(block):533                        content.append(_format_data_content_block(block))534                    elif block["type"] == "tool_use":535                        # If a tool_call with the same id as a tool_use content block536                        # exists, the tool_call is preferred.537                        if (538                            isinstance(message, AIMessage)539                            and (block["id"] in [tc["id"] for tc in message.tool_calls])540                            and not block.get("caller")541                        ):542                            overlapping = [543                                tc544                                for tc in message.tool_calls545                                if tc["id"] == block["id"]546                            ]547                            content.extend(548                                _lc_tool_calls_to_anthropic_tool_use_blocks(549                                    overlapping,550                                ),551                            )552                        else:553                            if tool_input := block.get("input"):554                                args = tool_input555                            elif "partial_json" in block:556                                try:557                                    args = json.loads(block["partial_json"] or "{}")558                                except json.JSONDecodeError:559                                    args = {}560                            else:561                                args = {}562                            tool_use_block = _AnthropicToolUse(563                                type="tool_use",564                                name=block["name"],565                                input=args,566                                id=cast("str", _normalize_tool_call_id(block["id"])),567                            )568                            if caller := block.get("caller"):569                                tool_use_block["caller"] = caller570                            content.append(tool_use_block)571                    elif block["type"] in ("server_tool_use", "mcp_tool_use"):572                        formatted_block = {573                            k: v574                            for k, v in block.items()575                            if k576                            in (577                                "type",578                                "id",579                                "input",580                                "name",581                                "server_name",  # for mcp_tool_use582                                "cache_control",583                            )584                        }585                        # Attempt to parse streamed output586                        if block.get("input") == {} and "partial_json" in block:587                            try:588                                input_ = json.loads(block["partial_json"])589                                if input_:590                                    formatted_block["input"] = input_591                            except json.JSONDecodeError:592                                pass593                        content.append(formatted_block)594                    elif block["type"] == "text":595                        text = block.get("text", "")596                        # Only add non-empty strings for now as empty ones are not597                        # accepted.598                        # https://github.com/anthropics/anthropic-sdk-python/issues/461599                        if text.strip():600                            formatted_block = {601                                k: v602                                for k, v in block.items()603                                if k in ("type", "text", "cache_control", "citations")604                            }605                            # Clean up citations to remove null file_id fields606                            if formatted_block.get("citations"):607                                cleaned_citations = []608                                for citation in formatted_block["citations"]:609                                    cleaned_citation = {610                                        k: v611                                        for k, v in citation.items()612                                        if not (k == "file_id" and v is None)613                                    }614                                    cleaned_citations.append(cleaned_citation)615                                formatted_block["citations"] = cleaned_citations616                            content.append(formatted_block)617                    elif block["type"] == "thinking":618                        content.append(619                            {620                                k: v621                                for k, v in block.items()622                                if k623                                in ("type", "thinking", "cache_control", "signature")624                            },625                        )626                    elif block["type"] == "redacted_thinking":627                        content.append(628                            {629                                k: v630                                for k, v in block.items()631                                if k in ("type", "cache_control", "data")632                            },633                        )634                    elif (635                        block["type"] == "tool_result"636                        and isinstance(block.get("content"), list)637                        and any(638                            isinstance(item, dict)639                            and item.get("type") == "tool_reference"640                            for item in block["content"]641                        )642                    ):643                        # Tool search results with tool_reference blocks644                        content.append(645                            _normalize_block_tool_use_id(646                                {647                                    k: v648                                    for k, v in block.items()649                                    if k650                                    in (651                                        "type",652                                        "content",653                                        "tool_use_id",654                                        "cache_control",655                                    )656                                },657                            ),658                        )659                    elif block["type"] == "tool_result":660                        # Regular tool results that need content formatting661                        tool_content = _format_messages(662                            [HumanMessage(block["content"])],663                        )[1][0]["content"]664                        content.append(665                            _normalize_block_tool_use_id(666                                {**block, "content": tool_content},667                            ),668                        )669                    elif block["type"] in (670                        "code_execution_tool_result",671                        "bash_code_execution_tool_result",672                        "text_editor_code_execution_tool_result",673                        "mcp_tool_result",674                        "web_search_tool_result",675                        "web_fetch_tool_result",676                    ):677                        content.append(678                            _normalize_block_tool_use_id(679                                {680                                    k: v681                                    for k, v in block.items()682                                    if k683                                    in (684                                        "type",685                                        "content",686                                        "tool_use_id",687                                        "is_error",  # for mcp_tool_result688                                        "cache_control",689                                        "retrieved_at",  # for web_fetch_tool_result690                                    )691                                },692                            ),693                        )694                    else:695                        content.append(block)696                else:697                    msg = (698                        f"Content blocks must be str or dict, instead was: "699                        f"{type(block)}"700                    )701                    raise ValueError(702                        msg,703                    )704        else:705            content = message.content706707        # Ensure all tool_calls have a tool_use content block708        if isinstance(message, AIMessage) and message.tool_calls:709            content = content or []710            content = (711                [{"type": "text", "text": message.content}]712                if isinstance(content, str) and content713                else content714            )715            tool_use_ids = [716                cast("dict", block)["id"]717                for block in content718                if cast("dict", block)["type"] == "tool_use"719            ]720            # `tool_use_ids` are already normalized via the branches above, so721            # compare against the normalized tool-call ID to avoid emitting a722            # duplicate `tool_use` block when the original ID was rewritten.723            missing_tool_calls = [724                tc725                for tc in message.tool_calls726                if _normalize_tool_call_id(tc["id"]) not in tool_use_ids727            ]728            cast("list", content).extend(729                _lc_tool_calls_to_anthropic_tool_use_blocks(missing_tool_calls),730            )731732        if role == "assistant" and _i == len(merged_messages) - 1:733            if isinstance(content, str):734                content = content.rstrip()735            elif (736                isinstance(content, list)737                and content738                and isinstance(content[-1], dict)739                and content[-1].get("type") == "text"740            ):741                content[-1]["text"] = content[-1]["text"].rstrip()742743        if not content and role == "assistant" and _i < len(merged_messages) - 1:744            # anthropic.BadRequestError: Error code: 400: all messages must have745            # non-empty content except for the optional final assistant message746            continue747        formatted_messages.append({"role": role, "content": content})748    return system, formatted_messages749750751def _collect_code_execution_tool_ids(formatted_messages: list[dict]) -> set[str]:752    """Collect `tool_use` IDs that were called by `code_execution`.753754    These blocks cannot have `cache_control` applied per Anthropic API755    requirements.756    """757    code_execution_tool_ids: set[str] = set()758759    for message in formatted_messages:760        if message.get("role") != "assistant":761            continue762        content = message.get("content", [])763        if not isinstance(content, list):764            continue765        for block in content:766            if not isinstance(block, dict):767                continue768            if block.get("type") != "tool_use":769                continue770            caller = block.get("caller")771            if isinstance(caller, dict):772                caller_type = caller.get("type", "")773                if caller_type.startswith("code_execution"):774                    tool_id = block.get("id")775                    if tool_id:776                        code_execution_tool_ids.add(tool_id)777778    return code_execution_tool_ids779780781def _is_code_execution_related_block(782    block: dict,783    code_execution_tool_ids: set[str],784) -> bool:785    """Return whether a content block is related to `code_execution`.786787    Returns `True` for blocks that should NOT have `cache_control` applied.788    """789    if not isinstance(block, dict):790        return False791792    block_type = block.get("type")793794    if block_type == "tool_use":795        caller = block.get("caller")796        if isinstance(caller, dict):797            caller_type = caller.get("type", "")798            if caller_type.startswith("code_execution"):799                return True800801    if block_type == "tool_result":802        tool_use_id = block.get("tool_use_id")803        if tool_use_id and tool_use_id in code_execution_tool_ids:804            return True805806    return False807808809def _is_direct_anthropic_llm_type(llm_type: object) -> bool:810    """Return whether an `_llm_type` reaches Claude via the direct Anthropic API.811812    Only the direct API accepts the top-level `cache_control` request param.813    Subclasses that route through other transports (Bedrock, future backends)814    override `_llm_type` and must expand `cache_control` kwargs into815    block-level breakpoints instead.816817    Non-string `_llm_type` values return `False` rather than raising, so a818    misbehaving subclass falls through to the safer non-direct branch.819    """820    return llm_type == "anthropic-chat"821822823def _apply_cache_control_to_last_eligible_block(824    formatted_messages: list[dict],825    cache_control: Any,826    code_execution_tool_ids: set[str],827) -> bool:828    """Place `cache_control` on the last block eligible for a breakpoint.829830    Walks messages newest-to-oldest and, within each, blocks newest-to-oldest,831    skipping `code_execution`-related blocks (Anthropic rejects breakpoints832    there). String message content is promoted to a single text block so the833    breakpoint can be attached.834835    Returns:836        `True` if a breakpoint was applied, `False` if every candidate was837            `code_execution`-related (caller should warn and drop the kwarg).838    """839    for formatted_message in reversed(formatted_messages):840        content = formatted_message.get("content")841        if isinstance(content, list) and content:842            for block in reversed(content):843                if not isinstance(block, dict):844                    continue845                if _is_code_execution_related_block(block, code_execution_tool_ids):846                    continue847                block["cache_control"] = cache_control848                return True849        elif isinstance(content, str):850            formatted_message["content"] = [851                {852                    "type": "text",853                    "text": content,854                    "cache_control": cache_control,855                }856            ]857            return True858    return False859860861class AnthropicContextOverflowError(anthropic.BadRequestError, ContextOverflowError):862    """BadRequestError raised when input exceeds Anthropic's context limit."""863864865def _handle_anthropic_bad_request(e: anthropic.BadRequestError) -> None:866    """Handle Anthropic BadRequestError."""867    if "prompt is too long" in e.message:868        raise AnthropicContextOverflowError(869            message=e.message, response=e.response, body=e.body870        ) from e871    if ("messages: at least one message is required") in e.message:872        message = "Received only system message(s). "873        warnings.warn(message, stacklevel=2)874        raise e875    raise876877878class ChatAnthropic(BaseChatModel):879    """Anthropic (Claude) chat models.880881    See the [LangChain docs for `ChatAnthropic`](https://docs.langchain.com/oss/python/integrations/chat/anthropic)882    for tutorials, feature walkthroughs, and examples.883884    See the [Claude Platform docs](https://platform.claude.com/docs/en/about-claude/models/overview)885    for a list of the latest models, their capabilities, and pricing.886887    Example:888        ```python889        # pip install -U langchain-anthropic890        # export ANTHROPIC_API_KEY="your-api-key"891892        from langchain_anthropic import ChatAnthropic893894        model = ChatAnthropic(895            model="claude-sonnet-4-5-20250929",896            # temperature=,897            # max_tokens=,898            # timeout=,899            # max_retries=,900            # base_url="...",901            # Refer to API reference for full list of parameters902        )903        ```904905    Note:906        Any param which is not explicitly supported will be passed directly to907        [`Anthropic.messages.create(...)`](https://platform.claude.com/docs/en/api/python/messages/create)908        each time to the model is invoked.909    """910911    model_config = ConfigDict(912        populate_by_name=True,913    )914915    model: str = Field(alias="model_name")916    """Model name to use."""917918    max_tokens: int | None = Field(default=None, alias="max_tokens_to_sample")919    """Denotes the number of tokens to predict per generation.920921    If not specified, this is set dynamically using the model's `max_output_tokens`922    from its model profile.923924    See docs on [model profiles](https://docs.langchain.com/oss/python/langchain/models#model-profiles)925    for more information.926    """927928    temperature: float | None = None929    """A non-negative float that tunes the degree of randomness in generation."""930931    top_k: int | None = None932    """Number of most likely tokens to consider at each step."""933934    top_p: float | None = None935    """Total probability mass of tokens to consider at each step."""936937    default_request_timeout: float | None = Field(None, alias="timeout")938    """Timeout for requests to Claude API."""939940    # sdk default = 2: https://github.com/anthropics/anthropic-sdk-python?tab=readme-ov-file#retries941    max_retries: int = 2942    """Number of retries allowed for requests sent to the Claude API."""943944    stop_sequences: list[str] | None = Field(None, alias="stop")945    """Default stop sequences."""946947    anthropic_api_url: str | None = Field(948        alias="base_url",949        default_factory=from_env(950            ["ANTHROPIC_API_URL", "ANTHROPIC_BASE_URL"],951            default="https://api.anthropic.com",952        ),953    )954    """Base URL for API requests. Only specify if using a proxy or service emulator.955956    If a value isn't passed in, will attempt to read the value first from957    `ANTHROPIC_API_URL` and if that is not set, `ANTHROPIC_BASE_URL`.958    """959960    anthropic_api_key: SecretStr = Field(961        alias="api_key",962        default_factory=secret_from_env("ANTHROPIC_API_KEY", default=""),963    )964    """Automatically read from env var `ANTHROPIC_API_KEY` if not provided."""965966    anthropic_proxy: str | None = Field(967        default_factory=from_env("ANTHROPIC_PROXY", default=None)968    )969    """Proxy to use for the Anthropic clients, will be used for every API call.970971    If not provided, will attempt to read from the `ANTHROPIC_PROXY` environment972    variable.973    """974975    default_headers: Mapping[str, str] | None = None976    """Headers to pass to the Anthropic clients, will be used for every API call."""977978    betas: list[str] | None = None979    """List of beta features to enable. If specified, invocations will be routed980    through `client.beta.messages.create`.981982    Example: `#!python betas=["token-efficient-tools-2025-02-19"]`983    """984    # Can also be passed in w/ model_kwargs, but having it as a param makes better devx985    #986    # Precedence order:987    # 1. Call-time kwargs (e.g., llm.invoke(..., betas=[...]))988    # 2. model_kwargs (e.g., ChatAnthropic(model_kwargs={"betas": [...]}))989    # 3. Direct parameter (e.g., ChatAnthropic(betas=[...]))990991    model_kwargs: dict[str, Any] = Field(default_factory=dict)992993    streaming: bool = False994    """Whether to use streaming or not."""995996    stream_usage: bool = True997    """Whether to include usage metadata in streaming output.998999    If `True`, additional message chunks will be generated during the stream including1000    usage metadata.1001    """10021003    thinking: dict[str, Any] | None = Field(default=None)1004    """Parameters for Claude reasoning.10051006    Examples:10071008    - `#!python {"type": "enabled", "budget_tokens": 10_000}` (pre-4.7 models)1009    - `#!python {"type": "adaptive"}` (Opus 4.6+, Sonnet 5)1010    - `#!python {"type": "adaptive", "display": "summarized"}` (Opus 4.7+, Sonnet 5)1011    - `#!python {"type": "disabled"}` (Sonnet 5, where adaptive thinking is1012      on by default)10131014    !!! note "Claude Opus 4.7+ and Sonnet 5"10151016        `budget_tokens` is removed on these models  use `{"type": "adaptive"}`1017        with `output_config.effort` to control reasoning effort. The default1018        `display` is `"omitted"`; set it to `"summarized"` to receive1019        summarized reasoning in the response.1020    """10211022    output_config: dict[str, Any] | None = None1023    """Configuration options for the model's output.10241025    Supports the following keys:10261027    - `effort`: Controls how many tokens Claude uses when responding.1028      One of `"max"`, `"xhigh"`, `"high"`, `"medium"`, or `"low"`.1029    - `format`: Structured output format configuration (typically set via1030      `with_structured_output`).1031    - `task_budget`: Advisory token budget for an agentic loop (beta).1032      E.g., `#!python {"type": "tokens", "total": 128_000}`.10331034    Example:10351036    .. code-block:: python10371038        ChatAnthropic(1039            model="claude-opus-4-7",1040            output_config={1041                "effort": "xhigh",1042                "task_budget": {"type": "tokens", "total": 128_000},1043            },1044        )10451046    See Anthropic docs on1047    [extended output](https://platform.claude.com/docs/en/api/go/beta/messages/create).1048    """10491050    effort: Literal["max", "xhigh", "high", "medium", "low"] | None = None1051    """Convenience shorthand for `output_config.effort`.10521053    When set, this value takes precedence over any `effort` key inside1054    `output_config`.10551056    Example: `effort="medium"`10571058    !!! note10591060        Setting `effort` to `'high'` produces exactly the same behavior as omitting the1061        parameter altogether.1062    """10631064    mcp_servers: list[dict[str, Any]] | None = None1065    """List of MCP servers to use for the request.10661067    Example: `#!python mcp_servers=[{"type": "url", "url": "https://mcp.example.com/mcp",1068    "name": "example-mcp"}]`1069    """10701071    context_management: dict[str, Any] | None = None1072    """Configuration for1073    [context management](https://platform.claude.com/docs/en/build-with-claude/context-editing).1074    """10751076    reuse_last_container: bool | None = None1077    """Automatically reuse container from most recent response (code execution).10781079    When using the built-in1080    [code execution tool](https://docs.langchain.com/oss/python/integrations/chat/anthropic#code-execution),1081    model responses will include container metadata. Set `reuse_last_container=True`1082    to automatically reuse the container from the most recent response for subsequent1083    invocations.1084    """10851086    inference_geo: str | None = None1087    """Controls where model inference runs. See Anthropic's1088    [data residency](https://platform.claude.com/docs/en/build-with-claude/data-residency)1089    docs for more information.1090    """10911092    @property1093    def _llm_type(self) -> str:1094        """Return type of chat model."""1095        return "anthropic-chat"10961097    @property1098    def lc_secrets(self) -> dict[str, str]:1099        """Return a mapping of secret keys to environment variables."""1100        return {1101            "anthropic_api_key": "ANTHROPIC_API_KEY",1102            "mcp_servers": "ANTHROPIC_MCP_SERVERS",1103        }11041105    @classmethod1106    def is_lc_serializable(cls) -> bool:1107        """Whether the class is serializable in langchain."""1108        return True11091110    @classmethod1111    def get_lc_namespace(cls) -> list[str]:1112        """Get the namespace of the LangChain object.11131114        Returns:1115            `["langchain", "chat_models", "anthropic"]`1116        """1117        return ["langchain", "chat_models", "anthropic"]11181119    @property1120    def _identifying_params(self) -> dict[str, Any]:1121        """Get the identifying parameters."""1122        return {1123            "model": self.model,1124            "max_tokens": self.max_tokens,1125            "temperature": self.temperature,1126            "top_k": self.top_k,1127            "top_p": self.top_p,1128            "model_kwargs": self.model_kwargs,1129            "streaming": self.streaming,1130            "max_retries": self.max_retries,1131            "default_request_timeout": self.default_request_timeout,1132            "thinking": self.thinking,1133            "output_config": self.output_config,1134        }11351136    def _get_ls_params(1137        self,1138        stop: list[str] | None = None,1139        **kwargs: Any,1140    ) -> LangSmithParams:1141        """Get standard params for tracing."""1142        params = self._get_invocation_params(stop=stop, **kwargs)1143        ls_params = LangSmithParams(1144            ls_provider="anthropic",1145            ls_model_name=params.get("model", self.model),1146            ls_model_type="chat",1147            ls_temperature=params.get("temperature", self.temperature),1148        )1149        if ls_max_tokens := params.get("max_tokens", self.max_tokens):1150            ls_params["ls_max_tokens"] = ls_max_tokens1151        if ls_stop := stop or params.get("stop", None):1152            ls_params["ls_stop"] = ls_stop1153        return ls_params11541155    @model_validator(mode="before")1156    @classmethod1157    def set_default_max_tokens(cls, values: dict[str, Any]) -> Any:1158        """Set default `max_tokens` from model profile with fallback."""1159        if values.get("max_tokens") is None:1160            model = values.get("model") or values.get("model_name")1161            profile = _get_default_model_profile(model) if model else {}1162            values["max_tokens"] = profile.get(1163                "max_output_tokens", _FALLBACK_MAX_OUTPUT_TOKENS1164            )1165        return values11661167    @model_validator(mode="before")1168    @classmethod1169    def build_extra(cls, values: dict) -> Any:1170        """Build model kwargs."""1171        all_required_field_names = get_pydantic_field_names(cls)1172        return _build_model_kwargs(values, all_required_field_names)11731174    @model_validator(mode="after")1175    def _set_anthropic_version(self) -> Self:1176        """Set package version in metadata."""1177        self._add_version("langchain-anthropic", __version__)1178        return self11791180    def _resolve_model_profile(self) -> ModelProfile | None:1181        profile = _get_default_model_profile(self.model) or None1182        if profile is not None and self.betas and "context-1m-2025-08-07" in self.betas:1183            profile["max_input_tokens"] = 1_000_0001184        return profile11851186    @cached_property1187    def _client_params(self) -> dict[str, Any]:1188        # Merge User-Agent with user-provided headers (user headers take precedence)1189        default_headers = {"User-Agent": _USER_AGENT}1190        if self.default_headers:1191            default_headers.update(self.default_headers)11921193        client_params: dict[str, Any] = {1194            "api_key": self.anthropic_api_key.get_secret_value(),1195            "base_url": self.anthropic_api_url,1196            "max_retries": self.max_retries,1197            "default_headers": default_headers,1198        }1199        # value <= 0 indicates the param should be ignored. None is a meaningful value1200        # for Anthropic client and treated differently than not specifying the param at1201        # all.1202        if self.default_request_timeout is None or self.default_request_timeout > 0:1203            client_params["timeout"] = self.default_request_timeout12041205        return client_params12061207    @cached_property1208    def _client(self) -> anthropic.Client:1209        client_params = self._client_params1210        http_client_params = {"base_url": client_params["base_url"]}1211        if "timeout" in client_params:1212            http_client_params["timeout"] = client_params["timeout"]1213        if self.anthropic_proxy:1214            http_client_params["anthropic_proxy"] = self.anthropic_proxy1215        http_client = _get_default_httpx_client(**http_client_params)1216        params = {1217            **client_params,1218            "http_client": http_client,1219        }1220        return anthropic.Client(**params)12211222    @cached_property1223    def _async_client(self) -> anthropic.AsyncClient:1224        client_params = self._client_params1225        http_client_params = {"base_url": client_params["base_url"]}1226        if "timeout" in client_params:1227            http_client_params["timeout"] = client_params["timeout"]1228        if self.anthropic_proxy:1229            http_client_params["anthropic_proxy"] = self.anthropic_proxy1230        http_client = _get_default_async_httpx_client(**http_client_params)1231        params = {1232            **client_params,1233            "http_client": http_client,1234        }1235        return anthropic.AsyncClient(**params)12361237    def _get_request_payload(1238        self,1239        input_: LanguageModelInput,1240        *,1241        stop: list[str] | None = None,1242        **kwargs: dict,1243    ) -> dict:1244        """Get the request payload for the Anthropic API."""1245        messages = self._convert_input(input_).to_messages()12461247        for idx, message in enumerate(messages):1248            # Translate v1 content1249            if (1250                isinstance(message, AIMessage)1251                and message.response_metadata.get("output_version") == "v1"1252            ):1253                tcs: list[types.ToolCall] = [1254                    {1255                        "type": "tool_call",1256                        "name": tool_call["name"],1257                        "args": tool_call["args"],1258                        "id": tool_call.get("id"),1259                    }1260                    for tool_call in message.tool_calls1261                ]1262                messages[idx] = message.model_copy(1263                    update={1264                        "content": _convert_from_v1_to_anthropic(1265                            cast(list[types.ContentBlock], message.content),1266                            tcs,1267                            message.response_metadata.get("model_provider"),1268                        )1269                    }1270                )12711272        system, formatted_messages = _format_messages(messages)12731274        # Only the direct Anthropic API accepts top-level `cache_control`.1275        # Subclasses that route through other transports (e.g. Bedrock) expand1276        # `cache_control` kwargs into block-level breakpoints, the only form1277        # those transports accept.1278        if not _is_direct_anthropic_llm_type(getattr(self, "_llm_type", None)):1279            cache_control = kwargs.pop("cache_control", None)1280            # Empty `formatted_messages` has nothing to attach a breakpoint to;1281            # skip silently. The warning below is reserved for the surprising1282            # case where messages exist but every candidate block is ineligible.1283            if cache_control and formatted_messages:1284                code_execution_tool_ids = _collect_code_execution_tool_ids(1285                    formatted_messages1286                )1287                applied = _apply_cache_control_to_last_eligible_block(1288                    formatted_messages, cache_control, code_execution_tool_ids1289                )1290                if not applied:1291                    warnings.warn(1292                        "`cache_control` kwarg was dropped: no eligible "1293                        "content block found (all candidates are "1294                        "`code_execution`-related, which Anthropic forbids "1295                        "breakpoints on).",1296                        UserWarning,1297                        stacklevel=2,1298                    )12991300        payload = {1301            "model": self.model,1302            "max_tokens": self.max_tokens,1303            "messages": formatted_messages,1304            "temperature": self.temperature,1305            "top_k": self.top_k,1306            "top_p": self.top_p,1307            "stop_sequences": stop or self.stop_sequences,1308            "betas": self.betas,1309            "context_management": self.context_management,1310            "mcp_servers": self.mcp_servers,1311            "system": system,1312            **self.model_kwargs,1313            **kwargs,1314        }1315        if self.thinking is not None:1316            payload["thinking"] = self.thinking1317        if self.inference_geo is not None:1318            payload["inference_geo"] = self.inference_geo13191320        # Handle output_config and effort parameter1321        # Priority: self.effort > kwargs output_config > self.output_config1322        output_config: dict[str, Any] = {}1323        if self.output_config:1324            output_config.update(self.output_config)1325        payload_oc = payload.get("output_config")1326        if isinstance(payload_oc, dict):1327            output_config.update(payload_oc)13281329        if self.effort:1330            output_config["effort"] = self.effort13311332        if output_config:1333            payload["output_config"] = output_config13341335        if "response_format" in payload:1336            # response_format present when using agents.create_agent's ProviderStrategy1337            # ---1338            # ProviderStrategy converts to OpenAI-style format, which passes kwargs to1339            # ChatAnthropic, ending up in our payload1340            response_format = payload.pop("response_format")1341            if (1342                isinstance(response_format, dict)1343                and response_format.get("type") == "json_schema"1344                and "schema" in response_format.get("json_schema", {})1345            ):1346                response_format = cast(dict, response_format["json_schema"]["schema"])1347            # Convert OpenAI-style response_format to Anthropic's output_config.format1348            output_config = payload.setdefault("output_config", {})1349            output_config["format"] = _convert_to_anthropic_output_config_format(1350                response_format1351            )13521353        # Handle deprecated output_format parameter for backward compatibility1354        if "output_format" in payload:1355            warnings.warn(1356                "The 'output_format' parameter is deprecated and will be removed in "1357                "langchain-anthropic 2.0.0. Use 'output_config={\"format\": ...}' "1358                "instead.",1359                DeprecationWarning,1360                stacklevel=2,1361            )1362            output_config = payload.setdefault("output_config", {})1363            output_config["format"] = payload.pop("output_format")13641365        if self.reuse_last_container:1366            # Check for most recent AIMessage with container set in response_metadata1367            # and set as a top-level param on the request1368            for message in reversed(messages):1369                if (1370                    isinstance(message, AIMessage)1371                    and (container := message.response_metadata.get("container"))1372                    and isinstance(container, dict)1373                    and (container_id := container.get("id"))1374                ):1375                    payload["container"] = container_id1376                    break13771378        # Note: Beta headers are no longer required for structured outputs1379        # (output_config.format or strict tool use) as they are now generally available1380        if "tools" in payload and isinstance(payload["tools"], list):1381            # Auto-append required betas for specific tool types and input_examples1382            has_input_examples = False1383            for tool in payload["tools"]:1384                if isinstance(tool, dict):1385                    tool_type = tool.get("type")1386                    if tool_type and tool_type in _TOOL_TYPE_TO_BETA:1387                        required_beta = _TOOL_TYPE_TO_BETA[tool_type]1388                        if payload["betas"]:1389                            if required_beta not in payload["betas"]:1390                                payload["betas"] = [1391                                    *payload["betas"],1392                                    required_beta,1393                                ]1394                        else:1395                            payload["betas"] = [required_beta]1396                    # Check for input_examples1397                    if tool.get("input_examples"):1398                        has_input_examples = True13991400            # Auto-append header for input_examples1401            if has_input_examples:1402                required_beta = "advanced-tool-use-2025-11-20"1403                if payload["betas"]:1404                    if required_beta not in payload["betas"]:1405                        payload["betas"] = [*payload["betas"], required_beta]1406                else:1407                    payload["betas"] = [required_beta]14081409        # Auto-append required beta for mcp_servers1410        if payload.get("mcp_servers"):1411            required_beta = "mcp-client-2025-11-20"1412            if payload["betas"]:1413                # Append to existing betas if not already present1414                if required_beta not in payload["betas"]:1415                    payload["betas"] = [*payload["betas"], required_beta]1416            else:1417                payload["betas"] = [required_beta]14181419        # Auto-append required beta for task_budget1420        resolved_oc = payload.get("output_config")1421        if isinstance(resolved_oc, dict) and resolved_oc.get("task_budget"):1422            required_beta = "task-budgets-2026-03-13"1423            if payload.get("betas"):1424                if required_beta not in payload["betas"]:1425                    payload["betas"] = [*payload["betas"], required_beta]1426            else:1427                payload["betas"] = [required_beta]14281429        return {k: v for k, v in payload.items() if v is not None}14301431    def _create(self, payload: dict) -> Any:1432        if "betas" in payload:1433            return self._client.beta.messages.create(**payload)1434        return self._client.messages.create(**payload)14351436    async def _acreate(self, payload: dict) -> Any:1437        if "betas" in payload:1438            return await self._async_client.beta.messages.create(**payload)1439        return await self._async_client.messages.create(**payload)14401441    def _stream(1442        self,1443        messages: list[BaseMessage],1444        stop: list[str] | None = None,1445        run_manager: CallbackManagerForLLMRun | None = None,1446        *,1447        stream_usage: bool | None = None,1448        **kwargs: Any,1449    ) -> Iterator[ChatGenerationChunk]:1450        if stream_usage is None:1451            stream_usage = self.stream_usage1452        kwargs["stream"] = True1453        payload = self._get_request_payload(messages, stop=stop, **kwargs)1454        try:1455            stream = self._create(payload)1456            coerce_content_to_string = (1457                not _tools_in_params(payload)1458                and not _documents_in_params(payload)1459                and not _thinking_in_params(payload)1460                and not _compact_in_params(payload)1461            )1462            block_start_event = None1463            for event in stream:1464                msg, block_start_event = self._make_message_chunk_from_anthropic_event(1465                    event,1466                    stream_usage=stream_usage,1467                    coerce_content_to_string=coerce_content_to_string,1468                    block_start_event=block_start_event,1469                )1470                if msg is not None:1471                    chunk = ChatGenerationChunk(message=msg)1472                    if run_manager and isinstance(msg.content, str):1473                        run_manager.on_llm_new_token(msg.content, chunk=chunk)1474                    yield chunk1475        except anthropic.BadRequestError as e:1476            _handle_anthropic_bad_request(e)14771478    async def _astream(1479        self,1480        messages: list[BaseMessage],1481        stop: list[str] | None = None,1482        run_manager: AsyncCallbackManagerForLLMRun | None = None,1483        *,1484        stream_usage: bool | None = None,1485        **kwargs: Any,1486    ) -> AsyncIterator[ChatGenerationChunk]:1487        if stream_usage is None:1488            stream_usage = self.stream_usage1489        kwargs["stream"] = True1490        payload = self._get_request_payload(messages, stop=stop, **kwargs)1491        try:1492            stream = await self._acreate(payload)1493            coerce_content_to_string = (1494                not _tools_in_params(payload)1495                and not _documents_in_params(payload)1496                and not _thinking_in_params(payload)1497                and not _compact_in_params(payload)1498            )1499            block_start_event = None1500            async for event in stream:1501                msg, block_start_event = self._make_message_chunk_from_anthropic_event(1502                    event,1503                    stream_usage=stream_usage,1504                    coerce_content_to_string=coerce_content_to_string,1505                    block_start_event=block_start_event,1506                )1507                if msg is not None:1508                    chunk = ChatGenerationChunk(message=msg)1509                    if run_manager and isinstance(msg.content, str):1510                        await run_manager.on_llm_new_token(msg.content, chunk=chunk)1511                    yield chunk1512        except anthropic.BadRequestError as e:1513            _handle_anthropic_bad_request(e)15141515    def _make_message_chunk_from_anthropic_event(1516        self,1517        event: anthropic.types.RawMessageStreamEvent,1518        *,1519        stream_usage: bool = True,1520        coerce_content_to_string: bool,1521        block_start_event: anthropic.types.RawMessageStreamEvent | None = None,1522    ) -> tuple[AIMessageChunk | None, anthropic.types.RawMessageStreamEvent | None]:1523        """Convert Anthropic streaming event to `AIMessageChunk`.15241525        Args:1526            event: Raw streaming event from Anthropic SDK1527            stream_usage: Whether to include usage metadata in the output chunks.1528            coerce_content_to_string: Whether to convert structured content to plain1529                text strings.15301531                When `True`, only text content is preserved; when `False`, structured1532                content like tool calls and citations are maintained.1533            block_start_event: Previous content block start event, used for tracking1534                tool use blocks and maintaining context across related events.15351536        Returns:1537            Tuple with1538                - `AIMessageChunk`: Converted message chunk with appropriate content and1539                    metadata, or `None` if the event doesn't produce a chunk1540                - `RawMessageStreamEvent`: Updated `block_start_event` for tracking1541                    content blocks across sequential events, or `None` if not applicable15421543        Note:1544            Not all Anthropic events result in message chunks. Events like internal1545            state changes return `None` for the message chunk while potentially1546            updating the `block_start_event` for context tracking.1547        """1548        message_chunk: AIMessageChunk | None = None1549        # Reference: Anthropic SDK streaming implementation1550        # https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/lib/streaming/_messages.py  # noqa: E5011551        if event.type == "message_start" and stream_usage:1552            # Capture model name, but don't include usage_metadata yet1553            # as it will be properly reported in message_delta with complete info1554            if hasattr(event.message, "model"):1555                response_metadata: dict[str, Any] = {"model_name": event.message.model}1556            else:1557                response_metadata = {}15581559            message_chunk = AIMessageChunk(1560                content="" if coerce_content_to_string else [],1561                response_metadata=response_metadata,1562            )15631564        elif (1565            event.type == "content_block_start"1566            and event.content_block is not None1567            and (1568                "tool_result" in event.content_block.type1569                or "tool_use" in event.content_block.type1570                or "document" in event.content_block.type1571                or "redacted_thinking" in event.content_block.type1572            )1573        ):1574            if coerce_content_to_string:1575                warnings.warn("Received unexpected tool content block.", stacklevel=2)15761577            content_block = event.content_block.model_dump()1578            if "caller" in content_block and content_block["caller"] is None:1579                content_block.pop("caller")1580            content_block["index"] = event.index1581            if event.content_block.type == "tool_use":1582                if (1583                    parsed_args := getattr(event.content_block, "input", None)1584                ) and isinstance(parsed_args, dict):1585                    # In some cases parsed args are represented in start event, with no1586                    # following input_json_delta events1587                    args = json.dumps(parsed_args)1588                else:1589                    args = ""1590                tool_call_chunk = create_tool_call_chunk(1591                    index=event.index,1592                    id=event.content_block.id,1593                    name=event.content_block.name,1594                    args=args,1595                )1596                tool_call_chunks = [tool_call_chunk]1597            else:1598                tool_call_chunks = []1599            message_chunk = AIMessageChunk(1600                content=[content_block],1601                tool_call_chunks=tool_call_chunks,1602            )1603            block_start_event = event16041605        elif (1606            event.type == "content_block_start"1607            and event.content_block is not None1608            and event.content_block.type in ("text", "thinking")1609        ):1610            # Anthropic can place the opening content of a text or thinking block1611            # directly on the `content_block_start` event instead of in a1612            # following delta. This is common for the assistant turn that follows1613            # a tool result. Emit that initial content here so it is not dropped1614            # from the aggregated message. The deltas that follow are emitted as1615            # separate chunks sharing this block's `index`; chunk addition1616            # (`AIMessageChunk.__add__`) later coalesces them into one block.1617            block_start_event = event1618            if event.content_block.type == "text":1619                text = getattr(event.content_block, "text", "") or ""1620                if text:1621                    if coerce_content_to_string:1622                        message_chunk = AIMessageChunk(content=text)1623                    else:1624                        content_block = event.content_block.model_dump()1625                        content_block["index"] = event.index1626                        if content_block.get("citations") is None:1627                            content_block.pop("citations", None)1628                        message_chunk = AIMessageChunk(content=[content_block])1629            else:  # thinking1630                thinking = getattr(event.content_block, "thinking", "") or ""1631                signature = getattr(event.content_block, "signature", "") or ""1632                if thinking or signature:1633                    content_block = event.content_block.model_dump()1634                    content_block["index"] = event.index1635                    content_block["type"] = "thinking"1636                    message_chunk = AIMessageChunk(content=[content_block])16371638        # Process incremental content updates1639        elif event.type == "content_block_delta":1640            # Text and citation deltas (incremental text content)1641            if event.delta.type in ("text_delta", "citations_delta"):1642                if coerce_content_to_string and hasattr(event.delta, "text"):1643                    text = getattr(event.delta, "text", "")1644                    message_chunk = AIMessageChunk(content=text)1645                else:1646                    content_block = event.delta.model_dump()1647                    content_block["index"] = event.index16481649                    # All citation deltas are part of a text block1650                    content_block["type"] = "text"1651                    if "citation" in content_block:1652                        # Assign citations to a list if present1653                        content_block["citations"] = [content_block.pop("citation")]1654                    message_chunk = AIMessageChunk(content=[content_block])16551656            # Reasoning1657            elif event.delta.type in {"thinking_delta", "signature_delta"}:1658                content_block = event.delta.model_dump()1659                content_block["index"] = event.index1660                content_block["type"] = "thinking"1661                message_chunk = AIMessageChunk(content=[content_block])16621663            # Tool input JSON (streaming tool arguments)1664            elif event.delta.type == "input_json_delta":1665                content_block = event.delta.model_dump()1666                content_block["index"] = event.index1667                start_event_block = (1668                    getattr(block_start_event, "content_block", None)1669                    if block_start_event1670                    else None1671                )1672                if (1673                    start_event_block is not None1674                    and getattr(start_event_block, "type", None) == "tool_use"1675                ):1676                    tool_call_chunk = create_tool_call_chunk(1677                        index=event.index,1678                        id=None,1679                        name=None,1680                        args=event.delta.partial_json,1681                    )1682                    tool_call_chunks = [tool_call_chunk]1683                else:1684                    tool_call_chunks = []1685                message_chunk = AIMessageChunk(1686                    content=[content_block],1687                    tool_call_chunks=tool_call_chunks,1688                )16891690            # Compaction block1691            elif event.delta.type == "compaction_delta":1692                content_block = event.delta.model_dump()1693                content_block["index"] = event.index1694                content_block["type"] = "compaction"1695                if (1696                    "encrypted_content" in content_block1697                    and content_block["encrypted_content"] is None1698                ):1699                    content_block.pop("encrypted_content")1700                message_chunk = AIMessageChunk(content=[content_block])17011702        # Process final usage metadata and completion info1703        elif event.type == "message_delta" and stream_usage:1704            usage_metadata = _create_usage_metadata(event.usage)1705            response_metadata = {1706                "stop_reason": event.delta.stop_reason,1707                "stop_sequence": event.delta.stop_sequence,1708            }1709            if context_management := getattr(event, "context_management", None):1710                response_metadata["context_management"] = (1711                    context_management.model_dump()1712                )1713            message_delta = getattr(event, "delta", None)1714            if message_delta and (1715                container := getattr(message_delta, "container", None)1716            ):1717                response_metadata["container"] = container.model_dump(mode="json")1718            message_chunk = AIMessageChunk(1719                content="" if coerce_content_to_string else [],1720                usage_metadata=usage_metadata,1721                response_metadata=response_metadata,1722            )1723            if message_chunk.response_metadata.get("stop_reason"):1724                # Mark final Anthropic stream chunk1725                message_chunk.chunk_position = "last"1726        # Unhandled event types (e.g., `content_block_stop`, `ping` events)1727        # https://platform.claude.com/docs/en/build-with-claude/streaming#other-events1728        else:1729            pass17301731        if message_chunk:1732            message_chunk.response_metadata["model_provider"] = "anthropic"1733        return message_chunk, block_start_event17341735    def _format_output(self, data: Any, **kwargs: Any) -> ChatResult:1736        """Format the output from the Anthropic API to LC."""1737        data_dict = data.model_dump()1738        content = data_dict["content"]17391740        # Remove citations if they are None - introduced in anthropic sdk 0.451741        for block in content:1742            if isinstance(block, dict):1743                if "citations" in block and block["citations"] is None:1744                    block.pop("citations")1745                if "caller" in block and block["caller"] is None:1746                    block.pop("caller")1747                if "encrypted_content" in block and block["encrypted_content"] is None:1748                    block.pop("encrypted_content")1749                if (1750                    block.get("type") == "thinking"1751                    and "text" in block1752                    and block["text"] is None1753                ):1754                    block.pop("text")17551756        llm_output = {1757            k: v for k, v in data_dict.items() if k not in ("content", "role", "type")1758        }1759        if (1760            (container := llm_output.get("container"))1761            and isinstance(container, dict)1762            and (expires_at := container.get("expires_at"))1763            and isinstance(expires_at, datetime.datetime)1764        ):1765            # TODO: dump all `data` with `mode="json"`1766            llm_output["container"]["expires_at"] = expires_at.isoformat()1767        response_metadata = {"model_provider": "anthropic"}1768        if "model" in llm_output and "model_name" not in llm_output:1769            llm_output["model_name"] = llm_output["model"]1770        if (1771            len(content) == 11772            and content[0]["type"] == "text"1773            and not content[0].get("citations")1774        ):1775            msg = AIMessage(1776                content=content[0]["text"], response_metadata=response_metadata1777            )1778        elif any(block["type"] == "tool_use" for block in content):1779            tool_calls = extract_tool_calls(content)1780            msg = AIMessage(1781                content=content,1782                tool_calls=tool_calls,1783                response_metadata=response_metadata,1784            )1785        else:1786            msg = AIMessage(content=content, response_metadata=response_metadata)1787        msg.usage_metadata = _create_usage_metadata(data.usage)1788        return ChatResult(1789            generations=[ChatGeneration(message=msg)],1790            llm_output=llm_output,1791        )17921793    def _generate(1794        self,1795        messages: list[BaseMessage],1796        stop: list[str] | None = None,1797        run_manager: CallbackManagerForLLMRun | None = None,1798        **kwargs: Any,1799    ) -> ChatResult:1800        payload = self._get_request_payload(messages, stop=stop, **kwargs)1801        try:1802            data = self._create(payload)1803        except anthropic.BadRequestError as e:1804            _handle_anthropic_bad_request(e)1805        return self._format_output(data, **kwargs)18061807    async def _agenerate(1808        self,1809        messages: list[BaseMessage],1810        stop: list[str] | None = None,1811        run_manager: AsyncCallbackManagerForLLMRun | None = None,1812        **kwargs: Any,1813    ) -> ChatResult:1814        payload = self._get_request_payload(messages, stop=stop, **kwargs)1815        try:1816            data = await self._acreate(payload)1817        except anthropic.BadRequestError as e:1818            _handle_anthropic_bad_request(e)1819        return self._format_output(data, **kwargs)18201821    def _get_llm_for_structured_output_when_thinking_is_enabled(1822        self,1823        schema: dict | type,1824        formatted_tool: AnthropicTool,1825    ) -> Runnable[LanguageModelInput, BaseMessage]:1826        thinking_admonition = (1827            "You are attempting to use structured output via forced tool calling, "1828            "which is not guaranteed when `thinking` is enabled. This method will "1829            "raise an OutputParserException if tool calls are not generated. Consider "1830            "disabling `thinking` or adjust your prompt to ensure the tool is called."1831        )1832        warnings.warn(thinking_admonition, stacklevel=2)1833        llm = self.bind_tools(1834            [schema],1835            # We don't specify tool_choice here since the API will reject attempts to1836            # force tool calls when thinking=true1837            ls_structured_output_format={1838                "kwargs": {"method": "function_calling"},1839                "schema": formatted_tool,1840            },1841        )18421843        def _raise_if_no_tool_calls(message: AIMessage) -> AIMessage:1844            if not message.tool_calls:1845                raise OutputParserException(thinking_admonition)1846            return message18471848        return llm | _raise_if_no_tool_calls18491850    def bind_tools(1851        self,1852        tools: Sequence[Mapping[str, Any] | type | Callable | BaseTool],1853        *,1854        tool_choice: dict[str, str] | str | None = None,1855        parallel_tool_calls: bool | None = None,1856        strict: bool | None = None,1857        **kwargs: Any,1858    ) -> Runnable[LanguageModelInput, AIMessage]:1859        r"""Bind tool-like objects to `ChatAnthropic`.18601861        Args:1862            tools: A list of tool definitions to bind to this chat model.18631864                Supports Anthropic format tool schemas and any tool definition handled1865                by [`convert_to_openai_tool`][langchain_core.utils.function_calling.convert_to_openai_tool].1866            tool_choice: Which tool to require the model to call. Options are:18671868                - Name of the tool as a string or as dict `{"type": "tool", "name": "<<tool_name>>"}`: calls corresponding tool1869                - `'auto'`, `{"type: "auto"}`, or `None`: automatically selects a tool (including no tool)1870                - `'any'` or `{"type: "any"}`: force at least one tool to be called1871            parallel_tool_calls: Set to `False` to disable parallel tool use.18721873                Defaults to `None` (no specification, which allows parallel tool use).18741875                !!! version-added "Added in `langchain-anthropic` 0.3.2"1876            strict: If `True`, Claude's schema adherence is applied to tool calls.18771878                See the [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#strict-tool-use) for more info.1879            kwargs: Any additional parameters are passed directly to `bind`.18801881        Example:1882            ```python1883            from langchain_anthropic import ChatAnthropic1884            from pydantic import BaseModel, Field188518861887            class GetWeather(BaseModel):1888                '''Get the current weather in a given location'''18891890                location: str = Field(..., description="The city and state, e.g. San Francisco, CA")189118921893            class GetPrice(BaseModel):1894                '''Get the price of a specific product.'''18951896                product: str = Field(..., description="The product to look up.")189718981899            model = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)1900            model_with_tools = model.bind_tools([GetWeather, GetPrice])1901            model_with_tools.invoke(1902                "What is the weather like in San Francisco",1903            )1904            # -> AIMessage(1905            #     content=[1906            #         {'text': '<thinking>\nBased on the user\'s question, the relevant function to call is GetWeather, which requires the "location" parameter.\n\nThe user has directly specified the location as "San Francisco". Since San Francisco is a well known city, I can reasonably infer they mean San Francisco, CA without needing the state specified.\n\nAll the required parameters are provided, so I can proceed with the API call.\n</thinking>', 'type': 'text'},1907            #         {'text': None, 'type': 'tool_use', 'id': 'toolu_01SCgExKzQ7eqSkMHfygvYuu', 'name': 'GetWeather', 'input': {'location': 'San Francisco, CA'}}1908            #     ],1909            #     response_metadata={'id': 'msg_01GM3zQtoFv8jGQMW7abLnhi', 'model': 'claude-sonnet-4-5-20250929', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 487, 'output_tokens': 145}},1910            #     id='run-87b1331e-9251-4a68-acef-f0a018b639cc-0'1911            # )1912            ```1913        """  # noqa: E5011914        # Allows built-in tools either by their:1915        # - Raw `dict` format1916        # - Extracting extras["provider_tool_definition"] if provided on a BaseTool1917        formatted_tools = [1918            tool1919            if _is_builtin_tool(tool)1920            else convert_to_anthropic_tool(tool, strict=strict)1921            for tool in tools1922        ]1923        if not tool_choice:1924            pass1925        elif isinstance(tool_choice, dict):1926            kwargs["tool_choice"] = tool_choice1927        elif isinstance(tool_choice, str) and tool_choice in ("any", "auto"):1928            kwargs["tool_choice"] = {"type": tool_choice}1929        elif isinstance(tool_choice, str):1930            kwargs["tool_choice"] = {"type": "tool", "name": tool_choice}1931        else:1932            msg = (1933                f"Unrecognized 'tool_choice' type {tool_choice=}. Expected dict, "1934                f"str, or None."1935            )1936            raise ValueError(1937                msg,1938            )19391940        # Anthropic API rejects forced tool use when thinking is enabled:1941        # "Thinking may not be enabled when tool_choice forces tool use."1942        # Drop forced tool_choice and warn, matching the behavior in1943        # _get_llm_for_structured_output_when_thinking_is_enabled.1944        if (1945            self.thinking is not None1946            and self.thinking.get("type") in ("enabled", "adaptive")1947            and "tool_choice" in kwargs1948            and kwargs["tool_choice"].get("type") in ("any", "tool")1949        ):1950            warnings.warn(1951                "tool_choice is forced but thinking is enabled. The Anthropic "1952                "API does not support forced tool use with thinking. "1953                "Dropping tool_choice to avoid an API error. Tool calls are "1954                "not guaranteed. Consider disabling thinking or adjusting "1955                "your prompt to ensure the tool is called.",1956                stacklevel=2,1957            )1958            del kwargs["tool_choice"]19591960        if parallel_tool_calls is not None:1961            disable_parallel_tool_use = not parallel_tool_calls1962            if "tool_choice" in kwargs:1963                kwargs["tool_choice"]["disable_parallel_tool_use"] = (1964                    disable_parallel_tool_use1965                )1966            else:1967                kwargs["tool_choice"] = {1968                    "type": "auto",1969                    "disable_parallel_tool_use": disable_parallel_tool_use,1970                }19711972        return self.bind(tools=formatted_tools, **kwargs)19731974    def with_structured_output(1975        self,1976        schema: dict | type,1977        *,1978        include_raw: bool = False,1979        method: Literal["function_calling", "json_schema"] = "function_calling",1980        **kwargs: Any,1981    ) -> Runnable[LanguageModelInput, dict | BaseModel]:1982        """Model wrapper that returns outputs formatted to match the given schema.19831984        See the [LangChain docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#structured-output)1985        for more details and examples.19861987        Args:1988            schema: The output schema. Can be passed in as:19891990                - An Anthropic tool schema,1991                - An OpenAI function/tool schema,1992                - A JSON Schema,1993                - A `TypedDict` class,1994                - Or a Pydantic class.19951996                If `schema` is a Pydantic class then the model output will be a1997                Pydantic instance of that class, and the model-generated fields will be1998                validated by the Pydantic class. Otherwise the model output will be a1999                dict and will not be validated.

Code quality findings 73

Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(tool, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not tool_type or not isinstance(tool_type, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(curr, ToolMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(curr.content, list)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(block, dict) and block.get("type") == "tool_result"
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(tool_content, list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, dict) and "cache_control" in block:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(cast("BaseMessage", last).content, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(curr.content, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(message.content, list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(message.content, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(message.content, list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(block, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
not isinstance(message, AIMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(message, AIMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(block.get("content"), list)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(item, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(message, AIMessage) and message.tool_calls:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(content, str) and content
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(content, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(content, list)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(content[-1], dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(content, list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(block, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(caller, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(block, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(caller, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(content, list) and content:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(block, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(content, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(message, AIMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(payload_oc, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(response_format, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(message, AIMessage)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(container, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if "tools" in payload and isinstance(payload["tools"], list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(tool, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(resolved_oc, dict) and resolved_oc.get("task_budget"):
Ensure try blocks have corresponding except or finally blocks
try-without-except
try:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if run_manager and isinstance(msg.content, str):
Ensure try blocks have corresponding except or finally blocks
try-without-except
try:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if run_manager and isinstance(msg.content, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
) and isinstance(parsed_args, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(block, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(container, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(expires_at, datetime.datetime)
Ensure functions have docstrings for documentation
missing-docstring
def bind_tools(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(tool_choice, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(tool_choice, str) and tool_choice in ("any", "auto"):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(tool_choice, str):
Avoid unless necessary; Python's garbage collector typically handles object deletion
unnecessary-del
del kwargs["tool_choice"]
Ensure functions have docstrings for documentation
missing-docstring
def with_structured_output(
Use logging module for better control and configurability
print-statement
print(response)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(schema, type) and is_basemodel_subclass(schema):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(schema, type) and is_basemodel_subclass(schema):
Ensure functions have docstrings for documentation
missing-docstring
def get_num_tokens_from_messages(
Ensure functions have docstrings for documentation
missing-docstring
def get_weather(location: str) -> str:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(formatted_system, str):
Ensure functions have docstrings for documentation
missing-docstring
def convert_to_anthropic_tool(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(tool, BaseTool)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(tool.extras, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(tool, dict) and all(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if "strict" in oai_formatted and isinstance(strict, bool):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(tool, BaseTool)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(tool.extras, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(message.get("content"), list):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(block, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
is_pydantic_class = isinstance(schema, type) and is_basemodel_subclass(schema)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if is_pydantic_class or isinstance(schema, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(cache_creation, BaseModel):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(specific_cache_creation_tokens, int):

Get this view in your editor

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