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