/ tests / agent / test_bedrock_integration.py
test_bedrock_integration.py
  1  """Integration tests for the AWS Bedrock provider wiring.
  2  
  3  Verifies that the Bedrock provider is correctly registered in the
  4  provider registry, model catalog, and runtime resolution pipeline.
  5  These tests do NOT require AWS credentials or boto3 — all AWS calls
  6  are mocked.
  7  
  8  Note: Tests that import ``hermes_cli.auth`` or ``hermes_cli.runtime_provider``
  9  require Python 3.10+ due to ``str | None`` type syntax in the import chain.
 10  """
 11  
 12  import os
 13  from unittest.mock import MagicMock, patch
 14  
 15  import pytest
 16  
 17  
 18  class TestProviderRegistry:
 19      """Verify Bedrock is registered in PROVIDER_REGISTRY."""
 20  
 21      def test_bedrock_in_registry(self):
 22          from hermes_cli.auth import PROVIDER_REGISTRY
 23          assert "bedrock" in PROVIDER_REGISTRY
 24  
 25      def test_bedrock_auth_type_is_aws_sdk(self):
 26          from hermes_cli.auth import PROVIDER_REGISTRY
 27          pconfig = PROVIDER_REGISTRY["bedrock"]
 28          assert pconfig.auth_type == "aws_sdk"
 29  
 30      def test_bedrock_has_no_api_key_env_vars(self):
 31          """Bedrock uses the AWS SDK credential chain, not API keys."""
 32          from hermes_cli.auth import PROVIDER_REGISTRY
 33          pconfig = PROVIDER_REGISTRY["bedrock"]
 34          assert pconfig.api_key_env_vars == ()
 35  
 36      def test_bedrock_base_url_env_var(self):
 37          from hermes_cli.auth import PROVIDER_REGISTRY
 38          pconfig = PROVIDER_REGISTRY["bedrock"]
 39          assert pconfig.base_url_env_var == "BEDROCK_BASE_URL"
 40  
 41  
 42  class TestProviderAliases:
 43      """Verify Bedrock aliases resolve correctly."""
 44  
 45      def test_aws_alias(self):
 46          from hermes_cli.models import _PROVIDER_ALIASES
 47          assert _PROVIDER_ALIASES.get("aws") == "bedrock"
 48  
 49      def test_aws_bedrock_alias(self):
 50          from hermes_cli.models import _PROVIDER_ALIASES
 51          assert _PROVIDER_ALIASES.get("aws-bedrock") == "bedrock"
 52  
 53      def test_amazon_bedrock_alias(self):
 54          from hermes_cli.models import _PROVIDER_ALIASES
 55          assert _PROVIDER_ALIASES.get("amazon-bedrock") == "bedrock"
 56  
 57      def test_amazon_alias(self):
 58          from hermes_cli.models import _PROVIDER_ALIASES
 59          assert _PROVIDER_ALIASES.get("amazon") == "bedrock"
 60  
 61  
 62  class TestProviderLabels:
 63      """Verify Bedrock appears in provider labels."""
 64  
 65      def test_bedrock_label(self):
 66          from hermes_cli.models import _PROVIDER_LABELS
 67          assert _PROVIDER_LABELS.get("bedrock") == "AWS Bedrock"
 68  
 69  
 70  class TestModelCatalog:
 71      """Verify Bedrock has a static model fallback list."""
 72  
 73      def test_bedrock_has_curated_models(self):
 74          from hermes_cli.models import _PROVIDER_MODELS
 75          models = _PROVIDER_MODELS.get("bedrock", [])
 76          assert len(models) > 0
 77  
 78      def test_bedrock_models_include_claude(self):
 79          from hermes_cli.models import _PROVIDER_MODELS
 80          models = _PROVIDER_MODELS.get("bedrock", [])
 81          claude_models = [m for m in models if "anthropic.claude" in m]
 82          assert len(claude_models) > 0
 83  
 84      def test_bedrock_models_include_nova(self):
 85          from hermes_cli.models import _PROVIDER_MODELS
 86          models = _PROVIDER_MODELS.get("bedrock", [])
 87          nova_models = [m for m in models if "amazon.nova" in m]
 88          assert len(nova_models) > 0
 89  
 90  
 91  class TestResolveProvider:
 92      """Verify resolve_provider() handles bedrock correctly."""
 93  
 94      def test_explicit_bedrock_resolves(self, monkeypatch):
 95          """When user explicitly requests 'bedrock', it should resolve."""
 96          from hermes_cli.auth import PROVIDER_REGISTRY
 97          # bedrock is in the registry, so resolve_provider should return it
 98          from hermes_cli.auth import resolve_provider
 99          result = resolve_provider("bedrock")
