From 12d5c09b62348c18c275ec2d9a6a3208e6c5851d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:45:43 +0000 Subject: [PATCH 1/6] feat(proxy): package litellm-gateway binary --- .github/scripts/verify_linux_native_wheel.py | 76 ++++++++++++------- .github/workflows/test-rust.yml | 23 ++++++ Dockerfile | 1 + docker/Dockerfile.database | 1 + docker/Dockerfile.non_root | 1 + litellm-rust/pyproject.toml | 36 +++++++++ pyproject.toml | 4 +- .../test_verify_linux_native_wheel.py | 50 +++++++++--- uv.lock | 8 ++ 9 files changed, 161 insertions(+), 39 deletions(-) create mode 100644 litellm-rust/pyproject.toml diff --git a/.github/scripts/verify_linux_native_wheel.py b/.github/scripts/verify_linux_native_wheel.py index 4c4cb399dce..428326ea40a 100644 --- a/.github/scripts/verify_linux_native_wheel.py +++ b/.github/scripts/verify_linux_native_wheel.py @@ -73,10 +73,15 @@ def main( ) -> 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) != 2: - sys.stderr.write(f"usage: {Path(arguments[0]).name} WHEEL\n") + gateway: Final = len(arguments) == 3 and arguments[2] == "--gateway" + if len(arguments) != 2 and not gateway: + sys.stderr.write(f"usage: {Path(arguments[0]).name} WHEEL [--gateway]\n") return 2 + expected_distribution: Final = "litellm_gateway" if gateway else "litellm" + expected_python_tag: Final = "py3" if gateway else EXPECTED_PYTHON_TAG + expected_abi_tag: Final = "none" if gateway else EXPECTED_ABI_TAG + native_label: Final = "Gateway binary" if gateway else "Native extension" wheel: Final = Path(arguments[1]) wheel_tags: Final = wheel.stem.rsplit("-", maxsplit=3) if len(wheel_tags) != 4: @@ -84,7 +89,7 @@ def main( return 1 wheel_identity: Final = wheel_tags[0].split("-") - if len(wheel_identity) != 2 or wheel_identity[0] != "litellm" or not wheel_identity[1]: + if len(wheel_identity) != 2 or wheel_identity[0] != expected_distribution or not wheel_identity[1]: sys.stderr.write(f"unexpected wheel identity: {wheel_tags[0]}\n") return 1 @@ -118,10 +123,14 @@ def main( native_members: Final = tuple( member for member in wheel_members - if member.filename.startswith("litellm/rust_bridge/_native.") and member.filename.endswith(".so") + if ( + member.filename == f"{wheel_tags[0]}.data/scripts/litellm-gateway" + if gateway + else member.filename.startswith("litellm/rust_bridge/_native.") and member.filename.endswith(".so") + ) ) if len(native_members) != 1: - sys.stderr.write(f"expected one native extension, found {len(native_members)}\n") + sys.stderr.write(f"expected one {native_label.lower()}, found {len(native_members)}\n") return 1 unexpected_members: Final = tuple( @@ -158,7 +167,8 @@ def main( capture_output=True, text=True, ).stdout.strip() - pyproject: Final = (Path(__file__).parents[2] / "pyproject.toml").read_text() + project_directory: Final = Path(__file__).parents[2] / ("litellm-rust" if gateway else ".") + pyproject: Final = (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") @@ -168,7 +178,7 @@ def main( native_percentage: Final = native_member.file_size / uncompressed_wheel_size * 100 size_report: Final = "\n".join( ( - "## Native wheel build report", + "## Gateway wheel build report" if gateway else "## Native wheel build report", "", "| Build | Value |", "| --- | --- |", @@ -183,7 +193,7 @@ def main( "| --- | ---: |", f"| Compressed wheel | {wheel.stat().st_size / 1_000_000:.2f} MB |", f"| Uncompressed wheel | {uncompressed_wheel_size / 1_000_000:.2f} MB |", - f"| Native extension | {native_member.file_size / 1_000_000:.2f} MB |", + f"| {native_label} | {native_member.file_size / 1_000_000:.2f} MB |", f"| Native share | {native_percentage:.2f}% |", "", ) @@ -191,8 +201,6 @@ def main( summary_path: Final = resolved_environment.get("GITHUB_STEP_SUMMARY") if summary_path is None: sys.stdout.write(size_report) - else: - Path(summary_path).write_text(size_report) sections: Final = run_command( ("readelf", "--sections", "--wide", str(native_path)), @@ -204,21 +212,25 @@ def main( debug_sections_absent: Final = not debug_sections static_symbol_table_absent: Final = ".symtab" not in sections - dynamic_symbols: Final = run_command( - ("readelf", "--dyn-syms", "--wide", str(native_path)), - check=True, - capture_output=True, - text=True, - ).stdout + dynamic_symbols: Final = ( + "" + if gateway + else run_command( + ("readelf", "--dyn-syms", "--wide", str(native_path)), + check=True, + capture_output=True, + text=True, + ).stdout + ) extension_entry_point_present: Final = "PyInit__native" in dynamic_symbols - native_module: Final = load_native_module(native_path) + native_module: Final = None if gateway else load_native_module(native_path) native_module_loads: Final = native_module is not None panic_test_hook_absent: Final = native_module is not None and not hasattr(native_module, "_panic_for_test") native_size_limit: Final = 48_000_000 native_size_within_limit: Final = native_member.file_size <= native_size_limit validations: Final = ( - (f"Python tag is {EXPECTED_PYTHON_TAG}", python_tag == EXPECTED_PYTHON_TAG), - (f"ABI tag is {EXPECTED_ABI_TAG}", abi_tag == EXPECTED_ABI_TAG), + (f"Python tag is {expected_python_tag}", python_tag == expected_python_tag), + (f"ABI tag is {expected_abi_tag}", abi_tag == expected_abi_tag), (f"Platform tag is {EXPECTED_PLATFORM_TAG}", platform_tag == EXPECTED_PLATFORM_TAG), ("Wheel dist-info directory matches the filename", dist_info_directories == expected_dist_info_directories), ( @@ -228,12 +240,17 @@ def main( ("Wheel metadata tags match the filename", wheel_metadata_tags_match), ("Debug sections are absent", debug_sections_absent), ("Static symbol table is absent", static_symbol_table_absent), - ("Python extension entry point is present", extension_entry_point_present), - ("Native module loads", native_module_loads), - ("Production module omits the panic test hook", panic_test_hook_absent), - (f"Native extension does not exceed {native_size_limit / 1_000_000:.0f} MB", native_size_within_limit), + (f"{native_label} does not exceed {native_size_limit / 1_000_000:.0f} MB", native_size_within_limit), ("Tokenizer vocabularies are not duplicated in the native extension", not duplicated_vocabularies), ("Wheel contents are valid", not unexpected_members), + ) + ( + () + if gateway + else ( + ("Python extension entry point is present", extension_entry_point_present), + ("Native module loads", native_module_loads), + ("Production module omits the panic test hook", panic_test_hook_absent), + ) ) verified_report: Final = size_report + "\n".join( @@ -242,7 +259,8 @@ def main( + ("",) ) if summary_path is not None: - Path(summary_path).write_text(verified_report) + with Path(summary_path).open("a") as summary: + summary.write(verified_report + "\n") invalid_dist_info_files: Final = any(count != 1 for count in dist_info_file_counts.values()) validation_errors: Final = tuple( @@ -250,12 +268,12 @@ def main( for failed, message in ( (bool(debug_sections), f"{native_member.filename} contains debug sections: {', '.join(debug_sections)}"), (not static_symbol_table_absent, f"{native_member.filename} contains a static symbol table"), - (not extension_entry_point_present, "native extension does not export PyInit__native"), + (not gateway and not extension_entry_point_present, "native extension does not export PyInit__native"), ( - python_tag != EXPECTED_PYTHON_TAG, - f"unexpected Python tag: expected {EXPECTED_PYTHON_TAG}, found {python_tag}", + python_tag != expected_python_tag, + f"unexpected Python tag: expected {expected_python_tag}, found {python_tag}", ), - (abi_tag != EXPECTED_ABI_TAG, f"unexpected ABI tag: expected {EXPECTED_ABI_TAG}, found {abi_tag}"), + (abi_tag != expected_abi_tag, f"unexpected ABI tag: expected {expected_abi_tag}, found {abi_tag}"), ( platform_tag != EXPECTED_PLATFORM_TAG, f"unexpected platform tag: expected {EXPECTED_PLATFORM_TAG}, found {platform_tag}", @@ -277,7 +295,7 @@ def main( ), ( not native_size_within_limit, - f"native extension exceeds {native_size_limit / 1_000_000:.0f} MB: " + f"{native_label.lower()} exceeds {native_size_limit / 1_000_000:.0f} MB: " f"{native_member.file_size / 1_000_000:.2f} MB", ), (bool(unexpected_members), f"wheel contains unexpected build artifacts: {', '.join(unexpected_members)}"), diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml index 636a5031e5f..08ecb4577c1 100644 --- a/.github/workflows/test-rust.yml +++ b/.github/workflows/test-rust.yml @@ -195,6 +195,29 @@ jobs: - run: python tests/unit/rust_bridge/native_route_wheel_test.py dist/*.whl + - name: Build and install the gateway binary wheel + run: | + uv build --package litellm-gateway --wheel --out-dir gateway-dist + python .github/scripts/verify_linux_native_wheel.py gateway-dist/*.whl --gateway + uv venv .venv-gateway + uv pip install --python .venv-gateway gateway-dist/*.whl + + - name: Smoke test the installed gateway binary + run: | + printf 'model_list: []\n' > "$RUNNER_TEMP/gateway-config.yaml" + LITELLM_CONFIG="$RUNNER_TEMP/gateway-config.yaml" HOST=127.0.0.1 PORT=4010 \ + .venv-gateway/bin/litellm-gateway > "$RUNNER_TEMP/gateway.log" 2>&1 & + gateway_pid=$! + trap 'kill "$gateway_pid" 2>/dev/null || true; cat "$RUNNER_TEMP/gateway.log"' EXIT + for attempt in $(seq 1 30); do + if curl --fail --silent http://127.0.0.1:4010/health/readiness; then + exit 0 + fi + kill -0 "$gateway_pid" + sleep 1 + done + exit 1 + - name: Run pytest tests/test_litellm_rust with the compiled extension run: make test-rust-extension diff --git a/Dockerfile b/Dockerfile index 4cef63c8d71..326785fa120 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,6 +76,7 @@ ENV UV_PROJECT_ENVIRONMENT=/app/.venv \ COPY pyproject.toml uv.lock ./ COPY enterprise/pyproject.toml enterprise/ COPY litellm-proxy-extras/pyproject.toml litellm-proxy-extras/ +COPY litellm-rust/pyproject.toml litellm-rust/ # Install third-party dependencies (cached unless pyproject.toml/uv.lock change) RUN uv sync --frozen --no-install-project --no-install-workspace --no-default-groups --no-editable \ diff --git a/docker/Dockerfile.database b/docker/Dockerfile.database index 20f810686a3..e7fa577f7d3 100644 --- a/docker/Dockerfile.database +++ b/docker/Dockerfile.database @@ -74,6 +74,7 @@ ENV UV_PROJECT_ENVIRONMENT=/app/.venv \ COPY pyproject.toml uv.lock ./ COPY enterprise/pyproject.toml enterprise/ COPY litellm-proxy-extras/pyproject.toml litellm-proxy-extras/ +COPY litellm-rust/pyproject.toml litellm-rust/ # Install third-party dependencies (cached unless pyproject.toml/uv.lock change) RUN uv sync --frozen --no-install-project --no-install-workspace --no-default-groups --no-editable \ diff --git a/docker/Dockerfile.non_root b/docker/Dockerfile.non_root index 90bcc523d9e..c2ee8661504 100644 --- a/docker/Dockerfile.non_root +++ b/docker/Dockerfile.non_root @@ -77,6 +77,7 @@ ENV UV_PROJECT_ENVIRONMENT=/app/.venv \ COPY pyproject.toml uv.lock ./ COPY enterprise/pyproject.toml enterprise/ COPY litellm-proxy-extras/pyproject.toml litellm-proxy-extras/ +COPY litellm-rust/pyproject.toml litellm-rust/ # Install third-party dependencies (cached unless pyproject.toml/uv.lock change) RUN --mount=type=cache,target=/app/.cache/uv,id=litellm-uv-cache \ diff --git a/litellm-rust/pyproject.toml b/litellm-rust/pyproject.toml new file mode 100644 index 00000000000..ebb1fef81fe --- /dev/null +++ b/litellm-rust/pyproject.toml @@ -0,0 +1,36 @@ +[project] +name = "litellm-gateway" +version = "0.1.0" +description = "Native gateway binary for the LiteLLM Proxy" +requires-python = ">=3.10, <3.16" +license = "MIT" + +[project.urls] +Repository = "https://github.com/BerriAI/litellm" + +[build-system] +requires = ["maturin==1.15.0"] +build-backend = "maturin" + +[tool.maturin] +manifest-path = "crates/gateway/Cargo.toml" +bindings = "bin" +profile = "release" +strip = true + +[tool.uv] +cache-keys = [ + { file = "pyproject.toml" }, + { file = "Cargo.toml" }, + { file = "Cargo.lock" }, + { file = "crates/**/*" }, + { file = "../rust-toolchain.toml" }, + { file = "../.cargo/config.toml" }, +] + +[tool.commitizen] +version = "0.1.0" +version_files = [ + "pyproject.toml:^version", + "../pyproject.toml:litellm-gateway==", +] diff --git a/pyproject.toml b/pyproject.toml index fc860a8fdf4..495954312f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,7 @@ proxy = [ "httpx2>=2.10.0,<3", "pydantic>=2.12.0,<3", "litellm-proxy-extras==0.4.111", + "litellm-gateway==0.1.0", "litellm-enterprise==0.1.79", "RestrictedPython>=8.5,<9.0", "rich>=13.9.4,<14.0", @@ -383,9 +384,10 @@ brotli = "2026-10-22T14:01:00Z" [tool.uv.sources] litellm-proxy-extras = { workspace = true } litellm-enterprise = { workspace = true } +litellm-gateway = { workspace = true } [tool.uv.workspace] -members = ["enterprise", "litellm-proxy-extras"] +members = ["enterprise", "litellm-proxy-extras", "litellm-rust"] [tool.commitizen] version = "1.108.0" diff --git a/tests/unit/rust_bridge/test_verify_linux_native_wheel.py b/tests/unit/rust_bridge/test_verify_linux_native_wheel.py index 0eae041f535..0373f9f25dc 100644 --- a/tests/unit/rust_bridge/test_verify_linux_native_wheel.py +++ b/tests/unit/rust_bridge/test_verify_linux_native_wheel.py @@ -57,26 +57,30 @@ def _write_wheel( dist_info: str = _DIST_INFO, duplicate_wheel: bool = False, native_bytes: bytes = b"synthetic native extension", + gateway: bool = False, ) -> Path: - wheel: Final = tmp_path / f"litellm-1.100.0-{filename_tag}.whl" + distribution: Final = "litellm_gateway" if gateway else "litellm" + metadata_directory: Final = f"{distribution}-1.100.0.dist-info" if gateway else dist_info + native_member: Final = f"{distribution}-1.100.0.data/scripts/litellm-gateway" if gateway else _NATIVE_MEMBER + wheel: Final = tmp_path / f"{distribution}-1.100.0-{filename_tag}.whl" with zipfile.ZipFile(wheel, "w", compression=zipfile.ZIP_DEFLATED) as archive: - archive.writestr(_NATIVE_MEMBER, native_bytes) + archive.writestr(native_member, native_bytes) archive.writestr( - f"{dist_info}/METADATA", - "Metadata-Version: 2.1\nName: litellm\nVersion: 1.100.0\n", + f"{metadata_directory}/METADATA", + f"Metadata-Version: 2.1\nName: {distribution}\nVersion: 1.100.0\n", ) archive.writestr( - f"{dist_info}/RECORD", - f"{_NATIVE_MEMBER},,\n{dist_info}/WHEEL,,\n", + f"{metadata_directory}/RECORD", + f"{native_member},,\n{metadata_directory}/WHEEL,,\n", ) if metadata_tags is not None: wheel_metadata: Final = ( "Wheel-Version: 1.0\nGenerator: regression-test\nRoot-Is-Purelib: false\n" + "".join(f"Tag: {tag}\n" for tag in metadata_tags) ) - archive.writestr(f"{dist_info}/WHEEL", wheel_metadata) + archive.writestr(f"{metadata_directory}/WHEEL", wheel_metadata) if duplicate_wheel: - archive.writestr(f"{dist_info}/WHEEL", wheel_metadata) + archive.writestr(f"{metadata_directory}/WHEEL", wheel_metadata) return wheel @@ -106,6 +110,7 @@ def _run_verifier( wheel: Path, *, exposes_panic: bool = False, + gateway: bool = False, ) -> int: native_module: Final = ( _NativeModuleWithPanicHook("litellm.rust_bridge._native") @@ -114,11 +119,12 @@ def _run_verifier( ) def _fake_load_native_module(_: Path) -> ModuleType: + assert not gateway, "A standalone binary must not be imported as a Python extension" return native_module environment: Final = MappingProxyType({"GITHUB_STEP_SUMMARY": str(wheel.parent / "summary.md")}) return verifier.main( - (str(_MODULE_PATH), str(wheel)), + (str(_MODULE_PATH), str(wheel)) + (("--gateway",) if gateway else ()), environment, _fake_load_native_module, _fake_subprocess_run, @@ -210,3 +216,29 @@ def test_vocabulary_is_packaged_once(tmp_path: Path, embedded: bool) -> None: archive.writestr("litellm/litellm_core_utils/tokenizers/" + "a" * 40, ranks) assert _run_verifier(wheel) == (1 if embedded else 0) + + +@pytest.mark.parametrize("gateway", (False, True), ids=("extension", "gateway")) +@pytest.mark.parametrize("size", (48_000_000, 48_000_001), ids=("at-limit", "over-limit")) +def test_enforces_uncompressed_native_size_limit(tmp_path: Path, gateway: bool, size: int) -> None: + tag: Final = "py3-none-linux_x86_64" if gateway else _EXPECTED_TAG + wheel: Final = _write_wheel( + tmp_path, filename_tag=tag, metadata_tags=(tag,), native_bytes=b"x" * size, gateway=gateway + ) + + assert _run_verifier(wheel, gateway=gateway) == (0 if size == 48_000_000 else 1) + summary: Final = (tmp_path / "summary.md").read_text() + assert f"| {'Gateway binary' if gateway else 'Native extension'} | 48.00 MB |" in summary + + +def test_keeps_both_artifact_reports_in_summary(tmp_path: Path) -> None: + extension: Final = _write_wheel(tmp_path, filename_tag=_EXPECTED_TAG) + gateway: Final = _write_wheel( + tmp_path, filename_tag="py3-none-linux_x86_64", metadata_tags=("py3-none-linux_x86_64",), gateway=True + ) + + assert _run_verifier(extension) == 0 + assert _run_verifier(gateway, gateway=True) == 0 + summary: Final = (tmp_path / "summary.md").read_text() + assert "## Native wheel build report" in summary + assert "## Gateway wheel build report" in summary diff --git a/uv.lock b/uv.lock index f86614046e1..5518fdbcf4f 100644 --- a/uv.lock +++ b/uv.lock @@ -21,6 +21,7 @@ brotli = "2026-10-22T14:01:00Z" members = [ "litellm", "litellm-enterprise", + "litellm-gateway", "litellm-proxy-extras", ] constraints = [ @@ -5002,6 +5003,7 @@ proxy = [ { name = "httpx2" }, { name = "inquirerpy" }, { name = "litellm-enterprise" }, + { name = "litellm-gateway" }, { name = "litellm-proxy-extras" }, { name = "mcp" }, { name = "orjson" }, @@ -5233,6 +5235,7 @@ requires-dist = [ { name = "keyring", marker = "extra == 'cli'", specifier = ">=25.6.0,<26.0" }, { name = "langfuse", marker = "extra == 'proxy-runtime'", specifier = ">=4.7,<5.0" }, { name = "litellm-enterprise", marker = "extra == 'proxy'", editable = "enterprise" }, + { name = "litellm-gateway", marker = "extra == 'proxy'", editable = "litellm-rust" }, { name = "litellm-proxy-extras", marker = "extra == 'proxy'", editable = "litellm-proxy-extras" }, { name = "llm-sandbox", marker = "extra == 'proxy-runtime'", specifier = ">=0.3.39,<1.0" }, { name = "mangum", marker = "extra == 'proxy-runtime'", specifier = ">=0.17.0,<1.0" }, @@ -5432,6 +5435,11 @@ name = "litellm-enterprise" version = "0.1.79" source = { editable = "enterprise" } +[[package]] +name = "litellm-gateway" +version = "0.1.0" +source = { editable = "litellm-rust" } + [[package]] name = "litellm-proxy-extras" version = "0.4.111" From 481da809c6b09662f872afdcb289de4910113925 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:21:31 +0000 Subject: [PATCH 2/6] fix(ci): avoid repeated gateway builds --- .github/workflows/test-linting.yml | 4 ++-- .github/workflows/test-merge-smoke.yml | 8 +++++++- .github/workflows/test-unit.yml | 11 ++++++----- docker/Dockerfile.database | 1 + 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-linting.yml b/.github/workflows/test-linting.yml index b9f8d7e7dfb..aed2f8cee34 100644 --- a/.github/workflows/test-linting.yml +++ b/.github/workflows/test-linting.yml @@ -171,7 +171,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - uv sync --frozen --group proxy-dev --group e2e-dev --extra proxy + uv sync --frozen --group proxy-dev --group e2e-dev --extra proxy --no-install-package litellm-gateway uv run --no-sync python scripts/openapi_docs_gate.py --base "$GATE_BASE_SHA" - name: Print OpenAI version @@ -516,7 +516,7 @@ jobs: uses: ./.github/actions/cache-cargo-build - name: Install dependencies - run: uv sync --frozen --all-groups --all-extras + run: uv sync --frozen --all-groups --all-extras --no-install-package litellm-gateway - name: check_licenses run: uv run --no-sync python ./tests/code_coverage_tests/check_licenses.py diff --git a/.github/workflows/test-merge-smoke.yml b/.github/workflows/test-merge-smoke.yml index f3f09523491..7519039507f 100644 --- a/.github/workflows/test-merge-smoke.yml +++ b/.github/workflows/test-merge-smoke.yml @@ -52,6 +52,9 @@ jobs: with: version: "0.10.9" + - name: Set up Rust before concurrent package builds + run: rustup toolchain install --no-self-update + - name: Install dependencies run: .github/scripts/uv_sync_with_retries.sh --frozen --extra proxy --extra cli --group dev --group proxy-dev --python ${{ matrix.python-version }} @@ -100,7 +103,7 @@ jobs: all-extras: name: all-extras-py${{ matrix.python-version }} runs-on: ubuntu-24.04 - timeout-minutes: 10 + timeout-minutes: 20 strategy: fail-fast: false matrix: @@ -123,6 +126,9 @@ jobs: with: version: "0.10.9" + - name: Set up Rust before concurrent package builds + run: rustup toolchain install --no-self-update + - name: Install every extra and dependency group run: .github/scripts/uv_sync_with_retries.sh --frozen --all-groups --all-extras --python ${{ matrix.python-version }} diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index 4d47a882891..d6756a96796 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -42,7 +42,7 @@ jobs: id: rust-bridge-key if: steps.changes.outputs.decision != 'skip' env: - RUST_BRIDGE_CACHE_KEY: ${{ runner.os }}-rust-bridge-editable-${{ hashFiles('litellm-rust/Cargo.lock', 'litellm-rust/Cargo.toml', 'litellm-rust/crates/**', 'pyproject.toml') }} + RUST_BRIDGE_CACHE_KEY: ${{ runner.os }}-rust-bridge-editable-${{ hashFiles('litellm-rust/Cargo.lock', 'litellm-rust/Cargo.toml', 'litellm-rust/pyproject.toml', 'litellm-rust/crates/**', 'pyproject.toml') }} run: echo "key=$RUST_BRIDGE_CACHE_KEY" >> "$GITHUB_OUTPUT" - name: Set up Python @@ -73,15 +73,16 @@ jobs: timeout-minutes: 5 uses: ./.github/actions/cache-cargo-build - - name: Build editable Rust bridge + - name: Build editable Rust bridge and gateway wheel if: steps.changes.outputs.decision != 'skip' && steps.rust-bridge-cache.outputs.cache-hit != 'true' - timeout-minutes: 15 + timeout-minutes: 20 run: | mkdir -p rust-bridge-dist uv run --no-project --with maturin==1.15.0 python -c 'import maturin; maturin.build_editable("rust-bridge-dist")' strip --strip-debug litellm/rust_bridge/_native.abi3.so mkdir -p rust-bridge-dist/litellm/rust_bridge cp litellm/rust_bridge/_native.abi3.so rust-bridge-dist/litellm/rust_bridge/_native.abi3.so + uv build --package litellm-gateway --wheel --out-dir rust-bridge-dist - name: Upload Rust bridge artifact if: steps.changes.outputs.decision != 'skip' @@ -1000,7 +1001,7 @@ jobs: if [ -z "$RUST_BRIDGE_ARTIFACT" ]; then .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils else - .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils --no-install-project + .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils --no-install-project --no-install-package litellm-gateway uv pip install --no-deps --python .venv/bin/python rust-bridge-dist/*.whl cp rust-bridge-dist/litellm/rust_bridge/_native.abi3.so litellm/rust_bridge/_native.abi3.so uv run --no-sync python -c "import importlib.metadata; import litellm.rust_bridge._native; print(importlib.metadata.version('litellm'))" @@ -1438,7 +1439,7 @@ jobs: if [ -z "$RUST_BRIDGE_ARTIFACT" ]; then .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils else - .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils --no-install-project + .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils --no-install-project --no-install-package litellm-gateway uv pip install --no-deps --python .venv/bin/python rust-bridge-dist/*.whl cp rust-bridge-dist/litellm/rust_bridge/_native.abi3.so litellm/rust_bridge/_native.abi3.so fi diff --git a/docker/Dockerfile.database b/docker/Dockerfile.database index e7fa577f7d3..047d146d93b 100644 --- a/docker/Dockerfile.database +++ b/docker/Dockerfile.database @@ -58,6 +58,7 @@ RUN apk add --no-cache \ gcc \ python-3.13 \ python-3.13-dev \ + rust \ openssl \ libssl3 \ openssl-dev \ From a029bf8fe4924784b64b8803308d3bd2b7dcbc8c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:26:35 +0000 Subject: [PATCH 3/6] fix(ci): authorize gateway MIT license --- tests/code_coverage_tests/liccheck.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/code_coverage_tests/liccheck.ini b/tests/code_coverage_tests/liccheck.ini index cd951c6cd28..7d4ae5381c5 100644 --- a/tests/code_coverage_tests/liccheck.ini +++ b/tests/code_coverage_tests/liccheck.ini @@ -89,6 +89,7 @@ importlib-metadata: >=6.8.0 # Apache 2.0 License tokenizers: >=0.20.2 # Apache 2.0 License jinja2: >=3.1.4 # BSD 3-Clause License litellm-proxy-extras: >=0.1.1 # MIT License +litellm-gateway: ==0.1.0 # MIT, verified in LICENSE and litellm-rust/Cargo.toml litellm-enterprise: >=0.1.1 # LiteLLM Enterprise License a2a-sdk: >=0.3.22 # Apache 2.0 license anyio: >=4.5.0 # Unknown license From b63f384a7762b0138b85a26c0918f43cf041bbde Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:53:37 +0000 Subject: [PATCH 4/6] fix(proxy): move gateway manifest into crate --- .github/scripts/verify_linux_native_wheel.py | 4 +++- .github/workflows/test-unit.yml | 2 +- Dockerfile | 2 +- docker/Dockerfile.database | 2 +- docker/Dockerfile.non_root | 2 +- litellm-rust/{ => crates/gateway}/pyproject.toml | 13 +++++++------ pyproject.toml | 2 +- uv.lock | 4 ++-- 8 files changed, 17 insertions(+), 14 deletions(-) rename litellm-rust/{ => crates/gateway}/pyproject.toml (67%) diff --git a/.github/scripts/verify_linux_native_wheel.py b/.github/scripts/verify_linux_native_wheel.py index 428326ea40a..83111b95ca5 100644 --- a/.github/scripts/verify_linux_native_wheel.py +++ b/.github/scripts/verify_linux_native_wheel.py @@ -167,7 +167,9 @@ def main( capture_output=True, text=True, ).stdout.strip() - project_directory: Final = Path(__file__).parents[2] / ("litellm-rust" if gateway else ".") + project_directory: Final = Path(__file__).parents[2] / ( + "litellm-rust/crates/gateway" if gateway else "." + ) pyproject: Final = (project_directory / "pyproject.toml").read_text() maturin_match: Final = re.search(r'"maturin==([^";]+)', pyproject) if maturin_match is None: diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index d6756a96796..b91ce565c97 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -42,7 +42,7 @@ jobs: id: rust-bridge-key if: steps.changes.outputs.decision != 'skip' env: - RUST_BRIDGE_CACHE_KEY: ${{ runner.os }}-rust-bridge-editable-${{ hashFiles('litellm-rust/Cargo.lock', 'litellm-rust/Cargo.toml', 'litellm-rust/pyproject.toml', 'litellm-rust/crates/**', 'pyproject.toml') }} + RUST_BRIDGE_CACHE_KEY: ${{ runner.os }}-rust-bridge-editable-${{ hashFiles('litellm-rust/Cargo.lock', 'litellm-rust/Cargo.toml', 'litellm-rust/crates/gateway/pyproject.toml', 'litellm-rust/crates/**', 'pyproject.toml') }} run: echo "key=$RUST_BRIDGE_CACHE_KEY" >> "$GITHUB_OUTPUT" - name: Set up Python diff --git a/Dockerfile b/Dockerfile index 326785fa120..fe18b6ba71d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,7 +76,7 @@ ENV UV_PROJECT_ENVIRONMENT=/app/.venv \ COPY pyproject.toml uv.lock ./ COPY enterprise/pyproject.toml enterprise/ COPY litellm-proxy-extras/pyproject.toml litellm-proxy-extras/ -COPY litellm-rust/pyproject.toml litellm-rust/ +COPY litellm-rust/crates/gateway/pyproject.toml litellm-rust/crates/gateway/ # Install third-party dependencies (cached unless pyproject.toml/uv.lock change) RUN uv sync --frozen --no-install-project --no-install-workspace --no-default-groups --no-editable \ diff --git a/docker/Dockerfile.database b/docker/Dockerfile.database index 047d146d93b..5a535f9e023 100644 --- a/docker/Dockerfile.database +++ b/docker/Dockerfile.database @@ -75,7 +75,7 @@ ENV UV_PROJECT_ENVIRONMENT=/app/.venv \ COPY pyproject.toml uv.lock ./ COPY enterprise/pyproject.toml enterprise/ COPY litellm-proxy-extras/pyproject.toml litellm-proxy-extras/ -COPY litellm-rust/pyproject.toml litellm-rust/ +COPY litellm-rust/crates/gateway/pyproject.toml litellm-rust/crates/gateway/ # Install third-party dependencies (cached unless pyproject.toml/uv.lock change) RUN uv sync --frozen --no-install-project --no-install-workspace --no-default-groups --no-editable \ diff --git a/docker/Dockerfile.non_root b/docker/Dockerfile.non_root index c2ee8661504..dc69e3d8073 100644 --- a/docker/Dockerfile.non_root +++ b/docker/Dockerfile.non_root @@ -77,7 +77,7 @@ ENV UV_PROJECT_ENVIRONMENT=/app/.venv \ COPY pyproject.toml uv.lock ./ COPY enterprise/pyproject.toml enterprise/ COPY litellm-proxy-extras/pyproject.toml litellm-proxy-extras/ -COPY litellm-rust/pyproject.toml litellm-rust/ +COPY litellm-rust/crates/gateway/pyproject.toml litellm-rust/crates/gateway/ # Install third-party dependencies (cached unless pyproject.toml/uv.lock change) RUN --mount=type=cache,target=/app/.cache/uv,id=litellm-uv-cache \ diff --git a/litellm-rust/pyproject.toml b/litellm-rust/crates/gateway/pyproject.toml similarity index 67% rename from litellm-rust/pyproject.toml rename to litellm-rust/crates/gateway/pyproject.toml index ebb1fef81fe..cb7301834cd 100644 --- a/litellm-rust/pyproject.toml +++ b/litellm-rust/crates/gateway/pyproject.toml @@ -13,7 +13,7 @@ requires = ["maturin==1.15.0"] build-backend = "maturin" [tool.maturin] -manifest-path = "crates/gateway/Cargo.toml" +manifest-path = "Cargo.toml" bindings = "bin" profile = "release" strip = true @@ -22,15 +22,16 @@ strip = true cache-keys = [ { file = "pyproject.toml" }, { file = "Cargo.toml" }, - { file = "Cargo.lock" }, - { file = "crates/**/*" }, - { file = "../rust-toolchain.toml" }, - { file = "../.cargo/config.toml" }, + { file = "../../Cargo.toml" }, + { file = "../../Cargo.lock" }, + { file = "../**/*" }, + { file = "../../../rust-toolchain.toml" }, + { file = "../../../.cargo/config.toml" }, ] [tool.commitizen] version = "0.1.0" version_files = [ "pyproject.toml:^version", - "../pyproject.toml:litellm-gateway==", + "../../../pyproject.toml:litellm-gateway==", ] diff --git a/pyproject.toml b/pyproject.toml index 495954312f5..fbf1f61d2ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -387,7 +387,7 @@ litellm-enterprise = { workspace = true } litellm-gateway = { workspace = true } [tool.uv.workspace] -members = ["enterprise", "litellm-proxy-extras", "litellm-rust"] +members = ["enterprise", "litellm-proxy-extras", "litellm-rust/crates/gateway"] [tool.commitizen] version = "1.108.0" diff --git a/uv.lock b/uv.lock index 5518fdbcf4f..83d2353dcc4 100644 --- a/uv.lock +++ b/uv.lock @@ -5235,7 +5235,7 @@ requires-dist = [ { name = "keyring", marker = "extra == 'cli'", specifier = ">=25.6.0,<26.0" }, { name = "langfuse", marker = "extra == 'proxy-runtime'", specifier = ">=4.7,<5.0" }, { name = "litellm-enterprise", marker = "extra == 'proxy'", editable = "enterprise" }, - { name = "litellm-gateway", marker = "extra == 'proxy'", editable = "litellm-rust" }, + { name = "litellm-gateway", marker = "extra == 'proxy'", editable = "litellm-rust/crates/gateway" }, { name = "litellm-proxy-extras", marker = "extra == 'proxy'", editable = "litellm-proxy-extras" }, { name = "llm-sandbox", marker = "extra == 'proxy-runtime'", specifier = ">=0.3.39,<1.0" }, { name = "mangum", marker = "extra == 'proxy-runtime'", specifier = ">=0.17.0,<1.0" }, @@ -5438,7 +5438,7 @@ source = { editable = "enterprise" } [[package]] name = "litellm-gateway" version = "0.1.0" -source = { editable = "litellm-rust" } +source = { editable = "litellm-rust/crates/gateway" } [[package]] name = "litellm-proxy-extras" From dd6e4a89ebe15b1df08b037f354269518a6178cc Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:12:02 +0000 Subject: [PATCH 5/6] refactor(proxy): isolate gateway wheel packaging from proxy installs --- .github/scripts/verify_linux_native_wheel.py | 290 ++++++++++-------- .github/workflows/test-linting.yml | 4 +- .github/workflows/test-merge-smoke.yml | 8 +- .github/workflows/test-rust.yml | 67 ++-- .github/workflows/test-unit.yml | 11 +- Dockerfile | 1 - docker/Dockerfile.database | 2 - docker/Dockerfile.non_root | 1 - litellm-rust/crates/gateway/pyproject.toml | 1 - pyproject.toml | 2 - tests/code_coverage_tests/liccheck.ini | 1 - .../test_verify_linux_native_wheel.py | 90 ++++-- uv.lock | 2 - 13 files changed, 292 insertions(+), 188 deletions(-) diff --git a/.github/scripts/verify_linux_native_wheel.py b/.github/scripts/verify_linux_native_wheel.py index 83111b95ca5..9df01c07a72 100644 --- a/.github/scripts/verify_linux_native_wheel.py +++ b/.github/scripts/verify_linux_native_wheel.py @@ -7,14 +7,62 @@ 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_PYTHON_TAG: Final = "cp310" -EXPECTED_ABI_TAG: Final = "abi3" 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): @@ -65,6 +113,57 @@ def _load_native_module(native_path: Path) -> ModuleType | 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, @@ -73,23 +172,19 @@ def main( ) -> int: arguments: Final = tuple(sys.argv if argv is None else argv) resolved_environment: Final = os.environ if environment is None else environment - gateway: Final = len(arguments) == 3 and arguments[2] == "--gateway" - if len(arguments) != 2 and not gateway: - sys.stderr.write(f"usage: {Path(arguments[0]).name} WHEEL [--gateway]\n") + 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 - expected_distribution: Final = "litellm_gateway" if gateway else "litellm" - expected_python_tag: Final = "py3" if gateway else EXPECTED_PYTHON_TAG - expected_abi_tag: Final = "none" if gateway else EXPECTED_ABI_TAG - native_label: Final = "Gateway binary" if gateway else "Native extension" - wheel: Final = Path(arguments[1]) + 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] != expected_distribution or not wheel_identity[1]: + 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 @@ -120,17 +215,9 @@ def main( 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 ( - member.filename == f"{wheel_tags[0]}.data/scripts/litellm-gateway" - if gateway - else member.filename.startswith("litellm/rust_bridge/_native.") and member.filename.endswith(".so") - ) - ) + 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 {native_label.lower()}, found {len(native_members)}\n") + sys.stderr.write(f"expected one {profile.label.lower()}, found {len(native_members)}\n") return 1 unexpected_members: Final = tuple( @@ -145,14 +232,6 @@ def main( native_path.parent.mkdir(parents=True, exist_ok=True) native_bytes: Final = archive.read(native_member) native_path.write_bytes(native_bytes) - duplicated_vocabularies: Final = tuple( - member.filename - for member in wheel_members - 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 - ) wheel_metadata_tags_match: Final = ( len(wheel_metadata_tags) == len(expanded_filename_tags) @@ -167,10 +246,7 @@ def main( capture_output=True, text=True, ).stdout.strip() - project_directory: Final = Path(__file__).parents[2] / ( - "litellm-rust/crates/gateway" if gateway else "." - ) - pyproject: Final = (project_directory / "pyproject.toml").read_text() + 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") @@ -180,7 +256,7 @@ def main( native_percentage: Final = native_member.file_size / uncompressed_wheel_size * 100 size_report: Final = "\n".join( ( - "## Gateway wheel build report" if gateway else "## Native wheel build report", + f"## {profile.report_title}", "", "| Build | Value |", "| --- | --- |", @@ -195,14 +271,13 @@ def main( "| --- | ---: |", f"| Compressed wheel | {wheel.stat().st_size / 1_000_000:.2f} MB |", f"| Uncompressed wheel | {uncompressed_wheel_size / 1_000_000:.2f} MB |", - f"| {native_label} | {native_member.file_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") - if summary_path is None: - sys.stdout.write(size_report) + _write_report(size_report, summary_path) sections: Final = run_command( ("readelf", "--sections", "--wide", str(native_path)), @@ -211,102 +286,77 @@ def main( text=True, ).stdout debug_sections: Final = tuple(section for section in (".debug_", ".zdebug_") if section in sections) - debug_sections_absent: Final = not debug_sections - static_symbol_table_absent: Final = ".symtab" not in sections - - dynamic_symbols: Final = ( - "" - if gateway - else run_command( - ("readelf", "--dyn-syms", "--wide", str(native_path)), - check=True, - capture_output=True, - text=True, - ).stdout - ) - extension_entry_point_present: Final = "PyInit__native" in dynamic_symbols - native_module: Final = None if gateway else load_native_module(native_path) - native_module_loads: Final = native_module is not None - panic_test_hook_absent: Final = native_module is not None and not hasattr(native_module, "_panic_for_test") - native_size_limit: Final = 48_000_000 - native_size_within_limit: Final = native_member.file_size <= native_size_limit validations: Final = ( - (f"Python tag is {expected_python_tag}", python_tag == expected_python_tag), - (f"ABI tag is {expected_abi_tag}", abi_tag == expected_abi_tag), - (f"Platform tag is {EXPECTED_PLATFORM_TAG}", platform_tag == EXPECTED_PLATFORM_TAG), - ("Wheel dist-info directory matches the filename", dist_info_directories == expected_dist_info_directories), - ( + 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)}", ), - ("Wheel metadata tags match the filename", wheel_metadata_tags_match), - ("Debug sections are absent", debug_sections_absent), - ("Static symbol table is absent", static_symbol_table_absent), - (f"{native_label} does not exceed {native_size_limit / 1_000_000:.0f} MB", native_size_within_limit), - ("Tokenizer vocabularies are not duplicated in the native extension", not duplicated_vocabularies), - ("Wheel contents are valid", not unexpected_members), ) + ( - () - if gateway - else ( - ("Python extension entry point is present", extension_entry_point_present), - ("Native module loads", native_module_loads), - ("Production module omits the panic test hook", panic_test_hook_absent), - ) + _extension_validations(wheel, native_path, native_bytes, load_native_module, run_command) + if profile.is_extension + else () ) - verified_report: Final = size_report + "\n".join( + verified_report: Final = "\n".join( ("", "| Validation | Expected | Result |", "| --- | --- | :---: |") - + tuple(f"| {label} | Yes | {'O' if passed else 'X'} |" for label, passed in validations) + + tuple(f"| {row.label} | Yes | {'O' if row.passed else 'X'} |" for row in validations) + ("",) ) - if summary_path is not None: - with Path(summary_path).open("a") as summary: - summary.write(verified_report + "\n") + _write_report(verified_report, summary_path) - invalid_dist_info_files: Final = any(count != 1 for count in dist_info_file_counts.values()) - validation_errors: Final = tuple( - message - for failed, message in ( - (bool(debug_sections), f"{native_member.filename} contains debug sections: {', '.join(debug_sections)}"), - (not static_symbol_table_absent, f"{native_member.filename} contains a static symbol table"), - (not gateway and not extension_entry_point_present, "native extension does not export PyInit__native"), - ( - python_tag != expected_python_tag, - f"unexpected Python tag: expected {expected_python_tag}, found {python_tag}", - ), - (abi_tag != expected_abi_tag, f"unexpected ABI tag: expected {expected_abi_tag}, found {abi_tag}"), - ( - platform_tag != EXPECTED_PLATFORM_TAG, - f"unexpected platform tag: expected {EXPECTED_PLATFORM_TAG}, found {platform_tag}", - ), - ( - dist_info_directories != expected_dist_info_directories, - f"unexpected dist-info directories: expected {expected_dist_info_directory}, " - f"found {', '.join(sorted(dist_info_directories))}", - ), - (invalid_dist_info_files, f"required dist-info file counts are invalid: {dist_info_file_counts}"), - ( - not invalid_dist_info_files and not wheel_metadata_tags_match, - f"WHEEL tags do not match filename: expected {', '.join(sorted(expanded_filename_tags))}, " - f"found {', '.join(sorted(wheel_metadata_tags))}", - ), - ( - native_module is not None and not panic_test_hook_absent, - "production native module exposes _panic_for_test", - ), - ( - not native_size_within_limit, - f"{native_label.lower()} exceeds {native_size_limit / 1_000_000:.0f} MB: " - f"{native_member.file_size / 1_000_000:.2f} MB", - ), - (bool(unexpected_members), f"wheel contains unexpected build artifacts: {', '.join(unexpected_members)}"), - ) - if failed - ) + 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 all(passed for _, passed in validations) else 1 + return 0 if not validation_errors else 1 if __name__ == "__main__": diff --git a/.github/workflows/test-linting.yml b/.github/workflows/test-linting.yml index aed2f8cee34..b9f8d7e7dfb 100644 --- a/.github/workflows/test-linting.yml +++ b/.github/workflows/test-linting.yml @@ -171,7 +171,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - uv sync --frozen --group proxy-dev --group e2e-dev --extra proxy --no-install-package litellm-gateway + uv sync --frozen --group proxy-dev --group e2e-dev --extra proxy uv run --no-sync python scripts/openapi_docs_gate.py --base "$GATE_BASE_SHA" - name: Print OpenAI version @@ -516,7 +516,7 @@ jobs: uses: ./.github/actions/cache-cargo-build - name: Install dependencies - run: uv sync --frozen --all-groups --all-extras --no-install-package litellm-gateway + run: uv sync --frozen --all-groups --all-extras - name: check_licenses run: uv run --no-sync python ./tests/code_coverage_tests/check_licenses.py diff --git a/.github/workflows/test-merge-smoke.yml b/.github/workflows/test-merge-smoke.yml index 7519039507f..f3f09523491 100644 --- a/.github/workflows/test-merge-smoke.yml +++ b/.github/workflows/test-merge-smoke.yml @@ -52,9 +52,6 @@ jobs: with: version: "0.10.9" - - name: Set up Rust before concurrent package builds - run: rustup toolchain install --no-self-update - - name: Install dependencies run: .github/scripts/uv_sync_with_retries.sh --frozen --extra proxy --extra cli --group dev --group proxy-dev --python ${{ matrix.python-version }} @@ -103,7 +100,7 @@ jobs: all-extras: name: all-extras-py${{ matrix.python-version }} runs-on: ubuntu-24.04 - timeout-minutes: 20 + timeout-minutes: 10 strategy: fail-fast: false matrix: @@ -126,9 +123,6 @@ jobs: with: version: "0.10.9" - - name: Set up Rust before concurrent package builds - run: rustup toolchain install --no-self-update - - name: Install every extra and dependency group run: .github/scripts/uv_sync_with_retries.sh --frozen --all-groups --all-extras --python ${{ matrix.python-version }} diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml index 08ecb4577c1..07e15bf19da 100644 --- a/.github/workflows/test-rust.yml +++ b/.github/workflows/test-rust.yml @@ -189,35 +189,12 @@ jobs: - run: uv build --wheel --out-dir dist - - run: python .github/scripts/verify_linux_native_wheel.py dist/*.whl + - run: python .github/scripts/verify_linux_native_wheel.py extension dist/*.whl env: RELEASE_WHEEL_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }} - run: python tests/unit/rust_bridge/native_route_wheel_test.py dist/*.whl - - name: Build and install the gateway binary wheel - run: | - uv build --package litellm-gateway --wheel --out-dir gateway-dist - python .github/scripts/verify_linux_native_wheel.py gateway-dist/*.whl --gateway - uv venv .venv-gateway - uv pip install --python .venv-gateway gateway-dist/*.whl - - - name: Smoke test the installed gateway binary - run: | - printf 'model_list: []\n' > "$RUNNER_TEMP/gateway-config.yaml" - LITELLM_CONFIG="$RUNNER_TEMP/gateway-config.yaml" HOST=127.0.0.1 PORT=4010 \ - .venv-gateway/bin/litellm-gateway > "$RUNNER_TEMP/gateway.log" 2>&1 & - gateway_pid=$! - trap 'kill "$gateway_pid" 2>/dev/null || true; cat "$RUNNER_TEMP/gateway.log"' EXIT - for attempt in $(seq 1 30); do - if curl --fail --silent http://127.0.0.1:4010/health/readiness; then - exit 0 - fi - kill -0 "$gateway_pid" - sleep 1 - done - exit 1 - - name: Run pytest tests/test_litellm_rust with the compiled extension run: make test-rust-extension @@ -226,3 +203,45 @@ jobs: --config-setting "maturin.build-args=--features panic-test,extension-module" - run: python .github/scripts/smoke_test_native_wheel.py panic-dist/*.whl + + gateway-wheel: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - uses: $/.github/actions/setup-uv-with-retries + with: + version: "0.10.9" + + - run: rustup toolchain install --no-self-update + + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: litellm-rust + cache-on-failure: true + save-if: ${{ github.ref == 'refs/heads/main' }} + + - run: uv build --package litellm-gateway --wheel --out-dir dist + + - run: python .github/scripts/verify_linux_native_wheel.py gateway dist/*.whl + env: + RELEASE_WHEEL_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + + - run: uv venv .venv-gateway && uv pip install --python .venv-gateway dist/*.whl + + - name: Smoke test the installed gateway binary + run: | + printf 'model_list: []\n' > "$RUNNER_TEMP/config.yaml" + LITELLM_CONFIG="$RUNNER_TEMP/config.yaml" HOST=127.0.0.1 PORT=4010 \ + .venv-gateway/bin/litellm-gateway > "$RUNNER_TEMP/gateway.log" 2>&1 & + gateway_pid=$! + trap 'kill "$gateway_pid" 2>/dev/null || true; cat "$RUNNER_TEMP/gateway.log"' EXIT + curl --fail --silent --retry 30 --retry-delay 1 --retry-all-errors --retry-connrefused \ + http://127.0.0.1:4010/health/readiness diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index b91ce565c97..4d47a882891 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -42,7 +42,7 @@ jobs: id: rust-bridge-key if: steps.changes.outputs.decision != 'skip' env: - RUST_BRIDGE_CACHE_KEY: ${{ runner.os }}-rust-bridge-editable-${{ hashFiles('litellm-rust/Cargo.lock', 'litellm-rust/Cargo.toml', 'litellm-rust/crates/gateway/pyproject.toml', 'litellm-rust/crates/**', 'pyproject.toml') }} + RUST_BRIDGE_CACHE_KEY: ${{ runner.os }}-rust-bridge-editable-${{ hashFiles('litellm-rust/Cargo.lock', 'litellm-rust/Cargo.toml', 'litellm-rust/crates/**', 'pyproject.toml') }} run: echo "key=$RUST_BRIDGE_CACHE_KEY" >> "$GITHUB_OUTPUT" - name: Set up Python @@ -73,16 +73,15 @@ jobs: timeout-minutes: 5 uses: ./.github/actions/cache-cargo-build - - name: Build editable Rust bridge and gateway wheel + - name: Build editable Rust bridge if: steps.changes.outputs.decision != 'skip' && steps.rust-bridge-cache.outputs.cache-hit != 'true' - timeout-minutes: 20 + timeout-minutes: 15 run: | mkdir -p rust-bridge-dist uv run --no-project --with maturin==1.15.0 python -c 'import maturin; maturin.build_editable("rust-bridge-dist")' strip --strip-debug litellm/rust_bridge/_native.abi3.so mkdir -p rust-bridge-dist/litellm/rust_bridge cp litellm/rust_bridge/_native.abi3.so rust-bridge-dist/litellm/rust_bridge/_native.abi3.so - uv build --package litellm-gateway --wheel --out-dir rust-bridge-dist - name: Upload Rust bridge artifact if: steps.changes.outputs.decision != 'skip' @@ -1001,7 +1000,7 @@ jobs: if [ -z "$RUST_BRIDGE_ARTIFACT" ]; then .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils else - .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils --no-install-project --no-install-package litellm-gateway + .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils --no-install-project uv pip install --no-deps --python .venv/bin/python rust-bridge-dist/*.whl cp rust-bridge-dist/litellm/rust_bridge/_native.abi3.so litellm/rust_bridge/_native.abi3.so uv run --no-sync python -c "import importlib.metadata; import litellm.rust_bridge._native; print(importlib.metadata.version('litellm'))" @@ -1439,7 +1438,7 @@ jobs: if [ -z "$RUST_BRIDGE_ARTIFACT" ]; then .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils else - .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils --no-install-project --no-install-package litellm-gateway + .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils --no-install-project uv pip install --no-deps --python .venv/bin/python rust-bridge-dist/*.whl cp rust-bridge-dist/litellm/rust_bridge/_native.abi3.so litellm/rust_bridge/_native.abi3.so fi diff --git a/Dockerfile b/Dockerfile index fe18b6ba71d..4cef63c8d71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,7 +76,6 @@ ENV UV_PROJECT_ENVIRONMENT=/app/.venv \ COPY pyproject.toml uv.lock ./ COPY enterprise/pyproject.toml enterprise/ COPY litellm-proxy-extras/pyproject.toml litellm-proxy-extras/ -COPY litellm-rust/crates/gateway/pyproject.toml litellm-rust/crates/gateway/ # Install third-party dependencies (cached unless pyproject.toml/uv.lock change) RUN uv sync --frozen --no-install-project --no-install-workspace --no-default-groups --no-editable \ diff --git a/docker/Dockerfile.database b/docker/Dockerfile.database index 5a535f9e023..20f810686a3 100644 --- a/docker/Dockerfile.database +++ b/docker/Dockerfile.database @@ -58,7 +58,6 @@ RUN apk add --no-cache \ gcc \ python-3.13 \ python-3.13-dev \ - rust \ openssl \ libssl3 \ openssl-dev \ @@ -75,7 +74,6 @@ ENV UV_PROJECT_ENVIRONMENT=/app/.venv \ COPY pyproject.toml uv.lock ./ COPY enterprise/pyproject.toml enterprise/ COPY litellm-proxy-extras/pyproject.toml litellm-proxy-extras/ -COPY litellm-rust/crates/gateway/pyproject.toml litellm-rust/crates/gateway/ # Install third-party dependencies (cached unless pyproject.toml/uv.lock change) RUN uv sync --frozen --no-install-project --no-install-workspace --no-default-groups --no-editable \ diff --git a/docker/Dockerfile.non_root b/docker/Dockerfile.non_root index dc69e3d8073..90bcc523d9e 100644 --- a/docker/Dockerfile.non_root +++ b/docker/Dockerfile.non_root @@ -77,7 +77,6 @@ ENV UV_PROJECT_ENVIRONMENT=/app/.venv \ COPY pyproject.toml uv.lock ./ COPY enterprise/pyproject.toml enterprise/ COPY litellm-proxy-extras/pyproject.toml litellm-proxy-extras/ -COPY litellm-rust/crates/gateway/pyproject.toml litellm-rust/crates/gateway/ # Install third-party dependencies (cached unless pyproject.toml/uv.lock change) RUN --mount=type=cache,target=/app/.cache/uv,id=litellm-uv-cache \ diff --git a/litellm-rust/crates/gateway/pyproject.toml b/litellm-rust/crates/gateway/pyproject.toml index cb7301834cd..f93d12889b6 100644 --- a/litellm-rust/crates/gateway/pyproject.toml +++ b/litellm-rust/crates/gateway/pyproject.toml @@ -33,5 +33,4 @@ cache-keys = [ version = "0.1.0" version_files = [ "pyproject.toml:^version", - "../../../pyproject.toml:litellm-gateway==", ] diff --git a/pyproject.toml b/pyproject.toml index fbf1f61d2ea..3bbe9e511f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,6 @@ proxy = [ "httpx2>=2.10.0,<3", "pydantic>=2.12.0,<3", "litellm-proxy-extras==0.4.111", - "litellm-gateway==0.1.0", "litellm-enterprise==0.1.79", "RestrictedPython>=8.5,<9.0", "rich>=13.9.4,<14.0", @@ -384,7 +383,6 @@ brotli = "2026-10-22T14:01:00Z" [tool.uv.sources] litellm-proxy-extras = { workspace = true } litellm-enterprise = { workspace = true } -litellm-gateway = { workspace = true } [tool.uv.workspace] members = ["enterprise", "litellm-proxy-extras", "litellm-rust/crates/gateway"] diff --git a/tests/code_coverage_tests/liccheck.ini b/tests/code_coverage_tests/liccheck.ini index 7d4ae5381c5..cd951c6cd28 100644 --- a/tests/code_coverage_tests/liccheck.ini +++ b/tests/code_coverage_tests/liccheck.ini @@ -89,7 +89,6 @@ importlib-metadata: >=6.8.0 # Apache 2.0 License tokenizers: >=0.20.2 # Apache 2.0 License jinja2: >=3.1.4 # BSD 3-Clause License litellm-proxy-extras: >=0.1.1 # MIT License -litellm-gateway: ==0.1.0 # MIT, verified in LICENSE and litellm-rust/Cargo.toml litellm-enterprise: >=0.1.1 # LiteLLM Enterprise License a2a-sdk: >=0.3.22 # Apache 2.0 license anyio: >=4.5.0 # Unknown license diff --git a/tests/unit/rust_bridge/test_verify_linux_native_wheel.py b/tests/unit/rust_bridge/test_verify_linux_native_wheel.py index 0373f9f25dc..cdd6109f236 100644 --- a/tests/unit/rust_bridge/test_verify_linux_native_wheel.py +++ b/tests/unit/rust_bridge/test_verify_linux_native_wheel.py @@ -57,11 +57,13 @@ def _write_wheel( dist_info: str = _DIST_INFO, duplicate_wheel: bool = False, native_bytes: bytes = b"synthetic native extension", - gateway: bool = False, + kind: str = "extension", ) -> Path: - distribution: Final = "litellm_gateway" if gateway else "litellm" - metadata_directory: Final = f"{distribution}-1.100.0.dist-info" if gateway else dist_info - native_member: Final = f"{distribution}-1.100.0.data/scripts/litellm-gateway" if gateway else _NATIVE_MEMBER + distribution: Final = "litellm_gateway" if kind == "gateway" else "litellm" + metadata_directory: Final = f"{distribution}-1.100.0.dist-info" if kind == "gateway" else dist_info + native_member: Final = ( + f"{distribution}-1.100.0.data/scripts/litellm-gateway" if kind == "gateway" else _NATIVE_MEMBER + ) wheel: Final = tmp_path / f"{distribution}-1.100.0-{filename_tag}.whl" with zipfile.ZipFile(wheel, "w", compression=zipfile.ZIP_DEFLATED) as archive: archive.writestr(native_member, native_bytes) @@ -110,7 +112,9 @@ def _run_verifier( wheel: Path, *, exposes_panic: bool = False, - gateway: bool = False, + kind: str = "extension", + loads: bool = True, + run_command: _CommandRunner = _fake_subprocess_run, ) -> int: native_module: Final = ( _NativeModuleWithPanicHook("litellm.rust_bridge._native") @@ -118,16 +122,16 @@ def _run_verifier( else ModuleType("litellm.rust_bridge._native") ) - def _fake_load_native_module(_: Path) -> ModuleType: - assert not gateway, "A standalone binary must not be imported as a Python extension" - return native_module + def _fake_load_native_module(_: Path) -> ModuleType | None: + assert kind != "gateway", "A standalone binary must not be imported as a Python extension" + return native_module if loads else None environment: Final = MappingProxyType({"GITHUB_STEP_SUMMARY": str(wheel.parent / "summary.md")}) return verifier.main( - (str(_MODULE_PATH), str(wheel)) + (("--gateway",) if gateway else ()), + (str(_MODULE_PATH), kind, str(wheel)), environment, _fake_load_native_module, - _fake_subprocess_run, + run_command, ) @@ -218,27 +222,75 @@ def test_vocabulary_is_packaged_once(tmp_path: Path, embedded: bool) -> None: assert _run_verifier(wheel) == (1 if embedded else 0) -@pytest.mark.parametrize("gateway", (False, True), ids=("extension", "gateway")) -@pytest.mark.parametrize("size", (48_000_000, 48_000_001), ids=("at-limit", "over-limit")) -def test_enforces_uncompressed_native_size_limit(tmp_path: Path, gateway: bool, size: int) -> None: - tag: Final = "py3-none-linux_x86_64" if gateway else _EXPECTED_TAG +@pytest.mark.parametrize("kind, limit", (("extension", 48_000_000), ("gateway", 37_000_000))) +@pytest.mark.parametrize("excess", (0, 1), ids=("at-limit", "over-limit")) +def test_enforces_uncompressed_native_size_limit(tmp_path: Path, kind: str, limit: int, excess: int) -> None: + tag: Final = "py3-none-linux_x86_64" if kind == "gateway" else _EXPECTED_TAG wheel: Final = _write_wheel( - tmp_path, filename_tag=tag, metadata_tags=(tag,), native_bytes=b"x" * size, gateway=gateway + tmp_path, filename_tag=tag, metadata_tags=(tag,), native_bytes=b"x" * (limit + excess), kind=kind ) - assert _run_verifier(wheel, gateway=gateway) == (0 if size == 48_000_000 else 1) + assert _run_verifier(wheel, kind=kind) == (0 if excess == 0 else 1) summary: Final = (tmp_path / "summary.md").read_text() - assert f"| {'Gateway binary' if gateway else 'Native extension'} | 48.00 MB |" in summary + label: Final = "Gateway binary" if kind == "gateway" else "Native extension" + assert f"| {label} | {(limit + excess) / 1_000_000:.2f} MB |" in summary def test_keeps_both_artifact_reports_in_summary(tmp_path: Path) -> None: extension: Final = _write_wheel(tmp_path, filename_tag=_EXPECTED_TAG) gateway: Final = _write_wheel( - tmp_path, filename_tag="py3-none-linux_x86_64", metadata_tags=("py3-none-linux_x86_64",), gateway=True + tmp_path, filename_tag="py3-none-linux_x86_64", metadata_tags=("py3-none-linux_x86_64",), kind="gateway" ) assert _run_verifier(extension) == 0 - assert _run_verifier(gateway, gateway=True) == 0 + assert _run_verifier(gateway, kind="gateway") == 0 summary: Final = (tmp_path / "summary.md").read_text() assert "## Native wheel build report" in summary assert "## Gateway wheel build report" in summary + + +@pytest.mark.parametrize("kind, other", (("extension", "gateway"), ("gateway", "extension"))) +def test_rejects_wheel_under_wrong_profile(tmp_path: Path, kind: str, other: str) -> None: + tag: Final = "py3-none-linux_x86_64" if kind == "gateway" else _EXPECTED_TAG + wheel: Final = _write_wheel(tmp_path, filename_tag=tag, metadata_tags=(tag,), kind=kind) + + assert _run_verifier(wheel, kind=other) == 1 + + +def test_rejects_unknown_profile(tmp_path: Path) -> None: + wheel: Final = _write_wheel(tmp_path, filename_tag=_EXPECTED_TAG) + + assert _run_verifier(wheel, kind="unknown") == 2 + + +@pytest.mark.parametrize("arguments", ((), ("extension",), ("extension", "wheel.whl", "extra"))) +def test_rejects_wrong_argument_count(arguments: tuple[str, ...]) -> None: + assert verifier.main((str(_MODULE_PATH), *arguments), None, lambda _: None, _fake_subprocess_run) == 2 + + +def test_load_failure_does_not_report_a_panic_hook_failure(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + wheel: Final = _write_wheel(tmp_path, filename_tag=_EXPECTED_TAG) + + assert _run_verifier(wheel, loads=False) == 1 + summary: Final = (tmp_path / "summary.md").read_text() + assert summary.count("| X |") == 1 + assert "| Native module loads | Yes | X |" in summary + assert "native module did not load" in capsys.readouterr().err + + +def test_preserves_size_report_when_readelf_fails(tmp_path: Path) -> None: + wheel: Final = _write_wheel(tmp_path, filename_tag=_EXPECTED_TAG) + + def _failing_readelf( + command: tuple[str, ...], *, check: bool, capture_output: bool, text: bool + ) -> subprocess.CompletedProcess[str]: + if command[0] == "readelf": + raise subprocess.CalledProcessError(1, command) + return _fake_subprocess_run(command, check=check, capture_output=capture_output, text=text) + + with pytest.raises(subprocess.CalledProcessError): + _run_verifier(wheel, run_command=_failing_readelf) + + summary: Final = (tmp_path / "summary.md").read_text() + assert "## Native wheel build report" in summary + assert "| Compressed wheel |" in summary diff --git a/uv.lock b/uv.lock index 83d2353dcc4..5af5d20dca8 100644 --- a/uv.lock +++ b/uv.lock @@ -5003,7 +5003,6 @@ proxy = [ { name = "httpx2" }, { name = "inquirerpy" }, { name = "litellm-enterprise" }, - { name = "litellm-gateway" }, { name = "litellm-proxy-extras" }, { name = "mcp" }, { name = "orjson" }, @@ -5235,7 +5234,6 @@ requires-dist = [ { name = "keyring", marker = "extra == 'cli'", specifier = ">=25.6.0,<26.0" }, { name = "langfuse", marker = "extra == 'proxy-runtime'", specifier = ">=4.7,<5.0" }, { name = "litellm-enterprise", marker = "extra == 'proxy'", editable = "enterprise" }, - { name = "litellm-gateway", marker = "extra == 'proxy'", editable = "litellm-rust/crates/gateway" }, { name = "litellm-proxy-extras", marker = "extra == 'proxy'", editable = "litellm-proxy-extras" }, { name = "llm-sandbox", marker = "extra == 'proxy-runtime'", specifier = ">=0.3.39,<1.0" }, { name = "mangum", marker = "extra == 'proxy-runtime'", specifier = ">=0.17.0,<1.0" }, From 8aec166f0e8e55f3a12110ba2c8184ac43e33d63 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:22:12 +0000 Subject: [PATCH 6/6] fix(ci): restore gateway build workflow guards --- .github/workflows/test-linting.yml | 6 +++--- .github/workflows/test-merge-smoke.yml | 8 +++++++- .github/workflows/test-rust.yml | 2 +- .github/workflows/test-unit.yml | 11 ++++++----- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test-linting.yml b/.github/workflows/test-linting.yml index b9f8d7e7dfb..15fcab3e3bb 100644 --- a/.github/workflows/test-linting.yml +++ b/.github/workflows/test-linting.yml @@ -171,7 +171,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - uv sync --frozen --group proxy-dev --group e2e-dev --extra proxy + uv sync --frozen --group proxy-dev --group e2e-dev --extra proxy --no-install-package litellm-gateway uv run --no-sync python scripts/openapi_docs_gate.py --base "$GATE_BASE_SHA" - name: Print OpenAI version @@ -412,7 +412,7 @@ jobs: - name: Install backend dependencies if: steps.changes.outputs.relevant == 'true' - run: .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router + run: .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --no-install-package litellm-gateway - name: Cache Prisma binaries if: steps.changes.outputs.relevant == 'true' @@ -516,7 +516,7 @@ jobs: uses: ./.github/actions/cache-cargo-build - name: Install dependencies - run: uv sync --frozen --all-groups --all-extras + run: uv sync --frozen --all-groups --all-extras --no-install-package litellm-gateway - name: check_licenses run: uv run --no-sync python ./tests/code_coverage_tests/check_licenses.py diff --git a/.github/workflows/test-merge-smoke.yml b/.github/workflows/test-merge-smoke.yml index f3f09523491..7519039507f 100644 --- a/.github/workflows/test-merge-smoke.yml +++ b/.github/workflows/test-merge-smoke.yml @@ -52,6 +52,9 @@ jobs: with: version: "0.10.9" + - name: Set up Rust before concurrent package builds + run: rustup toolchain install --no-self-update + - name: Install dependencies run: .github/scripts/uv_sync_with_retries.sh --frozen --extra proxy --extra cli --group dev --group proxy-dev --python ${{ matrix.python-version }} @@ -100,7 +103,7 @@ jobs: all-extras: name: all-extras-py${{ matrix.python-version }} runs-on: ubuntu-24.04 - timeout-minutes: 10 + timeout-minutes: 20 strategy: fail-fast: false matrix: @@ -123,6 +126,9 @@ jobs: with: version: "0.10.9" + - name: Set up Rust before concurrent package builds + run: rustup toolchain install --no-self-update + - name: Install every extra and dependency group run: .github/scripts/uv_sync_with_retries.sh --frozen --all-groups --all-extras --python ${{ matrix.python-version }} diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml index 07e15bf19da..01f45dae96f 100644 --- a/.github/workflows/test-rust.yml +++ b/.github/workflows/test-rust.yml @@ -216,7 +216,7 @@ jobs: with: python-version: "3.12" - - uses: $/.github/actions/setup-uv-with-retries + - uses: ./.github/actions/setup-uv-with-retries with: version: "0.10.9" diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index b4b7afb9a09..cff1fa17959 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -42,7 +42,7 @@ jobs: id: rust-bridge-key if: steps.changes.outputs.decision != 'skip' env: - RUST_BRIDGE_CACHE_KEY: ${{ runner.os }}-rust-bridge-editable-${{ hashFiles('litellm-rust/Cargo.lock', 'litellm-rust/Cargo.toml', 'litellm-rust/crates/**', 'pyproject.toml') }} + RUST_BRIDGE_CACHE_KEY: ${{ runner.os }}-rust-bridge-editable-${{ hashFiles('litellm-rust/Cargo.lock', 'litellm-rust/Cargo.toml', 'litellm-rust/crates/gateway/pyproject.toml', 'litellm-rust/crates/**', 'pyproject.toml') }} run: echo "key=$RUST_BRIDGE_CACHE_KEY" >> "$GITHUB_OUTPUT" - name: Set up Python @@ -73,15 +73,16 @@ jobs: timeout-minutes: 5 uses: ./.github/actions/cache-cargo-build - - name: Build editable Rust bridge + - name: Build editable Rust bridge and gateway wheel if: steps.changes.outputs.decision != 'skip' && steps.rust-bridge-cache.outputs.cache-hit != 'true' - timeout-minutes: 15 + timeout-minutes: 20 run: | mkdir -p rust-bridge-dist uv run --no-project --with maturin==1.15.0 python -c 'import maturin; maturin.build_editable("rust-bridge-dist")' strip --strip-debug litellm/rust_bridge/_native.abi3.so mkdir -p rust-bridge-dist/litellm/rust_bridge cp litellm/rust_bridge/_native.abi3.so rust-bridge-dist/litellm/rust_bridge/_native.abi3.so + uv build --package litellm-gateway --wheel --out-dir rust-bridge-dist - name: Upload Rust bridge artifact if: steps.changes.outputs.decision != 'skip' @@ -1001,7 +1002,7 @@ jobs: if [ -z "$RUST_BRIDGE_ARTIFACT" ]; then .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils else - .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils --no-install-project + .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils --no-install-project --no-install-package litellm-gateway uv pip install --no-deps --python .venv/bin/python rust-bridge-dist/*.whl cp rust-bridge-dist/litellm/rust_bridge/_native.abi3.so litellm/rust_bridge/_native.abi3.so uv run --no-sync python -c "import importlib.metadata; import litellm.rust_bridge._native; print(importlib.metadata.version('litellm'))" @@ -1439,7 +1440,7 @@ jobs: if [ -z "$RUST_BRIDGE_ARTIFACT" ]; then .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils else - .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils --no-install-project + .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml --extra caching --extra extra_proxy --extra proxy-runtime --extra utils --no-install-project --no-install-package litellm-gateway uv pip install --no-deps --python .venv/bin/python rust-bridge-dist/*.whl cp rust-bridge-dist/litellm/rust_bridge/_native.abi3.so litellm/rust_bridge/_native.abi3.so fi