Skip to content

feat(routing): add modality-aware target selection #1552

feat(routing): add modality-aware target selection

feat(routing): add modality-aware target selection #1552

Workflow file for this run

name: CI
on:
pull_request:
push:
branches: [main]
# Cancel superseded PR runs; keep main builds.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
jobs:
changes:
name: Detect code changes
runs-on: ubuntu-latest
permissions:
pull-requests: read
outputs:
full_ci: ${{ github.event_name != 'pull_request' || (!startsWith(github.event.pull_request.title, 'docs') && steps.filter.outputs.full_ci == 'true') }}
steps:
- name: Detect non-documentation changes
id: filter
if: github.event_name == 'pull_request' && !startsWith(github.event.pull_request.title, 'docs')
uses: dorny/paths-filter@v4
with:
predicate-quantifier: every
filters: |
full_ci:
- "**"
- "!docs/**"
- "!*.md"
- "!**/README.md"
- "!benchmark/*.md"
- "!.github/ISSUE_TEMPLATE/**"
- "!.github/PULL_REQUEST_TEMPLATE.md"
- "!.agents/skills/**/*.md"
# ruff is a hard gate — the codebase passes today, keep it that way.
lint:
name: Lint (ruff)
needs: changes
if: needs.changes.outputs.full_ci == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
python-version: "3.12"
enable-cache: true
cache-dependency-glob: "uv.lock"
- run: uv sync --locked
- name: ruff check
run: uv run ruff check .
# SPDX headers are a hard gate — every Python file must carry the exact
# NVIDIA OSRB-required two-line form (SPDX-FileCopyrightText with the
# canonical wording, immediately followed by SPDX-License-Identifier:
# Apache-2.0). A leading shebang is permitted; the header must appear
# within the first four lines.
spdx-headers:
name: SPDX License Headers
needs: changes
if: needs.changes.outputs.full_ci == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check SPDX headers
run: |
set -u
copyright_re='^# SPDX-FileCopyrightText: Copyright \(c\) [0-9]{4}(-[0-9]{4})? NVIDIA CORPORATION & AFFILIATES\. All rights reserved\.$'
license_re='^# SPDX-License-Identifier: Apache-2\.0$'
missing=0
while IFS= read -r file; do
head4=$(head -4 "$file")
if ! printf '%s\n' "$head4" | grep -Eq "$copyright_re"; then
echo "FAIL ($file): missing or malformed SPDX-FileCopyrightText line"
missing=$((missing+1))
continue
fi
if ! printf '%s\n' "$head4" | grep -Eq "$license_re"; then
echo "FAIL ($file): missing SPDX-License-Identifier: Apache-2.0"
missing=$((missing+1))
continue
fi
done < <(find . -name '*.py' -type f \
! -path './.venv/*' \
! -path './.git/*' \
! -path './__pycache__/*' \
! -path '*/.pytest_cache/*' \
! -path '*/.eggs/*' \
! -path '*.egg-info/*')
if [ "$missing" -gt 0 ]; then
echo "FAIL: $missing Python files missing or malformed SPDX header"
exit 1
fi
echo "OK: All Python files carry the required SPDX header"
rust:
name: Rust
needs: changes
if: needs.changes.outputs.full_ci == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust components
run: rustup component add rustfmt clippy
- name: cargo fmt
run: cargo fmt --all --check
- name: cargo clippy
run: cargo clippy --workspace --all-targets -- -D warnings
- name: cargo test
run: cargo test --workspace
typecheck:
name: Typecheck (mypy)
needs: changes
if: needs.changes.outputs.full_ci == 'true'
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
python-version: "3.12"
enable-cache: true
cache-dependency-glob: "uv.lock"
- run: uv sync --locked
- name: mypy
run: uv run mypy switchyard
# Unit tests on supported Python versions. Integration/e2e tests are
# intentionally deselected here because they may need Docker, live provider
# credentials, or network egress.
test:
name: Tests (Python ${{ matrix.python-version }})
needs: changes
if: needs.changes.outputs.full_ci == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-dependency-glob: "uv.lock"
- run: uv sync --locked
- name: pytest
run: uv run pytest tests/ -v -m "not integration"
# Regression guard: the default install of nemo-switchyard (without extras)
# must NOT drag in torch / transformers / routellm / litellm /
# openai-agents / docx. Catches accidental top-level imports of
# heavy packages before they hit users as ~1 GB of install bloat.
#
# This is the actual hard gate on the workflow.
slim-install-smoke:
name: Slim install smoke test
needs: changes
if: needs.changes.outputs.full_ci == 'true'
runs-on: ubuntu-latest
env:
SWITCHYARD_DEFAULT_PACKAGE: "nemo-switchyard @ file://${{ github.workspace }}"
SWITCHYARD_EXTRAS_PACKAGE: "nemo-switchyard[cli] @ file://${{ github.workspace }}"
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
python-version: "3.12"
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Verify slim install (no extras) succeeds
working-directory: /tmp
run: |
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 \
--with "${SWITCHYARD_DEFAULT_PACKAGE}" \
python -c "import switchyard; print('import OK, version:', switchyard.__version__)"
- name: Assert heavy packages absent from slim install
working-directory: /tmp
run: |
uv run --isolated --no-project --python 3.10 \
--with "${SWITCHYARD_DEFAULT_PACKAGE}" \
python - <<'PY'
import importlib.util
import sys
forbidden = [
"torch",
"transformers",
"huggingface_hub",
"routellm",
"litellm",
"datasets",
"tokenizers",
"safetensors",
"pyarrow",
"agents",
"mcp",
"docx",
]
extras = [m for m in forbidden if importlib.util.find_spec(m) is not None]
if extras:
sys.exit(f"FAIL: heavy packages pulled into slim install: {extras}")
print(f"OK: all {len(forbidden)} heavy packages absent from slim install.")
PY
- name: Verify CLI works with its extra
working-directory: /tmp
run: |
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.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.10 \
--with "${SWITCHYARD_EXTRAS_PACKAGE}" \
switchyard launch claude --help 2>&1 | grep -q -- '--config' || { echo 'FAIL: --config flag missing from help'; exit 1; }
# Single required check for branch protection. Gates on every job above so
# the ruleset never needs editing when jobs are added or renamed.
ci-success:
name: CI Success
if: always()
needs: [changes, lint, spdx-headers, rust, typecheck, test, slim-install-smoke]
runs-on: ubuntu-latest
steps:
- name: Verify all jobs succeeded
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: exit 1