100          assert result == "bedrock"
101  
102      def test_aws_alias_resolves_to_bedrock(self):
103          from hermes_cli.auth import resolve_provider
104          result = resolve_provider("aws")
105          assert result == "bedrock"
106  
107      def test_amazon_bedrock_alias_resolves(self):
108          from hermes_cli.auth import resolve_provider
109          result = resolve_provider("amazon-bedrock")
110          assert result == "bedrock"
111  
112      def test_auto_detect_with_aws_credentials(self, monkeypatch):
113          """When AWS credentials are present and no other provider is configured,
114          auto-detect should find bedrock."""
115          from hermes_cli.auth import resolve_provider
116  
117          # Clear all other provider env vars
118          for var in ["OPENAI_API_KEY", "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY",
119                       "ANTHROPIC_TOKEN", "GOOGLE_API_KEY", "DEEPSEEK_API_KEY"]:
120              monkeypatch.delenv(var, raising=False)
121  
122          # Set AWS credentials
123          monkeypatch.setenv("AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE")
124          monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
125  
126          # Mock the auth store to have no active provider
127          with patch("hermes_cli.auth._load_auth_store", return_value={}):
128              result = resolve_provider("auto")
129          assert result == "bedrock"
130  
131  
132  class TestRuntimeProvider:
133      """Verify resolve_runtime_provider() handles bedrock correctly."""
134  
135      def test_bedrock_runtime_resolution(self, monkeypatch):
136          from hermes_cli.runtime_provider import resolve_runtime_provider
137  
138          monkeypatch.setenv("AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE")
139          monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
140          monkeypatch.setenv("AWS_REGION", "eu-west-1")
141  
142          # Mock resolve_provider to return bedrock
143          with patch("hermes_cli.runtime_provider.resolve_provider", return_value="bedrock"), \
144               patch("hermes_cli.runtime_provider._get_model_config", return_value={"provider": "bedrock"}):
145              result = resolve_runtime_provider(requested="bedrock")
146  
147          assert result["provider"] == "bedrock"
148          assert result["api_mode"] == "bedrock_converse"
149          assert result["region"] == "eu-west-1"
150          assert "bedrock-runtime.eu-west-1.amazonaws.com" in result["base_url"]
151          assert result["api_key"] == "aws-sdk"
152  
153      def test_bedrock_runtime_default_region(self, monkeypatch):
154          from hermes_cli.runtime_provider import resolve_runtime_provider
155  
156          monkeypatch.setenv("AWS_PROFILE", "default")
157          monkeypatch.delenv("AWS_REGION", raising=False)
158          monkeypatch.delenv("AWS_DEFAULT_REGION", raising=False)
159  
160          with patch("hermes_cli.runtime_provider.resolve_provider", return_value="bedrock"), \
161               patch("hermes_cli.runtime_provider._get_model_config", return_value={"provider": "bedrock"}):
162              result = resolve_runtime_provider(requested="bedrock")
163  
164          assert result["region"] == "us-east-1"
165  
166      def test_bedrock_runtime_no_credentials_raises_on_auto_detect(self, monkeypatch):
167          """When bedrock is auto-detected (not explicitly requested) and no
168          credentials are found, runtime resolution should raise AuthError."""
169          from hermes_cli.runtime_provider import resolve_runtime_provider
170          from hermes_cli.auth import AuthError
171  
172          # Clear all AWS env vars
173          for var in ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_PROFILE",
174                       "AWS_BEARER_TOKEN_BEDROCK", "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
175                       "AWS_WEB_IDENTITY_TOKEN_FILE"]:
176              monkeypatch.delenv(var, raising=False)
177  
178          # Mock both the provider resolution and boto3's credential chain
179          mock_session = MagicMock()
180          mock_session.get_credentials.return_value = None
181          with patch("hermes_cli.runtime_provider.resolve_provider", return_value="bedrock"), \
182               patch("hermes_cli.runtime_provider._get_model_config", return_value={"provider": "bedrock"}), \
183               patch("hermes_cli.runtime_provider.resolve_requested_provider", return_value="auto"), \
184               patch.dict("sys.modules", {"botocore": MagicMock(), "botocore.session": MagicMock()}):
185              import botocore.session as _bs
186              _bs.get_session = MagicMock(return_value=mock_session)
187              with pytest.raises(AuthError, match="No AWS credentials"):
188                  resolve_runtime_provider(requested="auto")
189  
190      def test_bedrock_runtime_explicit_skips_credential_check(self, monkeypatch):
191          """When user explicitly requests bedrock, trust boto3's credential chain
192          even if env-var detection finds nothing (covers IMDS, SSO, etc.)."""
193          from hermes_cli.runtime_provider import resolve_runtime_provider
194  
195          # No AWS env vars set — but explicit bedrock request should not raise
196          for var in ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_PROFILE",
197                       "AWS_BEARER_TOKEN_BEDROCK"]:
198              monkeypatch.delenv(var, raising=False)
199  
200          with patch("hermes_cli.runtime_provider.resolve_provider", return_value="bedrock"), \
201               patch("hermes_cli.runtime_provider._get_model_config", return_value={"provider": "bedrock"}):
202              result = resolve_runtime_provider(requested="bedrock")
203          assert result["provider"] == "bedrock"
204          assert result["api_mode"] == "bedrock_converse"
205  
206  
207  # ---------------------------------------------------------------------------
208  # providers.py integration
209  # ---------------------------------------------------------------------------
210  
211  class TestProvidersModule:
212      """Verify bedrock is wired into hermes_cli/providers.py."""
213  
214      def test_bedrock_alias_in_providers(self):
215          from hermes_cli.providers import ALIASES
216          assert ALIASES.get("bedrock") is None  # "bedrock" IS the canonical name, not an alias
217          assert ALIASES.get("aws") == "bedrock"
218          assert ALIASES.get("aws-bedrock") == "bedrock"
219  
220      def test_bedrock_transport_mapping(self):
221          from hermes_cli.providers import TRANSPORT_TO_API_MODE
222          assert TRANSPORT_TO_API_MODE.get("bedrock_converse") == "bedrock_converse"
223  
224      def test_determine_api_mode_from_bedrock_url(self):
225          from hermes_cli.providers import determine_api_mode
226          assert determine_api_mode(
227              "unknown", "https://bedrock-runtime.us-east-1.amazonaws.com"
228          ) == "bedrock_converse"
229  
230      def test_label_override(self):
231          from hermes_cli.providers import _LABEL_OVERRIDES
232          assert _LABEL_OVERRIDES.get("bedrock") == "AWS Bedrock"
233  
234  
235  # ---------------------------------------------------------------------------
236  # Error classifier integration
237  # ---------------------------------------------------------------------------
238  
239  class TestErrorClassifierBedrock:
240      """Verify Bedrock error patterns are in the global error classifier."""
241  
242      def test_throttling_in_rate_limit_patterns(self):
243          from agent.error_classifier import _RATE_LIMIT_PATTERNS
244          assert "throttlingexception" in _RATE_LIMIT_PATTERNS
245  
246      def test_context_overflow_patterns(self):
247          from agent.error_classifier import _CONTEXT_OVERFLOW_PATTERNS
248          assert "input is too long" in _CONTEXT_OVERFLOW_PATTERNS
249  
250  
251  # ---------------------------------------------------------------------------
252  # pyproject.toml bedrock extra
253  # ---------------------------------------------------------------------------
254  
255  class TestPackaging:
256      """Verify bedrock optional dependency is declared."""
257  
258      def test_bedrock_extra_exists(self):
259          import configparser
260          from pathlib import Path
261          # Read pyproject.toml to verify [bedrock] extra
262          toml_path = Path(__file__).parent.parent.parent / "pyproject.toml"
263          content = toml_path.read_text()
264          assert 'bedrock = ["boto3' in content
265  
266      def test_bedrock_in_all_extra(self):
267          from pathlib import Path
268          content = (Path(__file__).parent.parent.parent / "pyproject.toml").read_text()
269          assert '"hermes-agent[bedrock]"' in content
270  
271  
272  # ---------------------------------------------------------------------------
273  # Model ID dot preservation — regression for #11976
274  # ---------------------------------------------------------------------------
275  # AWS Bedrock inference-profile model IDs embed structural dots:
276  #
277  #   global.anthropic.claude-opus-4-7
278  #   us.anthropic.claude-sonnet-4-5-20250929-v1:0
279  #   apac.anthropic.claude-haiku-4-5
280  #
281  # ``agent.anthropic_adapter.normalize_model_name`` converts dots to hyphens
282  # unless the caller opts in via ``preserve_dots=True``.  Before this fix,
283  # ``AIAgent._anthropic_preserve_dots`` returned False for the ``bedrock``
284  # provider, so Claude-on-Bedrock requests went out with
285  # ``global-anthropic-claude-opus-4-7`` (all dots mangled to hyphens) and
286  # Bedrock rejected them with:
287  #
288  #   HTTP 400: The provided model identifier is invalid.
289  #
290  # The fix adds ``bedrock`` to the preserve-dots provider allowlist and
291  # ``bedrock-runtime.`` to the base-URL heuristic, mirroring the shape of
292  # the opencode-go fix for #5211 (commit f77be22c), which extended this
293  # same allowlist.
294  
295  
296  class TestBedrockPreserveDotsFlag:
297      """``AIAgent._anthropic_preserve_dots`` must return True on Bedrock so
298      inference-profile IDs survive the normalize step intact."""
299  
300      def test_bedrock_provider_preserves_dots(self):
301          from types import SimpleNamespace
302          agent = SimpleNamespace(provider="bedrock", base_url="")
303          from run_agent import AIAgent
304          assert AIAgent._anthropic_preserve_dots(agent) is True
305  
306      def test_bedrock_runtime_us_east_1_url_preserves_dots(self):
307          """Defense-in-depth: even without an explicit ``provider="bedrock"``,
308          a ``bedrock-runtime.us-east-1.amazonaws.com`` base URL must not
309          mangle dots."""
310          from types import SimpleNamespace
311          agent = SimpleNamespace(
312              provider="custom",
313              base_url="https://bedrock-runtime.us-east-1.amazonaws.com",
314          )
315          from run_agent import AIAgent
316          assert AIAgent._anthropic_preserve_dots(agent) is True
317  
318      def test_bedrock_runtime_ap_northeast_2_url_preserves_dots(self):
319          """Reporter-reported region (ap-northeast-2) exercises the same
320          base-URL heuristic."""
321          from types import SimpleNamespace
322          agent = SimpleNamespace(
323              provider="custom",
324              base_url="https://bedrock-runtime.ap-northeast-2.amazonaws.com",
325          )
326          from run_agent import AIAgent
327          assert AIAgent._anthropic_preserve_dots(agent) is True
328  
329      def test_non_bedrock_aws_url_does_not_preserve_dots(self):
330          """Unrelated AWS endpoints (e.g. ``s3.us-east-1.amazonaws.com``)
331          must not accidentally activate the dot-preservation heuristic —
332          the heuristic is scoped to the ``bedrock-runtime.`` substring
333          specifically."""
334          from types import SimpleNamespace
335          agent = SimpleNamespace(
336              provider="custom",
337              base_url="https://s3.us-east-1.amazonaws.com",
338          )
339          from run_agent import AIAgent
340          assert AIAgent._anthropic_preserve_dots(agent) is False
341  
342      def test_anthropic_native_still_does_not_preserve_dots(self):
343          """Canary: adding Bedrock to the allowlist must not weaken the
344          existing Anthropic native behaviour — ``claude-sonnet-4.6`` still
345          becomes ``claude-sonnet-4-6`` for the Anthropic API."""
346          from types import SimpleNamespace
347          agent = SimpleNamespace(provider="anthropic", base_url="https://api.anthropic.com")
348          from run_agent import AIAgent
349          assert AIAgent._anthropic_preserve_dots(agent) is False
350  
351  
352  class TestBedrockModelNameNormalization:
353      """End-to-end: ``normalize_model_name`` + the preserve-dots flag
354      reproduce the exact production request shape for each Bedrock model
355      family, confirming the fix resolves the reporter's HTTP 400."""
356  
357      def test_global_anthropic_inference_profile_preserved(self):
358          """The reporter's exact model ID."""
359          from agent.anthropic_adapter import normalize_model_name
360          assert normalize_model_name(
361              "global.anthropic.claude-opus-4-7", preserve_dots=True
362          ) == "global.anthropic.claude-opus-4-7"
363  
364      def test_us_anthropic_dated_inference_profile_preserved(self):
365          """Regional + dated Sonnet inference profile."""
366          from agent.anthropic_adapter import normalize_model_name
367          assert normalize_model_name(
368              "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
369              preserve_dots=True,
370          ) == "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
371  
372      def test_apac_anthropic_haiku_inference_profile_preserved(self):
373          """APAC inference profile — same structural-dot shape."""
374          from agent.anthropic_adapter import normalize_model_name
375          assert normalize_model_name(
376              "apac.anthropic.claude-haiku-4-5", preserve_dots=True
377          ) == "apac.anthropic.claude-haiku-4-5"
378  
379      def test_bedrock_prefix_preserved_without_preserve_dots(self):
380          """Bedrock inference profile IDs are auto-detected by prefix and
381          always returned unmangled -- ``preserve_dots`` is irrelevant for
382          these IDs because the dots are namespace separators, not version
383          separators.  Regression for #12295."""
384          from agent.anthropic_adapter import normalize_model_name
385          assert normalize_model_name(
386              "global.anthropic.claude-opus-4-7", preserve_dots=False
387          ) == "global.anthropic.claude-opus-4-7"
388  
389      def test_bare_foundation_model_id_preserved(self):
390          """Non-inference-profile Bedrock IDs
391          (e.g. ``anthropic.claude-3-5-sonnet-20241022-v2:0``) use dots as
392          vendor separators and must also survive intact under
393          ``preserve_dots=True``."""
394          from agent.anthropic_adapter import normalize_model_name
395          assert normalize_model_name(
396              "anthropic.claude-3-5-sonnet-20241022-v2:0",
397              preserve_dots=True,
398          ) == "anthropic.claude-3-5-sonnet-20241022-v2:0"
399  
400  
401  class TestBedrockBuildAnthropicKwargsEndToEnd:
402      """Integration: calling ``build_anthropic_kwargs`` with a Bedrock-
403      shaped model ID and ``preserve_dots=True`` produces the unmangled
404      model string in the outgoing kwargs — the exact body sent to the
405      ``bedrock-runtime.`` endpoint.  This is the integration-level
406      regression for the reporter's HTTP 400."""
407  
408      def test_bedrock_inference_profile_survives_build_kwargs(self):
409          from agent.anthropic_adapter import build_anthropic_kwargs
410          kwargs = build_anthropic_kwargs(
411              model="global.anthropic.claude-opus-4-7",
412              messages=[{"role": "user", "content": "hi"}],
413              tools=None,
414              max_tokens=1024,
415              reasoning_config=None,
416              preserve_dots=True,
417          )
418          assert kwargs["model"] == "global.anthropic.claude-opus-4-7", (
419              "Bedrock inference-profile ID was mangled in build_anthropic_kwargs: "
420              f"{kwargs['model']!r}"
421          )
422  
423      def test_bedrock_model_preserved_without_preserve_dots(self):
424          """Bedrock inference profile IDs survive ``build_anthropic_kwargs``
425          even without ``preserve_dots=True`` -- the prefix auto-detection
426          in ``normalize_model_name`` is the load-bearing piece.
427          Regression for #12295."""
428          from agent.anthropic_adapter import build_anthropic_kwargs
429          kwargs = build_anthropic_kwargs(
430              model="global.anthropic.claude-opus-4-7",
431              messages=[{"role": "user", "content": "hi"}],
432              tools=None,
433              max_tokens=1024,
434              reasoning_config=None,
435              preserve_dots=False,
436          )
437          assert kwargs["model"] == "global.anthropic.claude-opus-4-7"
438  
439  
440  class TestBedrockModelIdDetection:
441      """Tests for ``_is_bedrock_model_id`` and the auto-detection that
442      makes ``normalize_model_name`` preserve dots for Bedrock IDs
443      regardless of ``preserve_dots``.  Regression for #12295."""
444  
445      def test_bare_bedrock_id_detected(self):
446          from agent.anthropic_adapter import _is_bedrock_model_id
447          assert _is_bedrock_model_id("anthropic.claude-opus-4-7") is True
448  
449      def test_regional_us_prefix_detected(self):
450          from agent.anthropic_adapter import _is_bedrock_model_id
451          assert _is_bedrock_model_id("us.anthropic.claude-sonnet-4-5-v1:0") is True
452  
453      def test_regional_global_prefix_detected(self):
454          from agent.anthropic_adapter import _is_bedrock_model_id
455          assert _is_bedrock_model_id("global.anthropic.claude-opus-4-7") is True
456  
457      def test_regional_eu_prefix_detected(self):
458          from agent.anthropic_adapter import _is_bedrock_model_id
459          assert _is_bedrock_model_id("eu.anthropic.claude-sonnet-4-6") is True
460  
461      def test_openrouter_format_not_detected(self):
462          from agent.anthropic_adapter import _is_bedrock_model_id
463          assert _is_bedrock_model_id("claude-opus-4.6") is False
464  
465      def test_bare_claude_not_detected(self):
466          from agent.anthropic_adapter import _is_bedrock_model_id
467          assert _is_bedrock_model_id("claude-opus-4-7") is False
468  
469      def test_bare_bedrock_id_preserved_without_flag(self):
470          """The primary bug from #12295: ``anthropic.claude-opus-4-7``
471          sent to bedrock-mantle via auxiliary clients that don't pass
472          ``preserve_dots=True``."""
473          from agent.anthropic_adapter import normalize_model_name
474          assert normalize_model_name(
475              "anthropic.claude-opus-4-7", preserve_dots=False
476          ) == "anthropic.claude-opus-4-7"
477  
478      def test_openrouter_dots_still_converted(self):
479          """Non-Bedrock dotted model names must still be converted."""
480          from agent.anthropic_adapter import normalize_model_name
481          assert normalize_model_name("claude-opus-4.6") == "claude-opus-4-6"
482  
483      def test_bare_bedrock_id_survives_build_kwargs(self):
484          """End-to-end: bare Bedrock ID through ``build_anthropic_kwargs``
485          without ``preserve_dots=True`` -- the auxiliary client path."""
486          from agent.anthropic_adapter import build_anthropic_kwargs
487          kwargs = build_anthropic_kwargs(
488              model="anthropic.claude-opus-4-7",
489              messages=[{"role": "user", "content": "hi"}],
490              tools=None,
491              max_tokens=1024,
492              reasoning_config=None,
493              preserve_dots=False,
494          )
495          assert kwargs["model"] == "anthropic.claude-opus-4-7"
496  
497  
498  # ---------------------------------------------------------------------------
499  # auxiliary_client Bedrock resolution — fix for #13919
500  # ---------------------------------------------------------------------------
501  # Before the fix, resolve_provider_client("bedrock", ...) fell through to the
502  # "unhandled auth_type" warning and returned (None, None), breaking all
503  # auxiliary tasks (compression, memory, summarization) for Bedrock users.
504  
505  
506  class TestAuxiliaryClientBedrockResolution:
507      """Verify resolve_provider_client handles Bedrock's aws_sdk auth type."""
508  
509      def test_bedrock_returns_client_with_credentials(self, monkeypatch):
510          """With valid AWS credentials, Bedrock should return a usable client."""
511          monkeypatch.setenv("AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE")
512          monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
513          monkeypatch.setenv("AWS_REGION", "us-west-2")
514  
515          mock_anthropic_bedrock = MagicMock()
516          with patch("agent.anthropic_adapter.build_anthropic_bedrock_client",
517                     return_value=mock_anthropic_bedrock):
518              from agent.auxiliary_client import resolve_provider_client, AnthropicAuxiliaryClient
519              client, model = resolve_provider_client("bedrock", None)
520  
521          assert client is not None, (
522              "resolve_provider_client('bedrock') returned None — "
523              "aws_sdk auth type is not handled"
524          )
525          assert isinstance(client, AnthropicAuxiliaryClient)
526          assert model is not None
527          assert client.api_key == "aws-sdk"
528          assert "us-west-2" in client.base_url
529  
530      def test_bedrock_returns_none_without_credentials(self, monkeypatch):
531          """Without AWS credentials, Bedrock should return (None, None) gracefully."""
532          with patch("agent.bedrock_adapter.has_aws_credentials", return_value=False):
533              from agent.auxiliary_client import resolve_provider_client
534              client, model = resolve_provider_client("bedrock", None)
535  
536          assert client is None
537          assert model is None
538  
539      def test_bedrock_uses_configured_region(self, monkeypatch):
540          """Bedrock client base_url should reflect AWS_REGION."""
541          monkeypatch.setenv("AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE")
542          monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
543          monkeypatch.setenv("AWS_REGION", "eu-central-1")
544  
545          with patch("agent.anthropic_adapter.build_anthropic_bedrock_client",
546                     return_value=MagicMock()):
547              from agent.auxiliary_client import resolve_provider_client
548              client, _ = resolve_provider_client("bedrock", None)
549  
550          assert client is not None
551          assert "eu-central-1" in client.base_url
552  
553      def test_bedrock_respects_explicit_model(self, monkeypatch):
554          """When caller passes an explicit model, it should be used."""
555          monkeypatch.setenv("AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE")
556          monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
557  
558          with patch("agent.anthropic_adapter.build_anthropic_bedrock_client",
559                     return_value=MagicMock()):
560              from agent.auxiliary_client import resolve_provider_client
561              _, model = resolve_provider_client(
562                  "bedrock", "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
563              )
564  
565          assert "claude-sonnet" in model
566  
567      def test_bedrock_async_mode(self, monkeypatch):
568          """Async mode should return an AsyncAnthropicAuxiliaryClient."""
569          monkeypatch.setenv("AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE")
570          monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
571  
572          with patch("agent.anthropic_adapter.build_anthropic_bedrock_client",
573                     return_value=MagicMock()):
574              from agent.auxiliary_client import resolve_provider_client, AsyncAnthropicAuxiliaryClient
575              client, model = resolve_provider_client("bedrock", None, async_mode=True)
576  
577          assert client is not None
578          assert isinstance(client, AsyncAnthropicAuxiliaryClient)
579  
580      def test_bedrock_default_model_is_haiku(self, monkeypatch):
581          """Default auxiliary model for Bedrock should be Haiku (fast, cheap)."""
582          monkeypatch.setenv("AWS_ACCESS_KEY_ID", "AKIAIOSFODNN7EXAMPLE")
583          monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
584  
585          with patch("agent.anthropic_adapter.build_anthropic_bedrock_client",
586                     return_value=MagicMock()):
587              from agent.auxiliary_client import resolve_provider_client
588              _, model = resolve_provider_client("bedrock", None)
589  
590          assert "haiku" in model.lower()