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