fix(anthropic): support automatic compaction (Opus 4.6) (#35034)

This commit is contained in:
ccurme
2026-02-05 16:41:25 -05:00
committed by GitHub
parent d181a59ebe
commit 74b3344679
6 changed files with 151 additions and 9 deletions

View File

@@ -848,6 +848,9 @@ class ChatAnthropic(BaseChatModel):
"""Parameters for Claude reasoning,
e.g., `#!python {"type": "enabled", "budget_tokens": 10_000}`
For Claude Opus 4.6, `budget_tokens` is deprecated in favor of
`#!python {"type": "adaptive"}`
"""
effort: Literal["high", "medium", "low"] | None = None
@@ -896,6 +899,12 @@ class ChatAnthropic(BaseChatModel):
invocations.
"""
inference_geo: str | None = None
"""Controls where model inference runs. See Anthropic's
[data residency](https://platform.claude.com/docs/en/build-with-claude/data-residency)
docs for more information.
"""
@property
def _llm_type(self) -> str:
"""Return type of chat model."""
@@ -1122,6 +1131,8 @@ class ChatAnthropic(BaseChatModel):
}
if self.thinking is not None:
payload["thinking"] = self.thinking
if self.inference_geo is not None:
payload["inference_geo"] = self.inference_geo
# Handle output_config and effort parameter
# Priority: self.effort > payload output_config
@@ -1273,6 +1284,7 @@ class ChatAnthropic(BaseChatModel):
not _tools_in_params(payload)
and not _documents_in_params(payload)
and not _thinking_in_params(payload)
and not _compact_in_params(payload)
)
block_start_event = None
for event in stream:
@@ -1309,6 +1321,7 @@ class ChatAnthropic(BaseChatModel):
not _tools_in_params(payload)
and not _documents_in_params(payload)
and not _thinking_in_params(payload)
and not _compact_in_params(payload)
)
block_start_event = None
async for event in stream:
@@ -1870,6 +1883,12 @@ def _documents_in_params(params: dict) -> bool:
return False
def _compact_in_params(params: dict) -> bool:
edits = params.get("context_management", {}).get("edits") or []
return any("compact" in (edit.get("type") or "") for edit in edits)
class _AnthropicToolUse(TypedDict):
type: Literal["tool_use"]
name: str
@@ -2049,6 +2068,13 @@ def _make_message_chunk_from_anthropic_event(
tool_call_chunks=tool_call_chunks,
)
# Compaction block
elif event.delta.type == "compaction_delta":
content_block = event.delta.model_dump()
content_block["index"] = event.index
content_block["type"] = "compaction"
message_chunk = AIMessageChunk(content=[content_block])
# Process final usage metadata and completion info
elif event.type == "message_delta" and stream_usage:
usage_metadata = _create_usage_metadata(event.usage)

View File

@@ -23,7 +23,7 @@ classifiers = [
version = "1.3.1"
requires-python = ">=3.10.0,<4.0.0"
dependencies = [
"anthropic>=0.75.0,<1.0.0",
"anthropic>=0.78.0,<1.0.0",
"langchain-core>=1.2.6,<2.0.0",
"pydantic>=2.7.4,<3.0.0",
]

View File

@@ -2409,3 +2409,112 @@ def test_fine_grained_tool_streaming() -> None:
assert write_doc_block is not None
assert write_doc_block["name"] == "write_document"
assert "args" in write_doc_block
@pytest.mark.vcr
def test_compaction() -> None:
"""Test the compation beta feature."""
llm = ChatAnthropic(
model="claude-opus-4-6", # type: ignore[call-arg]
betas=["compact-2026-01-12"],
max_tokens=4096,
context_management={
"edits": [
{
"type": "compact_20260112",
"trigger": {"type": "input_tokens", "value": 50000},
"pause_after_compaction": True,
}
]
},
)
input_message = {
"role": "user",
"content": f"Generate a one-sentence summary of this:\n\n{'a' * 100000}",
}
messages: list = [input_message]
first_response = llm.invoke(messages)
messages.append(first_response)
second_message = {
"role": "user",
"content": f"Generate a one-sentence summary of this:\n\n{'b' * 100000}",
}
messages.append(second_message)
second_response = llm.invoke(messages)
messages.append(second_response)
content_blocks = second_response.content_blocks
compaction_block = next(
(block for block in content_blocks if block["type"] == "non_standard"),
None,
)
assert compaction_block
assert compaction_block["value"].get("type") == "compaction"
third_message = {
"role": "user",
"content": "What are we talking about?",
}
messages.append(third_message)
third_response = llm.invoke(messages)
content_blocks = third_response.content_blocks
assert [block["type"] for block in content_blocks] == ["text"]
@pytest.mark.vcr
def test_compaction_streaming() -> None:
"""Test the compation beta feature."""
llm = ChatAnthropic(
model="claude-opus-4-6", # type: ignore[call-arg]
betas=["compact-2026-01-12"],
max_tokens=4096,
context_management={
"edits": [
{
"type": "compact_20260112",
"trigger": {"type": "input_tokens", "value": 50000},
"pause_after_compaction": False,
}
]
},
streaming=True,
)
input_message = {
"role": "user",
"content": f"Generate a one-sentence summary of this:\n\n{'a' * 100000}",
}
messages: list = [input_message]
first_response = llm.invoke(messages)
messages.append(first_response)
second_message = {
"role": "user",
"content": f"Generate a one-sentence summary of this:\n\n{'b' * 100000}",
}
messages.append(second_message)
second_response = llm.invoke(messages)
messages.append(second_response)
content_blocks = second_response.content_blocks
compaction_block = next(
(block for block in content_blocks if block["type"] == "non_standard"),
None,
)
assert compaction_block
assert compaction_block["value"].get("type") == "compaction"
third_message = {
"role": "user",
"content": "What are we talking about?",
}
messages.append(third_message)
third_response = llm.invoke(messages)
content_blocks = third_response.content_blocks
assert [block["type"] for block in content_blocks] == ["text"]

View File

@@ -1564,6 +1564,13 @@ def test_context_management_in_payload() -> None:
}
def test_inference_geo_in_payload() -> None:
llm = ChatAnthropic(model=MODEL_NAME, inference_geo="us")
input_message = HumanMessage("Hello, world!")
payload = llm._get_request_payload([input_message])
assert payload["inference_geo"] == "us"
def test_anthropic_model_params() -> None:
llm = ChatAnthropic(model=MODEL_NAME)

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'",
@@ -24,7 +24,7 @@ wheels = [
[[package]]
name = "anthropic"
version = "0.75.0"
version = "0.78.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -36,9 +36,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565, upload-time = "2025-11-24T20:41:45.28Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/51/32849a48f9b1cfe80a508fd269b20bd8f0b1357c70ba092890fde5a6a10b/anthropic-0.78.0.tar.gz", hash = "sha256:55fd978ab9b049c61857463f4c4e9e092b24f892519c6d8078cee1713d8af06e", size = 509136, upload-time = "2026-02-05T17:52:04.986Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164, upload-time = "2025-11-24T20:41:43.587Z" },
{ url = "https://files.pythonhosted.org/packages/3b/03/2f50931a942e5e13f80e24d83406714672c57964be593fc046d81369335b/anthropic-0.78.0-py3-none-any.whl", hash = "sha256:2a9887d2e99d1b0f9fe08857a1e9fe5d2d4030455dbf9ac65aab052e2efaeac4", size = 405485, upload-time = "2026-02-05T17:52:03.674Z" },
]
[[package]]
@@ -498,7 +498,7 @@ wheels = [
[[package]]
name = "langchain"
version = "1.2.7"
version = "1.2.9"
source = { editable = "../../langchain_v1" }
dependencies = [
{ name = "langchain-core" },
@@ -607,7 +607,7 @@ typing = [
[package.metadata]
requires-dist = [
{ name = "anthropic", specifier = ">=0.75.0,<1.0.0" },
{ name = "anthropic", specifier = ">=0.78.0,<1.0.0" },
{ name = "langchain-core", editable = "../../core" },
{ name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
]
@@ -646,7 +646,7 @@ typing = [
[[package]]
name = "langchain-core"
version = "1.2.7"
version = "1.2.9"
source = { editable = "../../core" }
dependencies = [
{ name = "jsonpatch" },
@@ -706,7 +706,7 @@ typing = [
[[package]]
name = "langchain-tests"
version = "1.1.2"
version = "1.1.4"
source = { editable = "../../standard-tests" }
dependencies = [
{ name = "httpx" },