From 6e46ee9df7b8f5ee68357d22e61f5378be4bcd88 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Thu, 3 Sep 2026 14:29:24 -0700 Subject: [PATCH] ci(rust): add an experimental free-threaded (3.14t) wheel lane --- .github/workflows/test-rust.yml | 77 ++++++++++++++++++++ litellm-rust/crates/python-bridge/Cargo.toml | 3 + 2 files changed, 80 insertions(+) diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml index 1b71232bc2e..82705958c63 100644 --- a/.github/workflows/test-rust.yml +++ b/.github/workflows/test-rust.yml @@ -126,3 +126,80 @@ jobs: - name: Test native route wheel run: python tests/test_litellm/rust_bridge/native_route_wheel_test.py dist/*.whl + + # EXPERIMENTAL: build and smoke-test the native module on a free-threaded + # (no-GIL) CPython. This lane is a signal, not a gate. + free-threaded-wheel: + continue-on-error: true + name: free-threaded wheel (3.14t, experimental) + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + env: + CARGO_TERM_COLOR: always + + steps: + - name: Checkout repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + + - name: Set up free-threaded Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.14t" + + - name: Set up Rust + run: rustup toolchain install + + - name: Build free-threaded wheel + run: | + python -m pip install maturin==1.15.0 + maturin build --profile release -i python --no-default-features --features freethreaded,extension-module -o dist + + - name: Assert free-threaded wheel tag + run: >- + python -c 'import glob, sys; wheels = glob.glob("dist/*.whl"); + assert wheels and all("cp314t" in w and "t-" in w for w in wheels), wheels' + + - name: Smoke-test free-threaded native module + run: | + python -m pip install --no-deps dist/*.whl + python - <<'EOF' + import importlib.util + import sys + import sysconfig + import tempfile + import zipfile + from pathlib import Path + + wheel = next(Path("dist").glob("*.whl")) + with tempfile.TemporaryDirectory() as temporary_directory, zipfile.ZipFile(wheel) as archive: + member = next( + member + for member in archive.namelist() + if member.startswith("litellm/rust_bridge/_native.") and member.endswith(".so") + ) + native = Path(temporary_directory) / Path(member).name + native.write_bytes(archive.read(member)) + + spec = importlib.util.spec_from_file_location("litellm.rust_bridge._native", native) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + assert sys.version_info[:2] == (3, 14), sys.version + assert sysconfig.get_config_var("Py_GIL_DISABLED") == 1, sys.version + info = module.build_info() + assert info["gil_disabled"] is True, info + stats = module.gil_stats() + assert isinstance(stats["releases"], int), stats + decline = module.chat_completions_decline( + "gpt-4o-mini", [{"role": "user", "content": "hi"}] + ) + assert decline is None or isinstance(decline, str), decline + print(f"free-threaded smoke ok on {sys.version}: gil_stats={dict(stats)}") + EOF + + - name: Test native route wheel + run: python tests/test_litellm/rust_bridge/native_route_wheel_test.py dist/*.whl diff --git a/litellm-rust/crates/python-bridge/Cargo.toml b/litellm-rust/crates/python-bridge/Cargo.toml index 0ca3399ecc1..4d7858ed538 100644 --- a/litellm-rust/crates/python-bridge/Cargo.toml +++ b/litellm-rust/crates/python-bridge/Cargo.toml @@ -12,6 +12,9 @@ crate-type = ["cdylib"] [features] default = ["abi3"] abi3 = ["pyo3/abi3-py310"] +# Free-threaded stable ABI (cp3XXt wheels), following the cryptography project's +# pyo3 setup; deliberately omits the abi3-py310 GIL-wheel pin. +freethreaded = ["pyo3/abi3", "pyo3/abi3t"] extension-module = ["pyo3/extension-module"] panic-test = []