From 74b3344679bd2919bee0eb62e1702c9b9502ee53 Mon Sep 17 00:00:00 2001 From: ccurme Date: Thu, 5 Feb 2026 16:41:25 -0500 Subject: [PATCH] fix(anthropic): support automatic compaction (Opus 4.6) (#35034) --- .../langchain_anthropic/chat_models.py | 26 +++++ libs/partners/anthropic/pyproject.toml | 2 +- .../tests/cassettes/test_compaction.yaml.gz | Bin 0 -> 7079 bytes .../integration_tests/test_chat_models.py | 109 ++++++++++++++++++ .../tests/unit_tests/test_chat_models.py | 7 ++ libs/partners/anthropic/uv.lock | 16 +-- 6 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 libs/partners/anthropic/tests/cassettes/test_compaction.yaml.gz diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index d014fd82d92..8c4fdfce5cb 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -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) diff --git a/libs/partners/anthropic/pyproject.toml b/libs/partners/anthropic/pyproject.toml index 6537fc7800e..642497448a4 100644 --- a/libs/partners/anthropic/pyproject.toml +++ b/libs/partners/anthropic/pyproject.toml @@ -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", ] diff --git a/libs/partners/anthropic/tests/cassettes/test_compaction.yaml.gz b/libs/partners/anthropic/tests/cassettes/test_compaction.yaml.gz new file mode 100644 index 0000000000000000000000000000000000000000..b206989a17400dd08f4cf4f586ad0e8988926bff GIT binary patch literal 7079 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 literal 0 HcmV?d00001 diff --git a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py index e8d5b11e9be..7fb53d4f5ab 100644 --- a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py @@ -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"] 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 948daf10c54..ca252e25013 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -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) diff --git a/libs/partners/anthropic/uv.lock b/libs/partners/anthropic/uv.lock index 2a6f542a5ab..b4421c03ce3 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'", @@ -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" },