diff --git a/providers/anthropic/src/airflow/providers/anthropic/hooks/anthropic.py b/providers/anthropic/src/airflow/providers/anthropic/hooks/anthropic.py index dbefdf84d5df4..4d13240422cc4 100644 --- a/providers/anthropic/src/airflow/providers/anthropic/hooks/anthropic.py +++ b/providers/anthropic/src/airflow/providers/anthropic/hooks/anthropic.py @@ -297,6 +297,20 @@ def _workload_identity_credentials(wif: dict[str, Any]) -> WorkloadIdentityCrede kwargs["scope"] = wif["scope"] return WorkloadIdentityCredentials(**kwargs) + def _resolve_model(self, model: str | None) -> str: + """Resolve the effective model id; Bedrock rejects a bare id, so require its prefix.""" + resolved = model or self.default_model + # Valid Bedrock ids either start with the ``anthropic.`` provider prefix or carry a + # region/profile prefix as a dotted component (e.g. ``us.anthropic.``, ``global.anthropic.``). + is_bedrock_model_id = resolved.startswith("anthropic.") or ".anthropic." in resolved + if self.platform == "bedrock" and not is_bedrock_model_id: + raise AnthropicError( + f"Model {resolved!r} is not a valid Amazon Bedrock model id. Bedrock ids carry a " + "provider/region prefix (e.g. 'global.anthropic.claude-opus-4-6-v1'); set one via " + "the 'model' argument or the connection's extra['model']." + ) + return resolved + def _require_first_party(self, feature: str) -> None: if self.platform not in FIRST_PARTY_PLATFORMS: raise AnthropicError( @@ -348,7 +362,7 @@ def create_message( :param system: Optional system prompt. """ params: dict[str, Any] = { - "model": model or self.default_model, + "model": self._resolve_model(model), "max_tokens": max_tokens, "messages": messages, **kwargs, diff --git a/providers/anthropic/tests/unit/anthropic/hooks/test_anthropic.py b/providers/anthropic/tests/unit/anthropic/hooks/test_anthropic.py index 1f0974d540cc1..44a448b20114f 100644 --- a/providers/anthropic/tests/unit/anthropic/hooks/test_anthropic.py +++ b/providers/anthropic/tests/unit/anthropic/hooks/test_anthropic.py @@ -185,6 +185,40 @@ def test_create_agent_uses_connection_model(self): hook.create_agent(name="a") assert client.beta.agents.create.call_args.kwargs["model"] == "claude-sonnet-4-6" + @pytest.mark.parametrize( + "model", + [None, "my-anthropic.model"], # bare default id and a substring that is not a real prefix + ) + def test_bedrock_rejects_invalid_model_id(self, model): + extra = {"platform": "bedrock", **({"model": model} if model else {})} + hook, client = _make_hook(extra=extra) + with pytest.raises(AnthropicError, match="not a valid Amazon Bedrock model id"): + hook.create_message([{"role": "user", "content": "hi"}]) + client.messages.create.assert_not_called() + + @pytest.mark.parametrize( + ("platform", "model", "expected"), + [ + ( + "bedrock", + "anthropic.claude-sonnet-4-5-20250929-v1:0", + "anthropic.claude-sonnet-4-5-20250929-v1:0", + ), + ("bedrock", "global.anthropic.claude-opus-4-6-v1", "global.anthropic.claude-opus-4-6-v1"), + ( + "bedrock", + "us.anthropic.claude-sonnet-4-5-20250929-v1:0", + "us.anthropic.claude-sonnet-4-5-20250929-v1:0", + ), + ("vertex", None, DEFAULT_MODEL), # non-Bedrock platforms skip the guard + ], + ) + def test_accepts_valid_model_id(self, platform, model, expected): + extra = {"platform": platform, **({"model": model} if model else {})} + hook, client = _make_hook(extra=extra) + hook.create_message([{"role": "user", "content": "hi"}]) + assert client.messages.create.call_args.kwargs["model"] == expected + class TestManagedAgentsHook: def _hook_with_client(self, extra=None):