Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
Expand Down Expand Up @@ -179,7 +179,7 @@ jobs:
- name: Verify slim install (no extras) succeeds
working-directory: /tmp
run: |
uv run --isolated --no-project --python 3.12 \
uv run --isolated --no-project --python 3.10 \
--with "${SWITCHYARD_DEFAULT_PACKAGE}" \
python -c "import switchyard; print('import OK, version:', switchyard.__version__)"
uv run --isolated --no-project --python 3.14 \
Expand All @@ -188,7 +188,7 @@ jobs:
- name: Assert heavy packages absent from slim install
working-directory: /tmp
run: |
uv run --isolated --no-project --python 3.12 \
uv run --isolated --no-project --python 3.10 \
--with "${SWITCHYARD_DEFAULT_PACKAGE}" \
python - <<'PY'
import importlib.util
Expand Down Expand Up @@ -216,13 +216,13 @@ jobs:
- name: Verify CLI works with its extra
working-directory: /tmp
run: |
uv run --isolated --no-project --python 3.12 \
uv run --isolated --no-project --python 3.10 \
--with "${SWITCHYARD_EXTRAS_PACKAGE}" \
switchyard launch claude --help > /dev/null
uv run --isolated --no-project --python 3.12 \
uv run --isolated --no-project --python 3.10 \
--with "${SWITCHYARD_EXTRAS_PACKAGE}" \
switchyard launch claude --help 2>&1 | grep -q -- '--model' || { echo 'FAIL: --model flag missing from help'; exit 1; }
uv run --isolated --no-project --python 3.12 \
uv run --isolated --no-project --python 3.10 \
--with "${SWITCHYARD_EXTRAS_PACKAGE}" \
switchyard launch claude --help 2>&1 | grep -q -- '--config' || { echo 'FAIL: --config flag missing from help'; exit 1; }

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/package-portability.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.10"
- name: Build manylinux x86_64 wheel
shell: bash
run: |
Expand All @@ -55,7 +55,7 @@ jobs:
set -euxo pipefail
curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
source "${HOME}/.cargo/env"
PYTHON=/opt/python/cp312-cp312/bin/python
PYTHON=/opt/python/cp310-cp310/bin/python
"${PYTHON}" -m pip install --upgrade pip
"${PYTHON}" -m pip install "maturin==${MATURIN_VERSION}"
"${PYTHON}" -m maturin build \
Expand All @@ -65,7 +65,7 @@ jobs:
--out dist \
--interpreter "${PYTHON}"
'
- name: Smoke install wheel on Python 3.12
- name: Smoke install wheel on Python 3.10
shell: bash
run: |
python -m pip install --upgrade pip
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
Expand Down Expand Up @@ -126,7 +126,7 @@ jobs:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
python-version: "3.12"
python-version: "3.10"
enable-cache: true
cache-dependency-glob: "uv.lock"
- run: uv sync --locked
Expand Down Expand Up @@ -201,7 +201,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.10"
- name: Stamp dev wheel metadata
if: ${{ github.event_name == 'workflow_dispatch' && inputs.build_dev_matrix }}
shell: bash
Expand Down Expand Up @@ -234,7 +234,7 @@ jobs:
set -euxo pipefail
curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
source "${HOME}/.cargo/env"
PYTHON=/opt/python/cp312-cp312/bin/python
PYTHON=/opt/python/cp310-cp310/bin/python
"${PYTHON}" -m pip install --upgrade pip
"${PYTHON}" -m pip install "maturin==${MATURIN_VERSION}"
"${PYTHON}" -m maturin build \
Expand All @@ -254,7 +254,7 @@ jobs:
--compatibility pypi \
--out dist \
--target "${{ matrix.rust_target }}"
- name: Smoke install native wheel on Python 3.12
- name: Smoke install native wheel on Python 3.10
if: ${{ matrix.smoke }}
shell: bash
run: |
Expand Down Expand Up @@ -317,7 +317,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.10"
- name: Stamp dev wheel metadata
shell: bash
run: |
Expand All @@ -335,7 +335,7 @@ jobs:
set -euxo pipefail
curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable
source "${HOME}/.cargo/env"
PYTHON=/opt/python/cp312-cp312/bin/python
PYTHON=/opt/python/cp310-cp310/bin/python
"${PYTHON}" -m pip install --upgrade pip
"${PYTHON}" -m pip install "maturin==${MATURIN_VERSION}"
"${PYTHON}" -m maturin build \
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ tests/ # Unit tests (pytest)
## Tech Stack

- **Rust 1.96.1**, edition 2024, Tokio, Axum, and PyO3
- **Python 3.12+** for launchers and native bindings
- **Python 3.10+** for launchers and native bindings
- **prompt-toolkit** for interactive launcher sessions
- **uv** as the package manager (preferred over pip)
- **Cargo test + pytest** for testing
Expand Down Expand Up @@ -247,7 +247,7 @@ cargo test --workspace
## Code Style

