Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-langchain-anthropic for 
openSUSE:Factory checked in at 2026-08-09 21:41:34
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-langchain-anthropic (Old)
 and      /work/SRC/openSUSE:Factory/.python-langchain-anthropic.new.16738 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-langchain-anthropic"

Sun Aug  9 21:41:34 2026 rev:5 rq:1370145 version:1.5.4

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-langchain-anthropic/python-langchain-anthropic.changes
    2026-07-29 19:01:28.370246400 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-langchain-anthropic.new.16738/python-langchain-anthropic.changes
 2026-08-09 21:41:50.483562202 +0200
@@ -1,0 +2,8 @@
+Fri Aug  7 05:04:55 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 1.5.4:
+  * Handle tool schemas with unsupported top-level composition
+  * Preserve caller tool_choice
+  * Add user_profile_id convenience attribute
+
+-------------------------------------------------------------------

Old:
----
  langchain_anthropic-1.5.3.tar.gz

New:
----
  langchain_anthropic-1.5.4.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-langchain-anthropic.spec ++++++
--- /var/tmp/diff_new_pack.fFro9W/_old  2026-08-09 21:41:51.139584614 +0200
+++ /var/tmp/diff_new_pack.fFro9W/_new  2026-08-09 21:41:51.143584751 +0200
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-langchain-anthropic
-Version:        1.5.3
+Version:        1.5.4
 Release:        0
 Summary:        Integration package connecting Claude (Anthropic) APIs and 
LangChain
 License:        MIT

++++++ langchain_anthropic-1.5.3.tar.gz -> langchain_anthropic-1.5.4.tar.gz 
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_anthropic-1.5.3/PKG-INFO 
new/langchain_anthropic-1.5.4/PKG-INFO
--- old/langchain_anthropic-1.5.3/PKG-INFO      2020-02-02 01:00:00.000000000 
+0100
+++ new/langchain_anthropic-1.5.4/PKG-INFO      2020-02-02 01:00:00.000000000 
+0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: langchain-anthropic
-Version: 1.5.3
+Version: 1.5.4
 Summary: Integration package connecting Claude (Anthropic) APIs and LangChain
 Project-URL: Homepage, 
https://docs.langchain.com/oss/python/integrations/providers/anthropic
 Project-URL: Documentation, 
https://reference.langchain.com/python/integrations/langchain_anthropic/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_anthropic-1.5.3/langchain_anthropic/_version.py 
new/langchain_anthropic-1.5.4/langchain_anthropic/_version.py
--- old/langchain_anthropic-1.5.3/langchain_anthropic/_version.py       
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_anthropic-1.5.4/langchain_anthropic/_version.py       
2020-02-02 01:00:00.000000000 +0100
@@ -1,3 +1,3 @@
 """Version information for `langchain-anthropic`."""
 
-__version__ = "1.5.3"
+__version__ = "1.5.4"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_anthropic-1.5.3/langchain_anthropic/chat_models.py 
new/langchain_anthropic-1.5.4/langchain_anthropic/chat_models.py
--- old/langchain_anthropic-1.5.3/langchain_anthropic/chat_models.py    
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_anthropic-1.5.4/langchain_anthropic/chat_models.py    
2020-02-02 01:00:00.000000000 +0100
@@ -11,7 +11,7 @@
 from collections.abc import AsyncIterator, Callable, Iterator, Mapping, 
Sequence
 from functools import cached_property
 from operator import itemgetter
