mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-06 07:48:06 +00:00
363 lines
14 KiB
Python
363 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import zipfile
|
|
from collections.abc import Callable, Mapping, Sequence
|
|
from dataclasses import dataclass
|
|
from itertools import product
|
|
from pathlib import Path, PurePosixPath
|
|
from types import MappingProxyType, ModuleType
|
|
from typing import Final, Protocol
|
|
|
|
EXPECTED_PLATFORM_TAG: Final = "linux_x86_64"
|
|
REPO_ROOT: Final = Path(__file__).parents[2]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class WheelProfile:
|
|
distribution: str
|
|
python_tag: str
|
|
abi_tag: str
|
|
project_directory: Path
|
|
label: str
|
|
report_title: str
|
|
size_limit: int
|
|
is_payload: Callable[[str], bool]
|
|
is_extension: bool
|
|
|
|
|
|
PROFILES: Final = MappingProxyType(
|
|
{
|
|
"extension": WheelProfile(
|
|
distribution="litellm",
|
|
python_tag="cp310",
|
|
abi_tag="abi3",
|
|
project_directory=REPO_ROOT,
|
|
label="Native extension",
|
|
report_title="Native wheel build report",
|
|
size_limit=48_000_000,
|
|
is_payload=lambda name: name.startswith("litellm/rust_bridge/_native.") and name.endswith(".so"),
|
|
is_extension=True,
|
|
),
|
|
"gateway": WheelProfile(
|
|
distribution="litellm_gateway",
|
|
python_tag="py3",
|
|
abi_tag="none",
|
|
project_directory=REPO_ROOT / "litellm-rust" / "crates" / "gateway",
|
|
label="Gateway binary",
|
|
report_title="Gateway wheel build report",
|
|
size_limit=37_000_000,
|
|
is_payload=lambda name: name.endswith(".data/scripts/litellm-gateway"),
|
|
is_extension=False,
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Validation:
|
|
label: str
|
|
passed: bool
|
|
failure: str
|
|
|
|
|
|
class CommandRunner(Protocol):
|
|
def __call__(
|
|
self,
|
|
command: tuple[str, ...],
|
|
*,
|
|
check: bool,
|
|
capture_output: bool,
|
|
text: bool,
|
|
) -> subprocess.CompletedProcess[str]: ...
|
|
|
|
|
|
def _run_command(
|
|
command: tuple[str, ...],
|
|
*,
|
|
check: bool,
|
|
capture_output: bool,
|
|
text: bool,
|
|
) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(command, check=check, capture_output=capture_output, text=text)
|
|
|
|
|
|
def _dist_info_directory(member: zipfile.ZipInfo) -> str | None:
|
|
parts: Final = PurePosixPath(member.filename).parts
|
|
if not parts or not parts[0].endswith(".dist-info"):
|
|
return None
|
|
return parts[0]
|
|
|
|
|
|
def _wheel_metadata_tags(archive: zipfile.ZipFile, members: tuple[zipfile.ZipInfo, ...]) -> tuple[str, ...]:
|
|
if len(members) != 1:
|
|
return ()
|
|
lines: Final = archive.read(members[0]).splitlines()
|
|
return tuple(line.removeprefix(b"Tag:").strip().decode("ascii") for line in lines if line.startswith(b"Tag:"))
|
|
|
|
|
|
def _load_native_module(native_path: Path) -> ModuleType | None:
|
|
module_spec: Final = importlib.util.spec_from_file_location("litellm.rust_bridge._native", native_path)
|
|
if module_spec is None or module_spec.loader is None:
|
|
return None
|
|
try:
|
|
native_module: Final = importlib.util.module_from_spec(module_spec)
|
|
module_spec.loader.exec_module(native_module)
|
|
except Exception as error: # noqa: BLE001 # native module initialization can raise arbitrary exceptions
|
|
sys.stderr.write(f"native module load failed: {error}\n")
|
|
return None
|
|
return native_module
|
|
|
|
|
|
def _extension_validations(
|
|
wheel: Path,
|
|
native_path: Path,
|
|
native_bytes: bytes,
|
|
load_native_module: Callable[[Path], ModuleType | None],
|
|
run_command: CommandRunner,
|
|
) -> tuple[Validation, ...]:
|
|
with zipfile.ZipFile(wheel) as archive:
|
|
duplicated_vocabularies: Final = tuple(
|
|
member.filename
|
|
for member in archive.infolist()
|
|
if member.filename.startswith("litellm/litellm_core_utils/tokenizers/")
|
|
and re.fullmatch(r"[0-9a-f]{40}", PurePosixPath(member.filename).name)
|
|
and member.file_size > 0
|
|
and archive.read(member) in native_bytes
|
|
)
|
|
dynamic_symbols: Final = run_command(
|
|
("readelf", "--dyn-syms", "--wide", str(native_path)),
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
).stdout
|
|
native_module: Final = load_native_module(native_path)
|
|
return (
|
|
Validation(
|
|
"Tokenizer vocabularies are not duplicated in the native extension",
|
|
not duplicated_vocabularies,
|
|
f"native extension duplicates tokenizer vocabularies: {', '.join(duplicated_vocabularies)}",
|
|
),
|
|
Validation(
|
|
"Python extension entry point is present",
|
|
"PyInit__native" in dynamic_symbols,
|
|
"native extension does not export PyInit__native",
|
|
),
|
|
Validation("Native module loads", native_module is not None, "native module did not load"),
|
|
Validation(
|
|
"Production module omits the panic test hook",
|
|
native_module is None or not hasattr(native_module, "_panic_for_test"),
|
|
"production native module exposes _panic_for_test",
|
|
),
|
|
)
|
|
|
|
|
|
def _write_report(report: str, summary_path: str | None) -> None:
|
|
if summary_path is None:
|
|
sys.stdout.write(report + "\n")
|
|
return
|
|
with Path(summary_path).open("a") as summary:
|
|
summary.write(report + "\n")
|
|
|
|
|
|
def main(
|
|
argv: Sequence[str] | None = None,
|
|
environment: Mapping[str, str] | None = None,
|
|
load_native_module: Callable[[Path], ModuleType | None] = _load_native_module,
|
|
run_command: CommandRunner = _run_command,
|
|
) -> int:
|
|
arguments: Final = tuple(sys.argv if argv is None else argv)
|
|
resolved_environment: Final = os.environ if environment is None else environment
|
|
if len(arguments) != 3 or arguments[1] not in PROFILES:
|
|
sys.stderr.write("usage: verify_linux_native_wheel.py {extension|gateway} WHEEL\n")
|
|
return 2
|
|
|
|
profile: Final = PROFILES[arguments[1]]
|
|
wheel: Final = Path(arguments[2])
|
|
wheel_tags: Final = wheel.stem.rsplit("-", maxsplit=3)
|
|
if len(wheel_tags) != 4:
|
|
sys.stderr.write(f"cannot parse wheel tags from {wheel.name}\n")
|
|
return 1
|
|
|
|
wheel_identity: Final = wheel_tags[0].split("-")
|
|
if len(wheel_identity) != 2 or wheel_identity[0] != profile.distribution or not wheel_identity[1]:
|
|
sys.stderr.write(f"unexpected wheel identity: {wheel_tags[0]}\n")
|
|
return 1
|
|
|
|
expected_dist_info_directory: Final = f"{wheel_tags[0]}.dist-info"
|
|
expected_dist_info_directories: Final = frozenset((expected_dist_info_directory,))
|
|
python_tag: Final = wheel_tags[1]
|
|
abi_tag: Final = wheel_tags[2]
|
|
platform_tag: Final = wheel_tags[3]
|
|
expanded_filename_tags: Final = frozenset(
|
|
"-".join(tag) for tag in product(python_tag.split("."), abi_tag.split("."), platform_tag.split("."))
|
|
)
|
|
|
|
with zipfile.ZipFile(wheel) as archive:
|
|
wheel_members: Final = archive.infolist()
|
|
dist_info_directories: Final = frozenset(
|
|
directory for member in wheel_members if (directory := _dist_info_directory(member)) is not None
|
|
)
|
|
required_dist_info_files: Final = ("METADATA", "RECORD", "WHEEL")
|
|
dist_info_file_counts: Final = MappingProxyType(
|
|
{
|
|
filename: sum(
|
|
member.filename == f"{expected_dist_info_directory}/{filename}" for member in wheel_members
|
|
)
|
|
for filename in required_dist_info_files
|
|
}
|
|
)
|
|
wheel_metadata_members: Final = tuple(
|
|
member for member in wheel_members if member.filename == f"{expected_dist_info_directory}/WHEEL"
|
|
)
|
|
wheel_metadata_tags: Final = _wheel_metadata_tags(archive, wheel_metadata_members)
|
|
native_members: Final = tuple(member for member in wheel_members if profile.is_payload(member.filename))
|
|
if len(native_members) != 1:
|
|
sys.stderr.write(f"expected one {profile.label.lower()}, found {len(native_members)}\n")
|
|
return 1
|
|
|
|
unexpected_members: Final = tuple(
|
|
member.filename
|
|
for member in wheel_members
|
|
if member.filename.endswith((".pdb", ".dwp", ".rlib", ".rmeta", "Cargo.toml", "Cargo.lock"))
|
|
or any(part.endswith(".dSYM") for part in PurePosixPath(member.filename).parts)
|
|
)
|
|
native_member: Final = native_members[0]
|
|
uncompressed_wheel_size: Final = sum(member.file_size for member in wheel_members)
|
|
native_path: Final = wheel.parent / "native" / Path(native_member.filename).name
|
|
native_path.parent.mkdir(parents=True, exist_ok=True)
|
|
native_bytes: Final = archive.read(native_member)
|
|
native_path.write_bytes(native_bytes)
|
|
|
|
wheel_metadata_tags_match: Final = (
|
|
len(wheel_metadata_tags) == len(expanded_filename_tags)
|
|
and frozenset(wheel_metadata_tags) == expanded_filename_tags
|
|
)
|
|
commit_sha: Final = resolved_environment.get(
|
|
"RELEASE_WHEEL_COMMIT_SHA", resolved_environment.get("GITHUB_SHA", "unknown")
|
|
)
|
|
rustc_version: Final = run_command(
|
|
("rustc", "--version"),
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
).stdout.strip()
|
|
pyproject: Final = (profile.project_directory / "pyproject.toml").read_text()
|
|
maturin_match: Final = re.search(r'"maturin==([^";]+)', pyproject)
|
|
if maturin_match is None:
|
|
sys.stderr.write("build-system does not pin an exact Maturin version\n")
|
|
return 1
|
|
|
|
maturin_version: Final = maturin_match.group(1)
|
|
native_percentage: Final = native_member.file_size / uncompressed_wheel_size * 100
|
|
size_report: Final = "\n".join(
|
|
(
|
|
f"## {profile.report_title}",
|
|
"",
|
|
"| Build | Value |",
|
|
"| --- | --- |",
|
|
f"| Commit | `{commit_sha}` |",
|
|
f"| Platform | `{platform_tag}` |",
|
|
f"| Python ABI | `{python_tag}-{abi_tag}` |",
|
|
f"| Rust compiler | `{rustc_version}` |",
|
|
f"| Maturin | `{maturin_version}` |",
|
|
"| Cargo profile | `release` |",
|
|
"",
|
|
"| Artifact | Size |",
|
|
"| --- | ---: |",
|
|
f"| Compressed wheel | {wheel.stat().st_size / 1_000_000:.2f} MB |",
|
|
f"| Uncompressed wheel | {uncompressed_wheel_size / 1_000_000:.2f} MB |",
|
|
f"| {profile.label} | {native_member.file_size / 1_000_000:.2f} MB |",
|
|
f"| Native share | {native_percentage:.2f}% |",
|
|
"",
|
|
)
|
|
)
|
|
summary_path: Final = resolved_environment.get("GITHUB_STEP_SUMMARY")
|
|
_write_report(size_report, summary_path)
|
|
|
|
sections: Final = run_command(
|
|
("readelf", "--sections", "--wide", str(native_path)),
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
).stdout
|
|
debug_sections: Final = tuple(section for section in (".debug_", ".zdebug_") if section in sections)
|
|
validations: Final = (
|
|
Validation(
|
|
f"Python tag is {profile.python_tag}",
|
|
python_tag == profile.python_tag,
|
|
f"unexpected Python tag: expected {profile.python_tag}, found {python_tag}",
|
|
),
|
|
Validation(
|
|
f"ABI tag is {profile.abi_tag}",
|
|
abi_tag == profile.abi_tag,
|
|
f"unexpected ABI tag: expected {profile.abi_tag}, found {abi_tag}",
|
|
),
|
|
Validation(
|
|
f"Platform tag is {EXPECTED_PLATFORM_TAG}",
|
|
platform_tag == EXPECTED_PLATFORM_TAG,
|
|
f"unexpected platform tag: expected {EXPECTED_PLATFORM_TAG}, found {platform_tag}",
|
|
),
|
|
Validation(
|
|
"Wheel dist-info directory matches the filename",
|
|
dist_info_directories == expected_dist_info_directories,
|
|
f"unexpected dist-info directories: expected {expected_dist_info_directory}, "
|
|
f"found {', '.join(sorted(dist_info_directories))}",
|
|
),
|
|
Validation(
|
|
"Required dist-info files are present exactly once",
|
|
all(count == 1 for count in dist_info_file_counts.values()),
|
|
f"required dist-info file counts are invalid: {dist_info_file_counts}",
|
|
),
|
|
Validation(
|
|
"Wheel metadata tags match the filename",
|
|
wheel_metadata_tags_match,
|
|
f"WHEEL tags do not match filename: expected {', '.join(sorted(expanded_filename_tags))}, "
|
|
f"found {', '.join(sorted(wheel_metadata_tags))}",
|
|
),
|
|
Validation(
|
|
"Debug sections are absent",
|
|
not debug_sections,
|
|
f"{native_member.filename} contains debug sections: {', '.join(debug_sections)}",
|
|
),
|
|
Validation(
|
|
"Static symbol table is absent",
|
|
".symtab" not in sections,
|
|
f"{native_member.filename} contains a static symbol table",
|
|
),
|
|
Validation(
|
|
f"{profile.label} does not exceed {profile.size_limit / 1_000_000:.0f} MB",
|
|
native_member.file_size <= profile.size_limit,
|
|
f"{profile.label.lower()} exceeds {profile.size_limit / 1_000_000:.0f} MB: "
|
|
f"{native_member.file_size / 1_000_000:.2f} MB",
|
|
),
|
|
Validation(
|
|
"Wheel contents are valid",
|
|
not unexpected_members,
|
|
f"wheel contains unexpected build artifacts: {', '.join(unexpected_members)}",
|
|
),
|
|
) + (
|
|
_extension_validations(wheel, native_path, native_bytes, load_native_module, run_command)
|
|
if profile.is_extension
|
|
else ()
|
|
)
|
|
|
|
verified_report: Final = "\n".join(
|
|
("", "| Validation | Expected | Result |", "| --- | --- | :---: |")
|
|
+ tuple(f"| {row.label} | Yes | {'O' if row.passed else 'X'} |" for row in validations)
|
|
+ ("",)
|
|
)
|
|
_write_report(verified_report, summary_path)
|
|
|
|
validation_errors: Final = tuple(row.failure for row in validations if not row.passed)
|
|
sys.stderr.write("".join(f"{message}\n" for message in validation_errors))
|
|
|
|
return 0 if not validation_errors else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|