diff --git a/.github/workflows/_test-unit-base.yml b/.github/workflows/_test-unit-base.yml index c4045a08ffb..46baf2ac26f 100644 --- a/.github/workflows/_test-unit-base.yml +++ b/.github/workflows/_test-unit-base.yml @@ -57,9 +57,15 @@ permissions: jobs: run: - name: Run tests + name: ${{ matrix.python-version == '3.12' && 'Run tests' || format('Run tests (Python {0})', matrix.python-version) }} runs-on: ubuntu-latest timeout-minutes: ${{ inputs.job-timeout-minutes }} + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + env: + UV_PYTHON: ${{ matrix.python-version }} permissions: contents: read pull-requests: read @@ -82,7 +88,7 @@ jobs: timeout-minutes: 3 uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: - python-version: "3.12" + python-version: ${{ matrix.python-version }} - name: Set up uv if: steps.changes.outputs.decision != 'skip' @@ -99,9 +105,9 @@ jobs: path: | ~/.cache/uv .venv - key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }} + key: ${{ runner.os }}-uv-py${{ matrix.python-version }}-${{ hashFiles('uv.lock') }} restore-keys: | - ${{ runner.os }}-uv- + ${{ runner.os }}-uv-py${{ matrix.python-version }}- - name: Cache the Rust build if: steps.changes.outputs.decision != 'skip' @@ -113,6 +119,7 @@ jobs: timeout-minutes: 8 run: | .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml + uv run --no-sync python -c 'import os, sys; print(sys.version); assert f"{sys.version_info.major}.{sys.version_info.minor}" == os.environ["UV_PYTHON"]' - name: Cache Prisma binaries if: steps.changes.outputs.decision != 'skip' @@ -134,13 +141,7 @@ jobs: WORKERS: ${{ inputs.workers }} RERUNS: ${{ inputs.reruns }} DIST: ${{ inputs.dist }} - # coverage.py's sys.monitoring backend (PEP 669), the cheapest core it has. - # It is only the default from Python 3.14, and these shards run 3.12, so it - # has to be asked for. Coverage refuses it when branch measurement is on - # (`branch_right_left` needs > 3.14.0a5) and falls back to the slow core with - # a `no-sysmon` warning, so turning on `branch = true` here means giving this - # back until the runners move to 3.14. - COVERAGE_CORE: sysmon + COVERAGE_CORE: ${{ contains(fromJSON('["3.10", "3.11"]'), matrix.python-version) && 'ctrace' || 'sysmon' }} run: | if [ "${WORKERS}" = "0" ]; then uv run --no-sync pytest ${TEST_PATH:?} \ @@ -167,7 +168,7 @@ jobs: fi - name: Save coverage report - if: always() && steps.changes.outputs.decision != 'skip' + if: always() && matrix.python-version == '3.12' && steps.changes.outputs.decision != 'skip' uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: coverage-${{ inputs.artifact-name }}-${{ github.run_id }}-${{ github.run_attempt }} diff --git a/.github/workflows/test-python-compatibility.yml b/.github/workflows/test-python-compatibility.yml new file mode 100644 index 00000000000..d3a89d7141c --- /dev/null +++ b/.github/workflows/test-python-compatibility.yml @@ -0,0 +1,105 @@ +name: Python Compatibility + +on: + pull_request: + branches: + - main + - litellm_internal_staging + - litellm_oss_staging + - "litellm_**" + push: + branches: + - main + - litellm_internal_staging + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + compatibility: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: read + pull-requests: read + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + env: + UV_PYTHON: ${{ matrix.python-version }} + LITELLM_LOCAL_MODEL_COST_MAP: "True" + + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + timeout-minutes: 3 + with: + persist-credentials: false + + - name: Detect relevant changes + id: changes + timeout-minutes: 2 + uses: ./.github/actions/detect-changes + + - name: Set up Python + if: steps.changes.outputs.decision != 'skip' + timeout-minutes: 3 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Set up uv + if: steps.changes.outputs.decision != 'skip' + timeout-minutes: 3 + uses: ./.github/actions/setup-uv-with-retries + with: + version: "0.10.9" + + - name: Cache uv dependencies + if: steps.changes.outputs.decision != 'skip' + timeout-minutes: 5 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.cache/uv + key: ${{ runner.os }}-uv-compat-py${{ matrix.python-version }}-${{ hashFiles('uv.lock') }} + restore-keys: | + ${{ runner.os }}-uv-compat-py${{ matrix.python-version }}- + + - name: Cache the Rust build + if: steps.changes.outputs.decision != 'skip' + timeout-minutes: 5 + uses: ./.github/actions/cache-cargo-build + + - name: Install SDK and proxy dependencies + id: install + if: steps.changes.outputs.decision != 'skip' + timeout-minutes: 8 + run: | + .github/scripts/uv_sync_with_retries.sh --frozen --no-dev --extra proxy + uv run --no-sync python -c 'import os, sys; print(sys.version); assert f"{sys.version_info.major}.{sys.version_info.minor}" == os.environ["UV_PYTHON"]' + + - name: Compile package + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + timeout-minutes: 2 + run: uv run --no-sync python -m compileall -q litellm + + - name: Import SDK + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + timeout-minutes: 2 + run: uv run --no-sync python -c 'from litellm import *; print("SDK import passed")' + + - name: Import proxy + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + timeout-minutes: 2 + run: uv run --no-sync python -c 'import litellm.proxy.proxy_server; print("Proxy import passed")' + + - name: Construct a completion response + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + timeout-minutes: 2 + run: uv run --no-sync python -c 'from litellm.types.utils import ModelResponse; ModelResponse(); print("Response construction passed")'