-from typing import Any, Final, Literal, cast
+from typing import Any, Final, Literal, TypeGuard, cast
 
 import anthropic
 from langchain_core.callbacks import (
@@ -175,7 +175,7 @@
 """Valid Anthropic-specific extra fields"""
 
 
-def _is_builtin_tool(tool: Any) -> bool:
+def _is_builtin_tool(tool: Any) -> TypeGuard[dict[str, Any]]:
     """Check if a tool is a built-in (server-side) Anthropic tool.
 
     `tool` must be a `dict` and have a `type` key starting with one of the 
known
@@ -1126,6 +1126,17 @@
     docs for more information.
     """
 
+    user_profile_id: str | None = None
+    """User profile ID to attribute the request to.
+
+    Use when acting on behalf of a party other than your organization. Setting 
this
+    automatically enables the required `user-profiles` beta, routing the 
request
+    through `client.beta.messages.create`.
+
+    Can also be passed at call time, which overrides the value set here (for 
example,
+    `model.invoke(..., user_profile_id="uprof_...")`).
+    """
+
     @property
     def effort(self) -> Literal["max", "xhigh", "high", "medium", "low"] | 
None:
         """Alias for `reasoning_effort`."""
@@ -1421,6 +1432,7 @@
             "betas": self.betas,
             "context_management": self.context_management,
             "mcp_servers": self.mcp_servers,
+            "user_profile_id": self.user_profile_id,
             "system": system,
             **self.model_kwargs,
             **kwargs,
@@ -1569,6 +1581,15 @@
             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"
+            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:
@@ -2023,6 +2044,15 @@
                 See the 
[docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#strict-tool-use)
 for more info.
             kwargs: Any additional parameters are passed directly to `bind`.
 
+        Raises:
+            ValueError: If every tool in `tools` was dropped for using 
top-level
+                schema composition, leaving the model with no callable tool.
+            ValueError: If `tool_choice` forces tool use (a specific tool, or
+                `'any'`) while any tool was dropped, since the forced tool may 
be
+                unreachable. Does not apply when `thinking` is enabled, as the
+                forced choice is discarded before the request is sent.
+            ValueError: If `tool_choice` is neither a `dict`, a `str`, nor 
`None`.
+
         Example:
             ```python
             from langchain_anthropic import ChatAnthropic
@@ -2059,16 +2089,88 @@
         # Allows built-in tools either by their:
         # - Raw `dict` format
         # - Extracting extras["provider_tool_definition"] if provided on a 
BaseTool
-        formatted_tools = [
+        formatted_tools: list[Mapping[str, Any]] = [
             tool
             if _is_builtin_tool(tool)
             else convert_to_anthropic_tool(tool, strict=strict)
             for tool in tools
         ]
+        formatted_tools, dropped_tool_names = 
_drop_unsupported_root_composition_tools(
+            formatted_tools
+        )
+
+        # Dropping salvages a request when usable tools remain. If every tool 
was
+        # dropped there is nothing to salvage: the caller asked for a model 
with
+        # tools and would get one that cannot call any, so fail loudly instead 
of
+        # letting it surface later as a tool call that never happens.
+        if tools and not formatted_tools:
+            msg = (
+                f"All {len(tools)} bound tool(s) use a top-level "
+                f"{'/'.join(_TOP_LEVEL_SCHEMA_COMPOSITION_KEYS)} in their "
+                f"input_schema, which the Anthropic API rejects: "
+                f"{sorted(dropped_tool_names)}. No tool is left for the model 
to "
+                "call. If you control the schema, move the combinator under "
+                "`properties`; for structured output, 
`with_structured_output(..., "
+                "method='json_schema')` accepts these schemas directly. 
Otherwise "
+                "these tools cannot be used with Anthropic -- bind a subset 
that "
+                "excludes them, or raise it with the tool's author."
+            )
+            raise ValueError(msg)
+
+        # Reconcile tool_choice with the filtered list: forcing a tool that was
+        # dropped, or forcing tool use at all when the set the caller depends 
on
+        # has silently shrunk, still produces a 400 or a stuck agent loop.
+        if tool_choice and dropped_tool_names:
+            choice_type: str | None = None
+            choice_name: str | None = None
+            if isinstance(tool_choice, dict):
+                choice_type = tool_choice.get("type")
+                choice_name = tool_choice.get("name")
+            elif isinstance(tool_choice, str):
+                if tool_choice in ("any", "auto"):
+                    choice_type = tool_choice
+                else:
+                    choice_type, choice_name = "tool", tool_choice
+            # Thinking discards forced choices before the request is sent, so
+            # they need not refer to a tool that remains after filtering.
+            thinking_discards_forced_choice = (
+                self.thinking is not None
+                and self.thinking.get("type") in ("enabled", "adaptive")
+                and choice_type in ("any", "tool")
+            )
+            if not thinking_discards_forced_choice:
+                if choice_type == "tool" and choice_name in dropped_tool_names:
+                    msg = (
+                        f"tool_choice forces {choice_name!r}, but that tool 
was "
+                        "dropped because its input_schema uses a top-level "
+                        f"{'/'.join(_TOP_LEVEL_SCHEMA_COMPOSITION_KEYS)}, 
which "
+                        "the Anthropic API rejects. Stop forcing it, or -- if 
you "
+                        "control the schema -- move the combinator under "
+                        "`properties`. For structured output, "
+                        "`with_structured_output(..., method='json_schema')` "
+                        "accepts these schemas directly."
+                    )
+                    raise ValueError(msg)
+                if choice_type == "any":
+                    # Forcing tool use means the caller depends on a specific
+                    # reachable tool set, so losing any member of it is fatal 
--
+                    # not just losing all of them.
+                    msg = (
+                        "tool_choice='any' forces the model to call a tool, 
but "
+                        f"{sorted(dropped_tool_names)} were dropped because 
their "
+                        "input_schema uses a top-level "
+                        f"{'/'.join(_TOP_LEVEL_SCHEMA_COMPOSITION_KEYS)}, 
which "
+                        "the Anthropic API rejects, so the model can no longer 
"
+                        "call them. Use tool_choice='auto' to proceed with the 
"
+                        "remaining tools, or -- if you control the schema -- 
move "
+                        "the combinator under `properties`."
+                    )
+                    raise ValueError(msg)
+
         if not tool_choice:
             pass
         elif isinstance(tool_choice, dict):
-            kwargs["tool_choice"] = tool_choice
+            kwargs["tool_choice"] = tool_choice.copy()
         elif isinstance(tool_choice, str) and tool_choice in ("any", "auto"):
             kwargs["tool_choice"] = {"type": tool_choice}
         elif isinstance(tool_choice, str):
@@ -2347,7 +2449,11 @@
         if isinstance(formatted_system, str):
             kwargs["system"] = formatted_system
         if tools:
-            kwargs["tools"] = [convert_to_anthropic_tool(tool) for tool in 
tools]
+            # Filter the same schemas `bind_tools` drops, so counting tokens 
and
+            # sending a request agree on which tools the API will accept.
+            kwargs["tools"], _ = _drop_unsupported_root_composition_tools(
+                [convert_to_anthropic_tool(tool) for tool in tools]
+            )
         if self.context_management is not None:
             kwargs["context_management"] = self.context_management
 
@@ -2367,6 +2473,56 @@
         return response.input_tokens
 
 
+_TOP_LEVEL_SCHEMA_COMPOSITION_KEYS = ("oneOf", "anyOf")
+
+
+def _drop_unsupported_root_composition_tools(
+    tools: Sequence[Mapping[str, Any]],
+) -> tuple[list[Mapping[str, Any]], set[str]]:
+    """Drop tools whose root `input_schema` uses `oneOf`/`anyOf`.
+
+    The Anthropic API rejects these at request validation, failing the entire
+    request. A tool is dropped only if its `input_schema` is a mapping carrying
+    a root combinator, so built-in (server-side) tools -- which have no
+    `input_schema` -- and tools whose combinators are nested under `properties`
+    are passed through as the same objects, unmodified.
+
+    A `UserWarning` is emitted per dropped tool.
+
+    Args:
+        tools: Already-formatted tool definitions, as built by `bind_tools`.
+
+    Returns:
+        The retained tools, and the names of the dropped tools. A dropped tool
+        with no string `name` contributes no entry to the name set.
+    """
+    kept: list[Mapping[str, Any]] = []
+    dropped_tool_names: set[str] = set()
+    for tool in tools:
+        input_schema = tool.get("input_schema")
+        offending_keys = (
+            [k for k in _TOP_LEVEL_SCHEMA_COMPOSITION_KEYS if k in 
input_schema]
+            if isinstance(input_schema, Mapping)
+            else []
+        )
+        if not offending_keys:
+            kept.append(tool)
+            continue
+        tool_name = tool.get("name")
+        if isinstance(tool_name, str):
+            dropped_tool_names.add(tool_name)
+            described = repr(tool_name)
+        else:
+            described = "with no name"
+        warnings.warn(
+            f"Dropping tool {described}: its input_schema has a "
+            f"top-level {'/'.join(offending_keys)}, which the Anthropic API 
does "
+            "not support. The tool will not be available to the model.",
+            stacklevel=3,
+        )
+    return kept, dropped_tool_names
+
+
 def convert_to_anthropic_tool(
     tool: Mapping[str, Any] | type | Callable | BaseTool,
     *,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_anthropic-1.5.3/pyproject.toml 
new/langchain_anthropic-1.5.4/pyproject.toml
--- old/langchain_anthropic-1.5.3/pyproject.toml        2020-02-02 
01:00:00.000000000 +0100
+++ new/langchain_anthropic-1.5.4/pyproject.toml        2020-02-02 
01:00:00.000000000 +0100
@@ -20,7 +20,7 @@
     "Topic :: Scientific/Engineering :: Artificial Intelligence",
 ]
 
-version = "1.5.3"
+version = "1.5.4"
 requires-python = ">=3.10.0,<4.0.0"
 dependencies = [
     "anthropic>=0.120.0,<1.0.0",
@@ -57,7 +57,7 @@
     "langchain-tests>=1.1.9,<2.0.0",
     "langchain>=1.0.0,<2.0.0",
 ]
-lint = ["ruff>=0.13.1,<0.16.0"]
+lint = ["ruff>=0.13.1,<0.17.0"]
 test_integration = ["requests>=2.32.3,<3.0.0"]
 typing = [
     "mypy>=2.1.0,<2.2.0",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/langchain_anthropic-1.5.3/tests/unit_tests/test_chat_models.py 
new/langchain_anthropic-1.5.4/tests/unit_tests/test_chat_models.py
--- old/langchain_anthropic-1.5.3/tests/unit_tests/test_chat_models.py  
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_anthropic-1.5.4/tests/unit_tests/test_chat_models.py  
2020-02-02 01:00:00.000000000 +0100
@@ -6,6 +6,7 @@
 import os
 import warnings
 from collections.abc import Callable
+from types import SimpleNamespace
 from typing import Any, Literal, cast
 from unittest.mock import MagicMock, patch
 
@@ -26,7 +27,7 @@
 from langchain_core.tools import BaseTool, tool
 from langchain_core.tracers.base import BaseTracer
 from langchain_core.tracers.schemas import Run
-from pydantic import BaseModel, Field, SecretStr, ValidationError
+from pydantic import BaseModel, Field, RootModel, SecretStr, ValidationError
 from pytest import CaptureFixture, MonkeyPatch
 
 from langchain_anthropic import ChatAnthropic
@@ -34,6 +35,7 @@
 from langchain_anthropic.chat_models import (
     _TOOL_CALL_ID_PATTERN,
     _create_usage_metadata,
+    _drop_unsupported_root_composition_tools,
     _format_image,
     _format_messages,
     _is_builtin_tool,
@@ -1603,6 +1605,360 @@
     }
 
 
+def test_anthropic_bind_tools_does_not_mutate_tool_choice() -> None:
+    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+    )
+    tool_choice = {"type": "tool", "name": "GetWeather"}
+
+    chat_model_with_tools = chat_model.bind_tools(
+        [GetWeather], tool_choice=tool_choice, parallel_tool_calls=False
+    )
+
+    assert tool_choice == {"type": "tool", "name": "GetWeather"}
+    assert cast("RunnableBinding", 
chat_model_with_tools).kwargs["tool_choice"] == {
+        "type": "tool",
+        "name": "GetWeather",
+        "disable_parallel_tool_use": True,
+    }
+
+
+def test_bind_tools_drops_top_level_composition() -> None:
+    """Tools with a root `oneOf`/`anyOf` are dropped with a warning.
+
+    The Anthropic API rejects tool schemas carrying these keywords at the top
+    level, failing the whole request. MCP servers can emit them. See
+    https://github.com/langchain-ai/langchain/issues/39271.
+    """
+    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+    )
+    valid_tool = {
+        "name": "search",
+        "description": "Search",
+        "input_schema": {
+            "type": "object",
+            "properties": {"query": {"type": "string"}},
+            "required": ["query"],
+        },
+    }
+    invalid_tool = {
+        "name": "notion_create_attachment",
+        "description": "Create an attachment",
+        "input_schema": {
+            "type": "object",
+            "anyOf": [
+                {
+                    "type": "object",
+                    "properties": {"content": {"type": "string"}},
+                    "required": ["content"],
+                },
+                {
+                    "type": "object",
+                    "properties": {"source_url": {"type": "string"}},
+                    "required": ["source_url"],
+                },
+            ],
+        },
+    }
+    with pytest.warns(UserWarning, match="notion_create_attachment"):
+        chat_model_with_tools = chat_model.bind_tools([valid_tool, 
invalid_tool])
+
+    bound = cast("RunnableBinding", chat_model_with_tools).kwargs["tools"]
+    assert [t["name"] for t in bound] == ["search"]
+
+
+def test_bind_tools_keeps_nested_composition_without_warning() -> None:
+    """Combinators nested under `properties` are valid and left untouched."""
+    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+    )
+    tool = {
+        "name": "search",
+        "description": "Search",
+        "input_schema": {
+            "type": "object",
+            "properties": {
+                "value": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
+            },
+            "required": ["value"],
+        },
+    }
+    with warnings.catch_warnings():
+        warnings.simplefilter("error")  # no warning expected
+        chat_model_with_tools = chat_model.bind_tools([tool])
+
+    bound = cast("RunnableBinding", chat_model_with_tools).kwargs["tools"]
+    assert [t["name"] for t in bound] == ["search"]
+    assert bound[0]["input_schema"] == tool["input_schema"]
+
+
+def _composition_tool(name: str, keyword: str = "anyOf") -> dict:
+    """A tool whose root `input_schema` uses a top-level combinator."""
+    return {
+        "name": name,
+        "description": "Root schema composition.",
+        "input_schema": {
+            "type": "object",
+            keyword: [
+                {
+                    "type": "object",
+                    "properties": {"content": {"type": "string"}},
+                    "required": ["content"],
+                }
+            ],
+        },
+    }
+
+
+def _plain_tool(name: str) -> dict:
+    """A tool with a supported root `input_schema`."""
+    return {
+        "name": name,
+        "description": "Supported.",
+        "input_schema": {"type": "object", "properties": {}},
+    }
+
+
[email protected]("keyword", ["oneOf", "anyOf"])
+def test_bind_tools_drops_each_root_combinator(keyword: str) -> None:
+    """Every combinator in the unsupported set is filtered and named in the 
warning."""
+    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+    )
+    with pytest.warns(UserWarning, match=f"top-level {keyword}") as record:
+        bound = chat_model.bind_tools(
+            [_plain_tool("search"), _composition_tool("attach", keyword)]
+        )
+
+    assert [t["name"] for t in cast("RunnableBinding", bound).kwargs["tools"]] 
== [
+        "search"
+    ]
+    assert "attach" in str(record[0].message)
+
+
+def test_bind_tools_keeps_root_all_of_without_warning() -> None:
+    """A root `allOf` schema is supported and remains available to the 
model."""
+    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+    )
+    tool = _composition_tool("attach", "allOf")
+    with warnings.catch_warnings():
+        warnings.simplefilter("error")
+        bound = chat_model.bind_tools([tool])
+
+    bound_tools = cast("RunnableBinding", bound).kwargs["tools"]
+    assert [tool["name"] for tool in bound_tools] == ["attach"]
+    assert bound_tools[0]["input_schema"] == tool["input_schema"]
+
+
+def test_bind_tools_warning_names_every_offending_combinator() -> None:
+    """A schema with several root combinators reports all of them."""
+    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+    )
+    tool = _composition_tool("attach")
+    tool["input_schema"]["oneOf"] = [{"type": "object", "properties": {}}]
+    with pytest.warns(UserWarning, match="top-level oneOf/anyOf"):
+        chat_model.bind_tools([_plain_tool("search"), tool])
+
+
+def test_bind_tools_passes_builtin_tools_through_unfiltered() -> None:
+    """Built-in server-side tools have no `input_schema` and are never 
dropped."""
+    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+    )
+    builtin = {"type": "mcp_toolset", "mcp_server_name": "notion"}
+    with warnings.catch_warnings():
+        warnings.simplefilter("error")  # no warning expected
+        bound = chat_model.bind_tools([builtin])
+
+    assert cast("RunnableBinding", bound).kwargs["tools"] == [builtin]
+
+
+def test_drop_unsupported_tools_describes_unnamed_tool() -> None:
+    """A dropped tool with no `name` is described, not rendered as `None`.
+
+    Exercised on the helper directly: `convert_to_anthropic_tool` rejects a
+    nameless tool before `bind_tools` could ever reach this branch.
+    """
+    unnamed = _composition_tool("attach")
+    del unnamed["name"]
+    with pytest.warns(UserWarning, match="Dropping tool with no name") as 
record:
+        kept, dropped_names = 
_drop_unsupported_root_composition_tools([unnamed])
+
+    assert kept == []
+    assert dropped_names == set()
+    assert "None" not in str(record[0].message)
+
+
+def test_bind_tools_all_tools_dropped_raises() -> None:
+    """No usable tool remains, so there is nothing to salvage by dropping."""
+    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+    )
+    with (
+        pytest.warns(UserWarning, match="Dropping tool"),
+        pytest.raises(ValueError, match="All 1 bound tool"),
+    ):
+        chat_model.bind_tools([_composition_tool("attach")])
+
+
+def test_bind_tools_no_tools_does_not_claim_tools_were_dropped() -> None:
+    """An empty tool list is not a dropped tool list, and must not say so."""
+    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+    )
+    bound = chat_model.bind_tools([], tool_choice="any")
+    assert cast("RunnableBinding", bound).kwargs["tools"] == []
+
+
[email protected]("tool_choice", ["attach", {"type": "tool", "name": 
"attach"}])
+def test_bind_tools_dropped_tool_forced_by_tool_choice_raises(
+    tool_choice: str | dict,
+) -> None:
+    """A dropped forced tool raises locally even when valid tools remain."""
+    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+    )
+    with (
+        pytest.warns(UserWarning, match="Dropping tool"),
+        pytest.raises(ValueError, match="tool_choice forces 'attach'"),
+    ):
+        chat_model.bind_tools(
+            [_plain_tool("search"), _composition_tool("attach")],
+            tool_choice=tool_choice,
+        )
+
+
[email protected]("tool_choice", ["any", {"type": "any"}])
+def test_bind_tools_partial_drop_under_forced_any_raises(
+    tool_choice: str | dict,
+) -> None:
+    """Forcing tool use depends on the whole tool set, so losing any member is 
fatal.
+
+    Under `create_agent`'s `ToolStrategy`, `tool_choice='any'` plus a dropped
+    structured-output tool would otherwise loop until `GraphRecursionError`.
+    """
+    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+    )
+    with (
+        pytest.warns(UserWarning, match="Dropping tool"),
+        pytest.raises(ValueError, match="tool_choice='any' forces the model"),
+    ):
+        chat_model.bind_tools(
+            [_plain_tool("search"), _composition_tool("attach")],
+            tool_choice=tool_choice,
+        )
+
+
+def test_bind_tools_unknown_forced_tool_choice_is_left_to_the_api() -> None:
+    """Names the client can't see are not rejected locally.
+
+    `mcp_toolset` tools expose no per-tool `name` -- the names live on the MCP
+    server -- so a forced choice naming one is only resolvable server-side.
+    """
+    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+    )
+    bound = chat_model.bind_tools(
+        [{"type": "mcp_toolset", "mcp_server_name": "notion"}],
+        tool_choice="notion_create_attachment",
+    )
+    assert cast("RunnableBinding", bound).kwargs["tool_choice"] == {
+        "type": "tool",
+        "name": "notion_create_attachment",
+    }
+
+
+def test_with_structured_output_root_combinator_raises_actionable_error() -> 
None:
+    """A root-combinator schema fails at bind time, naming the real remedy."""
+    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+    )
+
+    class _Left(BaseModel):
+        a: int
+
+    class _Right(BaseModel):
+        b: str
+
+    class _Either(RootModel):
+        root: _Left | _Right
+
+    with (
+        pytest.warns(UserWarning, match="Dropping tool"),
+        pytest.raises(ValueError, match="method='json_schema'"),
+    ):
+        chat_model.with_structured_output(_Either, method="function_calling")
+
+
+def test_with_structured_output_root_combinator_raises_when_thinking_enabled() 
-> None:
+    """The thinking path must not degrade to a toolless request and a wrong 
error.
+
+    Without the bind-time raise, this spends an API call and then reports the
+    failure as a `thinking` limitation rather than a schema problem.
+    """
+    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+        thinking={"type": "enabled", "budget_tokens": 1024},
+    )
+
+    class _Left(BaseModel):
+        a: int
+
+    class _Right(BaseModel):
+        b: str
+
+    class _Either(RootModel):
+        root: _Left | _Right
+
+    with (
+        pytest.warns(UserWarning, match="Dropping tool"),
+        pytest.raises(ValueError, match="All 1 bound tool"),
+    ):
+        chat_model.with_structured_output(_Either, method="function_calling")
+
+
+def test_get_num_tokens_from_messages_filters_unsupported_tools() -> None:
+    """Token counting and sending agree on which tools the API will accept."""
+    chat_model = ChatAnthropic(  # type: ignore[call-arg, call-arg]
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+    )
+    counted: dict[str, Any] = {}
+
+    def _count_tokens(**kwargs: Any) -> Any:
+        counted.update(kwargs)
+        return SimpleNamespace(input_tokens=42)
+
+    with (
+        patch.object(chat_model._client.messages, "count_tokens", 
_count_tokens),
+        pytest.warns(UserWarning, match="Dropping tool"),
+    ):
+        chat_model.get_num_tokens_from_messages(
+            [HumanMessage("hi")],
+            tools=[_plain_tool("search"), _composition_tool("attach")],
+        )
+
+    assert [t["name"] for t in counted["tools"]] == ["search"]
+
+
 def test_fine_grained_tool_streaming_beta() -> None:
     """Test that fine-grained tool streaming beta can be enabled."""
     # Test with betas parameter at initialization
