From e3f6adfe1976a773704a20fef7bb03ac0309e0e4 Mon Sep 17 00:00:00 2001 From: ccurme Date: Thu, 27 Aug 2026 11:19:28 -0400 Subject: [PATCH] feat(anthropic): support top-level param for skills via `container`; `updates` thinking display mode (#39962) --- .../langchain_anthropic/chat_models.py | 81 +++++++++++++-- .../tests/cassettes/test_skills.yaml.gz | Bin 0 -> 18682 bytes .../integration_tests/test_chat_models.py | 70 ++++++++++++- .../tests/unit_tests/test_chat_models.py | 96 ++++++++++++++++++ 4 files changed, 234 insertions(+), 13 deletions(-) create mode 100644 libs/partners/anthropic/tests/cassettes/test_skills.yaml.gz diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index bad2789fb1..10298786b2 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -177,7 +177,6 @@ class AnthropicTool(TypedDict): _TOOL_TYPE_TO_BETA: dict[str, str] = { "web_fetch_20250910": "web-fetch-2025-09-10", "code_execution_20250522": "code-execution-2025-05-22", - "code_execution_20250825": "code-execution-2025-08-25", "mcp_toolset": "mcp-client-2025-11-20", "memory_20250818": "context-management-2025-06-27", "computer_20250124": "computer-use-2025-01-24", @@ -816,6 +815,15 @@ def _format_messages( return system, formatted_messages +def _container_id(container: Any) -> str | None: + """Return the container ID from either accepted `container` shape.""" + if isinstance(container, str): + return container + if isinstance(container, dict): + return container.get("id") + return None + + def _collect_code_execution_tool_ids(formatted_messages: list[dict]) -> set[str]: """Collect `tool_use` IDs that were called by `code_execution`. @@ -1255,6 +1263,28 @@ class ChatAnthropic(BaseChatModel): [context management](https://platform.claude.com/docs/en/build-with-claude/context-editing). """ + container: dict[str, Any] | str | None = None + """Code execution container for the request. + + Either a container ID from a previous response, or a dict of container + parameters — notably + [skills](https://platform.claude.com/docs/en/build-with-claude/skills-guide) + to load into the container. Skills require a + [code execution](https://docs.langchain.com/oss/python/integrations/chat/anthropic#code-execution) + tool to be bound. + + ```python + model = ChatAnthropic( + model="claude-opus-5", + container={ + "skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}] + }, + ).bind_tools([{"type": "code_execution_20260521", "name": "code_execution"}]) + ``` + + Can also be passed at call time, which overrides the value set here. + """ + reuse_last_container: bool | None = None """Automatically reuse container from most recent response (code execution). @@ -1582,6 +1612,7 @@ class ChatAnthropic(BaseChatModel): "betas": self.betas, "context_management": self.context_management, "mcp_servers": self.mcp_servers, + "container": self.container, "user_profile_id": self.user_profile_id, "system": system, **self.model_kwargs, @@ -1667,19 +1698,43 @@ class ChatAnthropic(BaseChatModel): output_config = payload.setdefault("output_config", {}) output_config["format"] = payload.pop("output_format") - if self.reuse_last_container: - # Check for most recent AIMessage with container set in response_metadata - # and set as a top-level param on the request + container = payload.get("container") + + if self.reuse_last_container and not _container_id(container): + # Reuse the container from the most recent response (code execution) for message in reversed(messages): if ( isinstance(message, AIMessage) - and (container := message.response_metadata.get("container")) - and isinstance(container, dict) - and (container_id := container.get("id")) + and isinstance( + last_container := message.response_metadata.get("container"), + dict, + ) + and (container_id := last_container.get("id")) ): - payload["container"] = container_id + payload["container"] = ( + {**container, "id": container_id} + if isinstance(container, dict) + else container_id + ) break + if ( + isinstance(container, dict) + and container.get("skills") + and not any( + isinstance(tool, dict) + and str(tool.get("type", "")).startswith("code_execution") + for tool in (payload.get("tools") or []) + ) + ): + warnings.warn( + "Skills require a code execution tool to be bound, e.g. " + '`bind_tools([{"type": "code_execution_20260521", ' + '"name": "code_execution"}])`.', + UserWarning, + stacklevel=2, + ) + # Note: Beta headers are no longer required for structured outputs # (output_config.format or strict tool use) as they are now generally available if "tools" in payload and isinstance(payload["tools"], list): @@ -1731,6 +1786,16 @@ class ChatAnthropic(BaseChatModel): else: payload["betas"] = [required_beta] + # Auto-append required beta for the `updates` thinking display mode + thinking = payload.get("thinking") + if isinstance(thinking, dict) and thinking.get("display") == "updates": + required_beta = "thinking-display-updates-2026-08-18" + if payload.get("betas"): + if required_beta not in payload["betas"]: + payload["betas"] = [*payload["betas"], required_beta] + else: + payload["betas"] = [required_beta] + # Auto-append required beta for user_profile_id if payload.get("user_profile_id"): required_beta = "user-profiles-2026-03-24" diff --git a/libs/partners/anthropic/tests/cassettes/test_skills.yaml.gz b/libs/partners/anthropic/tests/cassettes/test_skills.yaml.gz new file mode 100644 index 0000000000000000000000000000000000000000..a9ac3920cf798ec2f45dca723d577d725b505360 GIT binary patch literal 18682 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 dSLWwmnfduEuk%-4=TFb;{6A None: ) +def _collect_file_ids(content: Any) -> list[str]: + """Recursively collect `file_id` values from response content.""" + if isinstance(content, dict): + found = [content["file_id"]] if "file_id" in content else [] + return found + [fid for v in content.values() for fid in _collect_file_ids(v)] + if isinstance(content, list): + return [fid for item in content for fid in _collect_file_ids(item)] + return [] + + @pytest.mark.default_cassette("test_code_execution.yaml.gz") @pytest.mark.vcr @pytest.mark.parametrize("output_version", ["v0", "v1"]) def test_code_execution(output_version: Literal["v0", "v1"]) -> None: - """Note: this is a beta feature. - - TODO: Update to remove beta once generally available. - """ llm = ChatAnthropic( model=MODEL_NAME, # type: ignore[call-arg] - betas=["code-execution-2025-08-25"], output_version=output_version, ) @@ -1952,6 +1957,61 @@ def test_code_execution(output_version: Literal["v0", "v1"]) -> None: ) +@pytest.mark.default_cassette("test_skills.yaml.gz") +@pytest.mark.vcr +@pytest.mark.parametrize("output_version", ["v0", "v1"]) +def test_skills(output_version: Literal["v0", "v1"]) -> None: + """Load an Anthropic skill into the code execution container.""" + skills = [{"type": "anthropic", "skill_id": "xlsx"}] + code_execution = {"type": "code_execution_20250825", "name": "code_execution"} + llm = ChatAnthropic( + model=MODEL_NAME, # type: ignore[call-arg] + container={"skills": skills}, + reuse_last_container=True, + output_version=output_version, + ) + llm_with_tools = llm.bind_tools([code_execution]) + + input_message = { + "role": "user", + "content": "Create an xlsx file with a single cell containing the number 42.", + } + + # Stream the first turn. `.output` blocks until the stream finishes and + # returns the aggregated message. + # `stream_events` is typed as `Iterator[Any]` on a bound model; the v3 + # protocol returns a `ChatModelStream`. + stream = cast("Any", llm_with_tools.stream_events([input_message], version="v3")) + first_response = stream.output + + # The skill ran in the container and wrote a file. + container_id = first_response.response_metadata["container"]["id"] + assert container_id + assert _collect_file_ids(first_response.content) + + # `reuse_last_container` supplies the container ID on the next turn without + # dropping the skills. + messages: list = [ + input_message, + first_response, + {"role": "user", "content": "Now change the cell to 43."}, + ] + payload = llm._get_request_payload(messages, tools=[code_execution]) + assert payload["container"] == {"id": container_id, "skills": skills} + + # The aggregated stream is valid history, so the follow-up round-trips. + second_response = llm_with_tools.invoke(messages) + block_types = {block["type"] for block in second_response.content} # type: ignore[index] + if output_version == "v0": + assert { + "text", + "server_tool_use", + "bash_code_execution_tool_result", + } <= block_types + else: + assert {"text", "server_tool_call", "server_tool_result"} <= block_types + + @pytest.mark.default_cassette("test_remote_mcp.yaml.gz") @pytest.mark.vcr @pytest.mark.parametrize("output_version", ["v0", "v1"]) 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 0f2c3802c4..6c40ccf632 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -4985,3 +4985,99 @@ def test_unrelated_type_error_propagates_unchanged() -> None: llm.invoke([HumanMessage(content="test")]) assert exc_info.value is unrelated_error + + +_CODE_EXECUTION_TOOL = [{"type": "code_execution_20250825", "name": "code_execution"}] +_PPTX_SKILL = [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}] + + +def test_container_init_param() -> None: + """`container` set at construction is included in the payload.""" + llm = ChatAnthropic(model=MODEL_NAME, container={"skills": _PPTX_SKILL}) + payload = llm._get_request_payload( + [HumanMessage("Hello, world!")], tools=_CODE_EXECUTION_TOOL + ) + assert payload["container"] == {"skills": _PPTX_SKILL} + + +def test_container_runtime_overrides_init() -> None: + """A call-time `container` takes precedence over the init value.""" + llm = ChatAnthropic(model=MODEL_NAME, container={"skills": _PPTX_SKILL}) + payload = llm._get_request_payload( + [HumanMessage("Hello, world!")], + tools=_CODE_EXECUTION_TOOL, + container="container_runtime", + ) + assert payload["container"] == "container_runtime" + + +def test_container_merged_with_reused_container() -> None: + """`reuse_last_container` supplies an ID without dropping other keys.""" + llm = ChatAnthropic( + model=MODEL_NAME, container={"skills": _PPTX_SKILL}, reuse_last_container=True + ) + messages = [ + HumanMessage("Hello, world!"), + AIMessage("Done.", response_metadata={"container": {"id": "container_123"}}), + HumanMessage("Now edit it."), + ] + payload = llm._get_request_payload(messages, tools=_CODE_EXECUTION_TOOL) + assert payload["container"] == {"id": "container_123", "skills": _PPTX_SKILL} + + +def test_reuse_last_container_without_container_param() -> None: + """Without a `container`, a reused container is passed as a bare ID.""" + llm = ChatAnthropic(model=MODEL_NAME, reuse_last_container=True) + messages = [ + HumanMessage("Hello, world!"), + AIMessage("Done.", response_metadata={"container": {"id": "container_123"}}), + HumanMessage("Again."), + ] + payload = llm._get_request_payload(messages, tools=_CODE_EXECUTION_TOOL) + assert payload["container"] == "container_123" + + +def test_reuse_last_container_does_not_override_explicit_id() -> None: + """An explicitly passed container ID wins over `reuse_last_container`.""" + llm = ChatAnthropic(model=MODEL_NAME, reuse_last_container=True) + messages = [ + HumanMessage("Hello, world!"), + AIMessage("Done.", response_metadata={"container": {"id": "container_123"}}), + HumanMessage("Again."), + ] + payload = llm._get_request_payload( + messages, tools=_CODE_EXECUTION_TOOL, container="container_explicit" + ) + assert payload["container"] == "container_explicit" + + +def test_container_absent_by_default() -> None: + """When unset, `container` is stripped from the payload.""" + llm = ChatAnthropic(model=MODEL_NAME) + payload = llm._get_request_payload([HumanMessage("Hello, world!")]) + assert "container" not in payload + + +def test_skills_without_code_execution_tool_warns() -> None: + """Skills are inert without a code execution tool, so warn.""" + llm = ChatAnthropic(model=MODEL_NAME, container={"skills": _PPTX_SKILL}) + with pytest.warns(UserWarning, match="code execution tool"): + llm._get_request_payload([HumanMessage("Hello, world!")]) + + +def test_thinking_display_updates_enables_beta() -> None: + """`display="updates"` auto-enables its beta, routing through beta.messages.""" + llm = ChatAnthropic( + model=MODEL_NAME, thinking={"type": "adaptive", "display": "updates"} + ) + payload = llm._get_request_payload([HumanMessage("Hello, world!")]) + assert "thinking-display-updates-2026-08-18" in payload["betas"] + + +def test_thinking_display_summarized_does_not_enable_beta() -> None: + """Other `display` values are generally available.""" + llm = ChatAnthropic( + model=MODEL_NAME, thinking={"type": "adaptive", "display": "summarized"} + ) + payload = llm._get_request_payload([HumanMessage("Hello, world!")]) + assert "betas" not in payload