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