@@ -2159,6 +2515,41 @@
     assert payload["inference_geo"] == "us"
 
 
+def test_user_profile_id_init_param() -> None:
+    """`user_profile_id` set at construction is included in the payload."""
+    llm = ChatAnthropic(model=MODEL_NAME, user_profile_id="uprof_init")
+    input_message = HumanMessage("Hello, world!")
+    payload = llm._get_request_payload([input_message])
+    assert payload["user_profile_id"] == "uprof_init"
+    # Setting it auto-enables the required beta, routing through 
beta.messages.create.
+    assert "user-profiles-2026-03-24" in payload["betas"]
+
+
+def test_user_profile_id_runtime_param() -> None:
+    """`user_profile_id` passed at call time is included in the payload."""
+    llm = ChatAnthropic(model=MODEL_NAME)
+    input_message = HumanMessage("Hello, world!")
+    payload = llm._get_request_payload([input_message], 
user_profile_id="uprof_runtime")
+    assert payload["user_profile_id"] == "uprof_runtime"
+    assert "user-profiles-2026-03-24" in payload["betas"]
+
+
+def test_user_profile_id_runtime_overrides_init() -> None:
+    """A call-time `user_profile_id` takes precedence over the init value."""
+    llm = ChatAnthropic(model=MODEL_NAME, user_profile_id="uprof_init")
+    input_message = HumanMessage("Hello, world!")
+    payload = llm._get_request_payload([input_message], 
user_profile_id="uprof_runtime")
+    assert payload["user_profile_id"] == "uprof_runtime"
+
+
+def test_user_profile_id_absent_by_default() -> None:
+    """When unset, `user_profile_id` is stripped from the payload."""
+    llm = ChatAnthropic(model=MODEL_NAME)
+    input_message = HumanMessage("Hello, world!")
+    payload = llm._get_request_payload([input_message])
+    assert "user_profile_id" not in payload
+
+
 def test_anthropic_model_params() -> None:
     llm = ChatAnthropic(model=MODEL_NAME)
 
