mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-28 09:38:17 +00:00
Compare commits
1 Commits
devin/1769
...
devin/1768
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d81fc4db9 |
@@ -410,14 +410,8 @@ class LLM(BaseLLM):
|
||||
|
||||
# FALLBACK to LiteLLM
|
||||
if not LITELLM_AVAILABLE:
|
||||
logger.error(
|
||||
f"Model '{model}' requires LiteLLM but it is not installed. "
|
||||
"Install it with: pip install 'crewai[litellm]' or pip install litellm"
|
||||
)
|
||||
raise ImportError(
|
||||
f"Model '{model}' requires LiteLLM for inference but LiteLLM is not installed. "
|
||||
"Please install it with: pip install 'crewai[litellm]' or pip install litellm"
|
||||
) from None
|
||||
logger.error("LiteLLM is not available, falling back to LiteLLM")
|
||||
raise ImportError("Fallback to LiteLLM is not available") from None
|
||||
|
||||
instance = object.__new__(cls)
|
||||
super(LLM, instance).__init__(model=model, is_litellm=True, **kwargs)
|
||||
@@ -1150,7 +1144,7 @@ class LLM(BaseLLM):
|
||||
if response_model:
|
||||
params["response_model"] = response_model
|
||||
response = litellm.completion(**params)
|
||||
|
||||
|
||||
if hasattr(response,"usage") and not isinstance(response.usage, type) and response.usage:
|
||||
usage_info = response.usage
|
||||
self._track_token_usage_internal(usage_info)
|
||||
@@ -1369,7 +1363,7 @@ class LLM(BaseLLM):
|
||||
"""
|
||||
full_response = ""
|
||||
chunk_count = 0
|
||||
|
||||
|
||||
usage_info = None
|
||||
|
||||
accumulated_tool_args: defaultdict[int, AccumulatedToolArgs] = defaultdict(
|
||||
|
||||
@@ -124,8 +124,11 @@ class AzureCompletion(BaseLLM):
|
||||
)
|
||||
|
||||
self.api_key = api_key or os.getenv("AZURE_API_KEY")
|
||||
# Support both 'endpoint' and 'base_url' parameters for consistency with other providers
|
||||
base_url = kwargs.get("base_url")
|
||||
self.endpoint = (
|
||||
endpoint
|
||||
or base_url
|
||||
or os.getenv("AZURE_ENDPOINT")
|
||||
or os.getenv("AZURE_OPENAI_ENDPOINT")
|
||||
or os.getenv("AZURE_API_BASE")
|
||||
@@ -170,11 +173,44 @@ class AzureCompletion(BaseLLM):
|
||||
prefix in model.lower() for prefix in ["gpt-", "o1-", "text-"]
|
||||
)
|
||||
|
||||
# Azure OpenAI endpoints use openai.azure.com domain and require deployment path
|
||||
# Other Azure AI endpoints (cognitiveservices.azure.com, etc.) are also valid
|
||||
self.is_azure_openai_endpoint = (
|
||||
"openai.azure.com" in self.endpoint
|
||||
and "/openai/deployments/" in self.endpoint
|
||||
)
|
||||
|
||||
# Check if this is any Azure endpoint (for proper API handling)
|
||||
self.is_azure_endpoint = self._is_azure_endpoint(self.endpoint)
|
||||
|
||||
@staticmethod
|
||||
def _is_azure_endpoint(endpoint: str) -> bool:
|
||||
"""Check if the endpoint is an Azure endpoint.
|
||||
|
||||
Azure endpoints can have various domain formats:
|
||||
- openai.azure.com (Azure OpenAI Service)
|
||||
- cognitiveservices.azure.com (Azure AI Services / Cognitive Services)
|
||||
- services.ai.azure.com (Azure AI Services)
|
||||
- Other *.azure.com domains
|
||||
|
||||
Args:
|
||||
endpoint: The endpoint URL to check
|
||||
|
||||
Returns:
|
||||
True if the endpoint is an Azure endpoint, False otherwise
|
||||
"""
|
||||
azure_domains = [
|
||||
"openai.azure.com",
|
||||
"cognitiveservices.azure.com",
|
||||
"services.ai.azure.com",
|
||||
]
|
||||
# Check for known Azure domains
|
||||
for domain in azure_domains:
|
||||
if domain in endpoint:
|
||||
return True
|
||||
# Also check for generic .azure.com pattern (e.g., cservices.azure.com)
|
||||
return ".azure.com" in endpoint
|
||||
|
||||
@staticmethod
|
||||
def _validate_and_fix_endpoint(endpoint: str, model: str) -> str:
|
||||
"""Validate and fix Azure endpoint URL format.
|
||||
@@ -182,6 +218,9 @@ class AzureCompletion(BaseLLM):
|
||||
Azure OpenAI endpoints should be in the format:
|
||||
https://<resource-name>.openai.azure.com/openai/deployments/<deployment-name>
|
||||
|
||||
Other Azure AI endpoints (cognitiveservices.azure.com, etc.) are used as-is
|
||||
since they may have different URL structures.
|
||||
|
||||
Args:
|
||||
endpoint: The endpoint URL
|
||||
model: The model/deployment name
|
||||
@@ -189,6 +228,8 @@ class AzureCompletion(BaseLLM):
|
||||
Returns:
|
||||
Validated and potentially corrected endpoint URL
|
||||
"""
|
||||
# Only auto-construct deployment path for Azure OpenAI endpoints (openai.azure.com)
|
||||
# Other Azure endpoints (cognitiveservices.azure.com, etc.) should be used as-is
|
||||
if "openai.azure.com" in endpoint and "/openai/deployments/" not in endpoint:
|
||||
endpoint = endpoint.rstrip("/")
|
||||
|
||||
|
||||
@@ -1215,3 +1215,177 @@ def test_azure_streaming_returns_usage_metrics():
|
||||
assert result.token_usage.prompt_tokens > 0
|
||||
assert result.token_usage.completion_tokens > 0
|
||||
assert result.token_usage.successful_requests >= 1
|
||||
|
||||
|
||||
def test_azure_cognitive_services_endpoint():
|
||||
"""
|
||||
Test that Azure Cognitive Services endpoints (cognitiveservices.azure.com) are supported.
|
||||
This addresses GitHub issue #4260 where non-openai.azure.com endpoints were not working.
|
||||
"""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
with patch.dict(os.environ, {
|
||||
"AZURE_API_KEY": "test-key",
|
||||
"AZURE_ENDPOINT": "https://my-resource.cognitiveservices.azure.com"
|
||||
}):
|
||||
llm = LLM(model="azure/gpt-4")
|
||||
|
||||
assert isinstance(llm, AzureCompletion)
|
||||
# Cognitive Services endpoints should NOT have deployment path auto-constructed
|
||||
assert llm.endpoint == "https://my-resource.cognitiveservices.azure.com"
|
||||
# Should be recognized as an Azure endpoint
|
||||
assert llm.is_azure_endpoint == True
|
||||
# But NOT as an Azure OpenAI endpoint (different URL structure)
|
||||
assert llm.is_azure_openai_endpoint == False
|
||||
|
||||
|
||||
def test_azure_ai_services_endpoint():
|
||||
"""
|
||||
Test that Azure AI Services endpoints (services.ai.azure.com) are supported.
|
||||
"""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
with patch.dict(os.environ, {
|
||||
"AZURE_API_KEY": "test-key",
|
||||
"AZURE_ENDPOINT": "https://my-resource.services.ai.azure.com"
|
||||
}):
|
||||
llm = LLM(model="azure/gpt-4")
|
||||
|
||||
assert isinstance(llm, AzureCompletion)
|
||||
assert llm.endpoint == "https://my-resource.services.ai.azure.com"
|
||||
assert llm.is_azure_endpoint == True
|
||||
assert llm.is_azure_openai_endpoint == False
|
||||
|
||||
|
||||
def test_azure_generic_azure_com_endpoint():
|
||||
"""
|
||||
Test that generic .azure.com endpoints are supported (e.g., cservices.azure.com).
|
||||
This addresses the specific case from GitHub issue #4260.
|
||||
"""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
with patch.dict(os.environ, {
|
||||
"AZURE_API_KEY": "test-key",
|
||||
"AZURE_ENDPOINT": "https://my-resource.cservices.azure.com"
|
||||
}):
|
||||
llm = LLM(model="azure/gpt-4")
|
||||
|
||||
assert isinstance(llm, AzureCompletion)
|
||||
assert llm.endpoint == "https://my-resource.cservices.azure.com"
|
||||
assert llm.is_azure_endpoint == True
|
||||
assert llm.is_azure_openai_endpoint == False
|
||||
|
||||
|
||||
def test_azure_is_azure_endpoint_detection():
|
||||
"""
|
||||
Test the _is_azure_endpoint static method for various endpoint formats.
|
||||
"""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
# Azure OpenAI endpoints
|
||||
assert AzureCompletion._is_azure_endpoint("https://my-resource.openai.azure.com") == True
|
||||
assert AzureCompletion._is_azure_endpoint("https://my-resource.openai.azure.com/openai/deployments/gpt-4") == True
|
||||
|
||||
# Azure Cognitive Services endpoints
|
||||
assert AzureCompletion._is_azure_endpoint("https://my-resource.cognitiveservices.azure.com") == True
|
||||
|
||||
# Azure AI Services endpoints
|
||||
assert AzureCompletion._is_azure_endpoint("https://my-resource.services.ai.azure.com") == True
|
||||
|
||||
# Generic .azure.com endpoints (like cservices.azure.com from issue #4260)
|
||||
assert AzureCompletion._is_azure_endpoint("https://my-resource.cservices.azure.com") == True
|
||||
|
||||
# Azure AI Inference endpoint
|
||||
assert AzureCompletion._is_azure_endpoint("https://models.inference.ai.azure.com") == True
|
||||
|
||||
# Non-Azure endpoints should return False
|
||||
assert AzureCompletion._is_azure_endpoint("https://api.openai.com") == False
|
||||
assert AzureCompletion._is_azure_endpoint("https://example.com") == False
|
||||
|
||||
|
||||
def test_azure_base_url_parameter_support():
|
||||
"""
|
||||
Test that the base_url parameter is supported as an alias for endpoint.
|
||||
This provides consistency with other LLM providers.
|
||||
"""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
# Test with base_url parameter directly
|
||||
llm = AzureCompletion(
|
||||
model="gpt-4",
|
||||
api_key="test-key",
|
||||
base_url="https://my-resource.cognitiveservices.azure.com"
|
||||
)
|
||||
|
||||
assert llm.endpoint == "https://my-resource.cognitiveservices.azure.com"
|
||||
assert llm.is_azure_endpoint == True
|
||||
|
||||
|
||||
def test_azure_endpoint_takes_precedence_over_base_url():
|
||||
"""
|
||||
Test that explicit endpoint parameter takes precedence over base_url.
|
||||
"""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
llm = AzureCompletion(
|
||||
model="gpt-4",
|
||||
api_key="test-key",
|
||||
endpoint="https://explicit-endpoint.openai.azure.com",
|
||||
base_url="https://base-url-endpoint.cognitiveservices.azure.com"
|
||||
)
|
||||
|
||||
# endpoint should take precedence
|
||||
assert "explicit-endpoint.openai.azure.com" in llm.endpoint
|
||||
|
||||
|
||||
def test_azure_non_openai_endpoint_model_parameter_included():
|
||||
"""
|
||||
Test that model parameter IS included for non-Azure OpenAI endpoints.
|
||||
This is important for Cognitive Services and other Azure AI endpoints.
|
||||
"""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
with patch.dict(os.environ, {
|
||||
"AZURE_API_KEY": "test-key",
|
||||
"AZURE_ENDPOINT": "https://my-resource.cognitiveservices.azure.com"
|
||||
}):
|
||||
llm = LLM(model="azure/gpt-4")
|
||||
|
||||
params = llm._prepare_completion_params(
|
||||
messages=[{"role": "user", "content": "test"}]
|
||||
)
|
||||
|
||||
# Model parameter should be included for non-Azure OpenAI endpoints
|
||||
assert "model" in params
|
||||
assert params["model"] == "gpt-4"
|
||||
|
||||
|
||||
def test_azure_validate_and_fix_endpoint_only_modifies_openai_azure():
|
||||
"""
|
||||
Test that _validate_and_fix_endpoint only auto-constructs deployment path
|
||||
for openai.azure.com endpoints, not for other Azure endpoints.
|
||||
"""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
# Azure OpenAI endpoint should have deployment path auto-constructed
|
||||
result = AzureCompletion._validate_and_fix_endpoint(
|
||||
"https://my-resource.openai.azure.com",
|
||||
"gpt-4"
|
||||
)
|
||||
assert "/openai/deployments/gpt-4" in result
|
||||
|
||||
# Cognitive Services endpoint should NOT be modified
|
||||
result = AzureCompletion._validate_and_fix_endpoint(
|
||||
"https://my-resource.cognitiveservices.azure.com",
|
||||
"gpt-4"
|
||||
)
|
||||
assert result == "https://my-resource.cognitiveservices.azure.com"
|
||||
assert "/openai/deployments/" not in result
|
||||
|
||||
# Generic Azure endpoint should NOT be modified
|
||||
result = AzureCompletion._validate_and_fix_endpoint(
|
||||
"https://my-resource.cservices.azure.com",
|
||||
"gpt-4"
|
||||
)
|
||||
assert result == "https://my-resource.cservices.azure.com"
|
||||
assert "/openai/deployments/" not in result
|
||||
|
||||
@@ -737,33 +737,6 @@ def test_native_provider_falls_back_to_litellm_when_not_in_supported_list():
|
||||
assert llm.model == "groq/llama-3.1-70b-versatile"
|
||||
|
||||
|
||||
def test_litellm_fallback_raises_helpful_error_when_litellm_not_available():
|
||||
"""Test that when LiteLLM is not available, a helpful error message is raised."""
|
||||
with patch("crewai.llm.LITELLM_AVAILABLE", False):
|
||||
with patch("crewai.llm.SUPPORTED_NATIVE_PROVIDERS", ["openai", "anthropic"]):
|
||||
with pytest.raises(ImportError) as excinfo:
|
||||
LLM(model="groq/llama-3.1-70b-versatile", is_litellm=False)
|
||||
|
||||
error_message = str(excinfo.value)
|
||||
assert "groq/llama-3.1-70b-versatile" in error_message
|
||||
assert "LiteLLM" in error_message
|
||||
assert "pip install" in error_message
|
||||
assert "crewai[litellm]" in error_message
|
||||
|
||||
|
||||
def test_litellm_fallback_error_includes_model_name():
|
||||
"""Test that the LiteLLM fallback error includes the model name for clarity."""
|
||||
with patch("crewai.llm.LITELLM_AVAILABLE", False):
|
||||
with patch("crewai.llm.SUPPORTED_NATIVE_PROVIDERS", ["openai", "anthropic"]):
|
||||
test_model = "together/meta-llama/Llama-3-70b"
|
||||
with pytest.raises(ImportError) as excinfo:
|
||||
LLM(model=test_model, is_litellm=False)
|
||||
|
||||
error_message = str(excinfo.value)
|
||||
assert test_model in error_message
|
||||
assert "requires LiteLLM for inference" in error_message
|
||||
|
||||
|
||||
def test_prefixed_models_with_valid_constants_use_native_sdk():
|
||||
"""Test that models with native provider prefixes use native SDK when model is in constants."""
|
||||
# Test openai/ prefix with actual OpenAI model in constants → Native SDK
|
||||
@@ -1016,4 +989,4 @@ async def test_usage_info_streaming_with_acall():
|
||||
assert llm._token_usage["completion_tokens"] > 0
|
||||
assert llm._token_usage["total_tokens"] > 0
|
||||
|
||||
assert len(result) > 0
|
||||
assert len(result) > 0
|
||||
Reference in New Issue
Block a user