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