mirror of
https://github.com/hwchase17/langchain.git
synced 2026-06-09 10:17:00 +00:00
feat(anthropic): support opus 4.7 features (#36847)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
12
libs/partners/anthropic/uv.lock
generated
12
libs/partners/anthropic/uv.lock
generated
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user