@@ -3719,6 +4110,49 @@
     assert len(w) == 1
 
 
[email protected](
+    "thinking",
+    [
+        pytest.param({"type": "enabled", "budget_tokens": 5000}, id="enabled"),
+        pytest.param({"type": "adaptive"}, id="adaptive"),
+    ],
+)
+def 
test_bind_tools_drops_forced_choice_for_filtered_tool_when_thinking_enabled(
+    thinking: dict[str, Any],
+) -> None:
+    """Thinking takes precedence over forced-choice validation.
+
+    Thinking discards a forced `tool_choice` before the request is sent, so the
+    choice need not survive filtering. A usable tool must remain, though --
+    otherwise the all-tools-dropped guard applies regardless of thinking.
+    """
+    chat_model = ChatAnthropic(
+        model=MODEL_NAME,
+        anthropic_api_key="secret-api-key",
+        thinking=thinking,
+    )
+    unsupported_tool = {
+        "name": "unsupported_tool",
+        "description": "A tool with an unsupported root schema composition.",
+        "input_schema": {"oneOf": [{"type": "string"}, {"type": "number"}]},
+    }
+
+    with warnings.catch_warnings(record=True) as w:
+        warnings.simplefilter("always")
+        result = chat_model.bind_tools(
+            [_plain_tool("search"), unsupported_tool],
+            tool_choice="unsupported_tool",
+        )
+
+    assert "tool_choice" not in cast("RunnableBinding", result).kwargs
+    assert [t["name"] for t in cast("RunnableBinding", 
result).kwargs["tools"]] == [
+        "search"
+    ]
+    assert len(w) == 2
+    assert "unsupported_tool" in str(w[0].message)
+    assert "thinking is enabled" in str(w[1].message)
+
+
 def test_bind_tools_drops_forced_tool_choice_when_adaptive_thinking() -> None:
     """Adaptive thinking has the same forced tool_choice restriction as 
enabled."""
     chat_model = ChatAnthropic(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/langchain_anthropic-1.5.3/uv.lock 
new/langchain_anthropic-1.5.4/uv.lock
--- old/langchain_anthropic-1.5.3/uv.lock       2020-02-02 01:00:00.000000000 
+0100
+++ new/langchain_anthropic-1.5.4/uv.lock       2020-02-02 01:00:00.000000000 
+0100
@@ -343,7 +343,7 @@
 version = "1.3.0"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
-    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+    { name = "typing-extensions" },
 ]
 sdist = { url = 
"https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz";,
 hash = 
"sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size 
= 29749, upload-time = "2025-05-10T17:42:51.123Z" }
 wheels = [
@@ -587,7 +587,7 @@
 provides-extras = ["community", "anthropic", "openai", "azure-ai", 
"google-vertexai", "google-genai", "fireworks", "ollama", "together", 
"mistralai", "huggingface", "groq", "aws", "baseten", "deepseek", "xai", 
"perplexity", "meta"]
 
 [package.metadata.requires-dev]
-lint = [{ name = "ruff", specifier = ">=0.15.0,<0.16.0" }]
+lint = [{ name = "ruff", specifier = ">=0.15.0,<0.17.0" }]
 test = [
     { name = "blockbuster", specifier = ">=1.5.26,<1.6.0" },
     { name = "langchain-openai", editable = "../openai" },
@@ -617,7 +617,7 @@
 
 [[package]]
 name = "langchain-anthropic"
-version = "1.5.3"
+version = "1.5.4"
 source = { editable = "." }
 dependencies = [
     { name = "anthropic" },
@@ -663,7 +663,7 @@
 ]
 
 [package.metadata.requires-dev]
-lint = [{ name = "ruff", specifier = ">=0.13.1,<0.16.0" }]
+lint = [{ name = "ruff", specifier = ">=0.13.1,<0.17.0" }]
 test = [
     { name = "blockbuster", specifier = ">=1.5.5,<1.6" },
     { name = "defusedxml", specifier = ">=0.7.1,<1.0.0" },
@@ -690,7 +690,7 @@
 
 [[package]]
 name = "langchain-core"
-version = "1.5.2"
+version = "1.5.3"
 source = { editable = "../../core" }
 dependencies = [
     { name = "jsonpatch" },
@@ -723,7 +723,7 @@
     { name = "jupyter", specifier = ">=1.0.0,<2.0.0" },
     { name = "setuptools", specifier = ">=67.6.1,<84.0.0" },
 ]
-lint = [{ name = "ruff", specifier = ">=0.15.0,<0.16.0" }]
+lint = [{ name = "ruff", specifier = ">=0.15.0,<0.17.0" }]
 test = [
     { name = "blockbuster", specifier = ">=1.5.18,<1.6.0" },
     { name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
@@ -798,11 +798,11 @@
 ]
 
 [package.metadata.requires-dev]
-lint = [{ name = "ruff", specifier = ">=0.15.0,<0.16.0" }]
+lint = [{ name = "ruff", specifier = ">=0.15.0,<0.17.0" }]
 test = []
 test-integration = []
 typing = [
-    { name = "mypy", specifier = ">=2.1.0,<2.2.0" },
+    { name = "mypy", specifier = ">=2.1.0,<2.4.0" },
     { name = "types-pyyaml", specifier = ">=6.0.12.2,<7.0.0.0" },
 ]
 

Reply via email to