diff --git a/docs/ar/changelog.mdx b/docs/ar/changelog.mdx
index d4353d210..b151cf950 100644
--- a/docs/ar/changelog.mdx
+++ b/docs/ar/changelog.mdx
@@ -4,6 +4,30 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
icon: "clock"
mode: "wide"
---
+
+ ## v1.14.2a4
+
+ [عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
+
+ ## ما الذي تغير
+
+ ### الميزات
+ - إضافة تلميحات استئناف إلى إصدار أدوات المطورين عند الفشل
+
+ ### إصلاحات الأخطاء
+ - إصلاح توجيه وضع الصرامة إلى واجهة برمجة تطبيقات Bedrock Converse
+ - إصلاح إصدار pytest إلى 9.0.3 لثغرة الأمان GHSA-6w46-j5rx-g56g
+ - رفع الحد الأدنى لـ OpenAI إلى >=2.0.0
+
+ ### الوثائق
+ - تحديث سجل التغييرات والإصدار لـ v1.14.2a3
+
+ ## المساهمون
+
+ @greysonlalonde
+
+
+
## v1.14.2a3
diff --git a/docs/en/changelog.mdx b/docs/en/changelog.mdx
index 340d33633..24c62b85f 100644
--- a/docs/en/changelog.mdx
+++ b/docs/en/changelog.mdx
@@ -4,6 +4,30 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
icon: "clock"
mode: "wide"
---
+
+ ## v1.14.2a4
+
+ [View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
+
+ ## What's Changed
+
+ ### Features
+ - Add resume hints to devtools release on failure
+
+ ### Bug Fixes
+ - Fix strict mode forwarding to Bedrock Converse API
+ - Fix pytest version to 9.0.3 for security vulnerability GHSA-6w46-j5rx-g56g
+ - Bump OpenAI lower bound to >=2.0.0
+
+ ### Documentation
+ - Update changelog and version for v1.14.2a3
+
+ ## Contributors
+
+ @greysonlalonde
+
+
+
## v1.14.2a3
diff --git a/docs/ko/changelog.mdx b/docs/ko/changelog.mdx
index e4c00fcf6..79c260bb4 100644
--- a/docs/ko/changelog.mdx
+++ b/docs/ko/changelog.mdx
@@ -4,6 +4,30 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
icon: "clock"
mode: "wide"
---
+
+ ## v1.14.2a4
+
+ [GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
+
+ ## 변경 사항
+
+ ### 기능
+ - 실패 시 devtools 릴리스에 이력서 힌트 추가
+
+ ### 버그 수정
+ - Bedrock Converse API로의 엄격 모드 포워딩 수정
+ - 보안 취약점 GHSA-6w46-j5rx-g56g에 대해 pytest 버전을 9.0.3으로 수정
+ - OpenAI 하한을 >=2.0.0으로 상향 조정
+
+ ### 문서
+ - v1.14.2a3에 대한 변경 로그 및 버전 업데이트
+
+ ## 기여자
+
+ @greysonlalonde
+
+
+
## v1.14.2a3
diff --git a/docs/pt-BR/changelog.mdx b/docs/pt-BR/changelog.mdx
index 52dc9af0b..a8b9bc4c2 100644
--- a/docs/pt-BR/changelog.mdx
+++ b/docs/pt-BR/changelog.mdx
@@ -4,6 +4,30 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
icon: "clock"
mode: "wide"
---
+
+ ## v1.14.2a4
+
+ [Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
+
+ ## O que Mudou
+
+ ### Recursos
+ - Adicionar dicas de retomar ao release do devtools em caso de falha
+
+ ### Correções de Bugs
+ - Corrigir o encaminhamento do modo estrito para a API Bedrock Converse
+ - Corrigir a versão do pytest para 9.0.3 devido à vulnerabilidade de segurança GHSA-6w46-j5rx-g56g
+ - Aumentar o limite inferior do OpenAI para >=2.0.0
+
+ ### Documentação
+ - Atualizar o changelog e a versão para v1.14.2a3
+
+ ## Contribuidores
+
+ @greysonlalonde
+
+
+
## v1.14.2a3
diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml
index 5049559fe..b0ad92c03 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..fd8957e08 100644
--- a/lib/crewai/src/crewai/llms/providers/azure/completion.py
+++ b/lib/crewai/src/crewai/llms/providers/azure/completion.py
@@ -35,6 +35,7 @@ try:
)
from azure.core.credentials import (
AzureKeyCredential,
+ TokenCredential,
)
from azure.core.exceptions import (
HttpResponseError,
@@ -88,6 +89,8 @@ class AzureCompletion(BaseLLM):
response_format: type[BaseModel] | None = None
is_openai_model: bool = False
is_azure_openai_endpoint: bool = False
+ azure_tenant_id: str | None = None
+ azure_client_id: str | None = None
_client: Any = PrivateAttr(default=None)
_async_client: Any = PrivateAttr(default=None)
@@ -115,6 +118,8 @@ class AzureCompletion(BaseLLM):
data["api_version"] = (
data.get("api_version") or os.getenv("AZURE_API_VERSION") or "2024-06-01"
)
+ data["azure_tenant_id"] = data.get("azure_tenant_id") or os.getenv("AZURE_TENANT_ID")
+ data["azure_client_id"] = data.get("azure_client_id") or os.getenv("AZURE_CLIENT_ID")
# Credentials and endpoint are validated lazily in `_init_clients`
# so the LLM can be constructed before deployment env vars are set.
@@ -183,24 +188,89 @@ 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."
- )
+ # Re-read identity env vars for deferred builds
+ if not self.azure_tenant_id:
+ self.azure_tenant_id = os.getenv("AZURE_TENANT_ID")
+ if not self.azure_client_id:
+ self.azure_client_id = os.getenv("AZURE_CLIENT_ID")
+
if not self.endpoint:
raise ValueError(
"Azure endpoint is required. Set AZURE_ENDPOINT environment "
"variable or pass endpoint parameter."
)
+
+ credential = self._resolve_credential()
+
client_kwargs: dict[str, Any] = {
"endpoint": self.endpoint,
- "credential": AzureKeyCredential(self.api_key),
+ "credential": credential,
}
if self.api_version:
client_kwargs["api_version"] = self.api_version
return client_kwargs
+ def _resolve_credential(self) -> AzureKeyCredential | TokenCredential:
+ """Resolve the Azure credential using a priority chain.
+
+ 1. OIDC federation (WorkloadIdentityCredential) — auto-discovered
+ from AZURE_FEDERATED_TOKEN_FILE + AZURE_TENANT_ID + AZURE_CLIENT_ID
+ 2. Client secret (ClientSecretCredential) — explicit SP credentials
+ 3. Default chain (DefaultAzureCredential) — Managed Identity et al.
+ 4. API key fallback (AzureKeyCredential) — existing path
+ """
+ federated_token_file = os.getenv("AZURE_FEDERATED_TOKEN_FILE")
+ client_secret = os.getenv("AZURE_CLIENT_SECRET")
+
+ # Path 1: OIDC Workload Identity Federation
+ if federated_token_file and self.azure_tenant_id and self.azure_client_id:
+ try:
+ from azure.identity import WorkloadIdentityCredential
+
+ return WorkloadIdentityCredential(
+ tenant_id=self.azure_tenant_id,
+ client_id=self.azure_client_id,
+ token_file_path=federated_token_file,
+ )
+ except ImportError:
+ raise ImportError(
+ "azure-identity is required for workload identity federation. "
+ 'Install with: uv add "crewai[azure-ai-inference]"'
+ ) from None
+
+ # Path 2: Client Secret (Service Principal)
+ if client_secret and self.azure_tenant_id and self.azure_client_id:
+ try:
+ from azure.identity import ClientSecretCredential
+
+ return ClientSecretCredential(
+ tenant_id=self.azure_tenant_id,
+ client_id=self.azure_client_id,
+ client_secret=client_secret,
+ )
+ except ImportError:
+ raise ImportError(
+ "azure-identity is required for service principal authentication. "
+ 'Install with: uv add "crewai[azure-ai-inference]"'
+ ) from None
+
+ # Path 3: DefaultAzureCredential (Managed Identity, Azure CLI, etc.)
+ # Only attempt if azure-identity is installed and no API key is available
+ if not self.api_key:
+ try:
+ from azure.identity import DefaultAzureCredential
+
+ return DefaultAzureCredential()
+ except ImportError:
+ raise ValueError(
+ "Azure API key is required when azure-identity is not installed. "
+ "Set AZURE_API_KEY environment variable, pass api_key parameter, "
+ 'or install azure-identity: uv add "crewai[azure-ai-inference]"'
+ ) from None
+
+ # Path 4: API Key (existing path)
+ return AzureKeyCredential(self.api_key)
+
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_credentials.py b/lib/crewai/tests/llms/azure/test_azure_credentials.py
new file mode 100644
index 000000000..157e3ab80
--- /dev/null
+++ b/lib/crewai/tests/llms/azure/test_azure_credentials.py
@@ -0,0 +1,257 @@
+"""Tests for Azure credential resolution chain in AzureCompletion.
+
+Covers the four credential paths:
+1. WorkloadIdentityCredential (OIDC federation)
+2. ClientSecretCredential (Service Principal)
+3. DefaultAzureCredential (Managed Identity / CLI fallback)
+4. AzureKeyCredential (API key - existing path)
+"""
+
+import os
+from unittest.mock import patch, MagicMock
+
+import pytest
+
+
+ENDPOINT = "https://test.openai.azure.com"
+
+
+@pytest.fixture
+def _clear_azure_env(monkeypatch):
+ """Remove all Azure env vars to start clean."""
+ for key in [
+ "AZURE_API_KEY", "AZURE_ENDPOINT", "AZURE_OPENAI_ENDPOINT",
+ "AZURE_API_BASE", "AZURE_API_VERSION", "AZURE_TENANT_ID",
+ "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_FEDERATED_TOKEN_FILE",
+ ]:
+ monkeypatch.delenv(key, raising=False)
+
+
+@pytest.mark.usefixtures("_clear_azure_env")
+class TestCredentialResolution:
+ """Tests for AzureCompletion._resolve_credential."""
+
+ def test_api_key_credential_when_api_key_set(self):
+ """Path 4: API key produces AzureKeyCredential."""
+ from crewai.llms.providers.azure.completion import AzureCompletion
+ from azure.core.credentials import AzureKeyCredential
+
+ completion = AzureCompletion(
+ model="gpt-4",
+ api_key="test-key",
+ endpoint=ENDPOINT,
+ )
+ cred = completion._resolve_credential()
+ assert isinstance(cred, AzureKeyCredential)
+
+ def test_api_key_from_env(self, monkeypatch):
+ """Path 4: api_key picked up from AZURE_API_KEY env var."""
+ from crewai.llms.providers.azure.completion import AzureCompletion
+ from azure.core.credentials import AzureKeyCredential
+
+ monkeypatch.setenv("AZURE_API_KEY", "env-key")
+ monkeypatch.setenv("AZURE_ENDPOINT", ENDPOINT)
+
+ completion = AzureCompletion(model="gpt-4")
+ cred = completion._resolve_credential()
+ assert isinstance(cred, AzureKeyCredential)
+
+ def test_workload_identity_credential(self, monkeypatch, tmp_path):
+ """Path 1: OIDC federation via WorkloadIdentityCredential."""
+ from crewai.llms.providers.azure.completion import AzureCompletion
+
+ token_file = tmp_path / "token.txt"
+ token_file.write_text("eyJhbGciOiJSUzI1NiJ9.test")
+
+ monkeypatch.setenv("AZURE_FEDERATED_TOKEN_FILE", str(token_file))
+ monkeypatch.setenv("AZURE_ENDPOINT", ENDPOINT)
+
+ mock_wi_cred = MagicMock()
+ with patch(
+ "azure.identity.WorkloadIdentityCredential",
+ return_value=mock_wi_cred,
+ ) as mock_cls:
+ completion = AzureCompletion(
+ model="gpt-4",
+ azure_tenant_id="tenant-123",
+ azure_client_id="client-456",
+ )
+ cred = completion._resolve_credential()
+ assert cred is mock_wi_cred
+ mock_cls.assert_called_once_with(
+ tenant_id="tenant-123",
+ client_id="client-456",
+ token_file_path=str(token_file),
+ )
+
+ def test_workload_identity_from_env_vars(self, monkeypatch, tmp_path):
+ """Path 1: All WI fields discovered from environment."""
+ from crewai.llms.providers.azure.completion import AzureCompletion
+
+ token_file = tmp_path / "token.txt"
+ token_file.write_text("eyJhbGciOiJSUzI1NiJ9.test")
+
+ monkeypatch.setenv("AZURE_FEDERATED_TOKEN_FILE", str(token_file))
+ monkeypatch.setenv("AZURE_TENANT_ID", "env-tenant")
+ monkeypatch.setenv("AZURE_CLIENT_ID", "env-client")
+ monkeypatch.setenv("AZURE_ENDPOINT", ENDPOINT)
+
+ mock_wi_cred = MagicMock()
+ with patch(
+ "azure.identity.WorkloadIdentityCredential",
+ return_value=mock_wi_cred,
+ ) as mock_cls:
+ completion = AzureCompletion(model="gpt-4")
+ cred = completion._resolve_credential()
+ assert cred is mock_wi_cred
+ mock_cls.assert_called_once_with(
+ tenant_id="env-tenant",
+ client_id="env-client",
+ token_file_path=str(token_file),
+ )
+
+ def test_client_secret_credential(self, monkeypatch):
+ """Path 2: Service Principal with client secret."""
+ from crewai.llms.providers.azure.completion import AzureCompletion
+
+ monkeypatch.setenv("AZURE_CLIENT_SECRET", "sp-secret")
+ monkeypatch.setenv("AZURE_ENDPOINT", ENDPOINT)
+
+ mock_cs_cred = MagicMock()
+ with patch(
+ "azure.identity.ClientSecretCredential",
+ return_value=mock_cs_cred,
+ ) as mock_cls:
+ completion = AzureCompletion(
+ model="gpt-4",
+ azure_tenant_id="tenant-123",
+ azure_client_id="client-456",
+ )
+ cred = completion._resolve_credential()
+ assert cred is mock_cs_cred
+ mock_cls.assert_called_once_with(
+ tenant_id="tenant-123",
+ client_id="client-456",
+ client_secret="sp-secret",
+ )
+
+ def test_default_azure_credential_when_no_api_key(self, monkeypatch):
+ """Path 3: DefaultAzureCredential when no api_key and no SP/WI vars."""
+ from crewai.llms.providers.azure.completion import AzureCompletion
+
+ monkeypatch.setenv("AZURE_ENDPOINT", ENDPOINT)
+
+ mock_default_cred = MagicMock()
+ with patch(
+ "azure.identity.DefaultAzureCredential",
+ return_value=mock_default_cred,
+ ):
+ completion = AzureCompletion(model="gpt-4")
+ cred = completion._resolve_credential()
+ assert cred is mock_default_cred
+
+ def test_workload_identity_takes_priority_over_api_key(self, monkeypatch, tmp_path):
+ """WI credential should take priority even when api_key is also set."""
+ from crewai.llms.providers.azure.completion import AzureCompletion
+
+ token_file = tmp_path / "token.txt"
+ token_file.write_text("eyJhbGciOiJSUzI1NiJ9.test")
+
+ monkeypatch.setenv("AZURE_FEDERATED_TOKEN_FILE", str(token_file))
+ monkeypatch.setenv("AZURE_API_KEY", "should-not-use-this")
+ monkeypatch.setenv("AZURE_ENDPOINT", ENDPOINT)
+
+ mock_wi_cred = MagicMock()
+ with patch(
+ "azure.identity.WorkloadIdentityCredential",
+ return_value=mock_wi_cred,
+ ):
+ completion = AzureCompletion(
+ model="gpt-4",
+ azure_tenant_id="tenant-123",
+ azure_client_id="client-456",
+ )
+ cred = completion._resolve_credential()
+ assert cred is mock_wi_cred
+
+ def test_client_secret_takes_priority_over_api_key(self, monkeypatch):
+ """SP credential should take priority over API key."""
+ from crewai.llms.providers.azure.completion import AzureCompletion
+
+ monkeypatch.setenv("AZURE_CLIENT_SECRET", "sp-secret")
+ monkeypatch.setenv("AZURE_API_KEY", "should-not-use-this")
+ monkeypatch.setenv("AZURE_ENDPOINT", ENDPOINT)
+
+ mock_cs_cred = MagicMock()
+ with patch(
+ "azure.identity.ClientSecretCredential",
+ return_value=mock_cs_cred,
+ ):
+ completion = AzureCompletion(
+ model="gpt-4",
+ azure_tenant_id="tenant-123",
+ azure_client_id="client-456",
+ )
+ cred = completion._resolve_credential()
+ assert cred is mock_cs_cred
+
+ def test_raises_when_no_api_key_and_no_azure_identity(self, monkeypatch):
+ """ValueError when no api_key and azure-identity not installed."""
+ from crewai.llms.providers.azure.completion import AzureCompletion
+
+ monkeypatch.setenv("AZURE_ENDPOINT", ENDPOINT)
+
+ with patch.dict("sys.modules", {"azure.identity": None}):
+ completion = AzureCompletion(model="gpt-4")
+ with pytest.raises(ValueError, match="Azure API key is required"):
+ completion._resolve_credential()
+
+ def test_endpoint_still_required(self, monkeypatch, tmp_path):
+ """Endpoint is always required regardless of credential type."""
+ from crewai.llms.providers.azure.completion import AzureCompletion
+
+ token_file = tmp_path / "token.txt"
+ token_file.write_text("test-jwt")
+
+ monkeypatch.setenv("AZURE_FEDERATED_TOKEN_FILE", str(token_file))
+ monkeypatch.setenv("AZURE_TENANT_ID", "tenant-123")
+ monkeypatch.setenv("AZURE_CLIENT_ID", "client-456")
+
+ completion = AzureCompletion(model="gpt-4")
+ with pytest.raises(ValueError, match="Azure endpoint is required"):
+ completion._make_client_kwargs()
+
+ def test_deferred_build_picks_up_wi_env_vars(self, monkeypatch, tmp_path):
+ """Env vars set after construction are picked up on deferred build."""
+ from crewai.llms.providers.azure.completion import AzureCompletion
+
+ # Construct with endpoint only — no credentials yet
+ monkeypatch.setenv("AZURE_ENDPOINT", ENDPOINT)
+ completion = AzureCompletion(model="gpt-4")
+
+ # Now set WI env vars (simulating WI manager setting them before crew run)
+ token_file = tmp_path / "token.txt"
+ token_file.write_text("eyJhbGciOiJSUzI1NiJ9.deferred")
+ monkeypatch.setenv("AZURE_FEDERATED_TOKEN_FILE", str(token_file))
+ monkeypatch.setenv("AZURE_TENANT_ID", "deferred-tenant")
+ monkeypatch.setenv("AZURE_CLIENT_ID", "deferred-client")
+
+ mock_wi_cred = MagicMock()
+ with patch(
+ "azure.identity.WorkloadIdentityCredential",
+ return_value=mock_wi_cred,
+ ):
+ kwargs = completion._make_client_kwargs()
+ assert kwargs["credential"] is mock_wi_cred
+
+ def test_make_client_kwargs_includes_api_version(self, monkeypatch):
+ """api_version is included in client kwargs."""
+ from crewai.llms.providers.azure.completion import AzureCompletion
+
+ monkeypatch.setenv("AZURE_API_KEY", "test-key")
+ monkeypatch.setenv("AZURE_ENDPOINT", ENDPOINT)
+
+ completion = AzureCompletion(model="gpt-4", api_version="2025-01-01")
+ kwargs = completion._make_client_kwargs()
+ assert kwargs["api_version"] == "2025-01-01"
+ assert kwargs["endpoint"] == ENDPOINT