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()