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 _convert_from_v1_to_anthropic87from langchain_anthropic._sdk_compat import (88 _aparse,89 _route_unsupported_sampling_params,90)91from langchain_anthropic.data._profiles import _PROFILES92from langchain_anthropic.output_parsers import extract_tool_calls9394_message_type_lookups = {95 "human": "user",96 "ai": "assistant",97 "AIMessageChunk": "assistant",98 "HumanMessageChunk": "user",99}100101_MODEL_PROFILES = cast(ModelProfileRegistry, _PROFILES)102103_USER_AGENT: Final[str] = f"langchain-anthropic/{__version__}"104105106def _add_gateway_metadata(generation_info: dict[str, Any], raw_response: Any) -> None:107 """Add parsed LangSmith gateway metadata to `generation_info`, if present.108109 Args:110 generation_info: Generation info to mutate in place.111 raw_response: The raw provider response, or None.112 """113 headers = getattr(raw_response, "headers", None)114 if headers is None:115 return116 gateway_metadata = _parse_gateway_metadata(headers)117 if gateway_metadata is not None:118 generation_info[GATEWAY_METADATA_RESPONSE_KEY] = gateway_metadata119120121def _get_default_model_profile(model_name: str) -> ModelProfile:122 """Get the default profile for a model.123124 Args:125 model_name: The model identifier.126127 Returns:128 The model profile dictionary, or an empty dict if not found.129 """130 default = _MODEL_PROFILES.get(model_name)131 if default:132 return default.copy()133 return {}134135136_FALLBACK_MAX_OUTPUT_TOKENS: Final[int] = 4096137138139class AnthropicTool(TypedDict):140 """Anthropic tool definition for custom (user-defined) tools.141142 Custom tools use `name` and `input_schema` fields to define the tool's143 interface. These are converted from LangChain tool formats (functions, Pydantic144 models, `BaseTool` objects) via `convert_to_anthropic_tool`.145 """146147 name: str148149 input_schema: dict[str, Any]150151 description: NotRequired[str]152153 strict: NotRequired[bool]154155 cache_control: NotRequired[dict[str, str]]156157 defer_loading: NotRequired[bool]158159 input_examples: NotRequired[list[dict[str, Any]]]160161 allowed_callers: NotRequired[list[str]]162163164# ---------------------------------------------------------------------------165# Built-in Tool Support166# ---------------------------------------------------------------------------167# When Anthropic releases new built-in tools, two places may need updating:168#169# 1. _TOOL_TYPE_TO_BETA (below) - Add mapping if the tool requires a beta header.170# Not all tools need this; only add if the API requires a beta header.171#172# 2. _is_builtin_tool() - Add the tool type prefix to _BUILTIN_TOOL_PREFIXES.173# This ensures the tool dict is passed through to the API unchanged (instead174# of being converted via convert_to_anthropic_tool, which may fail).175# ---------------------------------------------------------------------------176177_TOOL_TYPE_TO_BETA: dict[str, str] = {178 "web_fetch_20250910": "web-fetch-2025-09-10",179 "code_execution_20250522": "code-execution-2025-05-22",180 "mcp_toolset": "mcp-client-2025-11-20",181 "memory_20250818": "context-management-2025-06-27",182 "computer_20250124": "computer-use-2025-01-24",183 "computer_20251124": "computer-use-2025-11-24",184 "tool_search_tool_regex_20251119": "advanced-tool-use-2025-11-20",185 "tool_search_tool_bm25_20251119": "advanced-tool-use-2025-11-20",186 "advisor_20260301": "advisor-tool-2026-03-01",187}188"""Mapping of tool type to required beta header.189190Some tool types require specific beta headers to be enabled.191"""192193_BUILTIN_TOOL_PREFIXES = [194 "text_editor_",195 "computer_",196 "bash_",197 "web_search_",198 "web_fetch_",199 "code_execution_",200 "mcp_toolset",201 "memory_",202 "tool_search_",203 "advisor_",204]205206_ANTHROPIC_EXTRA_FIELDS: set[str] = {207 "allowed_callers",208 "cache_control",209 "defer_loading",210 "eager_input_streaming",211 "input_examples",212}213"""Valid Anthropic-specific extra fields"""214215216def _is_builtin_tool(tool: Any) -> TypeGuard[dict[str, Any]]:217 """Check if a tool is a built-in (server-side) Anthropic tool.218219 `tool` must be a `dict` and have a `type` key starting with one of the known220 built-in tool prefixes.221222 [Claude docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview)223 """224 if not isinstance(tool, dict):225 return False226227 tool_type = tool.get("type")228 if not tool_type or not isinstance(tool_type, str):229 return False230231 return any(tool_type.startswith(prefix) for prefix in _BUILTIN_TOOL_PREFIXES)232233234def _format_image(url: str) -> dict:235 """Convert part["image_url"]["url"] strings (OpenAI format) to Anthropic format.236237 {238 "type": "base64",239 "media_type": "image/jpeg",240 "data": "/9j/4AAQSkZJRg...",241 }242243 Or244245 {246 "type": "url",247 "url": "https://example.com/image.jpg",248 }249 """250 # Base64 encoded image251 base64_regex = r"^data:(?P<media_type>image/.+);base64,(?P<data>.+)$"252 base64_match = re.match(base64_regex, url)253254 if base64_match:255 return {256 "type": "base64",257 "media_type": base64_match.group("media_type"),258 "data": base64_match.group("data"),259 }260261 # Url262 url_regex = r"^https?://.*$"263 url_match = re.match(url_regex, url)264265 if url_match:266 return {267 "type": "url",268 "url": url,269 }270271 msg = (272 "Malformed url parameter."273 " Must be either an image URL (https://example.com/image.jpg)"274 " or base64 encoded string (data:image/png;base64,'/9j/4AAQSk'...)"275 )276 raise ValueError(277 msg,278 )279280281_TOOL_CALL_ID_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")282"""Anthropic requires `tool_use`/`tool_result` IDs to match this pattern."""283284285def _normalize_tool_call_id(tool_call_id: str | None) -> str | None:286 """Map a tool-call ID to an Anthropic-compatible form if needed.287288 Anthropic rejects `tool_use`/`tool_result` IDs that don't match289 `^[a-zA-Z0-9_-]+$`. IDs minted by other providers can violate this when a290 thread is replayed across providers (e.g. Fireworks/Kimi emits291 `functions.write_todos:0`, whose `.` and `:` are invalid). Valid IDs are292 returned unchanged; invalid ones are hashed deterministically so that a293 rewritten `tool_use.id` and its paired `tool_use_id` resolve to the same294 value, both within a request and across turns.295296 Empty and `None` IDs are passed through unchanged so that a genuinely297 malformed request surfaces as a clear error from Anthropic rather than298 being masked by a synthesized ID.299300 Args:301 tool_call_id: The tool-call ID to normalize.302303 Returns:304 The original ID if it is empty, `None`, or already valid; otherwise a305 deterministic Anthropic-compatible replacement.306 """307 if not tool_call_id or _TOOL_CALL_ID_PATTERN.match(tool_call_id):308 return tool_call_id309 digest = hashlib.sha256(tool_call_id.encode()).hexdigest()310 return f"toolu_{digest[:24]}"311312313def _normalize_block_tool_use_id(block: dict) -> dict:314 """Return `block` with its `tool_use_id` normalized, if it carries one.315316 Mirrors `_normalize_tool_call_id` for `tool_result`-style content blocks so317 that a `tool_use_id` arriving pre-structured (e.g. on a `ToolMessage` whose318 content is already a list of `tool_result` blocks) stays consistent with its319 paired, normalized `tool_use.id`. A no-op for already-valid IDs.320 """321 if "tool_use_id" in block:322 return {**block, "tool_use_id": _normalize_tool_call_id(block["tool_use_id"])}323 return block324325326def _merge_messages(327 messages: Sequence[BaseMessage],328) -> list[SystemMessage | AIMessage | HumanMessage]:329 """Merge runs of human/tool messages into single human messages with content blocks.""" # noqa: E501330 merged: list = []331 for curr in messages:332 if isinstance(curr, ToolMessage):333 if (334 isinstance(curr.content, list)335 and curr.content336 and all(337 isinstance(block, dict) and block.get("type") == "tool_result"338 for block in curr.content339 )340 ):341 curr = HumanMessage(curr.content) # type: ignore[misc]342 else:343 tool_content = curr.content344 cache_ctrl = None345 # Extract cache_control from content blocks and hoist it346 # to the tool_result level. Anthropic's API does not347 # support cache_control on tool_result content sub-blocks.348 if isinstance(tool_content, list):349 cleaned = []350 for block in tool_content:351 if isinstance(block, dict) and "cache_control" in block:352 cache_ctrl = block["cache_control"]353 block = {354 k: v for k, v in block.items() if k != "cache_control"355 }356 cleaned.append(block)357 tool_content = cleaned358 tool_result: dict = {359 "type": "tool_result",360 "content": tool_content,361 "tool_use_id": _normalize_tool_call_id(curr.tool_call_id),362 "is_error": curr.status == "error",363 }364 if cache_ctrl:365 tool_result["cache_control"] = cache_ctrl366 curr = HumanMessage( # type: ignore[misc]367 [tool_result],368 )369 last = merged[-1] if merged else None370 if any(371 all(isinstance(m, c) for m in (curr, last))372 for c in (SystemMessage, HumanMessage)373 ):374 if isinstance(cast("BaseMessage", last).content, str):375 new_content: list = [376 {"type": "text", "text": cast("BaseMessage", last).content},377 ]378 else:379 new_content = copy.copy(cast("list", cast("BaseMessage", last).content))380 if isinstance(curr.content, str):381 new_content.append({"type": "text", "text": curr.content})382 else:383 new_content.extend(curr.content)384 merged[-1] = curr.model_copy(update={"content": new_content})385 else:386 merged.append(curr)387 return merged388389390def _format_data_content_block(block: dict) -> dict:391 """Format standard data content block to format expected by Anthropic."""392 if block["type"] == "image":393 if "url" in block:394 if block["url"].startswith("data:"):395 # Data URI396 formatted_block = {397 "type": "image",398 "source": _format_image(block["url"]),399 }400 else:401 formatted_block = {402 "type": "image",403 "source": {"type": "url", "url": block["url"]},404 }405 elif "base64" in block or block.get("source_type") == "base64":406 formatted_block = {407 "type": "image",408 "source": {409 "type": "base64",410 "media_type": block["mime_type"],411 "data": block.get("base64") or block.get("data", ""),412 },413 }414 elif "file_id" in block:415 formatted_block = {416 "type": "image",417 "source": {418 "type": "file",419 "file_id": block["file_id"],420 },421 }422 elif block.get("source_type") == "id":423 formatted_block = {424 "type": "image",425 "source": {426 "type": "file",427 "file_id": block["id"],428 },429 }430 else:431 msg = (432 "Anthropic only supports 'url', 'base64', or 'id' keys for image "433 "content blocks."434 )435 raise ValueError(436 msg,437 )438439 elif block["type"] == "file":440 if "url" in block:441 formatted_block = {442 "type": "document",443 "source": {444 "type": "url",445 "url": block["url"],446 },447 }448 elif "base64" in block or block.get("source_type") == "base64":449 formatted_block = {450 "type": "document",451 "source": {452 "type": "base64",453 "media_type": block.get("mime_type") or "application/pdf",454 "data": block.get("base64") or block.get("data", ""),455 },456 }457 elif block.get("source_type") == "text":458 formatted_block = {459 "type": "document",460 "source": {461 "type": "text",462 "media_type": block.get("mime_type") or "text/plain",463 "data": block["text"],464 },465 }466 elif "file_id" in block:467 formatted_block = {468 "type": "document",469 "source": {470 "type": "file",471 "file_id": block["file_id"],472 },473 }474 elif block.get("source_type") == "id":475 formatted_block = {476 "type": "document",477 "source": {478 "type": "file",479 "file_id": block["id"],480 },481 }482 else:483 msg = (484 "Anthropic only supports 'url', 'base64', or 'id' keys for file "485 "content blocks."486 )487 raise ValueError(msg)488489 elif block["type"] == "text-plain":490 formatted_block = {491 "type": "document",492 "source": {493 "type": "text",494 "media_type": block.get("mime_type") or "text/plain",495 "data": block["text"],496 },497 }498499 else:500 msg = f"Block of type {block['type']} is not supported."501 raise ValueError(msg)502503 if formatted_block:504 for key in ["cache_control", "citations", "title", "context"]:505 if key in block:506 formatted_block[key] = block[key]507 elif (metadata := block.get("extras")) and key in metadata:508 formatted_block[key] = metadata[key]509 elif (metadata := block.get("metadata")) and key in metadata:510 # Backward compat511 formatted_block[key] = metadata[key]512513 return formatted_block514515516def _format_text_block(block: dict) -> dict:517 """Narrow a text content block to fields supported by Anthropic's API.518519 Drops LangChain-internal fields (e.g. the ``id`` minted by520 ``create_text_block``) that Anthropic rejects as extra inputs.521 """522 formatted_block = {523 k: v524 for k, v in block.items()525 if k in ("type", "text", "cache_control", "citations")526 }527 # Clean up citations to remove null file_id fields528 if formatted_block.get("citations"):529 cleaned_citations = []530 for citation in formatted_block["citations"]:531 cleaned_citation = {532 k: v for k, v in citation.items() if not (k == "file_id" and v is None)533 }534 cleaned_citations.append(cleaned_citation)535 formatted_block["citations"] = cleaned_citations536 return formatted_block537538539def _format_messages(540 messages: Sequence[BaseMessage],541) -> tuple[str | list[dict] | None, list[dict]]:542 """Format messages for Anthropic's API."""543 system: str | list[dict] | None = None544 formatted_messages: list[dict] = []545 merged_messages = _merge_messages(messages)546 for _i, message in enumerate(merged_messages):547 if message.type == "system":548 if system is not None:549 msg = "Received multiple non-consecutive system messages."550 raise ValueError(msg)551 if isinstance(message.content, list):552 system = [553 (554 (555 _format_text_block(block)556 if block.get("type") == "text"557 else block558 )559 if isinstance(block, dict)560 else {"type": "text", "text": block}561 )562 for block in message.content563 ]564 else:565 system = message.content566 continue567568 role = _message_type_lookups[message.type]569 content: str | list570571 if not isinstance(message.content, str):572 # parse as dict573 if not isinstance(message.content, list):574 msg = "Anthropic message content must be str or list of dicts"575 raise ValueError(576 msg,577 )578579 # populate content580 content = []581 for block in message.content:582 if isinstance(block, str):583 content.append({"type": "text", "text": block})584 elif isinstance(block, dict):585 if "type" not in block:586 msg = "Dict content block must have a type key"587 raise ValueError(msg)588 if block["type"] in ("reasoning", "function_call") and (589 not isinstance(message, AIMessage)590 or message.response_metadata.get("model_provider")591 != "anthropic"592 ):593 continue594 if block["type"] == "image_url":595 # convert format596 source = _format_image(block["image_url"]["url"])597 content.append({"type": "image", "source": source})598 elif is_data_content_block(block):599 content.append(_format_data_content_block(block))600 elif block["type"] == "tool_use":601 # If a tool_call with the same id as a tool_use content block602 # exists, the tool_call is preferred.603 if (604 isinstance(message, AIMessage)605 and (block["id"] in [tc["id"] for tc in message.tool_calls])606 and not block.get("caller")607 ):608 overlapping = [609 tc610 for tc in message.tool_calls611 if tc["id"] == block["id"]612 ]613 content.extend(614 _lc_tool_calls_to_anthropic_tool_use_blocks(615 overlapping,616 ),617 )618 else:619 if tool_input := block.get("input"):620 args = tool_input621 elif "partial_json" in block:622 try:623 args = json.loads(block["partial_json"] or "{}")624 except json.JSONDecodeError:625 args = {}626 else:627 args = {}628 tool_use_block = _AnthropicToolUse(629 type="tool_use",630 name=block["name"],631 input=args,632 id=cast("str", _normalize_tool_call_id(block["id"])),633 )634 if caller := block.get("caller"):635 tool_use_block["caller"] = caller636 content.append(tool_use_block)637 elif block["type"] in ("server_tool_use", "mcp_tool_use"):638 formatted_block = {639 k: v640 for k, v in block.items()641 if k642 in (643 "type",644 "id",645 "input",646 "name",647 "server_name", # for mcp_tool_use648 "cache_control",649 )650 }651 # Attempt to parse streamed output652 if block.get("input") == {} and "partial_json" in block:653 try:654 input_ = json.loads(block["partial_json"])655 if input_:656 formatted_block["input"] = input_657 except json.JSONDecodeError:658 pass659 content.append(formatted_block)660 elif block["type"] == "text":661 text = block.get("text", "")662 # Only add non-empty strings for now as empty ones are not663 # accepted.664 # https://github.com/anthropics/anthropic-sdk-python/issues/461665 if text.strip():666 content.append(_format_text_block(block))667 elif block["type"] == "thinking":668 formatted_thinking = {669 k: v670 for k, v in block.items()671 if k in ("type", "thinking", "cache_control", "signature")672 }673 if "signature" in formatted_thinking:674 formatted_thinking.setdefault("thinking", "")675 content.append(formatted_thinking)676 elif block["type"] == "redacted_thinking":677 content.append(678 {679 k: v680 for k, v in block.items()681 if k in ("type", "cache_control", "data")682 },683 )684 elif (685 block["type"] == "tool_result"686 and isinstance(block.get("content"), list)687 and any(688 isinstance(item, dict)689 and item.get("type") == "tool_reference"690 for item in block["content"]691 )692 ):693 # Tool search results with tool_reference blocks694 content.append(695 _normalize_block_tool_use_id(696 {697 k: v698 for k, v in block.items()699 if k700 in (701 "type",702 "content",703 "tool_use_id",704 "cache_control",705 )706 },707 ),708 )709 elif block["type"] == "tool_search_tool_result":710 # Omit streaming-only fields, such as `index`, from results.711 content.append(712 _normalize_block_tool_use_id(713 {714 k: v715 for k, v in block.items()716 if k717 in (718 "type",719 "content",720 "tool_use_id",721 "cache_control",722 )723 },724 ),725 )726 elif block["type"] == "tool_result":727 # Regular tool results that need content formatting728 tool_content = _format_messages(729 [HumanMessage(block["content"])],730 )[1][0]["content"]731 content.append(732 _normalize_block_tool_use_id(733 {**block, "content": tool_content},734 ),735 )736 elif block["type"] in (737 "code_execution_tool_result",738 "bash_code_execution_tool_result",739 "text_editor_code_execution_tool_result",740 "mcp_tool_result",741 "web_search_tool_result",742 "web_fetch_tool_result",743 ):744 content.append(745 _normalize_block_tool_use_id(746 {747 k: v748 for k, v in block.items()749 if k750 in (751 "type",752 "content",753 "tool_use_id",754 "is_error", # for mcp_tool_result755 "cache_control",756 "retrieved_at", # for web_fetch_tool_result757 )758 },759 ),760 )761 else:762 content.append(block)763 else:764 msg = (765 f"Content blocks must be str or dict, instead was: "766 f"{type(block)}"767 )768 raise ValueError(769 msg,770 )771 else:772 content = message.content773774 # Ensure all tool_calls have a tool_use content block775 if isinstance(message, AIMessage) and message.tool_calls:776 content = content or []777 content = (778 [{"type": "text", "text": message.content}]779 if isinstance(content, str) and content780 else content781 )782 tool_use_ids = [783 cast("dict", block)["id"]784 for block in content785 if cast("dict", block)["type"] == "tool_use"786 ]787 # `tool_use_ids` are already normalized via the branches above, so788 # compare against the normalized tool-call ID to avoid emitting a789 # duplicate `tool_use` block when the original ID was rewritten.790 missing_tool_calls = [791 tc792 for tc in message.tool_calls793 if _normalize_tool_call_id(tc["id"]) not in tool_use_ids794 ]795 cast("list", content).extend(796 _lc_tool_calls_to_anthropic_tool_use_blocks(missing_tool_calls),797 )798799 if role == "assistant" and _i == len(merged_messages) - 1:800 if isinstance(content, str):801 content = content.rstrip()802 elif (803 isinstance(content, list)804 and content805 and isinstance(content[-1], dict)806 and content[-1].get("type") == "text"807 ):808 content[-1]["text"] = content[-1]["text"].rstrip()809810 if not content and role == "assistant" and _i < len(merged_messages) - 1:811 # anthropic.BadRequestError: Error code: 400: all messages must have812 # non-empty content except for the optional final assistant message813 continue814 formatted_messages.append({"role": role, "content": content})815 return system, formatted_messages816817818def _container_id(container: Any) -> str | None:819 """Return the container ID from either accepted `container` shape."""820 if isinstance(container, str):821 return container822 if isinstance(container, dict):823 return container.get("id")824 return None825826827def _collect_code_execution_tool_ids(formatted_messages: list[dict]) -> set[str]:828 """Collect `tool_use` IDs that were called by `code_execution`.829830 These blocks cannot have `cache_control` applied per Anthropic API831 requirements.832 """833 code_execution_tool_ids: set[str] = set()834835 for message in formatted_messages:836 if message.get("role") != "assistant":837 continue838 content = message.get("content", [])839 if not isinstance(content, list):840 continue841 for block in content:842 if not isinstance(block, dict):843 continue844 if block.get("type") != "tool_use":845 continue846 caller = block.get("caller")847 if isinstance(caller, dict):848 caller_type = caller.get("type", "")849 if caller_type.startswith("code_execution"):850 tool_id = block.get("id")851 if tool_id:852 code_execution_tool_ids.add(tool_id)853854 return code_execution_tool_ids855856857def _is_code_execution_related_block(858 block: dict,859 code_execution_tool_ids: set[str],860) -> bool:861 """Return whether a content block is related to `code_execution`.862863 Returns `True` for blocks that should NOT have `cache_control` applied.864 """865 if not isinstance(block, dict):866 return False867868 block_type = block.get("type")869870 if block_type == "tool_use":871 caller = block.get("caller")872 if isinstance(caller, dict):873 caller_type = caller.get("type", "")874 if caller_type.startswith("code_execution"):875 return True876877 if block_type == "tool_result":878 tool_use_id = block.get("tool_use_id")879 if tool_use_id and tool_use_id in code_execution_tool_ids:880 return True881882 return False883884885def _reasoning_effort_levels(profile: object) -> tuple[str, ...]:886 """Return the reasoning-effort levels declared in a model's profile, if any.887888 Defensive against a missing/malformed profile: an absent `profile`, a889 non-mapping value, or a missing/non-list `reasoning_effort_levels` value is890 treated as "no levels declared" rather than raising.891 """892 if not isinstance(profile, Mapping):893 return ()894 levels = profile.get("reasoning_effort_levels")895 if not isinstance(levels, (list, tuple)):896 return ()897 return tuple(levels)898899900def _is_direct_anthropic_llm_type(llm_type: object) -> bool:901 """Return whether an `_llm_type` reaches Claude via the direct Anthropic API.902903 Only the direct API accepts the top-level `cache_control` request param.904 Subclasses that route through other transports (Bedrock, future backends)905 override `_llm_type` and must expand `cache_control` kwargs into906 block-level breakpoints instead.907908 Non-string `_llm_type` values return `False` rather than raising, so a909 misbehaving subclass falls through to the safer non-direct branch.910 """911 return llm_type == "anthropic-chat"912913914def _apply_cache_control_to_last_eligible_block(915 formatted_messages: list[dict],916 cache_control: Any,917 code_execution_tool_ids: set[str],918) -> bool:919 """Place `cache_control` on the last block eligible for a breakpoint.920921 Walks messages newest-to-oldest and, within each, blocks newest-to-oldest,922 skipping `code_execution`-related blocks (Anthropic rejects breakpoints923 there). String message content is promoted to a single text block so the924 breakpoint can be attached.925926 Returns:927 `True` if a breakpoint was applied, `False` if every candidate was928 `code_execution`-related (caller should warn and drop the kwarg).929 """930 for formatted_message in reversed(formatted_messages):931 content = formatted_message.get("content")932 if isinstance(content, list) and content:933 for block in reversed(content):934 if not isinstance(block, dict):935 continue936 if _is_code_execution_related_block(block, code_execution_tool_ids):937 continue938 block["cache_control"] = cache_control939 return True940 elif isinstance(content, str):941 formatted_message["content"] = [942 {943 "type": "text",944 "text": content,945 "cache_control": cache_control,946 }947 ]948 return True949 return False950951952class AnthropicContextOverflowError(anthropic.BadRequestError, ContextOverflowError):953 """BadRequestError raised when input exceeds Anthropic's context limit."""954955956class AnthropicAuthenticationError(957 anthropic.AuthenticationError, ModelAuthenticationError958):959 """Anthropic authentication error classified as a LangChain model error."""960961962class AnthropicPermissionDeniedError(963 anthropic.PermissionDeniedError, ModelPermissionDeniedError964):965 """Anthropic permission error classified as a LangChain model error."""966967968class AnthropicInvalidRequestError(anthropic.BadRequestError, ModelInvalidRequestError):969 """Anthropic bad-request error classified as a LangChain model error."""970971972class AnthropicModelNotFoundError(anthropic.NotFoundError, ModelNotFoundError):973 """Anthropic not-found error classified as a LangChain model error."""974975976class AnthropicRateLimitError(anthropic.RateLimitError, ModelRateLimitError):977 """Anthropic rate-limit error classified as a LangChain model error."""978979980class AnthropicAPIError(anthropic.InternalServerError, ModelAPIError):981 """Anthropic server error classified as a LangChain model error."""982983984class AnthropicOverloadedError(anthropic.OverloadedError, ModelAPIError):985 """Anthropic overloaded error (HTTP 529) classified as a LangChain model error."""986987988class AnthropicConnectionError(anthropic.APIConnectionError, ModelConnectionError):989 """Anthropic connection error classified as a LangChain model error."""990991992class AnthropicTimeoutError(anthropic.APITimeoutError, ModelTimeoutError):993 """Anthropic timeout error classified as a LangChain model error."""994995996def _raise_if_authentication_error(e: TypeError) -> None:997 """Re-raise anthropic SDK's missing-credentials `TypeError` with guidance."""998 if "Could not resolve authentication method" in str(e):999 msg = (1000 "Anthropic authentication failed: no API key or authorization "1001 "credentials were provided. Set the ANTHROPIC_API_KEY environment "1002 "variable, pass api_key=... to ChatAnthropic, or provide "1003 'credentials via default_headers={"Authorization": ...}. If you '1004 "are routing through the LangSmith gateway, set LANGSMITH_GATEWAY "1005 "and LANGSMITH_GATEWAY_API_KEY."1006 )1007 raise TypeError(msg) from e100810091010def _handle_anthropic_bad_request(e: anthropic.BadRequestError) -> None:1011 """Handle Anthropic BadRequestError."""1012 if "prompt is too long" in e.message:1013 raise AnthropicContextOverflowError(1014 message=e.message, response=e.response, body=e.body1015 ) from e1016 if ("messages: at least one message is required") in e.message:1017 message = "Received only system message(s). "1018 warnings.warn(message, stacklevel=2)1019 raise AnthropicInvalidRequestError(1020 message=e.message, response=e.response, body=e.body1021 ) from e102210231024def _handle_anthropic_api_error(e: anthropic.APIError) -> None:1025 """Re-raise an Anthropic SDK error as its LangChain-classified equivalent."""1026 if isinstance(e, anthropic.AuthenticationError):1027 raise AnthropicAuthenticationError(1028 message=e.message, response=e.response, body=e.body1029 ) from e1030 if isinstance(e, anthropic.PermissionDeniedError):1031 raise AnthropicPermissionDeniedError(1032 message=e.message, response=e.response, body=e.body1033 ) from e1034 if isinstance(e, anthropic.NotFoundError):1035 raise AnthropicModelNotFoundError(1036 message=e.message, response=e.response, body=e.body1037 ) from e1038 if isinstance(e, anthropic.RateLimitError):1039 raise AnthropicRateLimitError(1040 message=e.message, response=e.response, body=e.body1041 ) from e1042 if isinstance(e, anthropic.OverloadedError):1043 raise AnthropicOverloadedError(1044 message=e.message, response=e.response, body=e.body1045 ) from e1046 if isinstance(e, anthropic.InternalServerError):1047 raise AnthropicAPIError(1048 message=e.message, response=e.response, body=e.body1049 ) from e1050 # `APITimeoutError` subclasses `APIConnectionError`, so check it first.1051 if isinstance(e, anthropic.APITimeoutError):1052 raise AnthropicTimeoutError(e.request) from e1053 if isinstance(e, anthropic.APIConnectionError):1054 raise AnthropicConnectionError(message=e.message, request=e.request) from e1055 raise105610571058class ChatAnthropic(BaseChatModel):1059 """Anthropic (Claude) chat models.10601061 See the [LangChain docs for `ChatAnthropic`](https://docs.langchain.com/oss/python/integrations/chat/anthropic)1062 for tutorials, feature walkthroughs, and examples.10631064 See the [Claude Platform docs](https://platform.claude.com/docs/en/about-claude/models/overview)1065 for a list of the latest models, their capabilities, and pricing.10661067 Example:1068 ```python1069 # pip install -U langchain-anthropic1070 # export ANTHROPIC_API_KEY="your-api-key"10711072 from langchain_anthropic import ChatAnthropic10731074 model = ChatAnthropic(1075 model="claude-sonnet-4-5-20250929",1076 # temperature=,1077 # max_tokens=,1078 # timeout=,1079 # max_retries=,1080 # base_url="...",1081 # Refer to API reference for full list of parameters1082 )1083 ```10841085 Note:1086 Any param which is not explicitly supported will be passed directly to1087 [`Anthropic.messages.create(...)`](https://platform.claude.com/docs/en/api/python/messages/create)1088 each time to the model is invoked.1089 """10901091 model_config = ConfigDict(1092 populate_by_name=True,1093 )10941095 model: str = Field(alias="model_name")1096 """Model name to use."""10971098 max_tokens: int | None = Field(default=None, alias="max_tokens_to_sample")1099 """Denotes the number of tokens to predict per generation.11001101 If not specified, this is set dynamically using the model's `max_output_tokens`1102 from its model profile.11031104 See docs on [model profiles](https://docs.langchain.com/oss/python/langchain/models#model-profiles)1105 for more information.1106 """11071108 temperature: float | None = None1109 """A non-negative float that tunes the degree of randomness in generation."""11101111 top_k: int | None = None1112 """Number of most likely tokens to consider at each step."""11131114 top_p: float | None = None1115 """Total probability mass of tokens to consider at each step."""11161117 default_request_timeout: float | None = Field(None, alias="timeout")1118 """Timeout for requests to Claude API."""11191120 # sdk default = 2: https://github.com/anthropics/anthropic-sdk-python?tab=readme-ov-file#retries1121 max_retries: int = 21122 """Number of retries allowed for requests sent to the Claude API."""11231124 stop_sequences: list[str] | None = Field(None, alias="stop")1125 """Default stop sequences."""11261127 anthropic_api_url: str | None = Field(default=None, alias="base_url")1128 """Base URL for API requests. Only specify if using a proxy or service emulator.11291130 If a value isn't passed in, will attempt to read the value first from1131 `ANTHROPIC_API_URL` and if that is not set, `ANTHROPIC_BASE_URL`.11321133 If `LANGSMITH_GATEWAY` is set, it is used as a fallback after those env vars.1134 """11351136 anthropic_api_key: SecretStr = Field(default=SecretStr(""), alias="api_key")1137 """Automatically read from env var `ANTHROPIC_API_KEY` if not provided.11381139 If `LANGSMITH_GATEWAY` is enabled and the base URL points at the gateway,1140 `LANGSMITH_GATEWAY_API_KEY` is used instead.1141 """11421143 anthropic_proxy: str | None = Field(1144 default_factory=from_env("ANTHROPIC_PROXY", default=None)1145 )1146 """Proxy to use for the Anthropic clients, will be used for every API call.11471148 If not provided, will attempt to read from the `ANTHROPIC_PROXY` environment1149 variable.1150 """11511152 default_headers: Mapping[str, str] | None = None1153 """Headers to pass to the Anthropic clients, will be used for every API call."""11541155 betas: list[str] | None = None1156 """List of beta features to enable. If specified, invocations will be routed1157 through `client.beta.messages.create`.11581159 Example: `#!python betas=["token-efficient-tools-2025-02-19"]`1160 """1161 # Can also be passed in w/ model_kwargs, but having it as a param makes better devx1162 #1163 # Precedence order:1164 # 1. Call-time kwargs (e.g., llm.invoke(..., betas=[...]))1165 # 2. model_kwargs (e.g., ChatAnthropic(model_kwargs={"betas": [...]}))1166 # 3. Direct parameter (e.g., ChatAnthropic(betas=[...]))11671168 model_kwargs: dict[str, Any] = Field(default_factory=dict)11691170 streaming: bool = False1171 """Whether to use streaming or not."""11721173 stream_usage: bool = True1174 """Whether to include usage metadata in streaming output.11751176 If `True`, additional message chunks will be generated during the stream including1177 usage metadata.1178 """11791180 thinking: dict[str, Any] | None = Field(default=None)1181 """Parameters for Claude reasoning.11821183 Examples:11841185 - `#!python {"type": "enabled", "budget_tokens": 10_000}` (pre-4.7 models)1186 - `#!python {"type": "adaptive"}` (Opus 4.6+, Opus 5, Sonnet 5)1187 - `#!python {"type": "adaptive", "display": "summarized"}` (Opus 4.7+,1188 Opus 5, Sonnet 5)1189 - `#!python {"type": "disabled"}` (Opus 5 and Sonnet 5, where adaptive1190 thinking is on by default)11911192 !!! note "Claude Opus 4.7+, Opus 5, and Sonnet 5"11931194 `budget_tokens` is removed on these models — use `{"type": "adaptive"}`1195 with `output_config.effort` to control reasoning effort. The default1196 `display` is `"omitted"`; set it to `"summarized"` to receive1197 summarized reasoning in the response. On Opus 5, disabled thinking is1198 supported only at `"high"` effort or below.1199 """12001201 output_config: dict[str, Any] | None = None1202 """Configuration options for the model's output.12031204 Supports the following keys:12051206 - `effort`: Controls how many tokens Claude uses when responding.1207 One of `"max"`, `"xhigh"`, `"high"`, `"medium"`, or `"low"`.1208 - `format`: Structured output format configuration (typically set via1209 `with_structured_output`).1210 - `task_budget`: Advisory token budget for an agentic loop (beta).1211 E.g., `#!python {"type": "tokens", "total": 128_000}`.12121213 Example:12141215 .. code-block:: python12161217 ChatAnthropic(1218 model="claude-opus-4-7",1219 output_config={1220 "effort": "xhigh",1221 "task_budget": {"type": "tokens", "total": 128_000},1222 },1223 )12241225 See Anthropic docs on1226 [extended output](https://platform.claude.com/docs/en/api/go/beta/messages/create).1227 """12281229 reasoning_effort: Literal["max", "xhigh", "high", "medium", "low"] | None = Field(1230 default=None,1231 alias="effort",1232 )1233 """Reasoning effort.12341235 Configures `output_config.effort`. If `thinking` isn't set explicitly,1236 defaults it to `{"type": "adaptive", "display": "summarized"}`. Can also1237 be passed at call time (for example,1238 `model.invoke(..., reasoning_effort="high")`).12391240 !!! note "`effort` alias"12411242 `effort` is also accepted as an alias for this field, at both1243 construction and call time. If both `effort` and `reasoning_effort` are1244 set, `effort` wins (Pydantic's alias-resolution precedence).12451246 !!! note12471248 Setting `reasoning_effort` to `'high'` produces exactly the same behavior1249 as omitting the parameter altogether.12501251 Example: `reasoning_effort="medium"`1252 """12531254 mcp_servers: list[dict[str, Any]] | None = None1255 """List of MCP servers to use for the request.12561257 Example: `#!python mcp_servers=[{"type": "url", "url": "https://mcp.example.com/mcp",1258 "name": "example-mcp"}]`1259 """12601261 context_management: dict[str, Any] | None = None1262 """Configuration for1263 [context management](https://platform.claude.com/docs/en/build-with-claude/context-editing).1264 """12651266 container: dict[str, Any] | str | None = None1267 """Code execution container for the request.12681269 Either a container ID from a previous response, or a dict of container1270 parameters — notably1271 [skills](https://platform.claude.com/docs/en/build-with-claude/skills-guide)1272 to load into the container. Skills require a1273 [code execution](https://docs.langchain.com/oss/python/integrations/chat/anthropic#code-execution)1274 tool to be bound.12751276 ```python1277 model = ChatAnthropic(1278 model="claude-opus-5",1279 container={1280 "skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}]1281 },1282 ).bind_tools([{"type": "code_execution_20260521", "name": "code_execution"}])1283 ```12841285 Can also be passed at call time, which overrides the value set here.1286 """12871288 reuse_last_container: bool | None = None1289 """Automatically reuse container from most recent response (code execution).12901291 When using the built-in1292 [code execution tool](https://docs.langchain.com/oss/python/integrations/chat/anthropic#code-execution),1293 model responses will include container metadata. Set `reuse_last_container=True`1294 to automatically reuse the container from the most recent response for subsequent1295 invocations.1296 """12971298 inference_geo: str | None = None1299 """Controls where model inference runs. See Anthropic's1300 [data residency](https://platform.claude.com/docs/en/build-with-claude/data-residency)1301 docs for more information.1302 """13031304 user_profile_id: str | None = None1305 """User profile ID to attribute the request to.13061307 Use when acting on behalf of a party other than your organization. Setting this1308 automatically enables the required `user-profiles` beta, routing the request1309 through `client.beta.messages.create`.13101311 Can also be passed at call time, which overrides the value set here (for example,1312 `model.invoke(..., user_profile_id="uprof_...")`).1313 """13141315 @property1316 def effort(self) -> Literal["max", "xhigh", "high", "medium", "low"] | None:1317 """Alias for `reasoning_effort`."""1318 return self.reasoning_effort13191320 @property1321 def _llm_type(self) -> str:1322 """Return type of chat model."""1323 return "anthropic-chat"13241325 @property1326 def _uses_gateway(self) -> bool:1327 """Whether requests are routed through the LangSmith gateway."""1328 return self.anthropic_api_key.get_secret_value().startswith("lsv2_")13291330 @property1331 def lc_secrets(self) -> dict[str, str]:1332 """Return a mapping of secret keys to environment variables."""1333 return {1334 "anthropic_api_key": "ANTHROPIC_API_KEY",1335 "mcp_servers": "ANTHROPIC_MCP_SERVERS",1336 }13371338 @classmethod1339 def is_lc_serializable(cls) -> bool:1340 """Whether the class is serializable in langchain."""1341 return True13421343 @classmethod1344 def get_lc_namespace(cls) -> list[str]:1345 """Get the namespace of the LangChain object.13461347 Returns:1348 `["langchain", "chat_models", "anthropic"]`1349 """1350 return ["langchain", "chat_models", "anthropic"]13511352 @property1353 def _identifying_params(self) -> dict[str, Any]:1354 """Get the identifying parameters."""1355 return {1356 "model": self.model,1357 "max_tokens": self.max_tokens,1358 "temperature": self.temperature,1359 "top_k": self.top_k,1360 "top_p": self.top_p,1361 "model_kwargs": self.model_kwargs,1362 "streaming": self.streaming,1363 "max_retries": self.max_retries,1364 "default_request_timeout": self.default_request_timeout,1365 "thinking": self.thinking,1366 "output_config": self.output_config,1367 }13681369 def _get_ls_params(1370 self,1371 stop: list[str] | None = None,1372 **kwargs: Any,1373 ) -> LangSmithParams:1374 """Get standard params for tracing."""1375 params = self._get_invocation_params(stop=stop, **kwargs)1376 ls_params = LangSmithParams(1377 ls_provider="anthropic",1378 ls_model_name=params.get("model", self.model),1379 ls_model_type="chat",1380 ls_temperature=params.get("temperature", self.temperature),1381 )1382 if ls_max_tokens := params.get("max_tokens", self.max_tokens):1383 ls_params["ls_max_tokens"] = ls_max_tokens1384 if ls_stop := stop or params.get("stop", None):1385 ls_params["ls_stop"] = ls_stop1386 return ls_params13871388 @model_validator(mode="before")1389 @classmethod1390 def set_default_max_tokens(cls, values: dict[str, Any]) -> Any:1391 """Set default `max_tokens` from model profile with fallback."""1392 if values.get("max_tokens") is None:1393 model = values.get("model") or values.get("model_name")1394 profile = _get_default_model_profile(model) if model else {}1395 values["max_tokens"] = profile.get(1396 "max_output_tokens", _FALLBACK_MAX_OUTPUT_TOKENS1397 )1398 return values13991400 @model_validator(mode="before")1401 @classmethod1402 def build_extra(cls, values: dict) -> Any:1403 """Build model kwargs."""1404 all_required_field_names = get_pydantic_field_names(cls)1405 return _build_model_kwargs(values, all_required_field_names)14061407 @model_validator(mode="after")1408 def _set_anthropic_version(self) -> Self:1409 """Set package version in metadata."""1410 self._add_version("langchain-anthropic", __version__)1411 return self14121413 @model_validator(mode="before")1414 @classmethod1415 def _resolve_gateway(cls, values: Any) -> Any:1416 """Resolve the base URL and API key, applying LangSmith gateway settings.14171418 An explicit ``base_url``/``api_key`` always wins. Otherwise the base URL1419 falls back to ``ANTHROPIC_API_URL``/``ANTHROPIC_BASE_URL``, then the1420 LangSmith gateway, then the Anthropic default. The gateway key is1421 preferred only when the base URL came from the gateway; for any other1422 endpoint the provider key wins, and the gateway key is a candidate only1423 when the gateway is enabled.1424 """1425 if isinstance(values, dict):1426 _apply_gateway_config(1427 values,1428 cls,1429 base_url_field="anthropic_api_url",1430 api_key_field="anthropic_api_key",1431 provider_path="anthropic",1432 base_url_env=["ANTHROPIC_API_URL", "ANTHROPIC_BASE_URL"],1433 api_key_env="ANTHROPIC_API_KEY",1434 default_base_url="https://api.anthropic.com",1435 )1436 return values14371438 def _resolve_model_profile(self) -> ModelProfile | None:1439 profile = _get_default_model_profile(self.model) or None1440 if profile is not None and self.betas and "context-1m-2025-08-07" in self.betas:1441 profile["max_input_tokens"] = 1_000_0001442 return profile14431444 @cached_property1445 def _client_params(self) -> dict[str, Any]:1446 # Merge User-Agent with user-provided headers (user headers take precedence)1447 default_headers = {"User-Agent": _USER_AGENT}1448 if self.default_headers:1449 default_headers.update(self.default_headers)14501451 client_params: dict[str, Any] = {1452 "api_key": self.anthropic_api_key.get_secret_value(),1453 "base_url": self.anthropic_api_url,1454 "max_retries": self.max_retries,1455 "default_headers": default_headers,1456 }1457 # value <= 0 indicates the param should be ignored. None is a meaningful value1458 # for Anthropic client and treated differently than not specifying the param at1459 # all.1460 if self.default_request_timeout is None or self.default_request_timeout > 0:1461 client_params["timeout"] = self.default_request_timeout14621463 return client_params14641465 @cached_property1466 def _client(self) -> anthropic.Client:1467 client_params = self._client_params1468 http_client_params = {"base_url": client_params["base_url"]}1469 if "timeout" in client_params:1470 http_client_params["timeout"] = client_params["timeout"]1471 if self.anthropic_proxy:1472 http_client_params["anthropic_proxy"] = self.anthropic_proxy1473 http_client = _get_default_httpx_client(**http_client_params)1474 params = {1475 **client_params,1476 "http_client": http_client,1477 }1478 return anthropic.Client(**params)14791480 @cached_property1481 def _async_client(self) -> anthropic.AsyncClient:1482 client_params = self._client_params1483 http_client_params = {"base_url": client_params["base_url"]}1484 if "timeout" in client_params:1485 http_client_params["timeout"] = client_params["timeout"]1486 if self.anthropic_proxy:1487 http_client_params["anthropic_proxy"] = self.anthropic_proxy1488 http_client = _get_default_async_httpx_client(**http_client_params)1489 params = {1490 **client_params,1491 "http_client": http_client,1492 }1493 return anthropic.AsyncClient(**params)14941495 def _assert_valid_model_configuration(self, kwargs: Mapping[str, Any]) -> None:1496 """Validate resolved request configuration against model-specific invariants."""1497 request_config = {**self.model_kwargs, **kwargs}1498 thinking = request_config.get("thinking")1499 if self.thinking is not None:1500 thinking = self.thinking15011502 output_config = dict(self.output_config or {})1503 if self.reasoning_effort:1504 output_config["effort"] = self.reasoning_effort1505 request_output_config = request_config.get("output_config")1506 if isinstance(request_output_config, dict):1507 output_config.update(request_output_config)1508 effort = request_config.get("effort")1509 if effort is None:1510 effort = request_config.get("reasoning_effort")1511 if effort:1512 output_config["effort"] = effort15131514 is_fable_model = self.model.startswith("claude-fable-5")1515 if is_fable_model:1516 top_k = request_config.get("top_k", self.top_k)1517 top_p = request_config.get("top_p", self.top_p)1518 temperature = request_config.get("temperature", self.temperature)1519 if top_k is not None:1520 msg = f"`top_k` is not supported for {self.model}."1521 raise ValueError(msg)1522 if top_p is not None and top_p != 1:1523 msg = (1524 f"`top_p` is not supported for {self.model} at non-default values."1525 )1526 raise ValueError(msg)1527 if temperature is not None and temperature != 1:1528 msg = (1529 f"`temperature` is not supported for {self.model} at "1530 "non-default values."1531 )1532 raise ValueError(msg)1533 if isinstance(thinking, Mapping) and thinking.get("type") == "disabled":1534 msg = (1535 '`thinking={"type": "disabled"}` is not supported for '1536 f"{self.model}; omit `thinking` to use adaptive thinking."1537 )1538 raise ValueError(msg)15391540 if (1541 (self.model.startswith("claude-opus-5") or is_fable_model)1542 and isinstance(thinking, Mapping)1543 and thinking.get("type") == "enabled"1544 ):1545 msg = (1546 '`thinking={"type": "enabled", "budget_tokens": ...}` is not '1547 f"supported for {self.model}; use adaptive thinking and "1548 "`output_config.effort` instead."1549 )1550 raise ValueError(msg)15511552 if (1553 self.model.startswith("claude-opus-5")1554 and isinstance(thinking, Mapping)1555 and thinking.get("type") == "disabled"1556 and output_config.get("effort") in {"xhigh", "max"}1557 ):1558 msg = (1559 '`thinking={"type": "disabled"}` is not supported for '1560 f"{self.model} with "1561 f"`output_config.effort={output_config['effort']!r}`; use adaptive "1562 "thinking, omit `thinking`, or set effort to `high` or below."1563 )1564 raise ValueError(msg)15651566 def _get_request_payload(1567 self,1568 input_: LanguageModelInput,1569 *,1570 stop: list[str] | None = None,1571 **kwargs: Any,1572 ) -> dict:1573 """Get the request payload for the Anthropic API."""1574 self._assert_valid_model_configuration(kwargs)1575 messages = self._convert_input(input_).to_messages()15761577 for idx, message in enumerate(messages):1578 # Translate v1 content1579 if (1580 isinstance(message, AIMessage)1581 and message.response_metadata.get("output_version") == "v1"1582 ):1583 tcs: list[types.ToolCall] = [1584 {1585 "type": "tool_call",1586 "name": tool_call["name"],1587 "args": tool_call["args"],1588 "id": tool_call.get("id"),1589 }1590 for tool_call in message.tool_calls1591 ]1592 messages[idx] = message.model_copy(1593 update={1594 "content": _convert_from_v1_to_anthropic(1595 cast(list[types.ContentBlock], message.content),1596 tcs,1597 message.response_metadata.get("model_provider"),1598 )1599 }1600 )16011602 system, formatted_messages = _format_messages(messages)16031604 # Only the direct Anthropic API accepts top-level `cache_control`.1605 # Subclasses that route through other transports (e.g. Bedrock) expand1606 # `cache_control` kwargs into block-level breakpoints, the only form1607 # those transports accept.1608 if not _is_direct_anthropic_llm_type(getattr(self, "_llm_type", None)):1609 cache_control = kwargs.pop("cache_control", None)1610 # Empty `formatted_messages` has nothing to attach a breakpoint to;1611 # skip silently. The warning below is reserved for the surprising1612 # case where messages exist but every candidate block is ineligible.1613 if cache_control and formatted_messages:1614 code_execution_tool_ids = _collect_code_execution_tool_ids(1615 formatted_messages1616 )1617 applied = _apply_cache_control_to_last_eligible_block(1618 formatted_messages, cache_control, code_execution_tool_ids1619 )1620 if not applied:1621 warnings.warn(1622 "`cache_control` kwarg was dropped: no eligible "1623 "content block found (all candidates are "1624 "`code_execution`-related, which Anthropic forbids "1625 "breakpoints on).",1626 UserWarning,1627 stacklevel=2,1628 )16291630 payload = {1631 "model": self.model,1632 "max_tokens": self.max_tokens,1633 "messages": formatted_messages,1634 "temperature": self.temperature,1635 "top_k": self.top_k,1636 "top_p": self.top_p,1637 "stop_sequences": stop or self.stop_sequences,1638 "betas": self.betas,1639 "context_management": self.context_management,1640 "mcp_servers": self.mcp_servers,1641 "container": self.container,1642 "user_profile_id": self.user_profile_id,1643 "system": system,1644 **self.model_kwargs,1645 **kwargs,1646 }1647 # Captured before `self.thinking` is applied below, so a call-time1648 # `thinking` kwarg counts as "explicitly set" too.1649 thinking_explicitly_set = "thinking" in payload or self.thinking is not None1650 if self.thinking is not None:1651 payload["thinking"] = self.thinking1652 if self.inference_geo is not None:1653 payload["inference_geo"] = self.inference_geo1654 if self.model.startswith("claude-fable-5"):1655 payload.pop("temperature", None)1656 payload.pop("top_k", None)1657 payload.pop("top_p", None)16581659 # Handle output_config and effort parameter1660 # Priority: kwarg `effort`/`reasoning_effort` > kwarg `output_config`1661 # > self.reasoning_effort > self.output_config1662 output_config: dict[str, Any] = {}1663 if self.output_config:1664 output_config.update(self.output_config)1665 reasoning_effort_applied = False1666 if self.reasoning_effort:1667 output_config["effort"] = self.reasoning_effort1668 reasoning_effort_applied = True1669 payload_oc = payload.get("output_config")1670 if isinstance(payload_oc, dict):1671 output_config.update(payload_oc)16721673 # Neither `reasoning_effort` nor its `effort` alias are Anthropic API1674 # fields. Pop them so they never leak through as top-level keys.1675 effort_kwarg = payload.pop("effort", None)1676 reasoning_effort_kwarg = payload.pop("reasoning_effort", None)1677 # `effort` wins if both are set at call time, matching the1678 # construction-time alias-resolution precedence (`Field(alias="effort")`).1679 reasoning_effort_override = (1680 effort_kwarg if effort_kwarg is not None else reasoning_effort_kwarg1681 )1682 if reasoning_effort_override:1683 output_config["effort"] = reasoning_effort_override1684 reasoning_effort_applied = True16851686 if output_config:1687 payload["output_config"] = output_config16881689 # Default adaptive thinking when `reasoning_effort` is set, unless the1690 # caller explicitly provided `thinking`. Gated on `xhigh` support: only1691 # Opus 4.7+/Sonnet 5 accept the adaptive+summarized `thinking` shape —1692 # sending it to an older model (e.g. Opus 4.5, 4.6) is rejected by the1693 # API with "adaptive thinking is not supported on this model".1694 if (1695 reasoning_effort_applied1696 and not thinking_explicitly_set1697 and "xhigh" in _reasoning_effort_levels(self.profile)1698 ):1699 payload["thinking"] = {"type": "adaptive", "display": "summarized"}17001701 if "response_format" in payload:1702 # response_format present when using agents.create_agent's ProviderStrategy1703 # ---1704 # ProviderStrategy converts to OpenAI-style format, which passes kwargs to1705 # ChatAnthropic, ending up in our payload1706 response_format = payload.pop("response_format")1707 if (1708 isinstance(response_format, dict)1709 and response_format.get("type") == "json_schema"1710 and "schema" in response_format.get("json_schema", {})1711 ):1712 response_format = cast(dict, response_format["json_schema"]["schema"])1713 # Convert OpenAI-style response_format to Anthropic's output_config.format1714 output_config = payload.setdefault("output_config", {})1715 output_config["format"] = _convert_to_anthropic_output_config_format(1716 response_format1717 )17181719 # Handle deprecated output_format parameter for backward compatibility1720 if "output_format" in payload:1721 warnings.warn(1722 "The 'output_format' parameter is deprecated and will be removed in "1723 "langchain-anthropic 2.0.0. Use 'output_config={\"format\": ...}' "1724 "instead.",1725 DeprecationWarning,1726 stacklevel=2,1727 )1728 output_config = payload.setdefault("output_config", {})1729 output_config["format"] = payload.pop("output_format")17301731 container = payload.get("container")17321733 if self.reuse_last_container and not _container_id(container):1734 # Reuse the container from the most recent response (code execution)1735 for message in reversed(messages):1736 if (1737 isinstance(message, AIMessage)1738 and isinstance(1739 last_container := message.response_metadata.get("container"),1740 dict,1741 )1742 and (container_id := last_container.get("id"))1743 ):1744 payload["container"] = (1745 {**container, "id": container_id}1746 if isinstance(container, dict)1747 else container_id1748 )1749 break17501751 if (1752 isinstance(container, dict)1753 and container.get("skills")1754 and not any(1755 isinstance(tool, dict)1756 and str(tool.get("type", "")).startswith("code_execution")1757 for tool in (payload.get("tools") or [])1758 )1759 ):1760 warnings.warn(1761 "Skills require a code execution tool to be bound, e.g. "1762 '`bind_tools([{"type": "code_execution_20260521", '1763 '"name": "code_execution"}])`.',1764 UserWarning,1765 stacklevel=2,1766 )17671768 # Note: Beta headers are no longer required for structured outputs1769 # (output_config.format or strict tool use) as they are now generally available1770 if "tools" in payload and isinstance(payload["tools"], list):1771 # Auto-append required betas for specific tool types and input_examples1772 has_input_examples = False1773 for tool in payload["tools"]:1774 if isinstance(tool, dict):1775 tool_type = tool.get("type")1776 if tool_type and tool_type in _TOOL_TYPE_TO_BETA:1777 required_beta = _TOOL_TYPE_TO_BETA[tool_type]1778 if payload["betas"]:1779 if required_beta not in payload["betas"]:1780 payload["betas"] = [1781 *payload["betas"],1782 required_beta,1783 ]1784 else:1785 payload["betas"] = [required_beta]1786 # Check for input_examples1787 if tool.get("input_examples"):1788 has_input_examples = True17891790 # Auto-append header for input_examples1791 if has_input_examples:1792 required_beta = "advanced-tool-use-2025-11-20"1793 if payload["betas"]:1794 if required_beta not in payload["betas"]:1795 payload["betas"] = [*payload["betas"], required_beta]1796 else:1797 payload["betas"] = [required_beta]17981799 # Auto-append required beta for mcp_servers1800 if payload.get("mcp_servers"):1801 required_beta = "mcp-client-2025-11-20"1802 if payload["betas"]:1803 # Append to existing betas if not already present1804 if required_beta not in payload["betas"]:1805 payload["betas"] = [*payload["betas"], required_beta]1806 else:1807 payload["betas"] = [required_beta]18081809 # Auto-append required beta for task_budget1810 resolved_oc = payload.get("output_config")1811 if isinstance(resolved_oc, dict) and resolved_oc.get("task_budget"):1812 required_beta = "task-budgets-2026-03-13"1813 if payload.get("betas"):1814 if required_beta not in payload["betas"]:1815 payload["betas"] = [*payload["betas"], required_beta]1816 else:1817 payload["betas"] = [required_beta]18181819 # Auto-append required beta for the `updates` thinking display mode1820 thinking = payload.get("thinking")1821 if isinstance(thinking, dict) and thinking.get("display") == "updates":1822 required_beta = "thinking-display-updates-2026-08-18"1823 if payload.get("betas"):1824 if required_beta not in payload["betas"]:1825 payload["betas"] = [*payload["betas"], required_beta]1826 else:1827 payload["betas"] = [required_beta]18281829 # Auto-append required beta for user_profile_id1830 if payload.get("user_profile_id"):1831 required_beta = "user-profiles-2026-03-24"1832 if payload.get("betas"):1833 if required_beta not in payload["betas"]:1834 payload["betas"] = [*payload["betas"], required_beta]1835 else:1836 payload["betas"] = [required_beta]18371838 return _route_unsupported_sampling_params(1839 {k: v for k, v in payload.items() if v is not None}1840 )18411842 def _create(self, payload: dict) -> Any:1843 try:1844 if "betas" in payload:1845 return self._client.beta.messages.with_raw_response.create(**payload)1846 return self._client.messages.with_raw_response.create(**payload)1847 except TypeError as e:1848 _raise_if_authentication_error(e)1849 raise18501851 async def _acreate(self, payload: dict) -> Any:1852 try:1853 if "betas" in payload:1854 return await self._async_client.beta.messages.with_raw_response.create(1855 **payload1856 )1857 return await self._async_client.messages.with_raw_response.create(**payload)1858 except TypeError as e:1859 _raise_if_authentication_error(e)1860 raise18611862 def _stream(1863 self,1864 messages: list[BaseMessage],1865 stop: list[str] | None = None,1866 run_manager: CallbackManagerForLLMRun | None = None,1867 *,1868 stream_usage: bool | None = None,1869 **kwargs: Any,1870 ) -> Iterator[ChatGenerationChunk]:1871 if stream_usage is None:1872 stream_usage = self.stream_usage1873 kwargs["stream"] = True1874 payload = self._get_request_payload(messages, stop=stop, **kwargs)1875 try:1876 raw_response = self._create(payload)1877 base_generation_info: dict[str, Any] = {}1878 if self._uses_gateway:1879 _add_gateway_metadata(base_generation_info, raw_response)1880 stream = raw_response.parse()1881 coerce_content_to_string = (1882 not _tools_in_params(payload)1883 and not _documents_in_params(payload)1884 and not _thinking_in_params(payload)1885 and not _compact_in_params(payload)1886 )1887 block_start_event = None1888 is_first_chunk = True1889 for event in stream:1890 msg, block_start_event = self._make_message_chunk_from_anthropic_event(1891 event,1892 stream_usage=stream_usage,1893 coerce_content_to_string=coerce_content_to_string,1894 block_start_event=block_start_event,1895 )1896 if msg is not None:1897 chunk = ChatGenerationChunk(1898 message=msg,1899 generation_info=base_generation_info1900 if is_first_chunk1901 else None,1902 )1903 is_first_chunk = False1904 if run_manager and isinstance(msg.content, str):1905 run_manager.on_llm_new_token(msg.content, chunk=chunk)1906 yield chunk1907 except anthropic.BadRequestError as e:1908 _handle_anthropic_bad_request(e)1909 except anthropic.APIError as e:1910 _handle_anthropic_api_error(e)19111912 async def _astream(1913 self,1914 messages: list[BaseMessage],1915 stop: list[str] | None = None,1916 run_manager: AsyncCallbackManagerForLLMRun | None = None,1917 *,1918 stream_usage: bool | None = None,1919 **kwargs: Any,1920 ) -> AsyncIterator[ChatGenerationChunk]:1921 if stream_usage is None:1922 stream_usage = self.stream_usage1923 kwargs["stream"] = True1924 payload = self._get_request_payload(messages, stop=stop, **kwargs)1925 try:1926 raw_response = await self._acreate(payload)1927 base_generation_info: dict[str, Any] = {}1928 if self._uses_gateway:1929 _add_gateway_metadata(base_generation_info, raw_response)1930 stream = await _aparse(raw_response)1931 coerce_content_to_string = (1932 not _tools_in_params(payload)1933 and not _documents_in_params(payload)1934 and not _thinking_in_params(payload)1935 and not _compact_in_params(payload)1936 )1937 block_start_event = None1938 is_first_chunk = True1939 async for event in stream:1940 msg, block_start_event = self._make_message_chunk_from_anthropic_event(1941 event,1942 stream_usage=stream_usage,1943 coerce_content_to_string=coerce_content_to_string,1944 block_start_event=block_start_event,1945 )1946 if msg is not None:1947 chunk = ChatGenerationChunk(1948 message=msg,1949 generation_info=base_generation_info1950 if is_first_chunk1951 else None,1952 )1953 is_first_chunk = False1954 if run_manager and isinstance(msg.content, str):1955 await run_manager.on_llm_new_token(msg.content, chunk=chunk)1956 yield chunk1957 except anthropic.BadRequestError as e:1958 _handle_anthropic_bad_request(e)1959 except anthropic.APIError as e:1960 _handle_anthropic_api_error(e)19611962 def _make_message_chunk_from_anthropic_event(1963 self,1964 event: anthropic.types.RawMessageStreamEvent,1965 *,1966 stream_usage: bool = True,1967 coerce_content_to_string: bool,1968 block_start_event: anthropic.types.RawMessageStreamEvent | None = None,1969 ) -> tuple[AIMessageChunk | None, anthropic.types.RawMessageStreamEvent | None]:1970 """Convert Anthropic streaming event to `AIMessageChunk`.19711972 Args:1973 event: Raw streaming event from Anthropic SDK1974 stream_usage: Whether to include usage metadata in the output chunks.1975 coerce_content_to_string: Whether to convert structured content to plain1976 text strings.19771978 When `True`, only text content is preserved; when `False`, structured1979 content like tool calls and citations are maintained.1980 block_start_event: Previous content block start event, used for tracking1981 tool use blocks and maintaining context across related events.19821983 Returns:1984 Tuple with1985 - `AIMessageChunk`: Converted message chunk with appropriate content and1986 metadata, or `None` if the event doesn't produce a chunk1987 - `RawMessageStreamEvent`: Updated `block_start_event` for tracking1988 content blocks across sequential events, or `None` if not applicable19891990 Note:1991 Not all Anthropic events result in message chunks. Events like internal1992 state changes return `None` for the message chunk while potentially1993 updating the `block_start_event` for context tracking.1994 """1995 message_chunk: AIMessageChunk | None = None1996 # Reference: Anthropic SDK streaming implementation1997 # https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/lib/streaming/_messages.py # noqa: E5011998 if event.type == "message_start" and stream_usage:1999 # Capture model name, but don't include usage_metadata yet2000 # as it will be properly reported in message_delta with complete info
Findings
✓ No findings reported for this file.