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-10-01 16:44:44
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-langchain-anthropic (Old)
and /work/SRC/openSUSE:Factory/.python-langchain-anthropic.new.1253 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-langchain-anthropic"
Thu Oct 1 16:44:44 2026 rev:15 rq:1381681 version:1.7.5
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-langchain-anthropic/python-langchain-anthropic.changes
2026-09-24 22:59:20.412946984 +0200
+++
/work/SRC/openSUSE:Factory/.python-langchain-anthropic.new.1253/python-langchain-anthropic.changes
2026-10-01 16:45:34.571346830 +0200
@@ -1,0 +2,21 @@
+Wed Sep 30 08:05:57 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 1.7.5:
+ * Add Claude Sonnet 5.5 compatibility: model profile with
+ capability discovery, thinking and tool-choice validation as
+ for Opus 5 / Fable 5.1 (forced tool choice and unsupported
+ thinking settings raise; use method="json_schema" for
+ structured output), mid-conversation system/tool changes,
+ and preservation of toolset namespaces, encrypted advisor
+ blocks, refusal details and stop_details through streaming
+ and replay (gh#langchain-ai/langchain#40882)
+ * Serialize AIMessage.invalid_tool_calls as tool_use blocks
+ with valid dictionary arguments (or {}) on replay, so the
+ matching tool_result is no longer rejected as orphaned
+ (gh#40864, gh#40853)
+- Follow upstream and raise the langchain-core floor to 1.6.6
+- Not affected by CVE-2026-55443: file-search middleware path
+ traversal was fixed upstream in 1.4.6, before the initial
+ openSUSE package (1.4.8)
+
+-------------------------------------------------------------------
Old:
----
langchain_anthropic-1.7.4.tar.gz
New:
----
langchain_anthropic-1.7.5.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-langchain-anthropic.spec ++++++
--- /var/tmp/diff_new_pack.kHn7xd/_old 2026-10-01 16:45:35.283376678 +0200
+++ /var/tmp/diff_new_pack.kHn7xd/_new 2026-10-01 16:45:35.286376804 +0200
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-langchain-anthropic
-Version: 1.7.4
+Version: 1.7.5
Release: 0
Summary: Integration package connecting Claude (Anthropic) APIs and
LangChain
License: MIT
@@ -29,7 +29,7 @@
BuildRequires: fdupes
BuildRequires: python-rpm-macros
Requires: python-anthropic >= 0.120.0
-Requires: python-langchain-core >= 1.6.2
+Requires: python-langchain-core >= 1.6.6
Requires: python-pydantic >= 2.7.4
BuildArch: noarch
# SECTION test requirements
@@ -37,7 +37,7 @@
BuildRequires: %{python_module blockbuster}
BuildRequires: %{python_module defusedxml}
BuildRequires: %{python_module freezegun}
-BuildRequires: %{python_module langchain-core >= 1.6.2}
+BuildRequires: %{python_module langchain-core >= 1.6.6}
BuildRequires: %{python_module pydantic >= 2.7.4}
BuildRequires: %{python_module pytest-asyncio}
BuildRequires: %{python_module pytest-mock}
++++++ langchain_anthropic-1.7.4.tar.gz -> langchain_anthropic-1.7.5.tar.gz
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langchain_anthropic-1.7.4/PKG-INFO
new/langchain_anthropic-1.7.5/PKG-INFO
--- old/langchain_anthropic-1.7.4/PKG-INFO 2020-02-02 01:00:00.000000000
+0100
+++ new/langchain_anthropic-1.7.5/PKG-INFO 2020-02-02 01:00:00.000000000
+0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.5
Name: langchain-anthropic
-Version: 1.7.4
+Version: 1.7.5
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/
@@ -24,7 +24,7 @@
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: <4.0.0,>=3.10.0
Requires-Dist: anthropic<2.0.0,>=0.120.0
-Requires-Dist: langchain-core<2.0.0,>=1.6.4
+Requires-Dist: langchain-core<2.0.0,>=1.6.6
Requires-Dist: pydantic<3.0.0,>=2.7.4
Description-Content-Type: text/markdown
@@ -61,6 +61,27 @@
For detailed information on how to contribute, see the [Contributing
Guide](https://docs.langchain.com/oss/python/contributing/overview).
+## Migrating to Claude Sonnet 5.5
+
+```python
+from langchain_anthropic import ChatAnthropic
+
+model = ChatAnthropic(
+ model="claude-sonnet-5-5",
+ max_tokens=16000,
+ output_config={"effort": "medium"},
+)
+```
+
+- Use `with_structured_output(schema, method="json_schema")` for native
structured output. Sonnet 5.5 rejects forced tool choice (`"any"` or a tool
name). Function-calling structured output does not force a call and raises a
parsing error if the model answers without one.
+- Thinking is adaptive by default. For no up-front thinking, use
`thinking={"type": "between_tools"}` at `high` effort or below, with no
additional thinking fields. `disabled` and budgeted `enabled` thinking are
unsupported.
+- Omit sampling settings; non-default `temperature`, `top_p`, and `top_k` are
rejected. Budget output tokens for both thinking and text.
+- Preserve signed thinking blocks, including empty ones, and keep history
append-only. Use mid-conversation system messages to change instructions or
tools rather than editing earlier turns.
+- Progress updates can arrive as thinking blocks. Use adaptive thinking with
`display="summarized"` or `display="updates"` to display them; the latter's
beta header is added automatically.
+- Computer use on the direct Claude API requires `computer_toolset_20260801`.
Preserve the returned tool-use content: its `toolset_name` is retained on
replay and copied to matching tool results. Remove the old fine-grained
streaming beta when using toolsets.
+
+See the [migration
guide](https://platform.claude.com/docs/en/models/sonnet-5-5/migration-guide)
for platform-specific restrictions, advisor pairings, and refusal/fallback
behavior.
+
## Resources
- [LangChain Academy](https://academy.langchain.com/) — comprehensive, free
courses on LangChain libraries and products, made by the LangChain team
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langchain_anthropic-1.7.4/README.md
new/langchain_anthropic-1.7.5/README.md
--- old/langchain_anthropic-1.7.4/README.md 2020-02-02 01:00:00.000000000
+0100
+++ new/langchain_anthropic-1.7.5/README.md 2020-02-02 01:00:00.000000000
+0100
@@ -31,6 +31,27 @@
For detailed information on how to contribute, see the [Contributing
Guide](https://docs.langchain.com/oss/python/contributing/overview).
+## Migrating to Claude Sonnet 5.5
+
+```python
+from langchain_anthropic import ChatAnthropic
+
+model = ChatAnthropic(
+ model="claude-sonnet-5-5",
+ max_tokens=16000,
+ output_config={"effort": "medium"},
+)
+```
+
+- Use `with_structured_output(schema, method="json_schema")` for native
structured output. Sonnet 5.5 rejects forced tool choice (`"any"` or a tool
name). Function-calling structured output does not force a call and raises a
parsing error if the model answers without one.
+- Thinking is adaptive by default. For no up-front thinking, use
`thinking={"type": "between_tools"}` at `high` effort or below, with no
additional thinking fields. `disabled` and budgeted `enabled` thinking are
unsupported.
+- Omit sampling settings; non-default `temperature`, `top_p`, and `top_k` are
rejected. Budget output tokens for both thinking and text.
+- Preserve signed thinking blocks, including empty ones, and keep history
append-only. Use mid-conversation system messages to change instructions or
tools rather than editing earlier turns.
+- Progress updates can arrive as thinking blocks. Use adaptive thinking with
`display="summarized"` or `display="updates"` to display them; the latter's
beta header is added automatically.
+- Computer use on the direct Claude API requires `computer_toolset_20260801`.
Preserve the returned tool-use content: its `toolset_name` is retained on
replay and copied to matching tool results. Remove the old fine-grained
streaming beta when using toolsets.
+
+See the [migration
guide](https://platform.claude.com/docs/en/models/sonnet-5-5/migration-guide)
for platform-specific restrictions, advisor pairings, and refusal/fallback
behavior.
+
## Resources
- [LangChain Academy](https://academy.langchain.com/) — comprehensive, free
courses on LangChain libraries and products, made by the LangChain team
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langchain_anthropic-1.7.4/langchain_anthropic/_compat.py
new/langchain_anthropic-1.7.5/langchain_anthropic/_compat.py
--- old/langchain_anthropic-1.7.4/langchain_anthropic/_compat.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_anthropic-1.7.5/langchain_anthropic/_compat.py
2020-02-02 01:00:00.000000000 +0100
@@ -129,8 +129,9 @@
"input": block.get("args", {}),
"id": block.get("id", ""),
}
- if "caller" in block.get("extras", {}):
- tool_use_block["caller"] = block["extras"]["caller"]
+ for key in ("caller", "toolset_name"):
+ if key in block.get("extras", {}):
+ tool_use_block[key] = block["extras"][key]
new_content.append(tool_use_block)
elif block["type"] == "tool_call_chunk":
@@ -147,6 +148,11 @@
"name": block.get("name", ""),
"input": input_,
"id": block.get("id", ""),
+ **{
+ key: block["extras"][key]
+ for key in ("caller", "toolset_name")
+ if key in block.get("extras", {})
+ },
}
)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langchain_anthropic-1.7.4/langchain_anthropic/_version.py
new/langchain_anthropic-1.7.5/langchain_anthropic/_version.py
--- old/langchain_anthropic-1.7.4/langchain_anthropic/_version.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_anthropic-1.7.5/langchain_anthropic/_version.py
2020-02-02 01:00:00.000000000 +0100
@@ -1,3 +1,3 @@
"""Version information for `langchain-anthropic`."""
-__version__ = "1.7.4"
+__version__ = "1.7.5"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langchain_anthropic-1.7.4/langchain_anthropic/chat_models.py
new/langchain_anthropic-1.7.5/langchain_anthropic/chat_models.py
--- old/langchain_anthropic-1.7.4/langchain_anthropic/chat_models.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_anthropic-1.7.5/langchain_anthropic/chat_models.py
2020-02-02 01:00:00.000000000 +0100
@@ -720,6 +720,7 @@
"""Format messages for Anthropic's API."""
system: str | list[dict] | None = None
formatted_messages: list[dict] = []
+ toolsets: dict[str, str] = {}
merged_messages = _merge_messages(messages)
last_non_system_index = max(
(i for i, m in enumerate(merged_messages) if m.type != "system"),
@@ -795,11 +796,13 @@
for tc in message.tool_calls
if tc["id"] == block["id"]
]
- content.extend(
- _lc_tool_calls_to_anthropic_tool_use_blocks(
- overlapping,
- ),
+ tool_blocks =
_lc_tool_calls_to_anthropic_tool_use_blocks(
+ overlapping,
)
+ if toolset_name := block.get("toolset_name"):
+ for tool_block in tool_blocks:
+ tool_block["toolset_name"] = toolset_name
+ content.extend(tool_blocks)
else:
if tool_input := block.get("input"):
args = tool_input
@@ -818,6 +821,8 @@
)
if caller := block.get("caller"):
tool_use_block["caller"] = caller
+ if toolset_name := block.get("toolset_name"):
+ tool_use_block["toolset_name"] = toolset_name
content.append(tool_use_block)
elif block["type"] in ("server_tool_use", "mcp_tool_use"):
formatted_block = {
@@ -944,6 +949,8 @@
},
),
)
+ elif block["type"] == "advisor_tool_result":
+ content.append({k: v for k, v in block.items() if k !=
"index"})
else:
content.append(block)
else:
@@ -957,8 +964,9 @@
else:
content = message.content
- # Ensure all tool_calls have a tool_use content block
- if isinstance(message, AIMessage) and message.tool_calls:
+ if isinstance(message, AIMessage) and (
+ message.tool_calls or message.invalid_tool_calls
+ ):
content = content or []
content = (
[{"type": "text", "text": message.content}]
@@ -981,6 +989,31 @@
cast("list", content).extend(
_lc_tool_calls_to_anthropic_tool_use_blocks(missing_tool_calls),
)
+ tool_use_ids.extend(
+ cast("str", _normalize_tool_call_id(tc["id"]))
+ for tc in missing_tool_calls
+ )
+ for invalid_call in message.invalid_tool_calls:
+ tool_call_id = invalid_call.get("id")
+ tool_name = invalid_call.get("name")
+ if not tool_call_id or not tool_name:
+ continue
+ normalized_id = _normalize_tool_call_id(tool_call_id)
+ if normalized_id in tool_use_ids:
+ continue
+ try:
+ args = json.loads(invalid_call.get("args") or "{}")
+ except json.JSONDecodeError:
+ args = {}
+ cast("list", content).append(
+ _AnthropicToolUse(
+ type="tool_use",
+ name=tool_name,
+ input=args if isinstance(args, dict) else {},
+ id=cast("str", normalized_id),
+ )
+ )
+ tool_use_ids.append(normalized_id)
if role == "assistant" and _i == last_non_system_index:
if isinstance(content, str):
@@ -1010,6 +1043,16 @@
system = _format_system_content(pending.content,
model=model)
_warn_system_message_hoisted(model)
pending_system = []
+ if isinstance(content, list):
+ for block in content:
+ if not isinstance(block, dict):
+ continue
+ if block.get("type") == "tool_use" and
block.get("toolset_name"):
+ toolsets[block["id"]] = block["toolset_name"]
+ elif block.get("type") == "tool_result" and (
+ toolset_name := toolsets.get(block.get("tool_use_id", ""))
+ ):
+ block.setdefault("toolset_name", toolset_name)
formatted_messages.append({"role": role, "content": content})
formatted_messages.extend(
@@ -1110,13 +1153,16 @@
"claude-mythos-5",
"claude-opus-4-8",
"claude-opus-5",
+ "claude-sonnet-5-5",
)
)
def _supports_forced_tool_choice(model: str) -> bool:
"""Return whether the model accepts `tool_choice` types `any` and
`tool`."""
- return not model.startswith(("claude-fable-5-1", "claude-opus-5-5"))
+ return not model.startswith(
+ ("claude-fable-5-1", "claude-opus-5-5", "claude-sonnet-5-5")
+ )
def _is_direct_anthropic_llm_type(llm_type: object) -> bool:
@@ -1767,7 +1813,8 @@
output_config["effort"] = effort
is_fable_model = self.model.startswith("claude-fable-5")
- if is_fable_model:
+ is_sonnet_55 = self.model.startswith("claude-sonnet-5-5")
+ if is_fable_model or is_sonnet_55:
top_k = request_config.get("top_k", self.top_k)
top_p = request_config.get("top_p", self.top_p)
temperature = request_config.get("temperature", self.temperature)
@@ -1793,7 +1840,7 @@
raise ValueError(msg)
if (
- (self.model.startswith("claude-opus-5") or is_fable_model)
+ (self.model.startswith("claude-opus-5") or is_fable_model or
is_sonnet_55)
and isinstance(thinking, Mapping)
and thinking.get("type") == "enabled"
):
@@ -2305,6 +2352,12 @@
warnings.warn("Received unexpected tool content block.",
stacklevel=2)
content_block = event.content_block.model_dump()
+ if event.content_block.type == "advisor_tool_result":
+ content_block = {
+ key: content_block[key]
+ for key in ("type", "tool_use_id", "content",
"cache_control")
+ if key in content_block
+ }
if "caller" in content_block and content_block["caller"] is None:
content_block.pop("caller")
content_block["index"] = event.index
@@ -2438,6 +2491,8 @@
"stop_reason": event.delta.stop_reason,
"stop_sequence": event.delta.stop_sequence,
}
+ if stop_details := event.delta.model_dump().get("stop_details"):
+ response_metadata["stop_details"] = stop_details
if context_management := getattr(event, "context_management",
None):
response_metadata["context_management"] = (
context_management.model_dump()
@@ -2856,8 +2911,8 @@
- `'function_calling'` (default): Use forced tool calling to
get
structured output. When `thinking` is enabled, or on models
that don't support forced tool use (Claude Opus 5.5, Claude
- Fable 5.1), the tool call isn't forced, and a missing tool
- call raises `OutputParserException`.
+ Fable 5.1, Claude Sonnet 5.5), the tool call isn't forced,
+ and a missing tool call raises `OutputParserException`.
- `'json_schema'`: Use Claude's dedicated
[structured
output](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)
feature.
@@ -3212,6 +3267,7 @@
input: dict
id: str
caller: NotRequired[dict[str, Any]]
+ toolset_name: NotRequired[str]
def _lc_tool_calls_to_anthropic_tool_use_blocks(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langchain_anthropic-1.7.4/langchain_anthropic/data/_profiles.py
new/langchain_anthropic-1.7.5/langchain_anthropic/data/_profiles.py
--- old/langchain_anthropic-1.7.4/langchain_anthropic/data/_profiles.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_anthropic-1.7.5/langchain_anthropic/data/_profiles.py
2020-02-02 01:00:00.000000000 +0100
@@ -488,4 +488,39 @@
],
"reasoning_effort_default": "high",
},
+ "claude-sonnet-5-5": {
+ "name": "Claude Sonnet 5.5",
+ "release_date": "2026-09-28",
+ "last_updated": "2026-09-28",
+ "open_weights": False,
+ "max_input_tokens": 1000000,
+ "max_output_tokens": 128000,
+ "text_inputs": True,
+ "image_inputs": True,
+ "audio_inputs": False,
+ "pdf_inputs": True,
+ "video_inputs": False,
+ "text_outputs": True,
+ "image_outputs": False,
+ "audio_outputs": False,
+ "video_outputs": False,
+ "reasoning_output": True,
+ "tool_calling": True,
+ "structured_output": True,
+ "attachment": True,
+ "temperature": False,
+ "image_url_inputs": True,
+ "pdf_tool_message": True,
+ "image_tool_message": True,
+ "tool_call_streaming": True,
+ "tool_choice": False,
+ "reasoning_effort_levels": [
+ "low",
+ "medium",
+ "high",
+ "xhigh",
+ "max",
+ ],
+ "reasoning_effort_default": "high",
+ },
}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langchain_anthropic-1.7.4/langchain_anthropic/data/profile_augmentations.toml
new/langchain_anthropic-1.7.5/langchain_anthropic/data/profile_augmentations.toml
---
old/langchain_anthropic-1.7.4/langchain_anthropic/data/profile_augmentations.toml
2020-02-02 01:00:00.000000000 +0100
+++
new/langchain_anthropic-1.7.5/langchain_anthropic/data/profile_augmentations.toml
2020-02-02 01:00:00.000000000 +0100
@@ -53,6 +53,12 @@
reasoning_effort_levels = ["low", "medium", "high", "xhigh", "max"]
reasoning_effort_default = "medium"
+[overrides."claude-sonnet-5-5"]
+structured_output = true
+tool_choice = false
+reasoning_effort_levels = ["low", "medium", "high", "xhigh", "max"]
+reasoning_effort_default = "high"
+
[overrides."claude-sonnet-5"]
structured_output = true
reasoning_effort_levels = ["low", "medium", "high", "xhigh", "max"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langchain_anthropic-1.7.4/pyproject.toml
new/langchain_anthropic-1.7.5/pyproject.toml
--- old/langchain_anthropic-1.7.4/pyproject.toml 2020-02-02
01:00:00.000000000 +0100
+++ new/langchain_anthropic-1.7.5/pyproject.toml 2020-02-02
01:00:00.000000000 +0100
@@ -20,11 +20,11 @@
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
-version = "1.7.4"
+version = "1.7.5"
requires-python = ">=3.10.0,<4.0.0"
dependencies = [
"anthropic>=0.120.0,<2.0.0",
- "langchain-core>=1.6.4,<2.0.0",
+ "langchain-core>=1.6.6,<2.0.0",
"pydantic>=2.7.4,<3.0.0",
]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langchain_anthropic-1.7.4/tests/unit_tests/test_chat_models.py
new/langchain_anthropic-1.7.5/tests/unit_tests/test_chat_models.py
--- old/langchain_anthropic-1.7.4/tests/unit_tests/test_chat_models.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_anthropic-1.7.5/tests/unit_tests/test_chat_models.py
2020-02-02 01:00:00.000000000 +0100
@@ -4144,6 +4144,60 @@
assert block["signature"] == "sig_xyz"
[email protected](
+ ("args", "expected_input"),
+ [('{"city":', {}), ('{"city":"Paris"}', {"city": "Paris"}), ("[1]", {})],
+)
+def test_invalid_tool_call_retains_tool_use_for_error_result(
+ args: str, expected_input: dict[str, Any]
+) -> None:
+ tool_call_id = "toolu_invalid"
+ ai_message = AIMessage(
+ content=[{"type": "text", "text": "Calling tool"}],
+ invalid_tool_calls=[{"name": "get_weather", "args": args, "id":
tool_call_id}],
+ )
+ tool_message = ToolMessage(
+ "Tool call arguments were malformed.",
+ tool_call_id=tool_call_id,
+ status="error",
+ )
+
+ _, messages = _format_messages(
+ [HumanMessage("Check the weather"), ai_message, tool_message],
+ model=MODEL_NAME,
+ )
+
+ assert messages[1]["content"] == [
+ {"type": "text", "text": "Calling tool"},
+ {
+ "type": "tool_use",
+ "name": "get_weather",
+ "input": expected_input,
+ "id": tool_call_id,
+ },
+ ]
+ assert messages[2]["content"][0]["tool_use_id"] == tool_call_id
+ assert messages[2]["content"][0]["is_error"] is True
+
+
+def test_invalid_tool_call_does_not_duplicate_existing_tool_use() -> None:
+ ai_message = AIMessage(
+ content=[
+ {"type": "tool_use", "name": "get_weather", "input": {}, "id":
"toolu_1"}
+ ],
+ tool_calls=[{"name": "get_weather", "args": {}, "id": "toolu_2"}],
+ invalid_tool_calls=[
+ {"name": "get_weather", "args": "bad", "id": "toolu_1"},
+ {"name": "get_weather", "args": "bad", "id": "toolu_2"},
+ {"name": "get_weather", "args": "bad", "id": None},
+ ],
+ )
+
+ _, messages = _format_messages([ai_message], model=MODEL_NAME)
+
+ assert [block["id"] for block in messages[0]["content"]] == ["toolu_1",
"toolu_2"]
+
+
def test_v1_invalid_tool_call_retains_tool_use_for_error_result() -> None:
"""An error result must retain its Anthropic tool-use block on replay."""
tool_call_id = "toolu_invalid"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langchain_anthropic-1.7.4/tests/unit_tests/test_sonnet55.py
new/langchain_anthropic-1.7.5/tests/unit_tests/test_sonnet55.py
--- old/langchain_anthropic-1.7.4/tests/unit_tests/test_sonnet55.py
1970-01-01 01:00:00.000000000 +0100
+++ new/langchain_anthropic-1.7.5/tests/unit_tests/test_sonnet55.py
2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,400 @@
+from typing import Any, cast
+from unittest.mock import MagicMock, patch
+
+import pytest
+from anthropic._models import construct_type
+from anthropic.types import (
+ RawContentBlockStartEvent,
+ RawMessageDeltaEvent,
+ ThinkingBlock,
+)
+from anthropic.types.beta import BetaRawMessageStreamEvent
+from langchain_core.exceptions import OutputParserException
+from langchain_core.messages import (
+ AIMessage,
+ AIMessageChunk,
+ HumanMessage,
+ SystemMessage,
+ ToolMessage,
+)
+from langchain_core.runnables import RunnableBinding, RunnableSequence
+
+from langchain_anthropic import ChatAnthropic
+from langchain_anthropic.chat_models import _format_messages
+
+MODEL = "claude-sonnet-5-5"
+TOOL = {"name": "answer", "input_schema": {"type": "object", "properties": {}}}
+
+
+def model(**kwargs: Any) -> ChatAnthropic:
+ return ChatAnthropic(model=MODEL, api_key="test", **kwargs)
+
+
[email protected](
+ ("choice", "expected"),
+ [
+ ("any", {"type": "any"}),
+ ("answer", {"type": "tool", "name": "answer"}),
+ ({"type": "any"}, {"type": "any"}),
+ ({"type": "tool", "name": "answer"}, {"type": "tool", "name":
"answer"}),
+ ],
+)
+def test_forced_tool_choice_left_to_api(choice: Any, expected: dict[str, str])
-> None:
+ llm = model()
+ assert (
+ cast("RunnableBinding", llm.bind_tools([TOOL],
tool_choice=choice)).kwargs[
+ "tool_choice"
+ ]
+ == expected
+ )
+ assert (
+ llm._get_request_payload("hello", tool_choice=expected)["tool_choice"]
+ == expected
+ )
+
+
+def test_tool_choice_and_structured_output() -> None:
+ llm = model()
+ assert cast("RunnableBinding", llm.bind_tools([TOOL],
tool_choice="auto")).kwargs[
+ "tool_choice"
+ ] == {"type": "auto"}
+ with pytest.warns(UserWarning, match="json_schema"):
+ structured = llm.with_structured_output(TOOL)
+ assert (
+ "tool_choice"
+ not in cast(
+ "RunnableBinding", cast("RunnableSequence", structured).first
+ ).kwargs
+ )
+ native = llm.with_structured_output(
+ {"title": "Answer", "type": "object", "properties": {}},
method="json_schema"
+ )
+ assert (
+ cast("RunnableBinding", cast("RunnableSequence", native).first).kwargs[
+ "output_config"
+ ]["format"]["type"]
+ == "json_schema"
+ )
+ older = ChatAnthropic(model="claude-sonnet-5", api_key="test")
+ assert cast("RunnableBinding", older.bind_tools([TOOL],
tool_choice="any")).kwargs[
+ "tool_choice"
+ ] == {"type": "any"}
+
+
[email protected](
+ "kwargs",
+ [
+ {"thinking": {"type": "disabled"}},
+ {"thinking": {"type": "enabled", "budget_tokens": 1024}},
+ {"temperature": 0},
+ {"top_p": 0.5},
+ {"top_k": 10},
+ ],
+)
+def test_invalid_configuration(kwargs: dict[str, Any]) -> None:
+ with pytest.raises(ValueError):
+ model(**kwargs)._get_request_payload("hello")
+ with pytest.raises(ValueError):
+ model()._get_request_payload("hello", **kwargs)
+
+
[email protected]("effort", ["low", "medium", "high", "xhigh", "max"])
+def test_between_tools(effort: str) -> None:
+ payload = model(thinking={"type": "between_tools"})._get_request_payload(
+ "hello", effort=effort
+ )
+ assert payload["thinking"] == {"type": "between_tools"}
+ assert payload["output_config"] == {"effort": effort}
+
+
[email protected]("extra", [{"display": "summarized"},
{"budget_tokens": 1024}])
+def test_between_tools_extra_fields_left_to_api(extra: dict[str, Any]) -> None:
+ thinking = {"type": "between_tools", **extra}
+ assert (
+ model(thinking=thinking)._get_request_payload("hello")["thinking"] ==
thinking
+ )
+
+
+def test_profile_and_defaults() -> None:
+ llm = model()
+ assert llm.max_tokens == 128000
+ assert llm.profile is not None
+ assert llm.profile["max_input_tokens"] == 1000000
+ assert llm.profile["structured_output"] is True
+ assert llm.profile["tool_choice"] is False
+ assert llm.profile["reasoning_effort_levels"] == [
+ "low",
+ "medium",
+ "high",
+ "xhigh",
+ "max",
+ ]
+ assert llm.profile["reasoning_effort_default"] == "high"
+ payload = llm._get_request_payload("hello")
+ assert not {"temperature", "top_p", "top_k", "thinking"} & payload.keys()
+ assert llm._get_request_payload("hello", effort="medium")["thinking"] == {
+ "type": "adaptive",
+ "display": "summarized",
+ }
+
+
+def test_mid_conversation_system() -> None:
+ system, messages = _format_messages(
+ [
+ SystemMessage("initial"),
+ HumanMessage("hello"),
+ SystemMessage("new instructions"),
+ AIMessage("answer"),
+ HumanMessage("next"),
+ ],
+ model=MODEL,
+ )
+ assert system == "initial"
+ assert [m["role"] for m in messages] == ["user", "system", "assistant",
"user"]
+
+
[email protected]("standard", [False, True])
+def test_toolset_round_trip(*, standard: bool) -> None:
+ content: list[str | dict[str, Any]] = [
+ {"type": "thinking", "thinking": "", "signature": "opaque-signature"},
+ {
+ "type": "tool_use",
+ "id": "call_1",
+ "name": "click",
+ "toolset_name": "computer",
+ "input": {"x": 1},
+ },
+ ]
+ ai = AIMessage(
+ content=content,
+ tool_calls=[
+ {"name": "click", "id": "call_1", "args": {"x": 2}, "type":
"tool_call"}
+ ],
+ response_metadata={"model_provider": "anthropic"},
+ )
+ if standard:
+ ai = ai.model_copy(
+ update={
+ "content": ai.content_blocks,
+ "response_metadata": {
+ "model_provider": "anthropic",
+ "output_version": "v1",
+ },
+ }
+ )
+ payload = model()._get_request_payload(
+ [HumanMessage("click"), ai, ToolMessage("done", tool_call_id="call_1")]
+ )
+ assert payload["messages"][1]["content"][0] == content[0]
+ tool = payload["messages"][1]["content"][1]
+ assert tool["toolset_name"] == "computer"
+ assert tool["input"] == {"x": 2}
+ assert payload["messages"][2]["content"][0]["toolset_name"] == "computer"
+
+
+def test_encrypted_advisor_streaming() -> None:
+ advisor_result = {
+ "type": "advisor_tool_result",
+ "tool_use_id": "srvtoolu_abc123",
+ "content": {
+ "type": "advisor_redacted_result",
+ "encrypted_content": "opaque-ciphertext",
+ },
+ }
+ event = cast(
+ RawContentBlockStartEvent,
+ construct_type(
+ type_=RawContentBlockStartEvent,
+ value={
+ "type": "content_block_start",
+ "index": 0,
+ "content_block": advisor_result,
+ },
+ ),
+ )
+ llm = model()
+ chunk, _ = llm._make_message_chunk_from_anthropic_event(
+ event, stream_usage=True, coerce_content_to_string=False,
block_start_event=None
+ )
+ assert chunk is not None
+ assert chunk.content == [{**advisor_result, "index": 0}]
+ payload = llm._get_request_payload(
+ [HumanMessage("help"), chunk, HumanMessage("continue")]
+ )
+ assert payload["messages"][1]["content"] == [advisor_result]
+
+
[email protected]("output_version", ["v0", "v1"])
+def test_encrypted_advisor_stream_aggregate_replay(output_version: str) ->
None:
+ server_tool_use = {
+ "type": "server_tool_use",
+ "id": "srvtoolu_abc123",
+ "name": "advisor",
+ "input": {},
+ }
+ advisor_result = {
+ "type": "advisor_tool_result",
+ "tool_use_id": "srvtoolu_abc123",
+ "content": {
+ "type": "advisor_redacted_result",
+ "encrypted_content": "opaque-ciphertext",
+ "stop_reason": None,
+ },
+ }
+ raw_events = [
+ {
+ "type": "message_start",
+ "message": {
+ "id": "msg_1",
+ "type": "message",
+ "role": "assistant",
+ "model": MODEL,
+ "content": [],
+ "stop_reason": None,
+ "stop_sequence": None,
+ "usage": {"input_tokens": 10, "output_tokens": 1},
+ },
+ },
+ {"type": "content_block_start", "index": 0, "content_block":
server_tool_use},
+ {"type": "content_block_stop", "index": 0},
+ {"type": "content_block_start", "index": 1, "content_block":
advisor_result},
+ {"type": "content_block_stop", "index": 1},
+ {
+ "type": "content_block_start",
+ "index": 2,
+ "content_block": {"type": "text", "text": ""},
+ },
+ {
+ "type": "content_block_delta",
+ "index": 2,
+ "delta": {"type": "text_delta", "text": "Use a token bucket."},
+ },
+ {"type": "content_block_stop", "index": 2},
+ {
+ "type": "message_delta",
+ "delta": {"stop_reason": "end_turn", "stop_sequence": None},
+ "usage": {"output_tokens": 20},
+ },
+ {"type": "message_stop"},
+ ]
+ events = [
+ construct_type(type_=BetaRawMessageStreamEvent, value=event)
+ for event in raw_events
+ ]
+ llm = model(output_version=output_version).bind_tools(
+ [{"type": "advisor_20260301", "name": "advisor", "model":
"claude-opus-5"}]
+ )
+ with patch.object(
+ ChatAnthropic, "_create", return_value=MagicMock(parse=lambda:
iter(events))
+ ):
+ chunks = [cast("AIMessageChunk", chunk) for chunk in
llm.stream("help")]
+ full = chunks[0]
+ for chunk in chunks[1:]:
+ full += chunk
+
+ payload = model()._get_request_payload(
+ [HumanMessage("help"), full, HumanMessage("continue")]
+ )
+ assert payload["messages"][1]["content"] == [
+ server_tool_use,
+ advisor_result,
+ {"type": "text", "text": "Use a token bucket."},
+ ]
+
+
+def test_refusal_details_streaming() -> None:
+ event = RawMessageDeltaEvent.model_validate(
+ {
+ "type": "message_delta",
+ "delta": {
+ "stop_reason": "refusal",
+ "stop_sequence": None,
+ "stop_details": {"type": "refusal", "category": "cyber"},
+ },
+ "usage": {"output_tokens": 3},
+ }
+ )
+ chunk, _ = model()._make_message_chunk_from_anthropic_event(
+ event, stream_usage=True, coerce_content_to_string=False,
block_start_event=None
+ )
+ assert chunk is not None
+ assert chunk.response_metadata["stop_details"]["category"] == "cyber"
+
+
+def test_unforced_structured_output_requires_tool_call() -> None:
+ with pytest.warns(UserWarning, match="json_schema"):
+ structured = model().with_structured_output(TOOL)
+ check = cast("RunnableSequence", structured).steps[1]
+ with pytest.raises(OutputParserException):
+ check.invoke(AIMessage("No tool call"))
+
+
+def test_mid_conversation_tool_change() -> None:
+ block = {
+ "type": "tool_addition",
+ "tool": {"type": "tool_reference", "name": "answer"},
+ }
+ _, messages = _format_messages(
+ [HumanMessage("hello"), SystemMessage([block]), AIMessage("answer")],
+ model=MODEL,
+ )
+ assert messages[1] == {"role": "system", "content": [block]}
+
+
+def test_default_thinking_stream_preserves_signature() -> None:
+ event = RawContentBlockStartEvent(
+ type="content_block_start",
+ index=0,
+ content_block=ThinkingBlock(
+ type="thinking", thinking="", signature="opaque-signature"
+ ),
+ )
+ chunk, _ = model()._make_message_chunk_from_anthropic_event(
+ event, stream_usage=True, coerce_content_to_string=True,
block_start_event=None
+ )
+ assert chunk is not None
+ payload = model()._get_request_payload(
+ [HumanMessage("hello"), chunk, HumanMessage("next")]
+ )
+ assert payload["messages"][1]["content"] == [
+ {"type": "thinking", "thinking": "", "signature": "opaque-signature"}
+ ]
+
+
+def test_standard_tool_chunk_namespace_replay() -> None:
+ chunk = AIMessageChunk(
+ content=[
+ {
+ "type": "tool_use",
+ "name": "click",
+ "id": "call_1",
+ "input": {},
+ "toolset_name": "computer",
+ "index": 0,
+ }
+ ],
+ tool_call_chunks=[
+ {
+ "type": "tool_call_chunk",
+ "name": "click",
+ "id": "call_1",
+ "args": "{}",
+ "index": 0,
+ }
+ ],
+ response_metadata={"model_provider": "anthropic"},
+ )
+ chunk = chunk.model_copy(
+ update={
+ "content": chunk.content_blocks,
+ "response_metadata": {
+ "model_provider": "anthropic",
+ "output_version": "v1",
+ },
+ }
+ )
+ payload = model()._get_request_payload(
+ [HumanMessage("click"), chunk, ToolMessage("done",
tool_call_id="call_1")]
+ )
+ assert payload["messages"][1]["content"][0]["toolset_name"] == "computer"
+ assert payload["messages"][2]["content"][0]["toolset_name"] == "computer"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langchain_anthropic-1.7.4/uv.lock
new/langchain_anthropic-1.7.5/uv.lock
--- old/langchain_anthropic-1.7.4/uv.lock 2020-02-02 01:00:00.000000000
+0100
+++ new/langchain_anthropic-1.7.5/uv.lock 2020-02-02 01:00:00.000000000
+0100
@@ -582,7 +582,7 @@
[[package]]
name = "langchain"
-version = "1.4.2"
+version = "1.4.3"
source = { editable = "../../langchain_v1" }
dependencies = [
{ name = "langchain-core" },
@@ -650,7 +650,7 @@
[[package]]
name = "langchain-anthropic"
-version = "1.7.4"
+version = "1.7.5"
source = { editable = "." }
dependencies = [
{ name = "anthropic" },
@@ -723,7 +723,7 @@
[[package]]
name = "langchain-core"
-version = "1.6.4"
+version = "1.6.6"
source = { editable = "../../core" }
dependencies = [
{ name = "httpx" },