Files
crewAI/lib/crewai/tests/test_litellm_dependency_compat.py
Devin AI f6b09f4d5b Fix tomllib import for Python 3.10 compatibility
Use tomli (already a core dep) as fallback on Python <3.11 where
tomllib is not available.

Co-Authored-By: João <joao@crewai.com>
2026-06-09 17:19:34 +00:00

147 lines
5.9 KiB
Python

"""Tests for litellm optional dependency compatibility with crewAI core deps.
Regression tests for https://github.com/crewAIInc/crewAI/issues/6089:
the litellm extra must not pin versions whose transitive dependencies
conflict with crewAI's own requirements (openai, python-dotenv, etc.).
"""
import sys
from pathlib import Path
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
import pytest
from packaging.specifiers import SpecifierSet
from packaging.version import Version
PYPROJECT_PATH = Path(__file__).resolve().parents[1] / "pyproject.toml"
@pytest.fixture(scope="module")
def pyproject():
"""Load lib/crewai/pyproject.toml as a dict."""
with open(PYPROJECT_PATH, "rb") as f:
return tomllib.load(f)
def _parse_specifier(dep_string: str) -> tuple[str, SpecifierSet]:
"""Split 'pkg>=1.2,<3' into ('pkg', SpecifierSet('>=1.2,<3'))."""
for op in (">=", "<=", "~=", "==", "!=", ">", "<"):
idx = dep_string.find(op)
if idx != -1:
name = dep_string[:idx].strip()
spec = dep_string[idx:].split(";")[0].strip()
return name, SpecifierSet(spec)
return dep_string.strip(), SpecifierSet()
class TestLitellmDependencyBounds:
"""Verify the litellm optional-dependency range is resolvable."""
def test_litellm_extra_exists(self, pyproject):
"""The litellm optional dependency group must be defined."""
opt_deps = pyproject["project"]["optional-dependencies"]
assert "litellm" in opt_deps, "Missing 'litellm' optional-dependency group"
def test_litellm_lower_bound_at_least_1_84(self, pyproject):
"""litellm lower bound must be >= 1.84.0 to avoid transitive conflicts.
Versions < 1.84 pin openai and python-dotenv to ranges that are
incompatible with crewAI's core requirements.
"""
opt_deps = pyproject["project"]["optional-dependencies"]
litellm_deps = opt_deps["litellm"]
litellm_dep = [d for d in litellm_deps if d.startswith("litellm")]
assert litellm_dep, "No litellm dependency found in litellm extras"
_, spec = _parse_specifier(litellm_dep[0])
# 1.83.x versions have conflicting transitive deps; must be excluded
assert not spec.contains(Version("1.83.7")), (
"litellm 1.83.7 must be excluded (pins python-dotenv==1.0.1)"
)
assert not spec.contains(Version("1.83.8")), (
"litellm 1.83.8 must be excluded (pins openai==2.24.0)"
)
assert not spec.contains(Version("1.83.14")), (
"litellm 1.83.14 must be excluded (pins openai==2.24.0)"
)
# 1.84.0+ relaxes transitive pins to compatible bounds
assert spec.contains(Version("1.84.0")), (
"litellm 1.84.0 should be allowed (compatible transitive deps)"
)
def test_litellm_upper_bound_allows_recent_versions(self, pyproject):
"""litellm range must accept recent 1.x releases."""
opt_deps = pyproject["project"]["optional-dependencies"]
litellm_deps = opt_deps["litellm"]
litellm_dep = [d for d in litellm_deps if d.startswith("litellm")]
_, spec = _parse_specifier(litellm_dep[0])
# Ensure reasonable recent versions are included
assert spec.contains(Version("1.87.0")), (
"litellm 1.87.x should be allowed"
)
assert spec.contains(Version("1.90.0")), (
"litellm 1.90.x should be allowed"
)
def test_litellm_range_excludes_v2(self, pyproject):
"""litellm range must not include v2 (potential breaking changes)."""
opt_deps = pyproject["project"]["optional-dependencies"]
litellm_deps = opt_deps["litellm"]
litellm_dep = [d for d in litellm_deps if d.startswith("litellm")]
_, spec = _parse_specifier(litellm_dep[0])
assert not spec.contains(Version("2.0.0")), (
"litellm 2.x should be excluded to avoid breaking changes"
)
def test_core_openai_dep_compatible_with_litellm_range(self, pyproject):
"""crewAI's openai requirement must be satisfiable alongside litellm>=1.84.
litellm>=1.84.0 requires openai>=2.20.0,<3.0.0, which overlaps
with crewAI's openai>=2.30.0,<3.
"""
deps = pyproject["project"]["dependencies"]
openai_deps = [d for d in deps if d.startswith("openai")]
assert openai_deps, "openai must be a core dependency"
_, crewai_openai_spec = _parse_specifier(openai_deps[0])
# litellm>=1.84 allows openai>=2.20.0,<3.0.0
# crewAI requires openai>=2.30.0,<3
# The intersection should be non-empty
test_version = Version("2.30.0")
litellm_openai_spec = SpecifierSet(">=2.20.0,<3.0.0")
assert crewai_openai_spec.contains(test_version) and litellm_openai_spec.contains(test_version), (
"openai 2.30.0 must satisfy both crewAI and litellm>=1.84 requirements"
)
def test_core_python_dotenv_dep_compatible_with_litellm_range(self, pyproject):
"""crewAI's python-dotenv requirement must be satisfiable alongside litellm>=1.84.
litellm>=1.84.0 requires python-dotenv>=1.0.0,<2.0, which overlaps
with crewAI's python-dotenv>=1.2.2,<2.
"""
deps = pyproject["project"]["dependencies"]
dotenv_deps = [d for d in deps if d.startswith("python-dotenv")]
assert dotenv_deps, "python-dotenv must be a core dependency"
_, crewai_dotenv_spec = _parse_specifier(dotenv_deps[0])
# litellm>=1.84 allows python-dotenv>=1.0.0,<2.0
# crewAI requires python-dotenv>=1.2.2,<2
# The intersection should be non-empty
test_version = Version("1.2.2")
litellm_dotenv_spec = SpecifierSet(">=1.0.0,<2.0")
assert crewai_dotenv_spec.contains(test_version) and litellm_dotenv_spec.contains(test_version), (
"python-dotenv 1.2.2 must satisfy both crewAI and litellm>=1.84 requirements"
)