From fdf3101b39638634cd9cc0594298bb778f61b538 Mon Sep 17 00:00:00 2001 From: Matt Aitchison Date: Wed, 22 Apr 2026 15:21:35 -0500 Subject: [PATCH] feat(azure): fall back to DefaultAzureCredential when no API key Enables keyless Azure auth (OIDC Workload Identity Federation, Managed Identity, Azure CLI, env-configured Service Principal) without any crewAI-specific configuration. Customers whose deployment environment already sets the standard azure-identity env vars get keyless auth for free; the existing API-key path is unchanged. Linear: FAC-40 --- lib/crewai/pyproject.toml | 1 + .../crewai/llms/providers/azure/completion.py | 34 ++++++++++++++--- lib/crewai/tests/llms/azure/test_azure.py | 38 +++++++++++++++---- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index 001f2b8a6..cbf017801 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -94,6 +94,7 @@ google-genai = [ ] azure-ai-inference = [ "azure-ai-inference~=1.0.0b9", + "azure-identity>=1.17.0,<2", ] anthropic = [ "anthropic~=0.73.0", diff --git a/lib/crewai/src/crewai/llms/providers/azure/completion.py b/lib/crewai/src/crewai/llms/providers/azure/completion.py index 4b8d842a5..714a7f0e9 100644 --- a/lib/crewai/src/crewai/llms/providers/azure/completion.py +++ b/lib/crewai/src/crewai/llms/providers/azure/completion.py @@ -183,11 +183,6 @@ class AzureCompletion(BaseLLM): AzureCompletion._is_azure_openai_endpoint(self.endpoint) ) - if not self.api_key: - raise ValueError( - "Azure API key is required. Set AZURE_API_KEY environment " - "variable or pass api_key parameter." - ) if not self.endpoint: raise ValueError( "Azure endpoint is required. Set AZURE_ENDPOINT environment " @@ -195,12 +190,39 @@ class AzureCompletion(BaseLLM): ) client_kwargs: dict[str, Any] = { "endpoint": self.endpoint, - "credential": AzureKeyCredential(self.api_key), + "credential": self._resolve_credential(), } if self.api_version: client_kwargs["api_version"] = self.api_version return client_kwargs + def _resolve_credential(self) -> Any: + """Return an Azure credential, preferring the API key when set. + + Without an API key, fall back to ``DefaultAzureCredential`` from + ``azure-identity``. That chain auto-detects the standard keyless + paths the customer's environment may provide — OIDC Workload + Identity Federation (``AZURE_FEDERATED_TOKEN_FILE`` + + ``AZURE_TENANT_ID`` + ``AZURE_CLIENT_ID``), Managed Identity on + AKS/Azure VMs, environment-configured service principals, and + developer tools like the Azure CLI. Installing ``azure-identity`` + is what enables these paths; without it we raise the existing + API-key error. + """ + if self.api_key: + return AzureKeyCredential(self.api_key) + + try: + from azure.identity import DefaultAzureCredential + except ImportError: + raise ValueError( + "Azure API key is required when azure-identity is not " + "installed. Set AZURE_API_KEY, or install azure-identity " + 'for keyless auth: uv add "crewai[azure-ai-inference]"' + ) from None + + return DefaultAzureCredential() + def _get_sync_client(self) -> Any: if self._client is None: self._client = self._build_sync_client() diff --git a/lib/crewai/tests/llms/azure/test_azure.py b/lib/crewai/tests/llms/azure/test_azure.py index d42e2d7fe..774d23f20 100644 --- a/lib/crewai/tests/llms/azure/test_azure.py +++ b/lib/crewai/tests/llms/azure/test_azure.py @@ -389,17 +389,41 @@ def test_azure_raises_error_when_endpoint_missing(): llm._get_sync_client() -def test_azure_raises_error_when_api_key_missing(): - """Credentials are validated lazily: construction succeeds, first +def test_azure_raises_error_when_api_key_missing_without_azure_identity(): + """Without an API key AND without ``azure-identity`` installed, client build raises the descriptive error.""" from crewai.llms.providers.azure.completion import AzureCompletion with patch.dict(os.environ, {}, clear=True): - llm = AzureCompletion( - model="gpt-4", endpoint="https://test.openai.azure.com" - ) - with pytest.raises(ValueError, match="Azure API key is required"): - llm._get_sync_client() + with patch.dict("sys.modules", {"azure.identity": None}): + llm = AzureCompletion( + model="gpt-4", endpoint="https://test.openai.azure.com" + ) + with pytest.raises(ValueError, match="Azure API key is required"): + llm._get_sync_client() + + +def test_azure_uses_default_credential_when_api_key_missing(): + """With ``azure-identity`` installed, a missing API key falls back to + ``DefaultAzureCredential`` instead of raising. This is the path that + enables keyless auth (OIDC WIF on EKS/AKS, Managed Identity, Azure + CLI) without any crewAI-specific config.""" + from unittest.mock import MagicMock + + from crewai.llms.providers.azure.completion import AzureCompletion + + sentinel = MagicMock(name="DefaultAzureCredential()") + with patch.dict(os.environ, {}, clear=True): + with patch( + "azure.identity.DefaultAzureCredential", return_value=sentinel + ) as mock_cls: + llm = AzureCompletion( + model="gpt-4", + endpoint="https://test-ai.services.example.com", + ) + kwargs = llm._make_client_kwargs() + assert kwargs["credential"] is sentinel + mock_cls.assert_called() @pytest.mark.asyncio