litellm/scripts/build_core_distribution.py
yujonglee 79710a6ff1
feat(packaging): build litellm-proxy gateway wheels (#49605)
* feat(packaging): ship litellm-proxy wheels with the proxy extra

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(packaging): keep litellm-proxy out of the proxy extra until PyPI has it

The current release pipeline publishes litellm with whatever pyproject.toml lists, so pinning an unpublished package would break pip install litellm[proxy]. Images and CI still install the locally built gateway wheel

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:37:12 -08:00

106 lines
3.6 KiB
Python

"""Build standalone core or gateway distributions 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",
"model_prices_and_context_window.json",
"README.md",
"LICENSE",
)
IN_TREE_RUST_PATH: Final = "../../litellm-rust/"
class BuildOptions(argparse.Namespace):
package: str
out_dir: Path
sdist_only: bool
def stage_core_distribution(source: Path, destination: Path, *, package: str = "litellm-core") -> 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, path in ((name, source / name) for name in SOURCES if package == "litellm-core" or name != "litellm"):
if path.is_dir():
shutil.copytree(
path,
destination / name,
ignore=ignored_sources,
)
else:
shutil.copy2(path, destination / name)
template: Final = (source / "packaging" / package / "pyproject.toml").read_text()
if package == "litellm-proxy" and IN_TREE_RUST_PATH not in template:
raise SystemExit(f"packaging/{package}/pyproject.toml no longer points at {IN_TREE_RUST_PATH}")
metadata: Final = template.replace(IN_TREE_RUST_PATH, "litellm-rust/")
(destination / "pyproject.toml").write_text(metadata)
subprocess.run(["uv", "version", version, "--frozen"], cwd=destination, check=True)
def build_core_distribution(output: Path, *, sdist_only: bool = False, package: str = "litellm-core") -> None:
output_path: Final = output.resolve()
with tempfile.TemporaryDirectory(prefix="litellm-core-") as temporary:
stage: Final = Path(temporary)
stage_core_distribution(ROOT, stage, package=package)
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("--package", choices=("litellm-core", "litellm-proxy"), default="litellm-core")
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(namespace=BuildOptions())
build_core_distribution(Path(args.out_dir), sdist_only=args.sdist_only, package=args.package)
if __name__ == "__main__":
main()