mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-02 05:38:12 +00:00
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
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user