mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
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>
This commit is contained in:
parent
70fd2c19cf
commit
f48d837cd2
11 changed files with 558 additions and 7 deletions
|
|
@ -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
|
||||
|
|
|
|||
18
README.md
18
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
|
||||
|
|
|
|||
|
|
@ -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 *
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
77
packaging/litellm-core/pyproject.toml
Normal file
77
packaging/litellm-core/pyproject.toml
Normal file
|
|
@ -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/**",
|
||||
]
|
||||
86
scripts/build_core_distribution.py
Normal file
86
scripts/build_core_distribution.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
242
tests/base_sdk_tests/test_core_distribution.py
Normal file
242
tests/base_sdk_tests/test_core_distribution.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
40
tests/unit/test_version.py
Normal file
40
tests/unit/test_version.py
Normal file
|
|
@ -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"
|
||||
Loading…
Add table
Reference in a new issue