feat(anthropic): support opus 4.7 features (#36847)

This commit is contained in:
ccurme
2026-04-17 09:37:02 -04:00
committed by GitHub
parent 6fb37dba71
commit c59e8e1cff
6 changed files with 193 additions and 28 deletions

View File

@@ -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:

View File

@@ -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,
},
}

View File

@@ -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

View File

@@ -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",
]

View File

@@ -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

View File

@@ -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" },