- **Line length**: 100 chars (ruff, E501 ignored)
- **Target version**: Python 3.12 — use `X | Y` union syntax
- **Target version**: Python 3.10 — use `X | Y` union syntax
- **Imports**: sorted by ruff (`I` rules). Use `TYPE_CHECKING` guards for circular imports.
- **File naming**: file name = `snake_case` of its primary class. One class per file when practical.
- **Type hints**: throughout. `py.typed` marker present; mypy runs strict.
Expand Down
4 changes: 2 additions & 2 deletions INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ serving.

## Requirements

- Python 3.12 or newer for `nemo-switchyard`
- Python 3.10 or newer for `nemo-switchyard`
- Rust 1.96.1 or newer for `switchyard-server` and the Rust libraries
- Linux x86_64 wheels require an x86-64-v3 / AVX2-class CPU
- Linux aarch64 wheels require a Neoverse N1-class CPU
Expand All @@ -28,7 +28,7 @@ Install the CLI extra to launch Claude Code, Codex CLI, or OpenClaw through the
packaged native server:

```bash
uv tool install --python 3.12 "nemo-switchyard[cli]"
uv tool install --python 3.10 "nemo-switchyard[cli]"
export OPENROUTER_API_KEY="your-openrouter-key" # pragma: allowlist secret
switchyard launch claude --model switchyard
```
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ not already available, then install the published Switchyard tool:
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
source "$HOME/.local/bin/env"
uv tool install --python 3.12 "nemo-switchyard[cli]"
uv tool install --python 3.10 "nemo-switchyard[cli]"
```

The coding agent you launch must also be installed and on your `PATH`. This does
Expand Down
24 changes: 18 additions & 6 deletions benchmark/prepare_harbor_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,22 @@
import shlex
import shutil
import subprocess
import tomllib
from datetime import UTC, datetime
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

try:
import tomllib
except ModuleNotFoundError: # Python 3.10
import tomli as tomllib

try:
import yaml
except ImportError: # pragma: no cover - exercised only in a broken environment
yaml = None

UTC = timezone.utc

SCRIPT_DIR = Path(__file__).resolve().parent
DEFAULT_SOURCE_DATASET = "openthoughts-tblite@2.0"
DEFAULT_OUTPUT_DIR = SCRIPT_DIR / "datasets" / "openthoughts-tblite-closed-book"
Expand Down Expand Up @@ -87,20 +93,26 @@ def _read_env_file(path: Path) -> dict[str, str]:
return values


def _file_digest(path: Path) -> bytes:
hasher = hashlib.sha256()
with path.open("rb") as file:
for chunk in iter(lambda: file.read(1024 * 1024), b""):
hasher.update(chunk)
return hasher.digest()


def _path_digest(path: Path) -> str:
try:
resolved = path.resolve()
hasher = hashlib.sha256()
if resolved.is_file():
hasher.update(resolved.name.encode())
with resolved.open("rb") as fh:
hasher.update(hashlib.file_digest(fh, "sha256").digest())
hasher.update(_file_digest(resolved))
return f"sha256:{hasher.hexdigest()}"
if resolved.is_dir():
for item in sorted(p for p in resolved.rglob("*") if p.is_file()):
rel = item.relative_to(resolved).as_posix()
with item.open("rb") as fh:
file_hash = hashlib.file_digest(fh, "sha256").hexdigest()
file_hash = _file_digest(item).hex()
hasher.update(f"{rel}\n{file_hash}\n".encode())
return f"sha256:{hasher.hexdigest()}"
except OSError:
Expand Down
3 changes: 1 addition & 2 deletions benchmark/run-baseline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -644,8 +644,7 @@ patch_root = Path(sys.argv[3])
verify_method = sys.argv[4]
verify_status = sys.argv[5]
try:
with patch_file.open("rb") as fh:
digest = "sha256:" + hashlib.file_digest(fh, "sha256").hexdigest()
digest = "sha256:" + hashlib.sha256(patch_file.read_bytes()).hexdigest()
except OSError:
digest = "sha256:missing"
print(json.dumps({
Expand Down
17 changes: 12 additions & 5 deletions benchmark/run_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@
import shutil
import socket
import subprocess
from datetime import UTC, datetime
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

SCHEMA_VERSION = 1
UTC = timezone.utc


def _iso_timestamp() -> str:
Expand Down Expand Up @@ -78,21 +79,27 @@ def _harbor_version(harbor_command: list[str] | None = None) -> str | None:
return result[1].splitlines()[0] if result[1] else None


def _file_digest(path: Path) -> bytes:
hasher = hashlib.sha256()
with path.open("rb") as file:
for chunk in iter(lambda: file.read(1024 * 1024), b""):
hasher.update(chunk)
return hasher.digest()


def path_digest(path: Path) -> str:
"""Return a deterministic sha256 digest for a file or directory."""
try:
resolved = path.resolve()
hasher = hashlib.sha256()
if resolved.is_file():
hasher.update(resolved.name.encode())
with resolved.open("rb") as fh:
hasher.update(hashlib.file_digest(fh, "sha256").digest())
hasher.update(_file_digest(resolved))
return f"sha256:{hasher.hexdigest()}"
if resolved.is_dir():
for item in sorted(p for p in resolved.rglob("*") if p.is_file()):
rel = item.relative_to(resolved).as_posix()
with item.open("rb") as fh:
file_hash = hashlib.file_digest(fh, "sha256").hexdigest()
file_hash = _file_digest(item).hex()
hasher.update(f"{rel}\n{file_hash}\n".encode())
return f"sha256:{hasher.hexdigest()}"
except OSError:
Expand Down
2 changes: 1 addition & 1 deletion crates/switchyard-py/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ async-trait.workspace = true
http.workspace = true
switchyard-libsy.workspace = true
switchyard-llm-client.workspace = true
pyo3 = { version = "0.28.3", features = ["abi3-py312", "extension-module"] }
pyo3 = { version = "0.28.3", features = ["abi3-py310", "extension-module"] }
pyo3-async-runtimes = { version = "0.28", features = ["tokio-runtime"] }
pythonize = "0.28.0"
serde.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion docs/cli_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Switchyard has two command-line paths:

## Launcher Path: `switchyard launch`

Install the launcher with `uv tool install --python 3.12 "nemo-switchyard[cli]"`. The
Install the launcher with `uv tool install --python 3.10 "nemo-switchyard[cli]"`. The
selected coding agent must also be installed and available on `PATH`.

### Usage
Expand Down
2 changes: 1 addition & 1 deletion docs/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ source "$HOME/.local/bin/env"
Then install the published Switchyard tool:

```bash
uv tool install --python 3.12 "nemo-switchyard[cli]"
uv tool install --python 3.10 "nemo-switchyard[cli]"
```

This creates an isolated Python tool environment containing the `switchyard`
Expand Down
2 changes: 1 addition & 1 deletion docs/internal/release_workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ publish anything to PyPI.

Create a root `vMAJOR.MINOR.PATCH` tag only when a real release has been approved. Tag pushes run:

- Python release checks on Python 3.12, 3.13, and 3.14;
- Python release checks on Python 3.10 through 3.14;
- Rust fmt, clippy, and workspace tests;
- source distribution build;
- full abi3 wheel matrix for Linux x86_64, Linux aarch64, macOS x86_64, macOS arm64,
Expand Down
17 changes: 11 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ version = "0.2.0"
description = "Typed, composable LLM routing with request/response translation and multi-backend orchestration"
readme = "README.md"
license = "Apache-2.0"
requires-python = ">=3.12"
requires-python = ">=3.10"
authors = [{ name = "NVIDIA Corporation" }]
maintainers = [{ name = "NVIDIA Corporation" }]
keywords = ["llm", "switchyard", "routing", "openai", "nemo"]
Expand All @@ -17,6 +17,8 @@ classifiers = [
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
Expand Down Expand Up @@ -46,11 +48,13 @@ dev = [
"maturin>=1.9,<2.0",
"pytest>=9.0.3,<10.0",
"pytest-asyncio>=1.3.0,<2.0",
"pyyaml>=6.0.3,<7.0",
"pytest-timeout>=2.4.0,<3.0",
"ruff>=0.15.12,<1.0",
"mypy>=1.20.2,<2.0",
# Harbor is needed for local evaluation runs and supports the package floor.
# Package users still do not see it in published metadata.
"tomli>=2.0,<3.0 ; python_version < '3.11'",
# Harbor is needed only for local evaluation runs and currently requires Python 3.12.
# Package users and Python 3.10/3.11 contributors do not install it.
"harbor @ git+https://github.com/harbor-framework/harbor.git@v0.6.4 ; python_version >= '3.12'",
"nemo-switchyard[cli]",
"pytest-markdown-docs>=0.9.2",
Expand Down Expand Up @@ -106,7 +110,7 @@ markers = [
addopts = "-x" # Stop on first failure by default (use --no-x to override)

[tool.mypy]
python_version = "3.12"
python_version = "3.10"
strict = true
files = ["switchyard", "switchyard_rust"]
warn_return_any = true
Expand All @@ -131,11 +135,11 @@ exclude = [
"build",
"tests",
]
pythonVersion = "3.12"
pythonVersion = "3.10"

[tool.ruff]
line-length = 100
target-version = "py312"
target-version = "py310"
exclude = [".agents", ".worktrees"]

[tool.ruff.lint]
Expand All @@ -146,4 +150,5 @@ select = ["E", "F", "I", "W", "UP", "B", "C4"]
ignore = ["E501", "UP040", "UP042", "UP046", "UP047"]

[tool.ruff.lint.isort]
extra-standard-library = ["tomllib"]
known-first-party = ["switchyard"]
2 changes: 1 addition & 1 deletion tests/getting_started/test_getting_started.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def test_getting_started_documents_current_paths() -> None:
).read_text()

assert guide.index("## Launcher Path") < guide.index("## Server Path")
assert 'uv tool install --python 3.12 "nemo-switchyard[cli]"' in guide
assert 'uv tool install --python 3.10 "nemo-switchyard[cli]"' in guide
assert "switchyard launch claude --model switchyard" in guide
assert "cargo install --locked switchyard-server" in guide
assert "switchyard-server --config routes.toml --dry-run" in guide
Expand Down
Loading
Loading