libs/partners/anthropic/langchain_anthropic/chat_models.py PYTHON 2,364 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 2,364.
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, 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]165166_ANTHROPIC_EXTRA_FIELDS: set[str] = {167    "allowed_callers",168    "cache_control",169    "defer_loading",170    "eager_input_streaming",171    "input_examples",172}173"""Valid Anthropic-specific extra fields"""174175176def _is_builtin_tool(tool: Any) -> bool:177    """Check if a tool is a built-in (server-side) Anthropic tool.178179    `tool` must be a `dict` and have a `type` key starting with one of the known180    built-in tool prefixes.181182    [Claude docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview)183    """184    if not isinstance(tool, dict):185        return False186187    tool_type = tool.get("type")188    if not tool_type or not isinstance(tool_type, str):189        return False190191    return any(tool_type.startswith(prefix) for prefix in _BUILTIN_TOOL_PREFIXES)192193194def _format_image(url: str) -> dict:195    """Convert part["image_url"]["url"] strings (OpenAI format) to Anthropic format.196197    {198        "type": "base64",199        "media_type": "image/jpeg",200        "data": "/9j/4AAQSkZJRg...",201    }202203    Or204205    {206        "type": "url",207        "url": "https://example.com/image.jpg",208    }209    """210    # Base64 encoded image211    base64_regex = r"^data:(?P<media_type>image/.+);base64,(?P<data>.+)$"212    base64_match = re.match(base64_regex, url)213214    if base64_match:215        return {216            "type": "base64",217            "media_type": base64_match.group("media_type"),218            "data": base64_match.group("data"),219        }220221    # Url222    url_regex = r"^https?://.*$"223    url_match = re.match(url_regex, url)224225    if url_match:226        return {227            "type": "url",228            "url": url,229        }230231    msg = (232        "Malformed url parameter."233        " Must be either an image URL (https://example.com/image.jpg)"234        " or base64 encoded string (data:image/png;base64,'/9j/4AAQSk'...)"235    )236    raise ValueError(237        msg,238    )239240241_TOOL_CALL_ID_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")242"""Anthropic requires `tool_use`/`tool_result` IDs to match this pattern."""243244245def _normalize_tool_call_id(tool_call_id: str | None) -> str | None:246    """Map a tool-call ID to an Anthropic-compatible form if needed.247248    Anthropic rejects `tool_use`/`tool_result` IDs that don't match249    `^[a-zA-Z0-9_-]+$`. IDs minted by other providers can violate this when a250    thread is replayed across providers (e.g. Fireworks/Kimi emits251    `functions.write_todos:0`, whose `.` and `:` are invalid). Valid IDs are252    returned unchanged; invalid ones are hashed deterministically so that a253    rewritten `tool_use.id` and its paired `tool_use_id` resolve to the same254    value, both within a request and across turns.255256    Empty and `None` IDs are passed through unchanged so that a genuinely257    malformed request surfaces as a clear error from Anthropic rather than258    being masked by a synthesized ID.259260    Args:261        tool_call_id: The tool-call ID to normalize.262263    Returns:264        The original ID if it is empty, `None`, or already valid; otherwise a265            deterministic Anthropic-compatible replacement.266    """267    if not tool_call_id or _TOOL_CALL_ID_PATTERN.match(tool_call_id):268        return tool_call_id269    digest = hashlib.sha256(tool_call_id.encode()).hexdigest()270    return f"toolu_{digest[:24]}"271272273def _normalize_block_tool_use_id(block: dict) -> dict:274    """Return `block` with its `tool_use_id` normalized, if it carries one.275276    Mirrors `_normalize_tool_call_id` for `tool_result`-style content blocks so277    that a `tool_use_id` arriving pre-structured (e.g. on a `ToolMessage` whose278    content is already a list of `tool_result` blocks) stays consistent with its279    paired, normalized `tool_use.id`. A no-op for already-valid IDs.280    """281    if "tool_use_id" in block:282        return {**block, "tool_use_id": _normalize_tool_call_id(block["tool_use_id"])}283    return block284285286def _merge_messages(287    messages: Sequence[BaseMessage],288) -> list[SystemMessage | AIMessage | HumanMessage]:289    """Merge runs of human/tool messages into single human messages with content blocks."""  # noqa: E501290    merged: list = []291    for curr in messages:292        if isinstance(curr, ToolMessage):293            if (294                isinstance(curr.content, list)295                and curr.content296                and all(297                    isinstance(block, dict) and block.get("type") == "tool_result"298                    for block in curr.content299                )300            ):301                curr = HumanMessage(curr.content)  # type: ignore[misc]302            else:303                tool_content = curr.content304                cache_ctrl = None305                # Extract cache_control from content blocks and hoist it306                # to the tool_result level.  Anthropic's API does not307                # support cache_control on tool_result content sub-blocks.308                if isinstance(tool_content, list):309                    cleaned = []310                    for block in tool_content:311                        if isinstance(block, dict) and "cache_control" in block:312                            cache_ctrl = block["cache_control"]313                            block = {314                                k: v for k, v in block.items() if k != "cache_control"315                            }316                        cleaned.append(block)317                    tool_content = cleaned318                tool_result: dict = {319                    "type": "tool_result",320                    "content": tool_content,321                    "tool_use_id": _normalize_tool_call_id(curr.tool_call_id),322                    "is_error": curr.status == "error",323                }324                if cache_ctrl:325                    tool_result["cache_control"] = cache_ctrl326                curr = HumanMessage(  # type: ignore[misc]327                    [tool_result],328                )329        last = merged[-1] if merged else None330        if any(331            all(isinstance(m, c) for m in (curr, last))332            for c in (SystemMessage, HumanMessage)333        ):334            if isinstance(cast("BaseMessage", last).content, str):335                new_content: list = [336                    {"type": "text", "text": cast("BaseMessage", last).content},337                ]338            else:339                new_content = copy.copy(cast("list", cast("BaseMessage", last).content))340            if isinstance(curr.content, str):341                new_content.append({"type": "text", "text": curr.content})342            else:343                new_content.extend(curr.content)344            merged[-1] = curr.model_copy(update={"content": new_content})345        else:346            merged.append(curr)347    return merged348349350def _format_data_content_block(block: dict) -> dict:351    """Format standard data content block to format expected by Anthropic."""352    if block["type"] == "image":353        if "url" in block:354            if block["url"].startswith("data:"):355                # Data URI356                formatted_block = {357                    "type": "image",358                    "source": _format_image(block["url"]),359                }360            else:361                formatted_block = {362                    "type": "image",363                    "source": {"type": "url", "url": block["url"]},364                }365        elif "base64" in block or block.get("source_type") == "base64":366            formatted_block = {367                "type": "image",368                "source": {369                    "type": "base64",370                    "media_type": block["mime_type"],371                    "data": block.get("base64") or block.get("data", ""),372                },373            }374        elif "file_id" in block:375            formatted_block = {376                "type": "image",377                "source": {378                    "type": "file",379                    "file_id": block["file_id"],380                },381            }382        elif block.get("source_type") == "id":383            formatted_block = {384                "type": "image",385                "source": {386                    "type": "file",387                    "file_id": block["id"],388                },389            }390        else:391            msg = (392                "Anthropic only supports 'url', 'base64', or 'id' keys for image "393                "content blocks."394            )395            raise ValueError(396                msg,397            )398399    elif block["type"] == "file":400        if "url" in block:401            formatted_block = {402                "type": "document",403                "source": {404                    "type": "url",405                    "url": block["url"],406                },407            }408        elif "base64" in block or block.get("source_type") == "base64":409            formatted_block = {410                "type": "document",411                "source": {412                    "type": "base64",413                    "media_type": block.get("mime_type") or "application/pdf",414                    "data": block.get("base64") or block.get("data", ""),415                },416            }417        elif block.get("source_type") == "text":418            formatted_block = {419                "type": "document",420                "source": {421                    "type": "text",422                    "media_type": block.get("mime_type") or "text/plain",423                    "data": block["text"],424                },425            }426        elif "file_id" in block:427            formatted_block = {428                "type": "document",429                "source": {430                    "type": "file",431                    "file_id": block["file_id"],432                },433            }434        elif block.get("source_type") == "id":435            formatted_block = {436                "type": "document",437                "source": {438                    "type": "file",439                    "file_id": block["id"],440                },441            }442        else:443            msg = (444                "Anthropic only supports 'url', 'base64', or 'id' keys for file "445                "content blocks."446            )447            raise ValueError(msg)448449    elif block["type"] == "text-plain":450        formatted_block = {451            "type": "document",452            "source": {453                "type": "text",454                "media_type": block.get("mime_type") or "text/plain",455                "data": block["text"],456            },457        }458459    else:460        msg = f"Block of type {block['type']} is not supported."461        raise ValueError(msg)462463    if formatted_block:464        for key in ["cache_control", "citations", "title", "context"]:465            if key in block:466                formatted_block[key] = block[key]467            elif (metadata := block.get("extras")) and key in metadata:468                formatted_block[key] = metadata[key]469            elif (metadata := block.get("metadata")) and key in metadata:470                # Backward compat471                formatted_block[key] = metadata[key]472473    return formatted_block474475476def _format_messages(477    messages: Sequence[BaseMessage],478) -> tuple[str | list[dict] | None, list[dict]]:479    """Format messages for Anthropic's API."""480    system: str | list[dict] | None = None481    formatted_messages: list[dict] = []482    merged_messages = _merge_messages(messages)483    for _i, message in enumerate(merged_messages):484        if message.type == "system":485            if system is not None:486                msg = "Received multiple non-consecutive system messages."487                raise ValueError(msg)488            if isinstance(message.content, list):489                system = [490                    (491                        block492                        if isinstance(block, dict)493                        else {"type": "text", "text": block}494                    )495                    for block in message.content496                ]497            else:498                system = message.content499            continue500501        role = _message_type_lookups[message.type]502        content: str | list503504        if not isinstance(message.content, str):505            # parse as dict506            if not isinstance(message.content, list):507                msg = "Anthropic message content must be str or list of dicts"508                raise ValueError(509                    msg,510                )511512            # populate content513            content = []514            for block in message.content:515                if isinstance(block, str):516                    content.append({"type": "text", "text": block})517                elif isinstance(block, dict):518                    if "type" not in block:519                        msg = "Dict content block must have a type key"520                        raise ValueError(msg)521                    if block["type"] in ("reasoning", "function_call") and (522                        not isinstance(message, AIMessage)523                        or message.response_metadata.get("model_provider")524                        != "anthropic"525                    ):526                        continue527                    if block["type"] == "image_url":528                        # convert format529                        source = _format_image(block["image_url"]["url"])530                        content.append({"type": "image", "source": source})531                    elif is_data_content_block(block):532                        content.append(_format_data_content_block(block))533                    elif block["type"] == "tool_use":534                        # If a tool_call with the same id as a tool_use content block535                        # exists, the tool_call is preferred.536                        if (537                            isinstance(message, AIMessage)538                            and (block["id"] in [tc["id"] for tc in message.tool_calls])539                            and not block.get("caller")540                        ):541                            overlapping = [542                                tc543                                for tc in message.tool_calls544                                if tc["id"] == block["id"]545                            ]546                            content.extend(547                                _lc_tool_calls_to_anthropic_tool_use_blocks(548                                    overlapping,549                                ),550                            )551                        else:552                            if tool_input := block.get("input"):553                                args = tool_input554                            elif "partial_json" in block:555                                try:556                                    args = json.loads(block["partial_json"] or "{}")557                                except json.JSONDecodeError:558                                    args = {}559                            else:560                                args = {}561                            tool_use_block = _AnthropicToolUse(562                                type="tool_use",563                                name=block["name"],564                                input=args,565                                id=cast("str", _normalize_tool_call_id(block["id"])),566                            )567                            if caller := block.get("caller"):568                                tool_use_block["caller"] = caller569                            content.append(tool_use_block)570                    elif block["type"] in ("server_tool_use", "mcp_tool_use"):571                        formatted_block = {572                            k: v573                            for k, v in block.items()574                            if k575                            in (576                                "type",577                                "id",578                                "input",579                                "name",580                                "server_name",  # for mcp_tool_use581                                "cache_control",582                            )583                        }584                        # Attempt to parse streamed output585                        if block.get("input") == {} and "partial_json" in block:586                            try:587                                input_ = json.loads(block["partial_json"])588                                if input_:589                                    formatted_block["input"] = input_590                            except json.JSONDecodeError:591                                pass592                        content.append(formatted_block)593                    elif block["type"] == "text":594                        text = block.get("text", "")595                        # Only add non-empty strings for now as empty ones are not596                        # accepted.597                        # https://github.com/anthropics/anthropic-sdk-python/issues/461598                        if text.strip():599                            formatted_block = {600                                k: v601                                for k, v in block.items()602                                if k in ("type", "text", "cache_control", "citations")603                            }604                            # Clean up citations to remove null file_id fields605                            if formatted_block.get("citations"):606                                cleaned_citations = []607                                for citation in formatted_block["citations"]:608                                    cleaned_citation = {609                                        k: v610                                        for k, v in citation.items()611                                        if not (k == "file_id" and v is None)612                                    }613                                    cleaned_citations.append(cleaned_citation)614                                formatted_block["citations"] = cleaned_citations615                            content.append(formatted_block)616                    elif block["type"] == "thinking":617                        content.append(618                            {619                                k: v620                                for k, v in block.items()621                                if k622                                in ("type", "thinking", "cache_control", "signature")623                            },624                        )625                    elif block["type"] == "redacted_thinking":626                        content.append(627                            {628                                k: v629                                for k, v in block.items()630                                if k in ("type", "cache_control", "data")631                            },632                        )633                    elif (634                        block["type"] == "tool_result"635                        and isinstance(block.get("content"), list)636                        and any(637                            isinstance(item, dict)638                            and item.get("type") == "tool_reference"639                            for item in block["content"]640                        )641                    ):642                        # Tool search results with tool_reference blocks643                        content.append(644                            _normalize_block_tool_use_id(645                                {646                                    k: v647                                    for k, v in block.items()648                                    if k649                                    in (650                                        "type",651                                        "content",652                                        "tool_use_id",653                                        "cache_control",654                                    )655                                },656                            ),657                        )658                    elif block["type"] == "tool_result":659                        # Regular tool results that need content formatting660                        tool_content = _format_messages(661                            [HumanMessage(block["content"])],662                        )[1][0]["content"]663                        content.append(664                            _normalize_block_tool_use_id(665                                {**block, "content": tool_content},666                            ),667                        )668                    elif block["type"] in (669                        "code_execution_tool_result",670                        "bash_code_execution_tool_result",671                        "text_editor_code_execution_tool_result",672                        "mcp_tool_result",673                        "web_search_tool_result",674                        "web_fetch_tool_result",675                    ):676                        content.append(677                            _normalize_block_tool_use_id(678                                {679                                    k: v680                                    for k, v in block.items()681                                    if k682                                    in (683                                        "type",684                                        "content",685                                        "tool_use_id",686                                        "is_error",  # for mcp_tool_result687                                        "cache_control",688                                        "retrieved_at",  # for web_fetch_tool_result689                                    )690                                },691                            ),692                        )693                    else:694                        content.append(block)695                else:696                    msg = (697                        f"Content blocks must be str or dict, instead was: "698                        f"{type(block)}"699                    )700                    raise ValueError(701                        msg,702                    )703        else:704            content = message.content705706        # Ensure all tool_calls have a tool_use content block707        if isinstance(message, AIMessage) and message.tool_calls:708            content = content or []709            content = (710                [{"type": "text", "text": message.content}]711                if isinstance(content, str) and content712                else content713            )714            tool_use_ids = [715                cast("dict", block)["id"]716                for block in content717                if cast("dict", block)["type"] == "tool_use"718            ]719            # `tool_use_ids` are already normalized via the branches above, so720            # compare against the normalized tool-call ID to avoid emitting a721            # duplicate `tool_use` block when the original ID was rewritten.722            missing_tool_calls = [723                tc724                for tc in message.tool_calls725                if _normalize_tool_call_id(tc["id"]) not in tool_use_ids726            ]727            cast("list", content).extend(728                _lc_tool_calls_to_anthropic_tool_use_blocks(missing_tool_calls),729            )730731        if role == "assistant" and _i == len(merged_messages) - 1:732            if isinstance(content, str):733                content = content.rstrip()734            elif (735                isinstance(content, list)736                and content737                and isinstance(content[-1], dict)738                and content[-1].get("type") == "text"739            ):740                content[-1]["text"] = content[-1]["text"].rstrip()741742        if not content and role == "assistant" and _i < len(merged_messages) - 1:743            # anthropic.BadRequestError: Error code: 400: all messages must have744            # non-empty content except for the optional final assistant message745            continue746        formatted_messages.append({"role": role, "content": content})747    return system, formatted_messages748749750def _collect_code_execution_tool_ids(formatted_messages: list[dict]) -> set[str]:751    """Collect `tool_use` IDs that were called by `code_execution`.752753    These blocks cannot have `cache_control` applied per Anthropic API754    requirements.755    """756    code_execution_tool_ids: set[str] = set()757758    for message in formatted_messages:759        if message.get("role") != "assistant":760            continue761        content = message.get("content", [])762        if not isinstance(content, list):763            continue764        for block in content:765            if not isinstance(block, dict):766                continue767            if block.get("type") != "tool_use":768                continue769            caller = block.get("caller")770            if isinstance(caller, dict):771                caller_type = caller.get("type", "")772                if caller_type.startswith("code_execution"):773                    tool_id = block.get("id")774                    if tool_id:775                        code_execution_tool_ids.add(tool_id)776777    return code_execution_tool_ids778779780def _is_code_execution_related_block(781    block: dict,782    code_execution_tool_ids: set[str],783) -> bool:784    """Return whether a content block is related to `code_execution`.785786    Returns `True` for blocks that should NOT have `cache_control` applied.787    """788    if not isinstance(block, dict):789        return False790791    block_type = block.get("type")792793    if block_type == "tool_use":794        caller = block.get("caller")795        if isinstance(caller, dict):796            caller_type = caller.get("type", "")797            if caller_type.startswith("code_execution"):798                return True799800    if block_type == "tool_result":801        tool_use_id = block.get("tool_use_id")802        if tool_use_id and tool_use_id in code_execution_tool_ids:803            return True804805    return False806807808def _is_direct_anthropic_llm_type(llm_type: object) -> bool:809    """Return whether an `_llm_type` reaches Claude via the direct Anthropic API.810811    Only the direct API accepts the top-level `cache_control` request param.812    Subclasses that route through other transports (Bedrock, future backends)813    override `_llm_type` and must expand `cache_control` kwargs into814    block-level breakpoints instead.815816    Non-string `_llm_type` values return `False` rather than raising, so a817    misbehaving subclass falls through to the safer non-direct branch.818    """819    return llm_type == "anthropic-chat"820821822def _apply_cache_control_to_last_eligible_block(823    formatted_messages: list[dict],824    cache_control: Any,825    code_execution_tool_ids: set[str],826) -> bool:827    """Place `cache_control` on the last block eligible for a breakpoint.828829    Walks messages newest-to-oldest and, within each, blocks newest-to-oldest,830    skipping `code_execution`-related blocks (Anthropic rejects breakpoints831    there). String message content is promoted to a single text block so the832    breakpoint can be attached.833834    Returns:835        `True` if a breakpoint was applied, `False` if every candidate was836            `code_execution`-related (caller should warn and drop the kwarg).837    """838    for formatted_message in reversed(formatted_messages):839        content = formatted_message.get("content")840        if isinstance(content, list) and content:841            for block in reversed(content):842                if not isinstance(block, dict):843                    continue844                if _is_code_execution_related_block(block, code_execution_tool_ids):845                    continue846                block["cache_control"] = cache_control847                return True848        elif isinstance(content, str):849            formatted_message["content"] = [850                {851                    "type": "text",852                    "text": content,853                    "cache_control": cache_control,854                }855            ]856            return True857    return False858859860class AnthropicContextOverflowError(anthropic.BadRequestError, ContextOverflowError):861    """BadRequestError raised when input exceeds Anthropic's context limit."""862863864def _handle_anthropic_bad_request(e: anthropic.BadRequestError) -> None:865    """Handle Anthropic BadRequestError."""866    if "prompt is too long" in e.message:867        raise AnthropicContextOverflowError(868            message=e.message, response=e.response, body=e.body869        ) from e870    if ("messages: at least one message is required") in e.message:871        message = "Received only system message(s). "872        warnings.warn(message, stacklevel=2)873        raise e874    raise875876877class ChatAnthropic(BaseChatModel):878    """Anthropic (Claude) chat models.879880    See the [LangChain docs for `ChatAnthropic`](https://docs.langchain.com/oss/python/integrations/chat/anthropic)881    for tutorials, feature walkthroughs, and examples.882883    See the [Claude Platform docs](https://platform.claude.com/docs/en/about-claude/models/overview)884    for a list of the latest models, their capabilities, and pricing.885886    Example:887        ```python888        # pip install -U langchain-anthropic889        # export ANTHROPIC_API_KEY="your-api-key"890891        from langchain_anthropic import ChatAnthropic892893        model = ChatAnthropic(894            model="claude-sonnet-4-5-20250929",895            # temperature=,896            # max_tokens=,897            # timeout=,898            # max_retries=,899            # base_url="...",900            # Refer to API reference for full list of parameters901        )902        ```903904    Note:905        Any param which is not explicitly supported will be passed directly to906        [`Anthropic.messages.create(...)`](https://platform.claude.com/docs/en/api/python/messages/create)907        each time to the model is invoked.908    """909910    model_config = ConfigDict(911        populate_by_name=True,912    )913914    model: str = Field(alias="model_name")915    """Model name to use."""916917    max_tokens: int | None = Field(default=None, alias="max_tokens_to_sample")918    """Denotes the number of tokens to predict per generation.919920    If not specified, this is set dynamically using the model's `max_output_tokens`921    from its model profile.922923    See docs on [model profiles](https://docs.langchain.com/oss/python/langchain/models#model-profiles)924    for more information.925    """926927    temperature: float | None = None928    """A non-negative float that tunes the degree of randomness in generation."""929930    top_k: int | None = None931    """Number of most likely tokens to consider at each step."""932933    top_p: float | None = None934    """Total probability mass of tokens to consider at each step."""935936    default_request_timeout: float | None = Field(None, alias="timeout")937    """Timeout for requests to Claude API."""938939    # sdk default = 2: https://github.com/anthropics/anthropic-sdk-python?tab=readme-ov-file#retries940    max_retries: int = 2941    """Number of retries allowed for requests sent to the Claude API."""942943    stop_sequences: list[str] | None = Field(None, alias="stop")944    """Default stop sequences."""945946    anthropic_api_url: str | None = Field(947        alias="base_url",948        default_factory=from_env(949            ["ANTHROPIC_API_URL", "ANTHROPIC_BASE_URL"],950            default="https://api.anthropic.com",951        ),952    )953    """Base URL for API requests. Only specify if using a proxy or service emulator.954955    If a value isn't passed in, will attempt to read the value first from956    `ANTHROPIC_API_URL` and if that is not set, `ANTHROPIC_BASE_URL`.957    """958959    anthropic_api_key: SecretStr = Field(960        alias="api_key",961        default_factory=secret_from_env("ANTHROPIC_API_KEY", default=""),962    )963    """Automatically read from env var `ANTHROPIC_API_KEY` if not provided."""964965    anthropic_proxy: str | None = Field(966        default_factory=from_env("ANTHROPIC_PROXY", default=None)967    )968    """Proxy to use for the Anthropic clients, will be used for every API call.969970    If not provided, will attempt to read from the `ANTHROPIC_PROXY` environment971    variable.972    """973974    default_headers: Mapping[str, str] | None = None975    """Headers to pass to the Anthropic clients, will be used for every API call."""976977    betas: list[str] | None = None978    """List of beta features to enable. If specified, invocations will be routed979    through `client.beta.messages.create`.980981    Example: `#!python betas=["token-efficient-tools-2025-02-19"]`982    """983    # Can also be passed in w/ model_kwargs, but having it as a param makes better devx984    #985    # Precedence order:986    # 1. Call-time kwargs (e.g., llm.invoke(..., betas=[...]))987    # 2. model_kwargs (e.g., ChatAnthropic(model_kwargs={"betas": [...]}))988    # 3. Direct parameter (e.g., ChatAnthropic(betas=[...]))989990    model_kwargs: dict[str, Any] = Field(default_factory=dict)991992    streaming: bool = False993    """Whether to use streaming or not."""994995    stream_usage: bool = True996    """Whether to include usage metadata in streaming output.997998    If `True`, additional message chunks will be generated during the stream including999    usage metadata.1000    """10011002    thinking: dict[str, Any] | None = Field(default=None)1003    """Parameters for Claude reasoning.10041005    Examples:10061007    - `#!python {"type": "enabled", "budget_tokens": 10_000}` (pre-4.7 models)1008    - `#!python {"type": "adaptive"}` (Opus 4.6+)1009    - `#!python {"type": "adaptive", "display": "summarized"}` (Opus 4.7+)10101011    !!! note "Claude Opus 4.7"10121013        `budget_tokens` is removed on Opus 4.7  use `{"type": "adaptive"}`1014        with `output_config.effort` to control reasoning effort. Set `display`1015        to `"summarized"` to receive summarized reasoning in the response1016        (default is `"omitted"`).1017    """10181019    output_config: dict[str, Any] | None = None1020    """Configuration options for the model's output.10211022    Supports the following keys:10231024    - `effort`: Controls how many tokens Claude uses when responding.1025      One of `"max"`, `"xhigh"`, `"high"`, `"medium"`, or `"low"`.1026    - `format`: Structured output format configuration (typically set via1027      `with_structured_output`).1028    - `task_budget`: Advisory token budget for an agentic loop (beta).1029      E.g., `#!python {"type": "tokens", "total": 128_000}`.10301031    Example:10321033    .. code-block:: python10341035        ChatAnthropic(1036            model="claude-opus-4-7",1037            output_config={1038                "effort": "xhigh",1039                "task_budget": {"type": "tokens", "total": 128_000},1040            },1041        )10421043    See Anthropic docs on1044    [extended output](https://platform.claude.com/docs/en/api/go/beta/messages/create).1045    """10461047    effort: Literal["max", "xhigh", "high", "medium", "low"] | None = None1048    """Convenience shorthand for `output_config.effort`.10491050    When set, this value takes precedence over any `effort` key inside1051    `output_config`.10521053    Example: `effort="medium"`10541055    !!! note10561057        Setting `effort` to `'high'` produces exactly the same behavior as omitting the1058        parameter altogether.1059    """10601061    mcp_servers: list[dict[str, Any]] | None = None1062    """List of MCP servers to use for the request.10631064    Example: `#!python mcp_servers=[{"type": "url", "url": "https://mcp.example.com/mcp",1065    "name": "example-mcp"}]`1066    """10671068    context_management: dict[str, Any] | None = None1069    """Configuration for1070    [context management](https://platform.claude.com/docs/en/build-with-claude/context-editing).1071    """10721073    reuse_last_container: bool | None = None1074    """Automatically reuse container from most recent response (code execution).10751076    When using the built-in1077    [code execution tool](https://docs.langchain.com/oss/python/integrations/chat/anthropic#code-execution),1078    model responses will include container metadata. Set `reuse_last_container=True`1079    to automatically reuse the container from the most recent response for subsequent1080    invocations.1081    """10821083    inference_geo: str | None = None1084    """Controls where model inference runs. See Anthropic's1085    [data residency](https://platform.claude.com/docs/en/build-with-claude/data-residency)1086    docs for more information.1087    """10881089    @property1090    def _llm_type(self) -> str:1091        """Return type of chat model."""1092        return "anthropic-chat"10931094    @property1095    def lc_secrets(self) -> dict[str, str]:1096        """Return a mapping of secret keys to environment variables."""1097        return {1098            "anthropic_api_key": "ANTHROPIC_API_KEY",1099            "mcp_servers": "ANTHROPIC_MCP_SERVERS",1100        }11011102    @classmethod1103    def is_lc_serializable(cls) -> bool:1104        """Whether the class is serializable in langchain."""1105        return True11061107    @classmethod1108    def get_lc_namespace(cls) -> list[str]:1109        """Get the namespace of the LangChain object.11101111        Returns:1112            `["langchain", "chat_models", "anthropic"]`1113        """1114        return ["langchain", "chat_models", "anthropic"]11151116    @property1117    def _identifying_params(self) -> dict[str, Any]:1118        """Get the identifying parameters."""1119        return {1120            "model": self.model,1121            "max_tokens": self.max_tokens,1122            "temperature": self.temperature,1123            "top_k": self.top_k,1124            "top_p": self.top_p,1125            "model_kwargs": self.model_kwargs,1126            "streaming": self.streaming,1127            "max_retries": self.max_retries,1128            "default_request_timeout": self.default_request_timeout,1129            "thinking": self.thinking,1130            "output_config": self.output_config,1131        }11321133    def _get_ls_params(1134        self,1135        stop: list[str] | None = None,1136        **kwargs: Any,1137    ) -> LangSmithParams:1138        """Get standard params for tracing."""1139        params = self._get_invocation_params(stop=stop, **kwargs)1140        ls_params = LangSmithParams(1141            ls_provider="anthropic",1142            ls_model_name=params.get("model", self.model),1143            ls_model_type="chat",1144            ls_temperature=params.get("temperature", self.temperature),1145        )1146        if ls_max_tokens := params.get("max_tokens", self.max_tokens):1147            ls_params["ls_max_tokens"] = ls_max_tokens1148        if ls_stop := stop or params.get("stop", None):1149            ls_params["ls_stop"] = ls_stop1150        return ls_params11511152    @model_validator(mode="before")1153    @classmethod1154    def set_default_max_tokens(cls, values: dict[str, Any]) -> Any:1155        """Set default `max_tokens` from model profile with fallback."""1156        if values.get("max_tokens") is None:1157            model = values.get("model") or values.get("model_name")1158            profile = _get_default_model_profile(model) if model else {}1159            values["max_tokens"] = profile.get(1160                "max_output_tokens", _FALLBACK_MAX_OUTPUT_TOKENS1161            )1162        return values11631164    @model_validator(mode="before")1165    @classmethod1166    def build_extra(cls, values: dict) -> Any:1167        """Build model kwargs."""1168        all_required_field_names = get_pydantic_field_names(cls)1169        return _build_model_kwargs(values, all_required_field_names)11701171    def _resolve_model_profile(self) -> ModelProfile | None:1172        profile = _get_default_model_profile(self.model) or None1173        if profile is not None and self.betas and "context-1m-2025-08-07" in self.betas:1174            profile["max_input_tokens"] = 1_000_0001175        return profile11761177    @cached_property1178    def _client_params(self) -> dict[str, Any]:1179        # Merge User-Agent with user-provided headers (user headers take precedence)1180        default_headers = {"User-Agent": _USER_AGENT}1181        if self.default_headers:1182            default_headers.update(self.default_headers)11831184        client_params: dict[str, Any] = {1185            "api_key": self.anthropic_api_key.get_secret_value(),1186            "base_url": self.anthropic_api_url,1187            "max_retries": self.max_retries,1188            "default_headers": default_headers,1189        }1190        # value <= 0 indicates the param should be ignored. None is a meaningful value1191        # for Anthropic client and treated differently than not specifying the param at1192        # all.1193        if self.default_request_timeout is None or self.default_request_timeout > 0:1194            client_params["timeout"] = self.default_request_timeout11951196        return client_params11971198    @cached_property1199    def _client(self) -> anthropic.Client:1200        client_params = self._client_params1201        http_client_params = {"base_url": client_params["base_url"]}1202        if "timeout" in client_params:1203            http_client_params["timeout"] = client_params["timeout"]1204        if self.anthropic_proxy:1205            http_client_params["anthropic_proxy"] = self.anthropic_proxy1206        http_client = _get_default_httpx_client(**http_client_params)1207        params = {1208            **client_params,1209            "http_client": http_client,1210        }1211        return anthropic.Client(**params)12121213    @cached_property1214    def _async_client(self) -> anthropic.AsyncClient:1215        client_params = self._client_params1216        http_client_params = {"base_url": client_params["base_url"]}1217        if "timeout" in client_params:1218            http_client_params["timeout"] = client_params["timeout"]1219        if self.anthropic_proxy:1220            http_client_params["anthropic_proxy"] = self.anthropic_proxy1221        http_client = _get_default_async_httpx_client(**http_client_params)1222        params = {1223            **client_params,1224            "http_client": http_client,1225        }1226        return anthropic.AsyncClient(**params)12271228    def _get_request_payload(1229        self,1230        input_: LanguageModelInput,1231        *,1232        stop: list[str] | None = None,1233        **kwargs: dict,1234    ) -> dict:1235        """Get the request payload for the Anthropic API."""1236        messages = self._convert_input(input_).to_messages()12371238        for idx, message in enumerate(messages):1239            # Translate v1 content1240            if (1241                isinstance(message, AIMessage)1242                and message.response_metadata.get("output_version") == "v1"1243            ):1244                tcs: list[types.ToolCall] = [1245                    {1246                        "type": "tool_call",1247                        "name": tool_call["name"],1248                        "args": tool_call["args"],1249                        "id": tool_call.get("id"),1250                    }1251                    for tool_call in message.tool_calls1252                ]1253                messages[idx] = message.model_copy(1254                    update={1255                        "content": _convert_from_v1_to_anthropic(1256                            cast(list[types.ContentBlock], message.content),1257                            tcs,1258                            message.response_metadata.get("model_provider"),1259                        )1260                    }1261                )12621263        system, formatted_messages = _format_messages(messages)12641265        # Only the direct Anthropic API accepts top-level `cache_control`.1266        # Subclasses that route through other transports (e.g. Bedrock) expand1267        # `cache_control` kwargs into block-level breakpoints, the only form1268        # those transports accept.1269        if not _is_direct_anthropic_llm_type(getattr(self, "_llm_type", None)):1270            cache_control = kwargs.pop("cache_control", None)1271            # Empty `formatted_messages` has nothing to attach a breakpoint to;1272            # skip silently. The warning below is reserved for the surprising1273            # case where messages exist but every candidate block is ineligible.1274            if cache_control and formatted_messages:1275                code_execution_tool_ids = _collect_code_execution_tool_ids(1276                    formatted_messages1277                )1278                applied = _apply_cache_control_to_last_eligible_block(1279                    formatted_messages, cache_control, code_execution_tool_ids1280                )1281                if not applied:1282                    warnings.warn(1283                        "`cache_control` kwarg was dropped: no eligible "1284                        "content block found (all candidates are "1285                        "`code_execution`-related, which Anthropic forbids "1286                        "breakpoints on).",1287                        UserWarning,1288                        stacklevel=2,1289                    )12901291        payload = {1292            "model": self.model,1293            "max_tokens": self.max_tokens,1294            "messages": formatted_messages,1295            "temperature": self.temperature,1296            "top_k": self.top_k,1297            "top_p": self.top_p,1298            "stop_sequences": stop or self.stop_sequences,1299            "betas": self.betas,1300            "context_management": self.context_management,1301            "mcp_servers": self.mcp_servers,1302            "system": system,1303            **self.model_kwargs,1304            **kwargs,1305        }1306        if self.thinking is not None:1307            payload["thinking"] = self.thinking1308        if self.inference_geo is not None:1309            payload["inference_geo"] = self.inference_geo13101311        # Handle output_config and effort parameter1312        # Priority: self.effort > kwargs output_config > self.output_config1313        output_config: dict[str, Any] = {}1314        if self.output_config:1315            output_config.update(self.output_config)1316        payload_oc = payload.get("output_config")1317        if isinstance(payload_oc, dict):1318            output_config.update(payload_oc)13191320        if self.effort:1321            output_config["effort"] = self.effort13221323        if output_config:1324            payload["output_config"] = output_config13251326        if "response_format" in payload:1327            # response_format present when using agents.create_agent's ProviderStrategy1328            # ---1329            # ProviderStrategy converts to OpenAI-style format, which passes kwargs to1330            # ChatAnthropic, ending up in our payload1331            response_format = payload.pop("response_format")1332            if (1333                isinstance(response_format, dict)1334                and response_format.get("type") == "json_schema"1335                and "schema" in response_format.get("json_schema", {})1336            ):1337                response_format = cast(dict, response_format["json_schema"]["schema"])1338            # Convert OpenAI-style response_format to Anthropic's output_config.format1339            output_config = payload.setdefault("output_config", {})1340            output_config["format"] = _convert_to_anthropic_output_config_format(1341                response_format1342            )13431344        # Handle deprecated output_format parameter for backward compatibility1345        if "output_format" in payload:1346            warnings.warn(1347                "The 'output_format' parameter is deprecated and will be removed in "1348                "langchain-anthropic 2.0.0. Use 'output_config={\"format\": ...}' "1349                "instead.",1350                DeprecationWarning,1351                stacklevel=2,1352            )1353            output_config = payload.setdefault("output_config", {})1354            output_config["format"] = payload.pop("output_format")13551356        if self.reuse_last_container:1357            # Check for most recent AIMessage with container set in response_metadata1358            # and set as a top-level param on the request1359            for message in reversed(messages):1360                if (1361                    isinstance(message, AIMessage)1362                    and (container := message.response_metadata.get("container"))1363                    and isinstance(container, dict)1364                    and (container_id := container.get("id"))1365                ):1366                    payload["container"] = container_id1367                    break13681369        # Note: Beta headers are no longer required for structured outputs1370        # (output_config.format or strict tool use) as they are now generally available1371        if "tools" in payload and isinstance(payload["tools"], list):1372            # Auto-append required betas for specific tool types and input_examples1373            has_input_examples = False1374            for tool in payload["tools"]:1375                if isinstance(tool, dict):1376                    tool_type = tool.get("type")1377                    if tool_type and tool_type in _TOOL_TYPE_TO_BETA:1378                        required_beta = _TOOL_TYPE_TO_BETA[tool_type]1379                        if payload["betas"]:1380                            if required_beta not in payload["betas"]:1381                                payload["betas"] = [1382                                    *payload["betas"],1383                                    required_beta,1384                                ]1385                        else:1386                            payload["betas"] = [required_beta]1387                    # Check for input_examples1388                    if tool.get("input_examples"):1389                        has_input_examples = True13901391            # Auto-append header for input_examples1392            if has_input_examples:1393                required_beta = "advanced-tool-use-2025-11-20"1394                if payload["betas"]:1395                    if required_beta not in payload["betas"]:1396                        payload["betas"] = [*payload["betas"], required_beta]1397                else:1398                    payload["betas"] = [required_beta]13991400        # Auto-append required beta for mcp_servers1401        if payload.get("mcp_servers"):1402            required_beta = "mcp-client-2025-11-20"1403            if payload["betas"]:1404                # Append to existing betas if not already present1405                if required_beta not in payload["betas"]:1406                    payload["betas"] = [*payload["betas"], required_beta]1407            else:1408                payload["betas"] = [required_beta]14091410        # Auto-append required beta for task_budget1411        resolved_oc = payload.get("output_config")1412        if isinstance(resolved_oc, dict) and resolved_oc.get("task_budget"):1413            required_beta = "task-budgets-2026-03-13"1414            if payload.get("betas"):1415                if required_beta not in payload["betas"]:1416                    payload["betas"] = [*payload["betas"], required_beta]1417            else:1418                payload["betas"] = [required_beta]14191420        return {k: v for k, v in payload.items() if v is not None}14211422    def _create(self, payload: dict) -> Any:1423        if "betas" in payload:1424            return self._client.beta.messages.create(**payload)1425        return self._client.messages.create(**payload)14261427    async def _acreate(self, payload: dict) -> Any:1428        if "betas" in payload:1429            return await self._async_client.beta.messages.create(**payload)1430        return await self._async_client.messages.create(**payload)14311432    def _stream(1433        self,1434        messages: list[BaseMessage],1435        stop: list[str] | None = None,1436        run_manager: CallbackManagerForLLMRun | None = None,1437        *,1438        stream_usage: bool | None = None,1439        **kwargs: Any,1440    ) -> Iterator[ChatGenerationChunk]:1441        if stream_usage is None:1442            stream_usage = self.stream_usage1443        kwargs["stream"] = True1444        payload = self._get_request_payload(messages, stop=stop, **kwargs)1445        try:1446            stream = self._create(payload)1447            coerce_content_to_string = (1448                not _tools_in_params(payload)1449                and not _documents_in_params(payload)1450                and not _thinking_in_params(payload)1451                and not _compact_in_params(payload)1452            )1453            block_start_event = None1454            for event in stream:1455                msg, block_start_event = self._make_message_chunk_from_anthropic_event(1456                    event,1457                    stream_usage=stream_usage,1458                    coerce_content_to_string=coerce_content_to_string,1459                    block_start_event=block_start_event,1460                )1461                if msg is not None:1462                    chunk = ChatGenerationChunk(message=msg)1463                    if run_manager and isinstance(msg.content, str):1464                        run_manager.on_llm_new_token(msg.content, chunk=chunk)1465                    yield chunk1466        except anthropic.BadRequestError as e:1467            _handle_anthropic_bad_request(e)14681469    async def _astream(1470        self,1471        messages: list[BaseMessage],1472        stop: list[str] | None = None,1473        run_manager: AsyncCallbackManagerForLLMRun | None = None,1474        *,1475        stream_usage: bool | None = None,1476        **kwargs: Any,1477    ) -> AsyncIterator[ChatGenerationChunk]:1478        if stream_usage is None:1479            stream_usage = self.stream_usage1480        kwargs["stream"] = True1481        payload = self._get_request_payload(messages, stop=stop, **kwargs)1482        try:1483            stream = await self._acreate(payload)1484            coerce_content_to_string = (1485                not _tools_in_params(payload)1486                and not _documents_in_params(payload)1487                and not _thinking_in_params(payload)1488                and not _compact_in_params(payload)1489            )1490            block_start_event = None1491            async for event in stream:1492                msg, block_start_event = self._make_message_chunk_from_anthropic_event(1493                    event,1494                    stream_usage=stream_usage,1495                    coerce_content_to_string=coerce_content_to_string,1496                    block_start_event=block_start_event,1497                )1498                if msg is not None:1499                    chunk = ChatGenerationChunk(message=msg)1500                    if run_manager and isinstance(msg.content, str):1501                        await run_manager.on_llm_new_token(msg.content, chunk=chunk)1502                    yield chunk1503        except anthropic.BadRequestError as e:1504            _handle_anthropic_bad_request(e)15051506    def _make_message_chunk_from_anthropic_event(1507        self,1508        event: anthropic.types.RawMessageStreamEvent,1509        *,1510        stream_usage: bool = True,1511        coerce_content_to_string: bool,1512        block_start_event: anthropic.types.RawMessageStreamEvent | None = None,1513    ) -> tuple[AIMessageChunk | None, anthropic.types.RawMessageStreamEvent | None]:1514        """Convert Anthropic streaming event to `AIMessageChunk`.15151516        Args:1517            event: Raw streaming event from Anthropic SDK1518            stream_usage: Whether to include usage metadata in the output chunks.1519            coerce_content_to_string: Whether to convert structured content to plain1520                text strings.15211522                When `True`, only text content is preserved; when `False`, structured1523                content like tool calls and citations are maintained.1524            block_start_event: Previous content block start event, used for tracking1525                tool use blocks and maintaining context across related events.15261527        Returns:1528            Tuple with1529                - `AIMessageChunk`: Converted message chunk with appropriate content and1530                    metadata, or `None` if the event doesn't produce a chunk1531                - `RawMessageStreamEvent`: Updated `block_start_event` for tracking1532                    content blocks across sequential events, or `None` if not applicable15331534        Note:1535            Not all Anthropic events result in message chunks. Events like internal1536            state changes return `None` for the message chunk while potentially1537            updating the `block_start_event` for context tracking.1538        """1539        message_chunk: AIMessageChunk | None = None1540        # Reference: Anthropic SDK streaming implementation1541        # https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/lib/streaming/_messages.py  # noqa: E5011542        if event.type == "message_start" and stream_usage:1543            # Capture model name, but don't include usage_metadata yet1544            # as it will be properly reported in message_delta with complete info1545            if hasattr(event.message, "model"):1546                response_metadata: dict[str, Any] = {"model_name": event.message.model}1547            else:1548                response_metadata = {}15491550            message_chunk = AIMessageChunk(1551                content="" if coerce_content_to_string else [],1552                response_metadata=response_metadata,1553            )15541555        elif (1556            event.type == "content_block_start"1557            and event.content_block is not None1558            and (1559                "tool_result" in event.content_block.type1560                or "tool_use" in event.content_block.type1561                or "document" in event.content_block.type1562                or "redacted_thinking" in event.content_block.type1563            )1564        ):1565            if coerce_content_to_string:1566                warnings.warn("Received unexpected tool content block.", stacklevel=2)15671568            content_block = event.content_block.model_dump()1569            if "caller" in content_block and content_block["caller"] is None:1570                content_block.pop("caller")1571            content_block["index"] = event.index1572            if event.content_block.type == "tool_use":1573                if (1574                    parsed_args := getattr(event.content_block, "input", None)1575                ) and isinstance(parsed_args, dict):1576                    # In some cases parsed args are represented in start event, with no1577                    # following input_json_delta events1578                    args = json.dumps(parsed_args)1579                else:1580                    args = ""1581                tool_call_chunk = create_tool_call_chunk(1582                    index=event.index,1583                    id=event.content_block.id,1584                    name=event.content_block.name,1585                    args=args,1586                )1587                tool_call_chunks = [tool_call_chunk]1588            else:1589                tool_call_chunks = []1590            message_chunk = AIMessageChunk(1591                content=[content_block],1592                tool_call_chunks=tool_call_chunks,1593            )1594            block_start_event = event15951596        # Process incremental content updates1597        elif event.type == "content_block_delta":1598            # Text and citation deltas (incremental text content)1599            if event.delta.type in ("text_delta", "citations_delta"):1600                if coerce_content_to_string and hasattr(event.delta, "text"):1601                    text = getattr(event.delta, "text", "")1602                    message_chunk = AIMessageChunk(content=text)1603                else:1604                    content_block = event.delta.model_dump()1605                    content_block["index"] = event.index16061607                    # All citation deltas are part of a text block1608                    content_block["type"] = "text"1609                    if "citation" in content_block:1610                        # Assign citations to a list if present1611                        content_block["citations"] = [content_block.pop("citation")]1612                    message_chunk = AIMessageChunk(content=[content_block])16131614            # Reasoning1615            elif event.delta.type in {"thinking_delta", "signature_delta"}:1616                content_block = event.delta.model_dump()1617                content_block["index"] = event.index1618                content_block["type"] = "thinking"1619                message_chunk = AIMessageChunk(content=[content_block])16201621            # Tool input JSON (streaming tool arguments)1622            elif event.delta.type == "input_json_delta":1623                content_block = event.delta.model_dump()1624                content_block["index"] = event.index1625                start_event_block = (1626                    getattr(block_start_event, "content_block", None)1627                    if block_start_event1628                    else None1629                )1630                if (1631                    start_event_block is not None1632                    and getattr(start_event_block, "type", None) == "tool_use"1633                ):1634                    tool_call_chunk = create_tool_call_chunk(1635                        index=event.index,1636                        id=None,1637                        name=None,1638                        args=event.delta.partial_json,1639                    )1640                    tool_call_chunks = [tool_call_chunk]1641                else:1642                    tool_call_chunks = []1643                message_chunk = AIMessageChunk(1644                    content=[content_block],1645                    tool_call_chunks=tool_call_chunks,1646                )16471648            # Compaction block1649            elif event.delta.type == "compaction_delta":1650                content_block = event.delta.model_dump()1651                content_block["index"] = event.index1652                content_block["type"] = "compaction"1653                if (1654                    "encrypted_content" in content_block1655                    and content_block["encrypted_content"] is None1656                ):1657                    content_block.pop("encrypted_content")1658                message_chunk = AIMessageChunk(content=[content_block])16591660        # Process final usage metadata and completion info1661        elif event.type == "message_delta" and stream_usage:1662            usage_metadata = _create_usage_metadata(event.usage)1663            response_metadata = {1664                "stop_reason": event.delta.stop_reason,1665                "stop_sequence": event.delta.stop_sequence,1666            }1667            if context_management := getattr(event, "context_management", None):1668                response_metadata["context_management"] = (1669                    context_management.model_dump()1670                )1671            message_delta = getattr(event, "delta", None)1672            if message_delta and (1673                container := getattr(message_delta, "container", None)1674            ):1675                response_metadata["container"] = container.model_dump(mode="json")1676            message_chunk = AIMessageChunk(1677                content="" if coerce_content_to_string else [],1678                usage_metadata=usage_metadata,1679                response_metadata=response_metadata,1680            )1681            if message_chunk.response_metadata.get("stop_reason"):1682                # Mark final Anthropic stream chunk1683                message_chunk.chunk_position = "last"1684        # Unhandled event types (e.g., `content_block_stop`, `ping` events)1685        # https://platform.claude.com/docs/en/build-with-claude/streaming#other-events1686        else:1687            pass16881689        if message_chunk:1690            message_chunk.response_metadata["model_provider"] = "anthropic"1691        return message_chunk, block_start_event16921693    def _format_output(self, data: Any, **kwargs: Any) -> ChatResult:1694        """Format the output from the Anthropic API to LC."""1695        data_dict = data.model_dump()1696        content = data_dict["content"]16971698        # Remove citations if they are None - introduced in anthropic sdk 0.451699        for block in content:1700            if isinstance(block, dict):1701                if "citations" in block and block["citations"] is None:1702                    block.pop("citations")1703                if "caller" in block and block["caller"] is None:1704                    block.pop("caller")1705                if "encrypted_content" in block and block["encrypted_content"] is None:1706                    block.pop("encrypted_content")1707                if (1708                    block.get("type") == "thinking"1709                    and "text" in block1710                    and block["text"] is None1711                ):1712                    block.pop("text")17131714        llm_output = {1715            k: v for k, v in data_dict.items() if k not in ("content", "role", "type")1716        }1717        if (1718            (container := llm_output.get("container"))1719            and isinstance(container, dict)1720            and (expires_at := container.get("expires_at"))1721            and isinstance(expires_at, datetime.datetime)1722        ):1723            # TODO: dump all `data` with `mode="json"`1724            llm_output["container"]["expires_at"] = expires_at.isoformat()1725        response_metadata = {"model_provider": "anthropic"}1726        if "model" in llm_output and "model_name" not in llm_output:1727            llm_output["model_name"] = llm_output["model"]1728        if (1729            len(content) == 11730            and content[0]["type"] == "text"1731            and not content[0].get("citations")1732        ):1733            msg = AIMessage(1734                content=content[0]["text"], response_metadata=response_metadata1735            )1736        elif any(block["type"] == "tool_use" for block in content):1737            tool_calls = extract_tool_calls(content)1738            msg = AIMessage(1739                content=content,1740                tool_calls=tool_calls,1741                response_metadata=response_metadata,1742            )1743        else:1744            msg = AIMessage(content=content, response_metadata=response_metadata)1745        msg.usage_metadata = _create_usage_metadata(data.usage)1746        return ChatResult(1747            generations=[ChatGeneration(message=msg)],1748            llm_output=llm_output,1749        )17501751    def _generate(1752        self,1753        messages: list[BaseMessage],1754        stop: list[str] | None = None,1755        run_manager: CallbackManagerForLLMRun | None = None,1756        **kwargs: Any,1757    ) -> ChatResult:1758        payload = self._get_request_payload(messages, stop=stop, **kwargs)1759        try:1760            data = self._create(payload)1761        except anthropic.BadRequestError as e:1762            _handle_anthropic_bad_request(e)1763        return self._format_output(data, **kwargs)17641765    async def _agenerate(1766        self,1767        messages: list[BaseMessage],1768        stop: list[str] | None = None,1769        run_manager: AsyncCallbackManagerForLLMRun | None = None,1770        **kwargs: Any,1771    ) -> ChatResult:1772        payload = self._get_request_payload(messages, stop=stop, **kwargs)1773        try:1774            data = await self._acreate(payload)1775        except anthropic.BadRequestError as e:1776            _handle_anthropic_bad_request(e)1777        return self._format_output(data, **kwargs)17781779    def _get_llm_for_structured_output_when_thinking_is_enabled(1780        self,1781        schema: dict | type,1782        formatted_tool: AnthropicTool,1783    ) -> Runnable[LanguageModelInput, BaseMessage]:1784        thinking_admonition = (1785            "You are attempting to use structured output via forced tool calling, "1786            "which is not guaranteed when `thinking` is enabled. This method will "1787            "raise an OutputParserException if tool calls are not generated. Consider "1788            "disabling `thinking` or adjust your prompt to ensure the tool is called."1789        )1790        warnings.warn(thinking_admonition, stacklevel=2)1791        llm = self.bind_tools(1792            [schema],1793            # We don't specify tool_choice here since the API will reject attempts to1794            # force tool calls when thinking=true1795            ls_structured_output_format={1796                "kwargs": {"method": "function_calling"},1797                "schema": formatted_tool,1798            },1799        )18001801        def _raise_if_no_tool_calls(message: AIMessage) -> AIMessage:1802            if not message.tool_calls:1803                raise OutputParserException(thinking_admonition)1804            return message18051806        return llm | _raise_if_no_tool_calls18071808    def bind_tools(1809        self,1810        tools: Sequence[Mapping[str, Any] | type | Callable | BaseTool],1811        *,1812        tool_choice: dict[str, str] | str | None = None,1813        parallel_tool_calls: bool | None = None,1814        strict: bool | None = None,1815        **kwargs: Any,1816    ) -> Runnable[LanguageModelInput, AIMessage]:1817        r"""Bind tool-like objects to `ChatAnthropic`.18181819        Args:1820            tools: A list of tool definitions to bind to this chat model.18211822                Supports Anthropic format tool schemas and any tool definition handled1823                by [`convert_to_openai_tool`][langchain_core.utils.function_calling.convert_to_openai_tool].1824            tool_choice: Which tool to require the model to call. Options are:18251826                - Name of the tool as a string or as dict `{"type": "tool", "name": "<<tool_name>>"}`: calls corresponding tool1827                - `'auto'`, `{"type: "auto"}`, or `None`: automatically selects a tool (including no tool)1828                - `'any'` or `{"type: "any"}`: force at least one tool to be called1829            parallel_tool_calls: Set to `False` to disable parallel tool use.18301831                Defaults to `None` (no specification, which allows parallel tool use).18321833                !!! version-added "Added in `langchain-anthropic` 0.3.2"1834            strict: If `True`, Claude's schema adherence is applied to tool calls.18351836                See the [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#strict-tool-use) for more info.1837            kwargs: Any additional parameters are passed directly to `bind`.18381839        Example:1840            ```python1841            from langchain_anthropic import ChatAnthropic1842            from pydantic import BaseModel, Field184318441845            class GetWeather(BaseModel):1846                '''Get the current weather in a given location'''18471848                location: str = Field(..., description="The city and state, e.g. San Francisco, CA")184918501851            class GetPrice(BaseModel):1852                '''Get the price of a specific product.'''18531854                product: str = Field(..., description="The product to look up.")185518561857            model = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0)1858            model_with_tools = model.bind_tools([GetWeather, GetPrice])1859            model_with_tools.invoke(1860                "What is the weather like in San Francisco",1861            )1862            # -> AIMessage(1863            #     content=[1864            #         {'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'},1865            #         {'text': None, 'type': 'tool_use', 'id': 'toolu_01SCgExKzQ7eqSkMHfygvYuu', 'name': 'GetWeather', 'input': {'location': 'San Francisco, CA'}}1866            #     ],1867            #     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}},1868            #     id='run-87b1331e-9251-4a68-acef-f0a018b639cc-0'1869            # )1870            ```1871        """  # noqa: E5011872        # Allows built-in tools either by their:1873        # - Raw `dict` format1874        # - Extracting extras["provider_tool_definition"] if provided on a BaseTool1875        formatted_tools = [1876            tool1877            if _is_builtin_tool(tool)1878            else convert_to_anthropic_tool(tool, strict=strict)1879            for tool in tools1880        ]1881        if not tool_choice:1882            pass1883        elif isinstance(tool_choice, dict):1884            kwargs["tool_choice"] = tool_choice1885        elif isinstance(tool_choice, str) and tool_choice in ("any", "auto"):1886            kwargs["tool_choice"] = {"type": tool_choice}1887        elif isinstance(tool_choice, str):1888            kwargs["tool_choice"] = {"type": "tool", "name": tool_choice}1889        else:1890            msg = (1891                f"Unrecognized 'tool_choice' type {tool_choice=}. Expected dict, "1892                f"str, or None."1893            )1894            raise ValueError(1895                msg,1896            )18971898        # Anthropic API rejects forced tool use when thinking is enabled:1899        # "Thinking may not be enabled when tool_choice forces tool use."1900        # Drop forced tool_choice and warn, matching the behavior in1901        # _get_llm_for_structured_output_when_thinking_is_enabled.1902        if (1903            self.thinking is not None1904            and self.thinking.get("type") in ("enabled", "adaptive")1905            and "tool_choice" in kwargs1906            and kwargs["tool_choice"].get("type") in ("any", "tool")1907        ):1908            warnings.warn(1909                "tool_choice is forced but thinking is enabled. The Anthropic "1910                "API does not support forced tool use with thinking. "1911                "Dropping tool_choice to avoid an API error. Tool calls are "1912                "not guaranteed. Consider disabling thinking or adjusting "1913                "your prompt to ensure the tool is called.",1914                stacklevel=2,1915            )1916            del kwargs["tool_choice"]19171918        if parallel_tool_calls is not None:1919            disable_parallel_tool_use = not parallel_tool_calls1920            if "tool_choice" in kwargs:1921                kwargs["tool_choice"]["disable_parallel_tool_use"] = (1922                    disable_parallel_tool_use1923                )1924            else:1925                kwargs["tool_choice"] = {1926                    "type": "auto",1927                    "disable_parallel_tool_use": disable_parallel_tool_use,1928                }19291930        return self.bind(tools=formatted_tools, **kwargs)19311932    def with_structured_output(1933        self,1934        schema: dict | type,1935        *,1936        include_raw: bool = False,1937        method: Literal["function_calling", "json_schema"] = "function_calling",1938        **kwargs: Any,1939    ) -> Runnable[LanguageModelInput, dict | BaseModel]:1940        """Model wrapper that returns outputs formatted to match the given schema.19411942        See the [LangChain docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#structured-output)1943        for more details and examples.19441945        Args:1946            schema: The output schema. Can be passed in as:19471948                - An Anthropic tool schema,1949                - An OpenAI function/tool schema,1950                - A JSON Schema,1951                - A `TypedDict` class,1952                - Or a Pydantic class.19531954                If `schema` is a Pydantic class then the model output will be a1955                Pydantic instance of that class, and the model-generated fields will be1956                validated by the Pydantic class. Otherwise the model output will be a1957                dict and will not be validated.19581959                See `langchain_core.utils.function_calling.convert_to_openai_tool` for1960                more on how to properly specify types and descriptions of schema fields1961                when specifying a Pydantic or `TypedDict` class.1962            include_raw:1963                If `False` then only the parsed structured output is returned.19641965                If an error occurs during model output parsing it will be raised.19661967                If `True` then both the raw model response (a `BaseMessage`) and the1968                parsed model response will be returned.19691970                If an error occurs during output parsing it will be caught and returned1971                as well.19721973                The final output is always a `dict` with keys `'raw'`, `'parsed'`, and1974                `'parsing_error'`.1975            method: The structured output method to use. Options are:19761977                - `'function_calling'` (default): Use forced tool calling to get1978                    structured output.1979                - `'json_schema'`: Use Claude's dedicated1980                    [structured output](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)1981                    feature.19821983            kwargs: Additional keyword arguments are ignored.19841985        Returns:1986            A `Runnable` that takes same inputs as a1987                `langchain_core.language_models.chat.BaseChatModel`.19881989                If `include_raw` is `False` and `schema` is a Pydantic class, `Runnable`1990                outputs an instance of `schema` (i.e., a Pydantic object). Otherwise, if1991                `include_raw` is `False` then `Runnable` outputs a `dict`.19921993                If `include_raw` is `True`, then `Runnable` outputs a `dict` with keys:19941995                - `'raw'`: `BaseMessage`1996                - `'parsed'`: `None` if there was a parsing error, otherwise the type1997                    depends on the `schema` as described above.1998                - `'parsing_error'`: `BaseException | None`19992000        Example:

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.