From 17309cffba84c8dc444914edda4822ad5c92d0b7 Mon Sep 17 00:00:00 2001 From: SalemBajjali Date: Tue, 23 Jun 2026 15:19:20 -0500 Subject: [PATCH 1/5] test: add tests for BedrockClaudeJsonClient effort handling --- tests/integration/client/test_bedrock.py | 77 +++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/tests/integration/client/test_bedrock.py b/tests/integration/client/test_bedrock.py index 95442aa..bc7a5e2 100644 --- a/tests/integration/client/test_bedrock.py +++ b/tests/integration/client/test_bedrock.py @@ -5,8 +5,10 @@ import pytest from wags_llm.client.bedrock import ( + _EFFORT_BETA_HEADER, BedrockClaudeJsonClient, LLMEmptyResponseError, + LLMInvalidEffortError, LLMInvocationError, LLMJsonDecodeError, LLMResponseFormatError, @@ -31,14 +33,16 @@ def __init__(self, response=None, error=None): """ self.response = response self.error = error + self.captured_request = None - def converse(self, **kwargs): # noqa: ARG002 + def converse(self, **kwargs): """Return a fake converse response. :param kwargs: Converse request arguments. :return: Fake response payload. :raise Exception: If configured with an error. """ + self.captured_request = kwargs if self.error is not None: raise self.error return self.response @@ -66,6 +70,77 @@ def client(self, service_name: str, region_name: str): return self.runtime_client +def test_invoke_json_with_effort(): + """Test that invoke_json includes the effort beta config when effort is set.""" + fake_runtime_client = FakeBedrockRuntimeClient( + response={ + "output": { + "message": { + "content": [ + {"text": '{"value": 1}'}, + ] + } + } + } + ) + + with patch( + "wags_llm.client.bedrock.boto3.Session", + return_value=FakeSession(fake_runtime_client), + ): + client = BedrockClaudeJsonClient( + model_id=TEST_MODEL_ID, + region_name=TEST_REGION_NAME, + profile_name=TEST_PROFILE_NAME, + effort="medium", + ) + + client.invoke_json( + system_prompt=TEST_SYSTEM_PROMPT, + user_prompt=TEST_USER_PROMPT, + ) + + assert fake_runtime_client.captured_request["additionalModelRequestFields"] == { + "anthropic_beta": [_EFFORT_BETA_HEADER], + "output_config": {"effort": "medium"}, + } + + +def test_invoke_json_without_effort_omits_field(): + """Test that invoke_json omits additionalModelRequestFields when effort is unset.""" + fake_runtime_client = FakeBedrockRuntimeClient( + response={"output": {"message": {"content": [{"text": '{"value": 1}'}]}}} + ) + + with patch( + "wags_llm.client.bedrock.boto3.Session", + return_value=FakeSession(fake_runtime_client), + ): + client = BedrockClaudeJsonClient( + model_id=TEST_MODEL_ID, + region_name=TEST_REGION_NAME, + profile_name=TEST_PROFILE_NAME, + ) + + client.invoke_json( + system_prompt=TEST_SYSTEM_PROMPT, + user_prompt=TEST_USER_PROMPT, + ) + + assert "additionalModelRequestFields" not in fake_runtime_client.captured_request + + +def test_invalid_effort_raises(): + """Test that an invalid effort value raises LLMInvalidEffortError at construction.""" + with pytest.raises(LLMInvalidEffortError, match=r"Invalid effort"): + BedrockClaudeJsonClient( + model_id=TEST_MODEL_ID, + region_name=TEST_REGION_NAME, + profile_name=TEST_PROFILE_NAME, + effort="extreme", + ) + + def test_invoke_json_success(): """Test that invoke_json works correctly""" fake_runtime_client = FakeBedrockRuntimeClient( From f3ab37664b5964ad325e0aafa5376e4b26121e11 Mon Sep 17 00:00:00 2001 From: SalemBajjali Date: Tue, 23 Jun 2026 15:19:29 -0500 Subject: [PATCH 2/5] feat: add LLMInvalidEffortError for invalid effort parameter handling --- src/wags_llm/client/exceptions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/wags_llm/client/exceptions.py b/src/wags_llm/client/exceptions.py index 188ab1b..01b0ba8 100644 --- a/src/wags_llm/client/exceptions.py +++ b/src/wags_llm/client/exceptions.py @@ -19,3 +19,7 @@ class LLMEmptyResponseError(LLMClientError): class LLMJsonDecodeError(LLMClientError): """Raised when the model output is not valid JSON.""" + + +class LLMInvalidEffortError(LLMClientError): + """Raised when the effort parameter is not a valid value.""" From 093225d05e50a957df6666d312f6b997dc098bea Mon Sep 17 00:00:00 2001 From: SalemBajjali Date: Tue, 23 Jun 2026 15:20:29 -0500 Subject: [PATCH 3/5] feat: enhance BedrockClaudeJsonClient to support and validate effort parameter --- src/wags_llm/client/bedrock.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/wags_llm/client/bedrock.py b/src/wags_llm/client/bedrock.py index 28d2f2f..ef77db3 100644 --- a/src/wags_llm/client/bedrock.py +++ b/src/wags_llm/client/bedrock.py @@ -6,19 +6,22 @@ import json import logging -from typing import Any +from typing import Any, Literal import boto3 from wags_llm.client.base import InvokeJsonResponse, LLMJsonClient from wags_llm.client.exceptions import ( LLMEmptyResponseError, + LLMInvalidEffortError, LLMInvocationError, LLMJsonDecodeError, LLMResponseFormatError, ) _logger = logging.getLogger(__name__) +_VALID_EFFORT_LEVELS = ("high", "medium", "low") +_EFFORT_BETA_HEADER = "effort-2025-11-24" class BedrockClaudeJsonClient(LLMJsonClient): @@ -31,6 +34,7 @@ def __init__( profile_name: str, max_tokens: int = 300, temperature: float = 0.0, + effort: Literal["high", "medium", "low"] | None = None, ) -> None: """Initialize the Bedrock Claude client. @@ -39,18 +43,26 @@ def __init__( :param profile_name: AWS profile name. :param max_tokens: Maximum number of tokens to request from the model. :param temperature: Sampling temperature. + :param effort: effort level for Claude Opus 4.5 (beta): "high", "medium", "low", or None (default; Bedrock falls back to "high"). + :raise LLMInvalidEffortError: If effort is not "high", "medium", "low", or None. """ + if effort is not None and effort not in _VALID_EFFORT_LEVELS: + msg = f"Invalid effort '{effort}'; must be one of 'high', 'medium', 'low', or None." + raise LLMInvalidEffortError(msg) + _logger.debug( - "BedrockClaudeJsonClient config: model_id='%s', region_name='%s', profile_name='%s', max_tokens=%i, temperature=%f", + "BedrockClaudeJsonClient config: model_id='%s', region_name='%s', profile_name='%s', max_tokens=%i, temperature=%f, effort=%s", model_id, region_name, profile_name, max_tokens, temperature, + effort, ) self.model_id = model_id self.max_tokens = max_tokens self.temperature = temperature + self.effort = effort session = boto3.Session(profile_name=profile_name) self._client = session.client("bedrock-runtime", region_name=region_name) @@ -98,6 +110,14 @@ def invoke_json( }, } + if self.effort: + converse_params["additionalModelRequestFields"] = { + "anthropic_beta": [_EFFORT_BETA_HEADER], + "output_config": { + "effort": self.effort, + }, + } + if json_schema: converse_params["outputConfig"] = { "textFormat": { From d2aaaabe2fa1d884f83f90b7a0d12bbde3933a5c Mon Sep 17 00:00:00 2001 From: SalemBajjali Date: Wed, 24 Jun 2026 10:56:53 -0500 Subject: [PATCH 4/5] Add adaptive thinking effort support for Bedrock Claude --- src/wags_llm/client/bedrock.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/wags_llm/client/bedrock.py b/src/wags_llm/client/bedrock.py index ef77db3..38aee00 100644 --- a/src/wags_llm/client/bedrock.py +++ b/src/wags_llm/client/bedrock.py @@ -21,7 +21,6 @@ _logger = logging.getLogger(__name__) _VALID_EFFORT_LEVELS = ("high", "medium", "low") -_EFFORT_BETA_HEADER = "effort-2025-11-24" class BedrockClaudeJsonClient(LLMJsonClient): @@ -43,7 +42,7 @@ def __init__( :param profile_name: AWS profile name. :param max_tokens: Maximum number of tokens to request from the model. :param temperature: Sampling temperature. - :param effort: effort level for Claude Opus 4.5 (beta): "high", "medium", "low", or None (default; Bedrock falls back to "high"). + :param effort: Optional adaptive thinking effort level for supported Claude models using Bedrock Converse: "high", "medium", "low", or None to use the model default. :raise LLMInvalidEffortError: If effort is not "high", "medium", "low", or None. """ if effort is not None and effort not in _VALID_EFFORT_LEVELS: @@ -110,13 +109,11 @@ def invoke_json( }, } + adaptive_thinking_params: dict[str, Any] = {"thinking": {"type": "adaptive"}} if self.effort: - converse_params["additionalModelRequestFields"] = { - "anthropic_beta": [_EFFORT_BETA_HEADER], - "output_config": { - "effort": self.effort, - }, - } + adaptive_thinking_params["output_config"] = {"effort": self.effort} + + converse_params["additionalModelRequestFields"] = adaptive_thinking_params if json_schema: converse_params["outputConfig"] = { From 143532f845b6be6fb3d2b592b8825973a827b8dd Mon Sep 17 00:00:00 2001 From: SalemBajjali Date: Wed, 24 Jun 2026 10:57:25 -0500 Subject: [PATCH 5/5] feat: update effort parameter handling to use adaptive thinking type --- tests/integration/client/test_bedrock.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/integration/client/test_bedrock.py b/tests/integration/client/test_bedrock.py index bc7a5e2..faaa80f 100644 --- a/tests/integration/client/test_bedrock.py +++ b/tests/integration/client/test_bedrock.py @@ -5,7 +5,6 @@ import pytest from wags_llm.client.bedrock import ( - _EFFORT_BETA_HEADER, BedrockClaudeJsonClient, LLMEmptyResponseError, LLMInvalidEffortError, @@ -101,7 +100,7 @@ def test_invoke_json_with_effort(): ) assert fake_runtime_client.captured_request["additionalModelRequestFields"] == { - "anthropic_beta": [_EFFORT_BETA_HEADER], + "thinking": {"type": "adaptive"}, "output_config": {"effort": "medium"}, } @@ -127,7 +126,9 @@ def test_invoke_json_without_effort_omits_field(): user_prompt=TEST_USER_PROMPT, ) - assert "additionalModelRequestFields" not in fake_runtime_client.captured_request + assert fake_runtime_client.captured_request["additionalModelRequestFields"] == { + "thinking": {"type": "adaptive"}, + } def test_invalid_effort_raises():