diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index 5ed5d314664..1beba466a3b 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -831,19 +831,55 @@ class ChatAnthropic(BaseChatModel): """ thinking: dict[str, Any] | None = Field(default=None) - """Parameters for Claude reasoning, + """Parameters for Claude reasoning. - e.g., `#!python {"type": "enabled", "budget_tokens": 10_000}` + Examples: - For Claude Opus 4.6, `budget_tokens` is deprecated in favor of - `#!python {"type": "adaptive"}` + - `#!python {"type": "enabled", "budget_tokens": 10_000}` (pre-4.7 models) + - `#!python {"type": "adaptive"}` (Opus 4.6+) + - `#!python {"type": "adaptive", "display": "summarized"}` (Opus 4.7+) + + !!! note "Claude Opus 4.7" + + `budget_tokens` is removed on Opus 4.7 — use `{"type": "adaptive"}` + with `output_config.effort` to control reasoning effort. Set `display` + to `"summarized"` to receive summarized reasoning in the response + (default is `"omitted"`). """ - effort: Literal["max", "high", "medium", "low"] | None = None - """Control how many tokens Claude uses when responding. + output_config: dict[str, Any] | None = None + """Configuration options for the model's output. - This parameter will be merged into the `output_config` parameter when making - API calls. + Supports the following keys: + + - `effort`: Controls how many tokens Claude uses when responding. + One of `"max"`, `"xhigh"`, `"high"`, `"medium"`, or `"low"`. + - `format`: Structured output format configuration (typically set via + `with_structured_output`). + - `task_budget`: Advisory token budget for an agentic loop (beta). + E.g., `#!python {"type": "tokens", "total": 128_000}`. + + Example: + + .. code-block:: python + + ChatAnthropic( + model="claude-opus-4-7", + output_config={ + "effort": "xhigh", + "task_budget": {"type": "tokens", "total": 128_000}, + }, + ) + + See Anthropic docs on + [extended output](https://platform.claude.com/docs/en/api/go/beta/messages/create). + """ + + effort: Literal["max", "xhigh", "high", "medium", "low"] | None = None + """Convenience shorthand for `output_config.effort`. + + When set, this value takes precedence over any `effort` key inside + `output_config`. Example: `effort="medium"` @@ -851,11 +887,6 @@ class ChatAnthropic(BaseChatModel): Setting `effort` to `'high'` produces exactly the same behavior as omitting the parameter altogether. - - !!! note "Model Support" - - This feature is generally available on Claude Opus 4.6 and Claude Opus 4.5. - The `max` effort level is only supported by Claude Opus 4.6. """ mcp_servers: list[dict[str, Any]] | None = None @@ -927,6 +958,7 @@ class ChatAnthropic(BaseChatModel): "max_retries": self.max_retries, "default_request_timeout": self.default_request_timeout, "thinking": self.thinking, + "output_config": self.output_config, } def _get_ls_params( @@ -1082,9 +1114,13 @@ class ChatAnthropic(BaseChatModel): payload["inference_geo"] = self.inference_geo # Handle output_config and effort parameter - # Priority: self.effort > payload output_config - output_config = payload.get("output_config", {}) - output_config = output_config.copy() if isinstance(output_config, dict) else {} + # Priority: self.effort > kwargs output_config > self.output_config + output_config: dict[str, Any] = {} + if self.output_config: + output_config.update(self.output_config) + payload_oc = payload.get("output_config") + if isinstance(payload_oc, dict): + output_config.update(payload_oc) if self.effort: output_config["effort"] = self.effort @@ -1175,6 +1211,16 @@ class ChatAnthropic(BaseChatModel): else: payload["betas"] = [required_beta] + # Auto-append required beta for task_budget + resolved_oc = payload.get("output_config") + if isinstance(resolved_oc, dict) and resolved_oc.get("task_budget"): + required_beta = "task-budgets-2026-03-13" + if payload.get("betas"): + if required_beta not in payload["betas"]: + payload["betas"] = [*payload["betas"], required_beta] + else: + payload["betas"] = [required_beta] + return {k: v for k, v in payload.items() if v is not None} def _create(self, payload: dict) -> Any: diff --git a/libs/partners/anthropic/langchain_anthropic/data/_profiles.py b/libs/partners/anthropic/langchain_anthropic/data/_profiles.py index 829048d87de..b7b7857dbfc 100644 --- a/libs/partners/anthropic/langchain_anthropic/data/_profiles.py +++ b/libs/partners/anthropic/langchain_anthropic/data/_profiles.py @@ -239,7 +239,7 @@ _PROFILES: dict[str, dict[str, Any]] = { "image_url_inputs": True, "pdf_tool_message": True, "image_tool_message": True, - "structured_output": False, + "structured_output": True, }, "claude-haiku-4-5-20251001": { "name": "Claude Haiku 4.5", @@ -389,7 +389,7 @@ _PROFILES: dict[str, dict[str, Any]] = { "image_url_inputs": True, "pdf_tool_message": True, "image_tool_message": True, - "structured_output": False, + "structured_output": True, }, "claude-opus-4-5-20251101": { "name": "Claude Opus 4.5", @@ -439,7 +439,32 @@ _PROFILES: dict[str, dict[str, Any]] = { "image_url_inputs": True, "pdf_tool_message": True, "image_tool_message": True, - "structured_output": False, + "structured_output": True, + }, + "claude-opus-4-7": { + "name": "Claude Opus 4.7", + "release_date": "2026-04-16", + "last_updated": "2026-04-16", + "open_weights": False, + "max_input_tokens": 1000000, + "max_output_tokens": 128000, + "text_inputs": True, + "image_inputs": True, + "audio_inputs": False, + "pdf_inputs": True, + "video_inputs": False, + "text_outputs": True, + "image_outputs": False, + "audio_outputs": False, + "video_outputs": False, + "reasoning_output": True, + "tool_calling": True, + "attachment": True, + "temperature": False, + "image_url_inputs": True, + "pdf_tool_message": True, + "image_tool_message": True, + "structured_output": True, }, "claude-sonnet-4-0": { "name": "Claude Sonnet 4 (latest)", @@ -564,6 +589,6 @@ _PROFILES: dict[str, dict[str, Any]] = { "image_url_inputs": True, "pdf_tool_message": True, "image_tool_message": True, - "structured_output": False, + "structured_output": True, }, } diff --git a/libs/partners/anthropic/langchain_anthropic/data/profile_augmentations.toml b/libs/partners/anthropic/langchain_anthropic/data/profile_augmentations.toml index d0619ad6cf7..38480f92b69 100644 --- a/libs/partners/anthropic/langchain_anthropic/data/profile_augmentations.toml +++ b/libs/partners/anthropic/langchain_anthropic/data/profile_augmentations.toml @@ -7,8 +7,23 @@ pdf_tool_message = true image_tool_message = true structured_output = false +[overrides."claude-haiku-4-5"] +structured_output = true + [overrides."claude-sonnet-4-5"] structured_output = true +[overrides."claude-sonnet-4-6"] +structured_output = true + [overrides."claude-opus-4-1"] structured_output = true + +[overrides."claude-opus-4-5"] +structured_output = true + +[overrides."claude-opus-4-6"] +structured_output = true + +[overrides."claude-opus-4-7"] +structured_output = true diff --git a/libs/partners/anthropic/pyproject.toml b/libs/partners/anthropic/pyproject.toml index 9ade51cfd7e..3a1d3a84616 100644 --- a/libs/partners/anthropic/pyproject.toml +++ b/libs/partners/anthropic/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ version = "1.4.0" requires-python = ">=3.10.0,<4.0.0" dependencies = [ - "anthropic>=0.85.0,<1.0.0", + "anthropic>=0.96.0,<1.0.0", "langchain-core>=1.2.21,<2.0.0", "pydantic>=2.7.4,<3.0.0", ] diff --git a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py index 9b0e3b7cfd8..1a8db371380 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -2313,7 +2313,9 @@ def test_effort_in_output_config() -> None: model="claude-opus-4-5-20251101", output_config={"effort": "low"}, ) - assert model.model_kwargs["output_config"] == {"effort": "low"} + assert model.output_config == {"effort": "low"} + payload = model._get_request_payload("Test query") + assert payload["output_config"]["effort"] == "low" def test_effort_priority() -> None: @@ -2764,3 +2766,80 @@ def test_thinking_in_params_recognizes_adaptive() -> None: assert not _thinking_in_params({"thinking": {"type": "disabled"}}) assert not _thinking_in_params({"thinking": {}}) assert not _thinking_in_params({}) + + +def test_effort_xhigh() -> None: + """Test that xhigh effort level is accepted and lands in output_config.""" + model = ChatAnthropic(model="claude-opus-4-6", effort="xhigh") + assert model.effort == "xhigh" + payload = model._get_request_payload("Test query") + assert payload["output_config"]["effort"] == "xhigh" + + +def test_output_config_top_level_field() -> None: + """Test that output_config is a top-level field, not model_kwargs.""" + model = ChatAnthropic( + model=MODEL_NAME, + output_config={ + "effort": "low", + "task_budget": {"type": "tokens", "total": 50000}, + }, + ) + assert model.output_config == { + "effort": "low", + "task_budget": {"type": "tokens", "total": 50000}, + } + assert "output_config" not in model.model_kwargs + + payload = model._get_request_payload("Test query") + assert payload["output_config"]["effort"] == "low" + assert payload["output_config"]["task_budget"] == {"type": "tokens", "total": 50000} + + +def test_output_config_merged_with_kwargs() -> None: + """Test that call-time output_config overrides field-level output_config.""" + model = ChatAnthropic( + model=MODEL_NAME, + output_config={"effort": "low"}, + ) + payload = model._get_request_payload( + "Test query", + output_config={ + "effort": "high", + "task_budget": {"type": "tokens", "total": 50000}, + }, + ) + # Call-time kwargs override field-level + assert payload["output_config"]["effort"] == "high" + assert payload["output_config"]["task_budget"] == {"type": "tokens", "total": 50000} + + +def test_task_budget_auto_appends_beta() -> None: + """Test that task_budget in output_config triggers beta header.""" + model = ChatAnthropic( + model=MODEL_NAME, + output_config={"task_budget": {"type": "tokens", "total": 128000}}, + ) + payload = model._get_request_payload("Test query") + assert "betas" in payload + assert "task-budgets-2026-03-13" in payload["betas"] + + +def test_task_budget_beta_not_duplicated() -> None: + """Test that task_budget beta is not duplicated if already present.""" + model = ChatAnthropic( + model=MODEL_NAME, + betas=["task-budgets-2026-03-13"], + output_config={"task_budget": {"type": "tokens", "total": 128000}}, + ) + payload = model._get_request_payload("Test query") + assert payload["betas"].count("task-budgets-2026-03-13") == 1 + + +def test_no_task_budget_no_beta() -> None: + """Test that task_budget beta is not added when no task_budget is set.""" + model = ChatAnthropic(model=MODEL_NAME, output_config={"effort": "high"}) + payload = model._get_request_payload("Test query") + betas = payload.get("betas") + if betas: + assert "task-budgets-2026-03-13" not in betas diff --git a/libs/partners/anthropic/uv.lock b/libs/partners/anthropic/uv.lock index 3d5e75de306..df15759930c 100644 --- a/libs/partners/anthropic/uv.lock +++ b/libs/partners/anthropic/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10.0, <4.0.0" resolution-markers = [ "python_full_version >= '3.13' and platform_python_implementation == 'PyPy'", @@ -27,7 +27,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.85.0" +version = "0.96.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -39,9 +39,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/08/c620a0eb8625539a8ea9f5a6e06f13d131be0bc8b5b714c235d4b25dd1b5/anthropic-0.85.0.tar.gz", hash = "sha256:d45b2f38a1efb1a5d15515a426b272179a0d18783efa2bb4c3925fa773eb50b9", size = 542034, upload-time = "2026-03-16T17:00:44.324Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/7e/672f533dee813028d2c699bfd2a7f52c9118d7353680d9aa44b9e23f717f/anthropic-0.96.0.tar.gz", hash = "sha256:9de947b737f39452f68aa520f1c2239d44119c9b73b0fb6d4e6ca80f00279ee6", size = 658210, upload-time = "2026-04-16T14:28:02.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/5a/9d85b85686d5cdd79f5488c8667e668d7920d06a0a1a1beb454a5b77b2db/anthropic-0.85.0-py3-none-any.whl", hash = "sha256:b4f54d632877ed7b7b29c6d9ba7299d5e21c4c92ae8de38947e9d862bff74adf", size = 458237, upload-time = "2026-03-16T17:00:45.877Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/72f33204064b6e87601a71a6baf8d855769f8a0c1eaae8d06a1094872371/anthropic-0.96.0-py3-none-any.whl", hash = "sha256:9a6e335a354602a521cd9e777e92bfd46ba6e115bf9bbfe6135311e8fb2015b2", size = 635930, upload-time = "2026-04-16T14:28:01.436Z" }, ] [[package]] @@ -621,7 +621,7 @@ typing = [ [package.metadata] requires-dist = [ - { name = "anthropic", specifier = ">=0.85.0,<1.0.0" }, + { name = "anthropic", specifier = ">=0.96.0,<1.0.0" }, { name = "langchain-core", editable = "../../core" }, { name = "pydantic", specifier = ">=2.7.4,<3.0.0" }, ] @@ -660,7 +660,7 @@ typing = [ [[package]] name = "langchain-core" -version = "1.3.0a2" +version = "1.3.0a3" source = { editable = "../../core" } dependencies = [ { name = "jsonpatch" },