Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-langchain-aws for
openSUSE:Factory checked in at 2026-08-27 18:51:35
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-langchain-aws (Old)
and /work/SRC/openSUSE:Factory/.python-langchain-aws.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-langchain-aws"
Thu Aug 27 18:51:35 2026 rev:8 rq:1373868 version:1.7.4
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-langchain-aws/python-langchain-aws.changes
2026-08-21 16:54:52.304252906 +0200
+++
/work/SRC/openSUSE:Factory/.python-langchain-aws.new.1265/python-langchain-aws.changes
2026-08-27 18:55:01.352218038 +0200
@@ -1,0 +2,20 @@
+Wed Aug 26 17:55:37 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 1.7.4:
+ * ChatBedrockConverse now builds Converse reasoning blocks per
+ model id: reasoning is dropped entirely for models that reject
+ it (deepseek.r1 and the inline-reasoning models), unsigned
+ reasoning is kept only for openai.gpt-oss, amazon.nova-2,
+ deepseek.v3, minimax and kimi, and encrypted redactedContent
+ is passed through untouched
+ * Emit streaming redacted_content reasoning blocks into the
+ LangChain content list instead of discarding them
+ * ChatAnthropicMantle and ChatOpenAIMantle now raise ValueError
+ on guardrail_config/guardrails and on x-amzn-bedrock-guardrail*
+ headers, which the Mantle endpoint silently ignores
+ * Align ChatAnthropicMantle auth precedence with the Anthropic
+ SDK Mantle client: explicitly passed SigV4 credentials now
+ outrank a bearer key sourced from AWS_BEARER_TOKEN_BEDROCK
+- Follow upstream and raise the langchain-core floor to >= 1.6.0
+
+-------------------------------------------------------------------
Old:
----
langchain_aws-1.7.3.tar.gz
New:
----
langchain_aws-1.7.4.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-langchain-aws.spec ++++++
--- /var/tmp/diff_new_pack.oNX0Ip/_old 2026-08-27 18:55:02.296250997 +0200
+++ /var/tmp/diff_new_pack.oNX0Ip/_new 2026-08-27 18:55:02.297251032 +0200
@@ -17,7 +17,7 @@
Name: python-langchain-aws
-Version: 1.7.3
+Version: 1.7.4
Release: 0
Summary: LangChain integrations for AWS
License: MIT
@@ -28,14 +28,14 @@
BuildRequires: fdupes
BuildRequires: python-rpm-macros
Requires: python-boto3 >= 1.43.64
-Requires: python-langchain-core >= 1.4.7
+Requires: python-langchain-core >= 1.6.0
Requires: python-numpy >= 1.0.0
Requires: python-pydantic >= 2.10.6
BuildArch: noarch
# SECTION test requirements
BuildRequires: %{python_module boto3 >= 1.43.64}
BuildRequires: %{python_module langchain-anthropic}
-BuildRequires: %{python_module langchain-core >= 1.4.7}
+BuildRequires: %{python_module langchain-core >= 1.6.0}
BuildRequires: %{python_module langgraph}
BuildRequires: %{python_module numpy >= 1.0.0}
BuildRequires: %{python_module pydantic >= 2.10.6}
++++++ langchain_aws-1.7.3.tar.gz -> langchain_aws-1.7.4.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langchain_aws-1.7.3/PKG-INFO
new/langchain_aws-1.7.4/PKG-INFO
--- old/langchain_aws-1.7.3/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_aws-1.7.4/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.5
Name: langchain-aws
-Version: 1.7.3
+Version: 1.7.4
Summary: An integration package connecting AWS and LangChain
Project-URL: Source Code,
https://github.com/langchain-ai/langchain-aws/tree/main/libs/aws
Project-URL: Repository, https://github.com/langchain-ai/langchain-aws
@@ -8,7 +8,7 @@
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: boto3>=1.43.64
-Requires-Dist: langchain-core>=1.4.7
+Requires-Dist: langchain-core>=1.6.0
Requires-Dist: numpy<3,>=1.0.0
Requires-Dist: pydantic<3,>=2.10.6
Provides-Extra: anthropic
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langchain_aws-1.7.3/langchain_aws/chat_models/anthropic.py
new/langchain_aws-1.7.4/langchain_aws/chat_models/anthropic.py
--- old/langchain_aws-1.7.3/langchain_aws/chat_models/anthropic.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_aws-1.7.4/langchain_aws/chat_models/anthropic.py
2020-02-02 01:00:00.000000000 +0100
@@ -23,7 +23,11 @@
from langchain_aws._version import _add_langchain_aws_version
from langchain_aws.chat_models._anthropic_utils import
_create_bedrock_client_params
from langchain_aws.data._profiles import _PROFILES
-from langchain_aws.utils import MODEL_ID_GEO_PREFIXES
+from langchain_aws.utils import (
+ _MANTLE_GUARDRAILS_ERR_MSG,
+ MODEL_ID_GEO_PREFIXES,
+ _check_no_mantle_guardrail_headers,
+)
_MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES)
@@ -375,7 +379,16 @@
``AWS_BEARER_TOKEN_BEDROCK`` environment variable.
- **AWS SigV4** with standard AWS credentials — explicit keys, a named
profile, or the default credential chain (environment, instance profile,
- SSO, etc.). Used automatically whenever no API key is provided.
+ SSO, etc.).
+
+ Note that if multiple credential sources are provided/available, the
+ ``AnthropicBedrockMantle`` client resolves priority as follows:
+
+ 1. Explicit ``bedrock_api_key``
+ 2. Explicit ``aws_access_key_id``/``aws_secret_access_key``
+ 3. Explicit ``credentials_profile_name``
+ 4. ``AWS_BEARER_TOKEN_BEDROCK`` env variable
+ 5. Default AWS credential chain (SigV4)
See the [Claude Platform
docs](https://platform.claude.com/docs/en/about-claude/models/overview)
for the latest models, their capabilities, and pricing.
@@ -416,8 +429,9 @@
"""Amazon Bedrock API key used to authenticate to Mantle.
If not provided, read from the ``AWS_BEARER_TOKEN_BEDROCK`` environment
- variable. When neither is set, the client falls back to AWS SigV4 using
- the credentials below (or the default AWS credential chain).
+ variable. An explicitly passed key always selects bearer authentication;
+ an environment-sourced key is outranked by explicitly passed SigV4
+ credentials. See the class docstring for the full selection order.
"""
aws_access_key_id: SecretStr | None = Field(
@@ -469,6 +483,32 @@
values["anthropic_api_key"] = ""
return values
+ @model_validator(mode="before")
+ @classmethod
+ def _reject_guardrails(cls, values: Any) -> Any:
+ # TODO: remove after Mantle adds guardrails support
+ if isinstance(values, dict):
+ if any(
+ values.get(key) is not None
+ for key in ("guardrail_config", "guardrails")
+ ):
+ raise ValueError(_MANTLE_GUARDRAILS_ERR_MSG)
+ _check_no_mantle_guardrail_headers(values.get("default_headers"))
+ return values
+
+ def _get_request_payload(
+ self,
+ input_: Any,
+ *,
+ stop: list[str] | None = None,
+ **kwargs: Any,
+ ) -> dict:
+ # TODO: remove after Mantle adds guardrails support
+ if kwargs.get("guardrail_config") is not None:
+ raise ValueError(_MANTLE_GUARDRAILS_ERR_MSG)
+ _check_no_mantle_guardrail_headers(kwargs.get("extra_headers"))
+ return super()._get_request_payload(input_, stop=stop, **kwargs)
+
@property
def _client_params(self) -> dict[str, Any]:
"""Get client parameters for AnthropicBedrockMantle."""
@@ -485,7 +525,17 @@
}
if self.anthropic_api_url and "api.anthropic.com" not in
self.anthropic_api_url:
client_params["base_url"] = self.anthropic_api_url
- if self.bedrock_api_key:
+ explicit_sigv4_credentials = (
+ "credentials_profile_name" in self.model_fields_set
+ and bool(self.credentials_profile_name)
+ ) or (
+ bool({"aws_access_key_id", "aws_secret_access_key"} &
self.model_fields_set)
+ and self.aws_access_key_id is not None
+ and self.aws_secret_access_key is not None
+ )
+ if self.bedrock_api_key and (
+ "bedrock_api_key" in self.model_fields_set or not
explicit_sigv4_credentials
+ ):
client_params["api_key"] = self.bedrock_api_key.get_secret_value()
if self.aws_access_key_id:
client_params["aws_access_key"] =
self.aws_access_key_id.get_secret_value()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langchain_aws-1.7.3/langchain_aws/chat_models/bedrock_converse.py
new/langchain_aws-1.7.4/langchain_aws/chat_models/bedrock_converse.py
--- old/langchain_aws-1.7.3/langchain_aws/chat_models/bedrock_converse.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_aws-1.7.4/langchain_aws/chat_models/bedrock_converse.py
2020-02-02 01:00:00.000000000 +0100
@@ -1330,7 +1330,9 @@
logger.debug(f"Using raw blocks: {self.raw_blocks}")
bedrock_messages, system = self.raw_blocks, []
else:
- bedrock_messages, system = _messages_to_bedrock(messages,
self.system)
+ bedrock_messages, system = _messages_to_bedrock(
+ messages, self.system, model_id=self._get_base_model()
+ )
if self.guard_last_turn_only:
logger.debug("Applying selective guardrail to only the last
turn")
self._apply_guard_last_turn_only(bedrock_messages)
@@ -1406,7 +1408,9 @@
logger.debug(f"Using raw blocks: {self.raw_blocks}")
bedrock_messages, system = self.raw_blocks, []
else:
- bedrock_messages, system = _messages_to_bedrock(messages,
self.system)
+ bedrock_messages, system = _messages_to_bedrock(
+ messages, self.system, model_id=self._get_base_model()
+ )
if self.guard_last_turn_only:
logger.debug("Applying selective guardrail to only the last
turn")
self._apply_guard_last_turn_only(bedrock_messages)
@@ -2116,7 +2120,9 @@
bedrock_messages, system = (
(self.raw_blocks, [])
if self.raw_blocks
- else _messages_to_bedrock(messages, self.system)
+ else _messages_to_bedrock(
+ messages, self.system, model_id=self._get_base_model()
+ )
)
input_data = {"converse": {"messages": bedrock_messages}}
@@ -2253,6 +2259,8 @@
def _messages_to_bedrock(
messages: List[BaseMessage],
system: Optional[List[Union[str, Dict[str, Any]]]] = None,
+ *,
+ model_id: Optional[str] = None,
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
"""Handle Bedrock converse and Anthropic style content blocks"""
for idx, message in enumerate(messages):
@@ -2305,7 +2313,9 @@
# raising, so a block this module simply does not handle yet stays a
# loud bug rather than silently vanishing from the prompt.
content = _lc_content_to_bedrock(
- msg.content, drop_unsupported=isinstance(msg, AIMessage)
+ msg.content,
+ drop_unsupported=isinstance(msg, AIMessage),
+ model_id=model_id,
)
if isinstance(msg, HumanMessage):
# If there's a human, tool, human message sequence, the
@@ -2451,9 +2461,9 @@
) -> List[Dict[str, Any]]:
"""Split text into ordered text / reasoning_content blocks on complete tag
pairs.
- Each ``open_tag ... close_tag`` pair becomes a ``reasoning_content`` block
(no
- ``signature``, so ``_lc_content_to_bedrock`` drops it on round-trips);
surrounding
- text stays as ``text`` blocks.
+ Each ``open_tag ... close_tag`` pair becomes a ``reasoning_content`` block
(which
+ ``_bedrock_reasoning_block`` drops on round-trips, since these models
reject
+ reasoning content); surrounding text stays as ``text`` blocks.
"""
if open_tag not in text:
return [{"type": "text", "text": text}]
@@ -2759,10 +2769,60 @@
return blocks or [{"text": EMPTY_CONTENT}]
+def _bedrock_reasoning_block(
+ reasoning: Dict[str, Any], model_id: Optional[str]
+) -> Optional[Dict[str, Any]]:
+ """Build a Converse `reasoningContent` block, or `None` if `model_id`
rejects it."""
+ # Models that reject reasoning content in prior assistant turns entirely,
whether
+ # or not it carries a signature. Models that emit inline reasoning (see
+ # `_inline_reasoning_tags`) reject it too, and are detected rather than
listed.
+ _reasoning_unsupported_models = ("deepseek.r1",)
+ # Models verified to accept reasoning content carrying no signature.
Anything not
+ # listed keeps its reasoning only when signed.
+ _unsigned_reasoning_models = (
+ "openai.gpt-oss",
+ "amazon.nova-2",
+ "deepseek.v3",
+ "minimax",
+ "kimi",
+ )
+
+ model_id_lower = (model_id or "").lower()
+ # TODO: `_get_base_model()` returns the raw ARN when `model_id` is an ARN
and
+ # `base_model_id` is unset, so this misses Nova v1. Strip the ARN there,
then
+ # simplify this to use the resolved provider.
+ provider = model_id_lower.partition(".")[0]
+
+ if any(
+ model in model_id_lower for model in _reasoning_unsupported_models
+ ) or _inline_reasoning_tags(provider, model_id_lower):
+ logger.debug("Dropping reasoning block; %s rejects reasoning content",
model_id)
+ return None
+
+ # Encrypted reasoning is opaque, so there is no text or signature to gate
on.
+ if redacted := reasoning.get("redactedContent"):
+ return {"reasoningContent": {"redactedContent": redacted}}
+
+ text = reasoning.get("text", "")
+ signature = reasoning.get("signature", "")
+ if not signature and (
+ not text
+ or not any(model in model_id_lower for model in
_unsigned_reasoning_models)
+ ):
+ logger.debug("Dropping unsigned reasoning block for model %s",
model_id)
+ return None
+
+ reasoning_text: Dict[str, Any] = {"text": text}
+ if signature:
+ reasoning_text["signature"] = signature
+ return {"reasoningContent": {"reasoningText": reasoning_text}}
+
+
def _lc_content_to_bedrock(
content: Union[str, List[Union[str, Dict[str, Any]]]],
*,
drop_unsupported: bool = False,
+ model_id: Optional[str] = None,
) -> List[Dict[str, Any]]:
if isinstance(content, str):
if not content or content.isspace():
@@ -2935,7 +2995,9 @@
"toolUseId": block["toolUseId"],
"content": _empty_content_fallback(
_lc_content_to_bedrock(
- block["content"],
drop_unsupported=drop_unsupported
+ block["content"],
+ drop_unsupported=drop_unsupported,
+ model_id=model_id,
)
),
"status": "error" if block.get("isError") else
"success",
@@ -2948,32 +3010,22 @@
elif block["type"] == "guard_content":
bedrock_content.append({"guardContent": {"text": {"text":
block["text"]}}})
elif block["type"] == "thinking":
- if block.get("signature", ""):
- bedrock_content.append(
- {
- "reasoningContent": {
- "reasoningText": {
- "text": block.get("thinking", ""),
- "signature": block.get("signature", ""),
- }
- }
- }
- )
+ reasoning_block = _bedrock_reasoning_block(
+ {
+ "text": block.get("thinking", ""),
+ "signature": block.get("signature", ""),
+ },
+ model_id,
+ )
+ if reasoning_block:
+ bedrock_content.append(reasoning_block)
elif block["type"] == "reasoning_content":
reasoning_content = block.get("reasoningContent") or block.get(
"reasoning_content", {}
)
- if reasoning_content.get("signature", ""):
- bedrock_content.append(
- {
- "reasoningContent": {
- "reasoningText": {
- "text": reasoning_content.get("text", ""),
- "signature":
reasoning_content.get("signature", ""),
- }
- }
- }
- )
+ reasoning_block = _bedrock_reasoning_block(reasoning_content,
model_id)
+ if reasoning_block:
+ bedrock_content.append(reasoning_block)
elif block["type"] == "non_standard" and "value" in block:
# langchain-core's content_blocks property wraps provider-specific
# blocks (e.g. cachePoint, guardContent) that lack a recognized
@@ -3194,6 +3246,15 @@
)
# Streaming block format
else:
+ if "redacted_content" in reasoning_dict:
+ lc_content.append(
+ {
+ "type": "reasoning_content",
+ "reasoning_content": {
+ "redacted_content":
reasoning_dict["redacted_content"],
+ },
+ }
+ )
if "text" in reasoning_dict:
lc_content.append(
{
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langchain_aws-1.7.3/langchain_aws/chat_models/openai.py
new/langchain_aws-1.7.4/langchain_aws/chat_models/openai.py
--- old/langchain_aws-1.7.3/langchain_aws/chat_models/openai.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langchain_aws-1.7.4/langchain_aws/chat_models/openai.py 2020-02-02
01:00:00.000000000 +0100
@@ -33,7 +33,9 @@
from langchain_aws.data._profiles import _PROFILES
from langchain_aws.utils import (
_BEDROCK_API_KEY_MAX_TTL_SECONDS,
+ _MANTLE_GUARDRAILS_ERR_MSG,
_BedrockApiKeyProvider,
+ _check_no_mantle_guardrail_headers,
)
_MANTLE_BASE_URL_TEMPLATE = "https://bedrock-mantle.{region}.api.aws/v1"
@@ -167,6 +169,32 @@
@model_validator(mode="before")
@classmethod
+ def _reject_guardrails(cls, values: Any) -> Any:
+ # TODO: remove after Mantle adds guardrails support
+ if isinstance(values, dict):
+ if any(
+ values.get(key) is not None
+ for key in ("guardrail_config", "guardrails")
+ ):
+ raise ValueError(_MANTLE_GUARDRAILS_ERR_MSG)
+ _check_no_mantle_guardrail_headers(values.get("default_headers"))
+ return values
+
+ def _get_request_payload(
+ self,
+ input_: LanguageModelInput,
+ *,
+ stop: list[str] | None = None,
+ **kwargs: Any,
+ ) -> dict:
+ # TODO: remove after Mantle adds guardrails support
+ if kwargs.get("guardrail_config") is not None:
+ raise ValueError(_MANTLE_GUARDRAILS_ERR_MSG)
+ _check_no_mantle_guardrail_headers(kwargs.get("extra_headers"))
+ return super()._get_request_payload(input_, stop=stop, **kwargs)
+
+ @model_validator(mode="before")
+ @classmethod
def _set_mantle_defaults(cls, values: Any) -> Any:
"""Resolve the Mantle base URL and bearer key before the client is
built.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langchain_aws-1.7.3/langchain_aws/utils.py
new/langchain_aws-1.7.4/langchain_aws/utils.py
--- old/langchain_aws-1.7.3/langchain_aws/utils.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langchain_aws-1.7.4/langchain_aws/utils.py 2020-02-02
01:00:00.000000000 +0100
@@ -557,6 +557,23 @@
return messages
+_MANTLE_GUARDRAILS_ERR_MSG = (
+ "Amazon Bedrock Guardrails are not supported on the bedrock-mantle "
+ "endpoint. Please use ``ChatAnthropicBedrock`` or ``ChatBedrockConverse`` "
+ "instead, which support guardrails via the bedrock-runtime endpoint."
+)
+
+
+def _check_no_mantle_guardrail_headers(headers: Optional[Dict[str, Any]]) ->
None:
+ """Reject Bedrock guardrail headers, which Mantle silently ignores."""
+ # TODO: remove after Mantle adds guardrails support
+ if not headers:
+ return
+ for key in headers:
+ if key.lower().startswith("x-amzn-bedrock-guardrail"):
+ raise ValueError(_MANTLE_GUARDRAILS_ERR_MSG)
+
+
class _StaticCredentialProvider:
"""Wraps resolved botocore credentials in the shape ``provide_token``
expects.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langchain_aws-1.7.3/pyproject.toml
new/langchain_aws-1.7.4/pyproject.toml
--- old/langchain_aws-1.7.3/pyproject.toml 2020-02-02 01:00:00.000000000
+0100
+++ new/langchain_aws-1.7.4/pyproject.toml 2020-02-02 01:00:00.000000000
+0100
@@ -9,10 +9,10 @@
readme = "README.md"
authors = []
-version = "1.7.3"
+version = "1.7.4"
requires-python = ">=3.10"
dependencies = [
- "langchain-core>=1.4.7",
+ "langchain-core>=1.6.0",
"boto3>=1.43.64",
"pydantic>=2.10.6,<3",
"numpy>=1.0.0,<3",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langchain_aws-1.7.3/tests/integration_tests/chat_models/test_bedrock_converse.py
new/langchain_aws-1.7.4/tests/integration_tests/chat_models/test_bedrock_converse.py
---
old/langchain_aws-1.7.3/tests/integration_tests/chat_models/test_bedrock_converse.py
2020-02-02 01:00:00.000000000 +0100
+++
new/langchain_aws-1.7.4/tests/integration_tests/chat_models/test_bedrock_converse.py
2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,7 @@
"""Standard LangChain interface tests"""
import base64
+import json
import time
from typing import Any, Literal, Optional, Type
from uuid import uuid4
@@ -60,34 +61,142 @@
@property
def chat_model_params(self) -> dict:
- return {"model": "mistral.mistral-large-2402-v1:0"}
+ return {"model": "mistral.mistral-large-3-675b-instruct"}
@property
def standard_chat_model_params(self) -> dict:
- return {"temperature": 0, "max_tokens": 100, "stop": []}
+ return {"temperature": 0, "max_tokens": 100}
@property
def has_tool_choice(self) -> bool:
return False
- # This standard test feeds back an AIMessage whose content mixes a text
- # block and a `tool_use` block in a single assistant turn. Mistral models
on
- # Bedrock reject that turn shape with
- # `ValidationException: messages.1.content: Conversation blocks and tool
use
- # blocks cannot be provided in the same turn` (Anthropic models accept it,
so
- # the conversion in `_messages_to_bedrock` is correct and must not change).
@pytest.mark.xfail(
- reason=(
- "Mistral on Bedrock rejects an assistant turn that mixes text and "
- "tool_use blocks: 'Conversation blocks and tool use blocks cannot
be "
- "provided in the same turn'."
- )
+ reason="Mistral Large 3 does not support the stopSequences field."
)
+ def test_stop_sequence(self, model: BaseChatModel) -> None:
+ super().test_stop_sequence(model)
+
+ TOOL_CALL_ID = "abcd12345"
+ ID_XFAIL_MSG = (
+ "Mistral Large 3 requires 9-char alphanumeric tool call IDs vs the
'abc123' "
+ "hardcoded by the standard tests. Replaced by the *_mistral_id
variants below."
+ )
+
+ @pytest.mark.xfail(reason=ID_XFAIL_MSG)
+ def test_tool_message_histories_string_content(
+ self, model: BaseChatModel, my_adder_tool: BaseTool
+ ) -> None:
+ super().test_tool_message_histories_string_content(model,
my_adder_tool)
+
+ @pytest.mark.xfail(reason=ID_XFAIL_MSG)
def test_tool_message_histories_list_content(
self, model: BaseChatModel, my_adder_tool: BaseTool
) -> None:
super().test_tool_message_histories_list_content(model, my_adder_tool)
+ @pytest.mark.xfail(reason=ID_XFAIL_MSG)
+ def test_tool_message_error_status(
+ self, model: BaseChatModel, my_adder_tool: BaseTool
+ ) -> None:
+ super().test_tool_message_error_status(model, my_adder_tool)
+
+ def test_tool_message_histories_string_content_mistral_id(
+ self, model: BaseChatModel, my_adder_tool: BaseTool
+ ) -> None:
+ if not self.has_tool_calling:
+ pytest.skip("Test requires tool calling.")
+
+ model_with_tools = model.bind_tools([my_adder_tool])
+ messages = [
+ HumanMessage("What is 1 + 2"),
+ AIMessage(
+ "",
+ tool_calls=[
+ {
+ "name": "my_adder_tool",
+ "args": {"a": 1, "b": 2},
+ "id": self.TOOL_CALL_ID,
+ "type": "tool_call",
+ },
+ ],
+ ),
+ ToolMessage(
+ json.dumps({"result": 3}),
+ name="my_adder_tool",
+ tool_call_id=self.TOOL_CALL_ID,
+ ),
+ ]
+ result = model_with_tools.invoke(messages)
+ assert isinstance(result, AIMessage)
+
+ def test_tool_message_histories_list_content_mistral_id(
+ self, model: BaseChatModel, my_adder_tool: BaseTool
+ ) -> None:
+ if not self.has_tool_calling:
+ pytest.skip("Test requires tool calling.")
+
+ model_with_tools = model.bind_tools([my_adder_tool])
+ messages = [
+ HumanMessage("What is 1 + 2"),
+ AIMessage(
+ [
+ {"type": "text", "text": "some text"},
+ {
+ "type": "tool_use",
+ "id": self.TOOL_CALL_ID,
+ "name": "my_adder_tool",
+ "input": {"a": 1, "b": 2},
+ },
+ ],
+ tool_calls=[
+ {
+ "name": "my_adder_tool",
+ "args": {"a": 1, "b": 2},
+ "id": self.TOOL_CALL_ID,
+ "type": "tool_call",
+ },
+ ],
+ ),
+ ToolMessage(
+ json.dumps({"result": 3}),
+ name="my_adder_tool",
+ tool_call_id=self.TOOL_CALL_ID,
+ ),
+ ]
+ result = model_with_tools.invoke(messages)
+ assert isinstance(result, AIMessage)
+
+ def test_tool_message_error_status_mistral_id(
+ self, model: BaseChatModel, my_adder_tool: BaseTool
+ ) -> None:
+ if not self.has_tool_calling:
+ pytest.skip("Test requires tool calling.")
+
+ model_with_tools = model.bind_tools([my_adder_tool])
+ messages = [
+ HumanMessage("What is 1 + 2"),
+ AIMessage(
+ "",
+ tool_calls=[
+ {
+ "name": "my_adder_tool",
+ "args": {"a": 1},
+ "id": self.TOOL_CALL_ID,
+ "type": "tool_call",
+ },
+ ],
+ ),
+ ToolMessage(
+ "Error: Missing required argument 'b'.",
+ name="my_adder_tool",
+ tool_call_id=self.TOOL_CALL_ID,
+ status="error",
+ ),
+ ]
+ result = model_with_tools.invoke(messages)
+ assert isinstance(result, AIMessage)
+
class TestBedrockNovaStandard(ChatModelIntegrationTests):
@property
@@ -186,10 +295,12 @@
) -> None:
pass
- # See `TestBedrockMistralStandard` above: the synthetic history mixes a
text
- # block and a tool_use block in one assistant turn, which Meta models on
Bedrock
- # reject with 'Conversation blocks and tool use blocks cannot be provided
in the
- # same turn' (Anthropic models accept it).
+ # This standard test feeds back an AIMessage whose content mixes a text
+ # block and a `tool_use` block in a single assistant turn. Meta models on
+ # Bedrock reject that turn shape with `ValidationException: Conversation
+ # blocks and tool use blocks cannot be provided in the same turn`
+ # (Anthropic models accept it, so the conversion in `_messages_to_bedrock`
+ # is correct and must not change).
@pytest.mark.xfail(
reason=(
"Meta on Bedrock rejects an assistant turn that mixes text and "
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langchain_aws-1.7.3/tests/unit_tests/chat_models/test_anthropic_mantle.py
new/langchain_aws-1.7.4/tests/unit_tests/chat_models/test_anthropic_mantle.py
---
old/langchain_aws-1.7.3/tests/unit_tests/chat_models/test_anthropic_mantle.py
2020-02-02 01:00:00.000000000 +0100
+++
new/langchain_aws-1.7.4/tests/unit_tests/chat_models/test_anthropic_mantle.py
2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,8 @@
"""ChatAnthropicMantle unit tests."""
-from typing import Tuple, Type, cast
+from collections.abc import Mapping
+from typing import Any, Tuple, Type, cast
+from unittest.mock import patch
import pytest
from langchain_core.language_models import BaseChatModel, ModelProfile
@@ -13,6 +15,23 @@
MODEL_NAME = "anthropic.claude-sonnet-5"
+def _constructed_client_params(
+ model: ChatAnthropicMantle,
+) -> tuple[Mapping[str, Any], Mapping[str, Any]]:
+ with (
+ patch(
+ "langchain_aws.chat_models.anthropic.AnthropicBedrockMantle"
+ ) as sync_client,
+ patch(
+ "langchain_aws.chat_models.anthropic.AsyncAnthropicBedrockMantle"
+ ) as async_client,
+ ):
+ _ = model._client
+ _ = model._async_client
+
+ return sync_client.call_args.kwargs, async_client.call_args.kwargs
+
+
class TestAnthropicMantleStandard(ChatModelUnitTests):
@property
def chat_model_class(self) -> Type[BaseChatModel]:
@@ -128,6 +147,110 @@
assert model._client_params["aws_profile"] == "my-profile"
[email protected](
+ ("credential_environment", "sigv4_params", "expected_client_params"),
+ [
+ (
+ {},
+ {"credentials_profile_name": "my-profile"},
+ {"aws_profile": "my-profile"},
+ ),
+ (
+ {},
+ {
+ "aws_access_key_id": SecretStr("AKIA-test"),
+ "aws_secret_access_key": SecretStr("secret-test"),
+ "aws_session_token": SecretStr("token-test"),
+ },
+ {
+ "aws_access_key": "AKIA-test",
+ "aws_secret_key": "secret-test",
+ "aws_session_token": "token-test",
+ },
+ ),
+ (
+ {"AWS_SECRET_ACCESS_KEY": "secret-from-env"},
+ {"aws_access_key_id": SecretStr("AKIA-explicit")},
+ {
+ "aws_access_key": "AKIA-explicit",
+ "aws_secret_key": "secret-from-env",
+ },
+ ),
+ (
+ {"AWS_ACCESS_KEY_ID": "AKIA-from-env"},
+ {"aws_secret_access_key": SecretStr("secret-explicit")},
+ {
+ "aws_access_key": "AKIA-from-env",
+ "aws_secret_key": "secret-explicit",
+ },
+ ),
+ ],
+ ids=[
+ "profile",
+ "explicit-keys",
+ "explicit-access-key",
+ "explicit-secret-key",
+ ],
+)
+def test_explicit_sigv4_credentials_outrank_ambient_api_key(
+ credential_environment: dict[str, str],
+ sigv4_params: dict[str, Any],
+ expected_client_params: dict[str, str],
+) -> None:
+ """An ambient bearer token does not override explicit SigV4 credentials."""
+ with MonkeyPatch().context() as m:
+ m.delenv("AWS_ACCESS_KEY_ID", raising=False)
+ m.delenv("AWS_SECRET_ACCESS_KEY", raising=False)
+ m.delenv("AWS_SESSION_TOKEN", raising=False)
+ m.setenv("AWS_BEARER_TOKEN_BEDROCK", "ambient-key")
+ for name, value in credential_environment.items():
+ m.setenv(name, value)
+ model = ChatAnthropicMantle( # type: ignore[call-arg]
+ model=MODEL_NAME,
+ region_name="us-east-1",
+ **sigv4_params,
+ )
+
+ client_params_by_type = _constructed_client_params(model)
+
+ for client_params in client_params_by_type:
+ for name, value in expected_client_params.items():
+ assert client_params[name] == value
+ assert "api_key" not in client_params
+
+
+def test_explicit_bedrock_api_key_outranks_sigv4_credentials() -> None:
+ """An explicitly passed bearer key keeps precedence over SigV4 signals."""
+ with MonkeyPatch().context() as m:
+ m.setenv("AWS_BEARER_TOKEN_BEDROCK", "ambient-key")
+ model = ChatAnthropicMantle( # type: ignore[call-arg]
+ model=MODEL_NAME,
+ region_name="us-east-1",
+ bedrock_api_key=SecretStr("explicit-key"),
+ credentials_profile_name="my-profile",
+ )
+
+ client_params_by_type = _constructed_client_params(model)
+
+ for client_params in client_params_by_type:
+ assert client_params["api_key"] == "explicit-key"
+
+
+def test_ambient_api_key_is_forwarded_without_explicit_sigv4_credentials() ->
None:
+ """Ambient bearer authentication remains the default without SigV4
signals."""
+ with MonkeyPatch().context() as m:
+ m.setenv("AWS_BEARER_TOKEN_BEDROCK", "ambient-key")
+ model = ChatAnthropicMantle( # type: ignore[call-arg]
+ model=MODEL_NAME,
+ region_name="us-east-1",
+ )
+
+ client_params_by_type = _constructed_client_params(model)
+
+ for client_params in client_params_by_type:
+ assert client_params["api_key"] == "ambient-key"
+
+
def test_ls_params_provider() -> None:
"""Tracing provider is reported as anthropic-mantle."""
model = ChatAnthropicMantle( # type: ignore[call-arg]
@@ -218,3 +341,68 @@
"_agenerate",
):
assert hasattr(model, attr)
+
+
+def _make_model(**kwargs: Any) -> ChatAnthropicMantle:
+ return ChatAnthropicMantle( # type: ignore[call-arg]
+ model_name=MODEL_NAME,
+ region_name="us-east-1",
+ bedrock_api_key=SecretStr("test-key"),
+ **kwargs,
+ )
+
+
+def test_guardrail_default_headers_rejected_at_construction() -> None:
+ with pytest.raises(ValueError, match="not supported on the
bedrock-mantle"):
+ _make_model(
+ default_headers={
+ "X-Amzn-Bedrock-GuardrailIdentifier": "gr-1",
+ "X-Amzn-Bedrock-GuardrailVersion": "1",
+ },
+ )
+
+
+def test_guardrail_extra_headers_rejected_per_request() -> None:
+ model = _make_model()
+ with pytest.raises(ValueError, match="not supported on the
bedrock-mantle"):
+ model._get_request_payload(
+ "hello",
+ extra_headers={"X-Amzn-Bedrock-GuardrailIdentifier": "gr-1"},
+ )
+
+
+def test_non_guardrail_headers_still_allowed() -> None:
+ model = _make_model(default_headers={"X-Custom-Header": "ok"})
+ payload = model._get_request_payload(
+ "hello", extra_headers={"X-Another-Header": "ok"}
+ )
+ assert payload["extra_headers"] == {"X-Another-Header": "ok"}
+
+
+def test_explicit_sigv4_credentials_select_sigv4_at_sdk_level() -> None:
+ with MonkeyPatch().context() as m:
+ m.setenv("AWS_BEARER_TOKEN_BEDROCK", "api-key")
+ model = ChatAnthropicMantle( # type: ignore[call-arg]
+ model_name=MODEL_NAME,
+ region_name="us-east-1",
+ aws_access_key_id=SecretStr("key-id"),
+ aws_secret_access_key=SecretStr("sec-key"),
+ )
+ client = model._client
+ assert client._use_sigv4 is True
+ assert client.api_key is None
+
+
+def test_env_sigv4_credentials_do_not_outrank_ambient_api_key() -> None:
+ with MonkeyPatch().context() as m:
+ m.setenv("AWS_BEARER_TOKEN_BEDROCK", "api-key")
+ m.setenv("AWS_ACCESS_KEY_ID", "key-id")
+ m.setenv("AWS_SECRET_ACCESS_KEY", "sec-key")
+ model = ChatAnthropicMantle( # type: ignore[call-arg]
+ model_name=MODEL_NAME, region_name="us-east-1"
+ )
+
+ client_params_by_type = _constructed_client_params(model)
+
+ for client_params in client_params_by_type:
+ assert client_params["api_key"] == "api-key"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langchain_aws-1.7.3/tests/unit_tests/chat_models/test_bedrock_converse.py
new/langchain_aws-1.7.4/tests/unit_tests/chat_models/test_bedrock_converse.py
---
old/langchain_aws-1.7.3/tests/unit_tests/chat_models/test_bedrock_converse.py
2020-02-02 01:00:00.000000000 +0100
+++
new/langchain_aws-1.7.4/tests/unit_tests/chat_models/test_bedrock_converse.py
2020-02-02 01:00:00.000000000 +0100
@@ -7363,3 +7363,149 @@
bedrock_messages, _ = _messages_to_bedrock(messages)
assert bedrock_messages[1]["content"][0]["toolUse"]["toolUseId"] ==
"call_1"
assert bedrock_messages[2]["content"][0]["toolResult"]["toolUseId"] ==
"call_1"
+
+
+_REASONING_BLOCK = {"reasoningContent": {"reasoningText": {"text":
"Thinking."}}}
+_SIGNED_REASONING_BLOCK = {
+ "reasoningContent": {"reasoningText": {"text": "Thinking.", "signature":
"sig"}}
+}
+_ANSWER_BLOCK = {"text": "Answer."}
+
+
[email protected](
+ ("model_id", "signature", "expected_content"),
+ [
+ # Models that accept unsigned reasoning have it forwarded.
+ ("openai.gpt-oss-120b-1:0", "", [_REASONING_BLOCK, _ANSWER_BLOCK]),
+ ("deepseek.v3.2", "", [_REASONING_BLOCK, _ANSWER_BLOCK]),
+ ("minimax.minimax-m2.5", "", [_REASONING_BLOCK, _ANSWER_BLOCK]),
+ ("moonshotai.kimi-k2.5", "", [_REASONING_BLOCK, _ANSWER_BLOCK]),
+ # Models that emit unsigned reasoning but reject it on the way back are
+ # absent from the allowlist, so their reasoning is still dropped.
+ ("openai.gpt-5.6-luna", "", [_ANSWER_BLOCK]),
+ ("xai.grok-4.6", "", [_ANSWER_BLOCK]),
+ # DeepSeek R1 rejects reasoning content whether or not it is signed.
+ ("deepseek.r1-v1:0", "", [_ANSWER_BLOCK]),
+ ("deepseek.r1-v1:0", "sig", [_ANSWER_BLOCK]),
+ # Anthropic models require a signature.
+ ("anthropic.claude-sonnet-4-5-20250929-v1:0", "", [_ANSWER_BLOCK]),
+ (
+ "anthropic.claude-sonnet-4-5-20250929-v1:0",
+ "sig",
+ [_SIGNED_REASONING_BLOCK, _ANSWER_BLOCK],
+ ),
+ # Nova v1 emits inline reasoning and rejects it on the way back; Nova 2
+ # uses native reasoning and accepts it.
+ ("amazon.nova-pro-v1:0", "", [_ANSWER_BLOCK]),
+ ("amazon.nova-pro-v1:0", "sig", [_ANSWER_BLOCK]),
+ ("amazon.nova-2-lite-v1:0", "", [_REASONING_BLOCK, _ANSWER_BLOCK]),
+ # A model not vetted for unsigned reasoning falls back to requiring a
+ # signature, as does an unknown model.
+ ("cohere.command-r-plus-v1:0", "", [_ANSWER_BLOCK]),
+ (
+ "cohere.command-r-plus-v1:0",
+ "sig",
+ [_SIGNED_REASONING_BLOCK, _ANSWER_BLOCK],
+ ),
+ (None, "", [_ANSWER_BLOCK]),
+ (None, "sig", [_SIGNED_REASONING_BLOCK, _ANSWER_BLOCK]),
+ ],
+)
+def test__messages_to_bedrock_reasoning_by_model(
+ model_id: Optional[str], signature: str, expected_content: List[dict]
+) -> None:
+ messages: List[BaseMessage] = [
+ HumanMessage(content="Question?"),
+ AIMessage(
+ content=[
+ {
+ "type": "reasoning_content",
+ "reasoning_content": {"text": "Thinking.", "signature":
signature},
+ },
+ {"type": "text", "text": "Answer."},
+ ]
+ ),
+ HumanMessage(content="Follow-up?"),
+ ]
+
+ actual_messages, _ = _messages_to_bedrock(messages, model_id=model_id)
+
+ assert actual_messages[1] == {"role": "assistant", "content":
expected_content}
+
+
+def test__messages_to_bedrock_reasoning_only_content() -> None:
+ """An unsigned reasoning block should survive as the sole content block."""
+ messages: List[BaseMessage] = [
+ HumanMessage(content="Question?"),
+ AIMessage(
+ content=[
+ {
+ "type": "reasoning_content",
+ "reasoning_content": {"text": "Thinking.", "signature":
""},
+ }
+ ]
+ ),
+ HumanMessage(content="Follow-up?"),
+ ]
+
+ actual_messages, _ = _messages_to_bedrock(messages,
model_id="deepseek.v3.2")
+
+ assert actual_messages[1] == {"role": "assistant", "content":
[_REASONING_BLOCK]}
+
+
+def test__bedrock_to_lc_redacted_reasoning_delta() -> None:
+ """A streamed delta carrying only redacted reasoning should not be
dropped."""
+ assert _bedrock_to_lc([{"reasoningContent": {"redactedContent": b"abc"}}])
== [
+ {
+ "type": "reasoning_content",
+ "reasoning_content": {"redacted_content": b"abc"},
+ }
+ ]
+
+
+def test__messages_to_bedrock_redacted_reasoning_round_trip() -> None:
+ """Redacted reasoning survives a round trip without a signature."""
+ messages: List[BaseMessage] = [
+ HumanMessage(content="Question?"),
+ AIMessage(
+ content=[
+ {
+ "type": "reasoning_content",
+ "reasoning_content": {"redacted_content": b"abc"},
+ },
+ {"type": "text", "text": "Answer."},
+ ]
+ ),
+ HumanMessage(content="Follow-up?"),
+ ]
+
+ actual_messages, _ = _messages_to_bedrock(messages,
model_id="xai.grok-4.6")
+
+ assert actual_messages[1] == {
+ "role": "assistant",
+ "content": [
+ {"reasoningContent": {"redactedContent": b"abc"}},
+ _ANSWER_BLOCK,
+ ],
+ }
+
+
+def test__messages_to_bedrock_redacted_reasoning_dropped_when_rejected() ->
None:
+ """A model that rejects reasoning outright also rejects the encrypted
form."""
+ messages: List[BaseMessage] = [
+ HumanMessage(content="Question?"),
+ AIMessage(
+ content=[
+ {
+ "type": "reasoning_content",
+ "reasoning_content": {"redacted_content": b"abc"},
+ },
+ {"type": "text", "text": "Answer."},
+ ]
+ ),
+ HumanMessage(content="Follow-up?"),
+ ]
+
+ actual_messages, _ = _messages_to_bedrock(messages,
model_id="deepseek.r1-v1:0")
+
+ assert actual_messages[1] == {"role": "assistant", "content":
[_ANSWER_BLOCK]}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langchain_aws-1.7.3/tests/unit_tests/chat_models/test_openai.py
new/langchain_aws-1.7.4/tests/unit_tests/chat_models/test_openai.py
--- old/langchain_aws-1.7.3/tests/unit_tests/chat_models/test_openai.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_aws-1.7.4/tests/unit_tests/chat_models/test_openai.py
2020-02-02 01:00:00.000000000 +0100
@@ -330,3 +330,39 @@
provider = cast("_BedrockApiKeyProvider", model.openai_api_key)
with patch("aws_bedrock_token_generator.provide_token",
return_value="tok-xyz"):
assert provider() == "tok-xyz"
+
+
+def _make_model(**kwargs: object) -> ChatOpenAIMantle:
+ return ChatOpenAIMantle(
+ model=MODEL_NAME,
+ region_name="us-east-1",
+ bedrock_api_key=SecretStr("test-key"),
+ **kwargs, # type: ignore[arg-type]
+ )
+
+
+def test_guardrail_default_headers_rejected_at_construction() -> None:
+ with pytest.raises(ValueError, match="not supported on the
bedrock-mantle"):
+ _make_model(
+ default_headers={
+ "X-Amzn-Bedrock-GuardrailIdentifier": "gr-1",
+ "X-Amzn-Bedrock-GuardrailVersion": "1",
+ },
+ )
+
+
+def test_guardrail_extra_headers_rejected_per_request() -> None:
+ model = _make_model()
+ with pytest.raises(ValueError, match="not supported on the
bedrock-mantle"):
+ model._get_request_payload(
+ "hello",
+ extra_headers={"X-Amzn-Bedrock-GuardrailIdentifier": "gr-1"},
+ )
+
+
+def test_non_guardrail_headers() -> None:
+ model = _make_model(default_headers={"X-Custom-Header": "ok"})
+ payload = model._get_request_payload(
+ "hello", extra_headers={"X-Another-Header": "ok"}
+ )
+ assert payload["extra_headers"] == {"X-Another-Header": "ok"}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langchain_aws-1.7.3/uv.lock
new/langchain_aws-1.7.4/uv.lock
--- old/langchain_aws-1.7.3/uv.lock 2020-02-02 01:00:00.000000000 +0100
+++ new/langchain_aws-1.7.4/uv.lock 2020-02-02 01:00:00.000000000 +0100
@@ -1221,7 +1221,7 @@
[[package]]
name = "langchain-aws"
-version = "1.7.3"
+version = "1.7.4"
source = { editable = "." }
dependencies = [
{ name = "boto3" },
@@ -1303,7 +1303,7 @@
{ name = "bedrock-agentcore", marker = "python_full_version >= '3.10' and
extra == 'tools'", specifier = ">=1.4.0" },
{ name = "boto3", specifier = ">=1.43.64" },
{ name = "langchain-anthropic", marker = "extra == 'anthropic'" },
- { name = "langchain-core", specifier = ">=1.4.7" },
+ { name = "langchain-core", specifier = ">=1.6.0" },
{ name = "langchain-openai", marker = "extra == 'openai'", specifier =
">=1.0.0" },
{ name = "numpy", specifier = ">=1.0.0,<3" },
{ name = "openai", marker = "extra == 'openai'", specifier = ">=1.106.0" },