From f48d837cd21208771ee5b15770e4425a32907f1b Mon Sep 17 00:00:00 2001 From: joshua-berri Date: Thu, 8 Oct 2026 18:40:14 -0700 Subject: [PATCH] feat(sdk): build core from an independent packaging manifest (#44340) * feat(sdk): build core from an independent packaging manifest * test(sdk): compare rebuilt core payload without generated SBOM identity * fix(sdk): retain native build configuration in the core sdist * test(sdk): collect coverage from the core build entry point * fix(ci): isolate core packaging coverage configuration * fix(test): identify installed core metadata on Python 3.10 * fix(packaging): preserve Git ignore rules in core staging * fix(packaging): support source-only core builds * fix(packaging): reject overlapping SDK distributions * fix(packaging): align core requirements with current main --------- Co-authored-by: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com> --- .../test-mcp-dependency-resolution.yml | 59 +++++ README.md | 18 ++ litellm/__init__.py | 1 - litellm/_lazy_imports_registry.py | 2 + litellm/_version.py | 24 +- packaging/litellm-core/pyproject.toml | 77 ++++++ scripts/build_core_distribution.py | 86 +++++++ .../base_sdk_tests/check_base_sdk_install.py | 14 +- .../base_sdk_tests/test_core_distribution.py | 242 ++++++++++++++++++ tests/unit/test_lazy_imports.py | 2 +- tests/unit/test_version.py | 40 +++ 11 files changed, 558 insertions(+), 7 deletions(-) create mode 100644 packaging/litellm-core/pyproject.toml create mode 100644 scripts/build_core_distribution.py create mode 100644 tests/base_sdk_tests/test_core_distribution.py create mode 100644 tests/unit/test_version.py diff --git a/.github/workflows/test-mcp-dependency-resolution.yml b/.github/workflows/test-mcp-dependency-resolution.yml index 1772bdeabc6..7c88a8a579d 100644 --- a/.github/workflows/test-mcp-dependency-resolution.yml +++ b/.github/workflows/test-mcp-dependency-resolution.yml @@ -13,6 +13,9 @@ on: - "rust-toolchain.toml" - "litellm-rust/**" - "litellm/__init__.py" + - "litellm/_version.py" + - "litellm/_lazy_imports_registry.py" + - "scripts/build_core_distribution.py" - "litellm/proxy/proxy_server.py" - "litellm/**/*mcp*" - "litellm/**/*mcp*/**" @@ -115,3 +118,59 @@ jobs: fi (cd "$RUNNER_TEMP" && "$GITHUB_WORKSPACE/.venv-lowest-$extra/bin/python" "${checker[@]}") done + + core-distribution: + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + UV_PYTHON: ${{ matrix.python-version }} + LITELLM_LOCAL_MODEL_COST_MAP: "True" + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: ${{ matrix.python-version }} + - uses: ./.github/actions/setup-uv-with-retries + with: + version: "0.10.9" + - uses: ./.github/actions/cache-cargo-build + - name: Build the core wheel and source distribution + run: >- + uv run --no-project --with coverage==7.14.0 python -m coverage run --rcfile=/dev/null + --branch --include="*/scripts/build_core_distribution.py" + scripts/build_core_distribution.py --out-dir dist/core + - name: Verify metadata, resources, and independent source rebuild + env: + CORE_DISTRIBUTION_DIR: ${{ github.workspace }}/dist/core + run: >- + uv run --no-project --with pytest==9.0.3 --with pytest-cov==5.0.0 --with coverage==7.14.0 --with 'tomli==2.4.1; python_version < "3.11"' + python -m pytest tests/base_sdk_tests/test_core_distribution.py -v + --cov-config=/dev/null --cov=scripts.build_core_distribution --cov-branch --cov-append --cov-report= + - name: Verify isolated core installations + run: | + wheel=$(realpath dist/core/litellm_core-*.whl) + for resolution in highest lowest-direct; do + uv venv --python ${{ matrix.python-version }} ".venv-core-$resolution" + uv pip install --python ".venv-core-$resolution" --resolution "$resolution" "$wheel" + uv pip check --python ".venv-core-$resolution" + (cd "$RUNNER_TEMP" && "$GITHUB_WORKSPACE/.venv-core-$resolution/bin/python" -I "$GITHUB_WORKSPACE/tests/base_sdk_tests/check_base_sdk_install.py") + done + uv pip install --python .venv-core-highest coverage==7.14.0 + .venv-core-highest/bin/python -I -m coverage run --rcfile=/dev/null --append --branch --include="*/tests/base_sdk_tests/check_base_sdk_install.py" tests/base_sdk_tests/check_base_sdk_install.py + uv run --no-project --with coverage==7.14.0 python -m coverage xml --rcfile=/dev/null -o coverage-core.xml + - name: Upload core packaging coverage + uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5.5.5 + with: + use_oidc: true + files: coverage-core.xml + flags: core-packaging + fail_ci_if_error: false diff --git a/README.md b/README.md index ebf75e31729..141e8232de8 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,24 @@ Managing LLM calls across providers gets complicated fast — different SDKs, au uv add litellm ``` +An independent `litellm-core` distribution provides the Python SDK with the same +`import litellm` API and runtime dependencies. It has no optional extras, CLI entry +points, or bundled dashboard. Install one SDK distribution per environment because +`litellm` and `litellm-core` own overlapping Python files. Use `litellm` for the +proxy, CLI, and optional extras + +Build and install core from this checkout while its release integration is pending: + +```shell +python scripts/build_core_distribution.py --out-dir dist/core +python -m pip install dist/core/litellm_core-*.whl +``` + +The builder requires Git, `uv` and the Rust build toolchain. It reads the release version +from the root `pyproject.toml`, leaves source files unchanged, and produces a wheel +and a self-contained sdist in `dist/core`. Run installation in a fresh environment +without `litellm` + ```python from litellm import completion import os diff --git a/litellm/__init__.py b/litellm/__init__.py index 3f93166ecc8..a44af3ceac8 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -1421,7 +1421,6 @@ from .exceptions import ( ModelNotMappedError as ModelNotMappedError, ) from .budget_manager import BudgetManager -from .proxy.proxy_cli import run_server from .router import Router from .assistants.main import * from .batches.main import * diff --git a/litellm/_lazy_imports_registry.py b/litellm/_lazy_imports_registry.py index abefab95e9f..dd41e49d320 100644 --- a/litellm/_lazy_imports_registry.py +++ b/litellm/_lazy_imports_registry.py @@ -22,6 +22,7 @@ LITELLM_LOGGING_NAMES: Final = ( # Utils names that support lazy loading via _lazy_import_utils UTILS_NAMES: Final = ( + "run_server", "exception_type", "get_optional_params", "get_response_string", @@ -459,6 +460,7 @@ UTILS_MODULE_NAMES: Final = ( # Import maps for registry pattern - reduces repetition _UTILS_IMPORT_MAP: Final = { + "run_server": ("litellm.proxy.proxy_cli", "run_server"), "exception_type": (".utils", "exception_type"), "get_optional_params": (".utils", "get_optional_params"), "get_response_string": (".utils", "get_response_string"), diff --git a/litellm/_version.py b/litellm/_version.py index 2034cc4f332..140adcb66cf 100644 --- a/litellm/_version.py +++ b/litellm/_version.py @@ -1,6 +1,22 @@ +from typing import Final + import importlib_metadata -try: - version = importlib_metadata.version("litellm") -except Exception: - version = "unknown" + +def _installed_version(distribution: str) -> str | None: + try: + return importlib_metadata.version(distribution) + except Exception: + return None + + +_legacy_version: Final = _installed_version("litellm") +_core_version: Final = _installed_version("litellm-core") + +if _legacy_version is not None and _core_version is not None: + raise RuntimeError( + "litellm and litellm-core are both installed and share the litellm namespace. " + "Install them in separate environments." + ) + +version = _legacy_version or _core_version or "unknown" diff --git a/packaging/litellm-core/pyproject.toml b/packaging/litellm-core/pyproject.toml new file mode 100644 index 00000000000..d7702a02b51 --- /dev/null +++ b/packaging/litellm-core/pyproject.toml @@ -0,0 +1,77 @@ +[project] +name = "litellm-core" +version = "1.105.0" +description = "Library to easily interface with LLM API providers" +readme = "README.md" +requires-python = ">=3.10, <3.15" +license = "MIT" +license-files = ["LICENSE"] +authors = [ + { name = "BerriAI" }, +] +dependencies = [ + "fastuuid>=0.14.0,<1.0", + "filelock>=3.16.1,<4.0", + "httpx[http2]>=0.28.0,<1.0", + "openai>=2.20.0,<3.0.0", + "python-dotenv>=1.0.0,<2.0", + "pyyaml>=6.0.3,<7.0", + "packaging>=24.0", + "importlib-metadata>=8.0.0,<9.0", + "tiktoken>=0.8.0,<1.0; python_version < '3.14'", + "tiktoken>=0.12.0,<1.0; python_version >= '3.14'", + "tokenizers>=0.21.0,<1.0", + "huggingface-hub>=0.34.0,<2.0", + "click>=8.0.0,<9.0", + "jinja2>=3.1.6,<4.0", + "aiohttp>=3.14.2,<4.0", + "async-timeout>=4.0.3,<6.0; python_version < '3.11'", + "pydantic>=2.11.0,<3.0.0; python_version < '3.14'", + "pydantic>=2.12.0,<3.0.0; python_version >= '3.14'", + "pydantic-settings>=2.14.1,<3.0", + "jsonschema>=4.0.0,<5.0", + "boto3>=1.43.1,<2.0", + "typing-extensions>=4.13.0,<5.0", +] + +[project.urls] +Homepage = "https://litellm.ai" +Repository = "https://github.com/BerriAI/litellm" +Documentation = "https://docs.litellm.ai" + +[build-system] +requires = ["maturin==1.15.0"] +build-backend = "maturin" + +[tool.maturin] +manifest-path = "litellm-rust/crates/python-bridge/Cargo.toml" +module-name = "litellm.rust_bridge._native" +python-source = "." +bindings = "pyo3" +features = ["extension-module"] +profile = "release" +editable-profile = "dev" +include = [ + { path = "rust-toolchain.toml", format = "sdist" }, + { path = ".cargo/config.toml", format = "sdist" }, + "litellm/router_strategy/complexity_router/artifacts/*.json", + "litellm/router_strategy/complexity_router/fuse_presets.json", + "litellm/proxy/model_insights_tasks.json", + "litellm/proxy/common_utils/codex_base_instructions.md", + "litellm/proxy/common_utils/codex_bundled_models_0.159.3.json", + "litellm/proxy/lens/prompts/*.md", +] +exclude = [ + "litellm/proxy/_experimental/out", + "litellm/proxy/_experimental/out/**", + "litellm/proxy/enterprise", + "litellm/proxy/enterprise/**", + "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/guardrail_benchmarks", + "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/guardrail_benchmarks/**", + "**/__pycache__", + "**/__pycache__/**", + "**/.pytest_cache", + "**/.pytest_cache/**", + "**/.ruff_cache", + "**/.ruff_cache/**", +] diff --git a/scripts/build_core_distribution.py b/scripts/build_core_distribution.py new file mode 100644 index 00000000000..36ec20a9031 --- /dev/null +++ b/scripts/build_core_distribution.py @@ -0,0 +1,86 @@ +"""Build the independent core distribution from shared repository sources.""" + +import argparse +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import Final + +ROOT: Final = Path(__file__).resolve().parents[1] +SOURCES: Final = ("litellm", "litellm-rust", ".cargo", "rust-toolchain.toml", "README.md", "LICENSE") + + +def stage_core_distribution(source: Path, destination: Path) -> None: + version: Final = subprocess.run( + ["uv", "version", "--short"], cwd=source, check=True, capture_output=True, text=True + ).stdout.strip() + ignored: Final = frozenset( + source / name + for name in subprocess.run( + [ + "git", + "ls-files", + "--ignored", + "--cached", + "--others", + "--exclude-standard", + "--directory", + "-z", + "--", + *SOURCES, + ], + cwd=source, + check=True, + capture_output=True, + text=True, + ).stdout.split("\0") + if name + ) + + def ignored_sources(directory: str, names: list[str]) -> set[str]: + return {name for name in names if Path(directory) / name in ignored} | shutil.ignore_patterns( + "__pycache__", ".pytest_cache", ".ruff_cache", "target", ".git", "*.so", "*.pyd" + )(directory, names) + + destination.mkdir(parents=True, exist_ok=True) + for name in SOURCES: + path: Final = source / name + if path.is_dir(): + shutil.copytree( + path, + destination / name, + ignore=ignored_sources, + ) + else: + shutil.copy2(path, destination / name) + shutil.copy2(source / "packaging/litellm-core/pyproject.toml", destination / "pyproject.toml") + subprocess.run(["uv", "version", version, "--frozen"], cwd=destination, check=True) + + +def build_core_distribution(output: Path, *, sdist_only: bool = False) -> None: + output_path: Final = output.resolve() + with tempfile.TemporaryDirectory(prefix="litellm-core-") as temporary: + stage: Final = Path(temporary) + stage_core_distribution(ROOT, stage) + subprocess.run( + ["uv", "build", "--python", sys.executable, "--out-dir", str(output_path)] + + (["--sdist"] if sdist_only else []), + cwd=stage, + check=True, + ) + + +def main() -> None: + parser: Final = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out-dir", type=Path, default=ROOT / "dist/core") + parser.add_argument( + "--sdist-only", action="store_true", help="Build only the source archive, without compiling a wheel" + ) + args: Final = parser.parse_args() + build_core_distribution(Path(args.out_dir), sdist_only=args.sdist_only) + + +if __name__ == "__main__": + main() diff --git a/tests/base_sdk_tests/check_base_sdk_install.py b/tests/base_sdk_tests/check_base_sdk_install.py index f680ba92645..ad4e659c470 100644 --- a/tests/base_sdk_tests/check_base_sdk_install.py +++ b/tests/base_sdk_tests/check_base_sdk_install.py @@ -29,12 +29,24 @@ def check_environment_is_base_only() -> str: def check_import() -> str: + from importlib.metadata import distributions as installed_distributions from importlib.metadata import version import litellm _require(bool(litellm.__file__), "litellm has no __file__") - return f"imported litellm {version('litellm')}" + from litellm._version import version as sdk_version + + distributions = tuple( + distribution.metadata["Name"] + for distribution in installed_distributions() + if distribution.metadata["Name"] in ("litellm", "litellm-core") + ) + _require(len(distributions) == 1, f"expected one SDK distribution, found {distributions}") + distribution = distributions[0] + _require(sdk_version == version(distribution), "SDK version does not match installed metadata") + _require("litellm.proxy.proxy_cli" not in sys.modules, "SDK import loaded the proxy CLI") + return f"imported {distribution} {sdk_version}" def check_completion() -> str: diff --git a/tests/base_sdk_tests/test_core_distribution.py b/tests/base_sdk_tests/test_core_distribution.py new file mode 100644 index 00000000000..5bcb7120d7d --- /dev/null +++ b/tests/base_sdk_tests/test_core_distribution.py @@ -0,0 +1,242 @@ +import email +import os +import subprocess +import sys +import tarfile +import zipfile +from pathlib import Path +from typing import Final +from unittest.mock import patch + +import pytest +from packaging.requirements import Requirement + +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + + +ROOT: Final = Path(__file__).resolve().parents[2] + + +@pytest.mark.parametrize("sdist_only", [False, True]) +def test_build_command_selects_requested_distributions(tmp_path: Path, sdist_only: bool) -> None: + from scripts.build_core_distribution import main + + output: Final = tmp_path / "dist" + arguments: Final = ["build_core_distribution.py", "--out-dir", str(output)] + ( + ["--sdist-only"] if sdist_only else [] + ) + with ( + patch.object(sys, "argv", arguments), + patch("scripts.build_core_distribution.stage_core_distribution") as stage, + patch("scripts.build_core_distribution.subprocess.run") as run, + ): + main() + run.assert_called_once() + command: Final = run.call_args.args[0] + assert command[:2] == ["uv", "build"] + assert ("--sdist" in command) is sdist_only + assert "--wheel" not in command + assert command[command.index("--out-dir") + 1] == str(output) + assert run.call_args.kwargs["check"] is True + assert run.call_args.kwargs["cwd"] == stage.call_args.args[1] + assert not stage.call_args.args[1].exists() + + +@pytest.fixture +def source_repository(tmp_path: Path) -> Path: + source: Final = tmp_path / "source" + source.mkdir() + subprocess.run(["git", "init", "--quiet", str(source)], check=True) + return source + + +def test_core_manifest_preserves_runtime_dependencies_without_extras() -> None: + manifest: Final = ROOT / "packaging/litellm-core/pyproject.toml" + assert manifest.is_file(), "The core distribution needs its own build manifest" + core: Final = tomllib.loads(manifest.read_text()) + legacy: Final = tomllib.loads((ROOT / "pyproject.toml").read_text()) + assert core["project"]["name"] == "litellm-core" + assert core["project"]["dependencies"] == legacy["project"]["dependencies"] + assert not core["project"].get("optional-dependencies") + assert not core["project"].get("scripts") + + +def test_staging_stamps_release_version_without_modifying_sources(tmp_path: Path, source_repository: Path) -> None: + from scripts.build_core_distribution import SOURCES, stage_core_distribution + + source: Final = source_repository + for name in SOURCES: + path: Final = source / name + path.write_text(f"shared {name}") + (source / "pyproject.toml").write_text('[project]\nname = "litellm"\nversion = "9.8.7rc1"\n') + manifest: Final = source / "packaging/litellm-core/pyproject.toml" + manifest.parent.mkdir(parents=True) + manifest.write_bytes((ROOT / "packaging/litellm-core/pyproject.toml").read_bytes()) + original: Final = manifest.read_bytes() + stage: Final = tmp_path / "stage" + stage_core_distribution(source, stage) + assert tomllib.loads((stage / "pyproject.toml").read_text())["project"]["version"] == "9.8.7rc1" + assert manifest.read_bytes() == original + assert (source / "pyproject.toml").read_text() == '[project]\nname = "litellm"\nversion = "9.8.7rc1"\n' + assert all((stage / name).read_bytes() == (source / name).read_bytes() for name in SOURCES) + + +@pytest.fixture(scope="module") +def distribution_directory() -> Path: + directory: Final = os.environ.get("CORE_DISTRIBUTION_DIR") + if directory is None: + pytest.skip("CORE_DISTRIBUTION_DIR is supplied by the installed-distribution CI job") + return Path(directory) + + +@pytest.fixture(scope="module") +def distributions(distribution_directory: Path) -> tuple[Path, Path]: + wheels: Final = tuple(distribution_directory.glob("litellm_core-*.whl")) + sdists: Final = tuple(distribution_directory.glob("litellm_core-*.tar.gz")) + assert len(wheels) == len(sdists) == 1 + return wheels[0], sdists[0] + + +def test_core_wheel_metadata_and_resources(distributions: tuple[Path, Path]) -> None: + with zipfile.ZipFile(distributions[0]) as wheel: + names: Final = wheel.namelist() + metadata: Final = email.message_from_bytes( + wheel.read(next(n for n in names if n.endswith(".dist-info/METADATA"))) + ) + assert metadata["Name"] == "litellm-core" + assert metadata["Version"] == tomllib.loads((ROOT / "pyproject.toml").read_text())["project"]["version"] + assert not metadata.get_all("Provides-Extra") + assert not any(n.endswith(".dist-info/entry_points.txt") for n in names) + requirements: Final = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"]["dependencies"] + for python_version in ("3.10", "3.11", "3.12", "3.13", "3.14"): + environment: Final = {"python_version": python_version, "python_full_version": python_version + ".0"} + assert { + (requirement.name, requirement.specifier, frozenset(requirement.extras)) + for item in metadata.get_all("Requires-Dist", ()) + for requirement in (Requirement(item),) + if requirement.marker is None or requirement.marker.evaluate(environment) + } == { + (requirement.name, requirement.specifier, frozenset(requirement.extras)) + for item in requirements + for requirement in (Requirement(item),) + if requirement.marker is None or requirement.marker.evaluate(environment) + } + assert "litellm/model_prices_and_context_window_backup.json" in names + assert "litellm/router_strategy/complexity_router/fuse_presets.json" in names + assert "litellm/proxy/proxy_cli.py" in names + assert any(n.startswith("litellm/rust_bridge/_native.") and n.endswith((".so", ".pyd")) for n in names) + assert not any(n.startswith("litellm/proxy/_experimental/out/") for n in names) + + +def test_core_artifacts_exclude_local_configuration(distributions: tuple[Path, Path]) -> None: + configurations: Final = ( + "litellm/proxy/_new_secret_config.yaml", + "litellm/proxy/_new_new_secret_config.yaml", + "litellm/proxy/_super_secret_config.yaml", + ) + with zipfile.ZipFile(distributions[0]) as wheel, tarfile.open(distributions[1]) as archive: + for name in configurations: + assert name not in wheel.namelist(), f"Core wheel contains ignored configuration {name}" + assert not any(member.name.endswith(f"/{name}") for member in archive.getmembers()) + + +@pytest.mark.parametrize("relative_path", ["rust-toolchain.toml", ".cargo/config.toml"]) +def test_core_sdist_preserves_native_build_configuration(distribution_directory: Path, relative_path: str) -> None: + sdists: Final = tuple(distribution_directory.glob("litellm_core-*.tar.gz")) + assert len(sdists) == 1 + with tarfile.open(sdists[0]) as archive: + name: Final = f"{sdists[0].name.removesuffix('.tar.gz')}/{relative_path}" + assert name in archive.getnames(), f"Core source distribution is missing {relative_path}" + content: Final = archive.extractfile(name) + assert content is not None + assert content.read() == (ROOT / relative_path).read_bytes() + + +def test_core_sdist_rebuilds_without_repository(distributions: tuple[Path, Path], tmp_path: Path) -> None: + with tarfile.open(distributions[1]) as archive: + archive.extractall(tmp_path, filter="data") + source: Final = next(tmp_path.glob("litellm_core-*")) + assert not (source / "scripts/build_core_distribution.py").exists() + assert tomllib.loads((source / "pyproject.toml").read_text())["project"]["name"] == "litellm-core" + result: Final = subprocess.run( + ["uv", "build", "--python", sys.executable, "--wheel", "--out-dir", str(tmp_path / "rebuilt")], + cwd=source, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + rebuilt: Final = next((tmp_path / "rebuilt").glob("*.whl")) + with zipfile.ZipFile(distributions[0]) as original, zipfile.ZipFile(rebuilt) as wheel: + assert set(original.namelist()) == set(wheel.namelist()) + for name in original.namelist(): + if (name.startswith("litellm/") or name.endswith("/METADATA")) and not name.endswith((".so", ".pyd")): + assert original.read(name) == wheel.read(name), name + + +def test_staging_copies_sources_without_build_artifacts(tmp_path: Path, source_repository: Path) -> None: + from scripts.build_core_distribution import SOURCES, stage_core_distribution + + source: Final = source_repository + for name in SOURCES: + (source / name).mkdir() + (source / name / "shared.txt").write_text("source payload") + (source / name / "stale.so").write_text("old native extension") + (source / name / "__pycache__").mkdir() + (source / name / "__pycache__/old.pyc").write_bytes(b"stale bytecode") + (source / "pyproject.toml").write_text('[project]\nname = "litellm"\nversion = "1.2.3"\n') + manifest: Final = source / "packaging/litellm-core/pyproject.toml" + manifest.parent.mkdir(parents=True) + manifest.write_bytes((ROOT / "packaging/litellm-core/pyproject.toml").read_bytes()) + stage: Final = tmp_path / "stage" + stage_core_distribution(source, stage) + assert all((stage / name / "shared.txt").read_text() == "source payload" for name in SOURCES) + assert not tuple(stage.rglob("*.so")) + assert not tuple(stage.rglob("*.pyc")) + assert all((source / name / "stale.so").is_file() for name in SOURCES) + + +def test_staging_preserves_gitignore_rules(tmp_path: Path, source_repository: Path) -> None: + from scripts.build_core_distribution import SOURCES, stage_core_distribution + + source: Final = source_repository + for name in SOURCES: + (source / name).mkdir() + (source / name / "shared.txt").write_text("shared payload") + (source / "pyproject.toml").write_text('[project]\nname = "litellm"\nversion = "1.2.3"\n') + manifest: Final = source / "packaging/litellm-core/pyproject.toml" + manifest.parent.mkdir(parents=True) + manifest.write_bytes((ROOT / "packaging/litellm-core/pyproject.toml").read_bytes()) + (source / ".gitignore").write_text("*.cfg\n!keep.cfg\nbuild-artifacts/\n") + (source / "litellm/.gitignore").write_text(".env\n") + (source / ".git/info/exclude").write_text("litellm/private-local.json\n") + (source / "litellm/private-local.json").write_text("synthetic packaging canary") + for name in ("tracked.cfg", "local secret.cfg", "keep.cfg", ".env"): + (source / "litellm" / name).write_text("synthetic packaging canary") + (source / "litellm/build-artifacts").mkdir() + (source / "litellm/build-artifacts/local.txt").write_text("generated artifact") + subprocess.run(["git", "add", "--force", "litellm/tracked.cfg"], cwd=source, check=True) + stage: Final = tmp_path / "stage" + stage_core_distribution(source, stage) + assert (stage / "litellm/keep.cfg").read_text() == "synthetic packaging canary" + assert (stage / "litellm/shared.txt").read_text() == "shared payload" + assert not (stage / "litellm/tracked.cfg").exists() + assert not (stage / "litellm/local secret.cfg").exists() + assert not (stage / "litellm/.env").exists() + assert not (stage / "litellm/private-local.json").exists() + assert not (stage / "litellm/build-artifacts").exists() + assert (source / "litellm/tracked.cfg").is_file() + + +def test_staging_rejects_missing_release_version(tmp_path: Path) -> None: + from scripts.build_core_distribution import stage_core_distribution + + manifest: Final = tmp_path / "packaging/litellm-core/pyproject.toml" + manifest.parent.mkdir(parents=True) + manifest.write_bytes((ROOT / "packaging/litellm-core/pyproject.toml").read_bytes()) + (tmp_path / "pyproject.toml").write_text('[project]\nname = "litellm"\n') + with pytest.raises(subprocess.CalledProcessError): + stage_core_distribution(tmp_path, tmp_path / "stage") + assert not (tmp_path / "stage").exists() diff --git a/tests/unit/test_lazy_imports.py b/tests/unit/test_lazy_imports.py index 10986f8a140..0416020a9b5 100644 --- a/tests/unit/test_lazy_imports.py +++ b/tests/unit/test_lazy_imports.py @@ -47,7 +47,7 @@ def test_import_litellm_does_not_load_fastapi_or_bpe_table(): [ sys.executable, "-c", - "import sys, litellm; print(','.join(m for m in ('fastapi','starlette','litellm.litellm_core_utils.default_encoding') if m in sys.modules))", + "import sys, litellm; print(','.join(m for m in ('fastapi','starlette','litellm.proxy.proxy_cli','litellm.litellm_core_utils.default_encoding') if m in sys.modules))", ], check=True, capture_output=True, diff --git a/tests/unit/test_version.py b/tests/unit/test_version.py new file mode 100644 index 00000000000..30b1f259f1e --- /dev/null +++ b/tests/unit/test_version.py @@ -0,0 +1,40 @@ +import runpy +from pathlib import Path +from typing import Final +from unittest.mock import patch + +import importlib_metadata +import pytest + + +@pytest.mark.parametrize( + ("installed", "expected"), + [ + ({"litellm": "1.2.3"}, "1.2.3"), + ({"litellm-core": "2.3.4"}, "2.3.4"), + ({}, "unknown"), + ], +) +def test_version_uses_installed_distribution(installed: dict[str, str], expected: str) -> None: + def lookup(name: str) -> str: + if name not in installed: + raise importlib_metadata.PackageNotFoundError(name) + return installed[name] + + with patch("importlib_metadata.version", side_effect=lookup): + result: Final = runpy.run_path(str(Path(__file__).resolve().parents[2] / "litellm/_version.py")) + assert result["version"] == expected + + +@pytest.mark.parametrize("core_version", ["1.2.3", "2.3.4"]) +def test_version_rejects_overlapping_distributions(core_version: str) -> None: + installed: Final = {"litellm": "1.2.3", "litellm-core": core_version} + with patch("importlib_metadata.version", side_effect=installed.__getitem__): + with pytest.raises(RuntimeError, match=r"litellm and litellm-core.*separate environments"): + runpy.run_path(str(Path(__file__).resolve().parents[2] / "litellm/_version.py")) + + +def test_version_handles_unreadable_metadata() -> None: + with patch("importlib_metadata.version", side_effect=ValueError("Invalid metadata")): + result: Final = runpy.run_path(str(Path(__file__).resolve().parents[2] / "litellm/_version.py")) + assert result["version"] == "unknown"