diff --git a/.github/actions/rust-bridge/action.yml b/.github/actions/rust-bridge/action.yml new file mode 100644 index 00000000000..99002d4f287 --- /dev/null +++ b/.github/actions/rust-bridge/action.yml @@ -0,0 +1,19 @@ +name: Set up the Rust bridge +description: Select the shared Rust bridge artifact or the Cargo cache +inputs: + artifact: + description: Rust bridge artifact name + required: false + default: "" +runs: + using: composite + steps: + - name: Restore the Cargo build cache + if: inputs.artifact == '' + uses: ./.github/actions/cache-cargo-build + - name: Download the Rust bridge artifact + if: inputs.artifact != '' + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e + with: + name: ${{ inputs.artifact }} + path: rust-bridge-dist diff --git a/.github/workflows/_test-unit-base.yml b/.github/workflows/_test-unit-base.yml index 7db01d721cf..851230be849 100644 --- a/.github/workflows/_test-unit-base.yml +++ b/.github/workflows/_test-unit-base.yml @@ -69,6 +69,11 @@ on: description: "Unique name for the coverage artifact (must be unique per run)" required: true type: string + rust-bridge-artifact: + description: "Prebuilt editable Rust bridge artifact" + required: false + type: string + default: "" permissions: contents: read @@ -118,17 +123,28 @@ jobs: timeout-minutes: 5 uses: ./.github/actions/cache-uv-downloads - - name: Cache the Rust build + - name: Set up the Rust build if: steps.changes.outputs.decision != 'skip' timeout-minutes: 5 - uses: ./.github/actions/cache-cargo-build + uses: ./.github/actions/rust-bridge + with: + artifact: ${{ inputs.rust-bridge-artifact }} - name: Install dependencies if: steps.changes.outputs.decision != 'skip' timeout-minutes: 8 + env: + RUST_BRIDGE_ARTIFACT: ${{ inputs.rust-bridge-artifact }} run: | diff -u model_prices_and_context_window.json litellm/model_prices_and_context_window_backup.json - .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 + 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 + 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 --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'))" + fi 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 diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index 5f9ea430069..6f0da90fd66 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -21,10 +21,8 @@ concurrency: # files that each wrapped a single call to _test-unit-base.yml. Adding a shard is # now one matrix entry rather than a new file. # -# `name` is the shard id and nothing else, so each check reports as -# " / Run tests" exactly as it did when the shard had its own file. Those -# strings are the branch ruleset's required contexts, so they are load-bearing: -# renaming an entry renames a required check and the ruleset stops matching it. +# `name` is the shard id, and each check reports as " / Run tests". +# Unit shard names are not required ruleset contexts, so matrix entries can be split freely. # # Every entry states its timeouts even when they equal the base workflow's # defaults. An absent matrix key renders as an empty string, which is not a @@ -36,8 +34,101 @@ concurrency: # Folding it in here is a follow-up, together with generalising that guard into # assert_ci_coverage.py. jobs: + rust-bridge: + name: Build the Rust bridge + outputs: + artifact: ${{ steps.rust-bridge-artifact.outputs.name }} + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + env: + UV_PYTHON: "3.12" + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 + with: + persist-credentials: false + + - name: Detect relevant changes + id: changes + timeout-minutes: 2 + uses: ./.github/actions/detect-changes + + - name: Define editable Rust bridge cache key + 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') }} + run: echo "key=$RUST_BRIDGE_CACHE_KEY" >> "$GITHUB_OUTPUT" + + - name: Set up Python + if: steps.changes.outputs.decision != 'skip' + timeout-minutes: 3 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: ${{ env.UV_PYTHON }} + + - 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: Restore editable Rust bridge + id: rust-bridge-cache + if: steps.changes.outputs.decision != 'skip' + timeout-minutes: 5 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: rust-bridge-dist + key: ${{ steps.rust-bridge-key.outputs.key }} + + - name: Cache the Rust build + if: steps.changes.outputs.decision != 'skip' && steps.rust-bridge-cache.outputs.cache-hit != 'true' + timeout-minutes: 5 + uses: ./.github/actions/cache-cargo-build + + - name: Build editable Rust bridge + if: steps.changes.outputs.decision != 'skip' && steps.rust-bridge-cache.outputs.cache-hit != 'true' + 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 + + - name: Upload Rust bridge artifact + if: steps.changes.outputs.decision != 'skip' + timeout-minutes: 10 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 + with: + name: rust-bridge-${{ github.run_id }}-${{ github.run_attempt }} + path: rust-bridge-dist/ + retention-days: 1 + + - name: Save editable Rust bridge + if: steps.changes.outputs.decision != 'skip' && github.ref == 'refs/heads/main' && steps.rust-bridge-cache.outputs.cache-hit != 'true' + timeout-minutes: 10 + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: rust-bridge-dist + key: ${{ steps.rust-bridge-key.outputs.key }} + + - name: Expose the Rust bridge artifact + id: rust-bridge-artifact + if: steps.changes.outputs.decision != 'skip' + timeout-minutes: 1 + env: + RUN_ID: ${{ github.run_id }} + RUN_ATTEMPT: ${{ github.run_attempt }} + run: echo "name=rust-bridge-${RUN_ID}-${RUN_ATTEMPT}" >> "$GITHUB_OUTPUT" + unit: name: ${{ matrix.shard }} + needs: rust-bridge + if: ${{ !cancelled() }} permissions: contents: read id-token: write @@ -51,8 +142,8 @@ jobs: test-path: >- tests/unit/decisions tests/unit/litellm_core_utils - workers: 2 - reruns: 1 + workers: 4 + reruns: 0 timeout-minutes: 20 job-timeout-minutes: 60 @@ -74,7 +165,7 @@ jobs: tests/unit/enterprise/proxy/test_file_deletion_blocking.py tests/unit/enterprise/proxy/test_managed_files_access_check.py tests/unit/enterprise/proxy/test_managed_files_hook.py - workers: 2 + workers: 4 reruns: 2 timeout-minutes: 20 job-timeout-minutes: 60 @@ -85,8 +176,8 @@ jobs: tests/test_litellm/integrations tests/test_litellm/tracing tests/unit/integrations - workers: 2 - reruns: 3 + workers: 4 + reruns: 0 timeout-minutes: 20 job-timeout-minutes: 60 @@ -94,8 +185,8 @@ jobs: artifact-name: llm-vertex-ai test-path: >- tests/unit/llms/vertex_ai - workers: 1 - reruns: 2 + workers: 4 + reruns: 0 timeout-minutes: 20 job-timeout-minutes: 60 @@ -104,9 +195,21 @@ jobs: test-path: >- tests/unit/llms --ignore=tests/unit/llms/vertex_ai + --ignore=tests/unit/llms/openai + --ignore=tests/unit/llms/meta --ignore=tests/unit/llms/base_llm/batches/base_batches_config_test.py - workers: 2 - reruns: 2 + workers: 4 + reruns: 0 + timeout-minutes: 20 + job-timeout-minutes: 60 + + - shard: OpenAI and Meta Providers + artifact-name: llm-openai-meta + test-path: >- + tests/unit/llms/openai + tests/unit/llms/meta + workers: 4 + reruns: 0 timeout-minutes: 20 job-timeout-minutes: 60 @@ -115,6 +218,14 @@ jobs: test-path: >- tests/test_litellm/test_*.py tests/unit/test_*.py + workers: 4 + reruns: 2 + timeout-minutes: 20 + job-timeout-minutes: 60 + + - shard: misc-dirs + artifact-name: misc-dirs + test-path: >- tests/unit/test_router tests/unit/a2a_protocol tests/unit/batches @@ -135,8 +246,8 @@ jobs: tests/unit/vector_stores tests/unit/videos --ignore=tests/unit/rust_bridge/native_route_wheel_test.py - workers: 2 - reruns: 2 + workers: 4 + reruns: 0 timeout-minutes: 20 job-timeout-minutes: 60 @@ -144,9 +255,6 @@ jobs: artifact-name: proxy-auth test-path: >- tests/unit/proxy/auth - tests/unit/proxy/hooks - tests/unit/proxy/policy_engine - tests/unit/proxy/client --ignore=tests/unit/proxy/auth/test_auth_checks.py --ignore=tests/unit/proxy/auth/test_user_api_key_auth.py --ignore=tests/unit/proxy/auth/test_default_end_user_budget_simple.py @@ -154,35 +262,56 @@ jobs: --ignore=tests/unit/proxy/auth/test_models_fallback_endpoint.py --ignore=tests/unit/proxy/auth/test_multipart_bypass_repro.py --ignore=tests/unit/proxy/auth/test_proxy_routes.py + workers: 4 + reruns: 0 + timeout-minutes: 20 + job-timeout-minutes: 60 + + - shard: proxy-hooks-client + artifact-name: proxy-hooks-client + test-path: >- + tests/unit/proxy/hooks + tests/unit/proxy/policy_engine + tests/unit/proxy/client --ignore=tests/unit/proxy/hooks/test_banned_keyword_list.py --ignore=tests/unit/proxy/hooks/test_unit_test_max_model_budget_limiter.py - workers: 2 - reruns: 2 + workers: 4 + reruns: 0 timeout-minutes: 20 job-timeout-minutes: 60 - shard: proxy-endpoints artifact-name: proxy-endpoints test-path: >- + tests/unit/proxy/management_endpoints + tests/unit/proxy/management_helpers + tests/unit/proxy/list_api tests/unit/proxy/analytics_endpoints tests/unit/proxy/decisions_endpoints - tests/unit/proxy/management_endpoints - tests/unit/proxy/list_api tests/unit/proxy/memory - tests/unit/proxy/guardrails - tests/unit/proxy/management_helpers + tests/unit/proxy/agent_endpoints + tests/unit/proxy/openai_files_endpoint + tests/unit/proxy/health_endpoints + tests/unit/proxy/batches_endpoints --ignore=tests/unit/proxy/management_endpoints/test_jwt_key_mapping.py --ignore=tests/unit/proxy/management_endpoints/test_key_generate_prisma.py --ignore=tests/unit/proxy/management_endpoints/test_roi_calculator_endpoints.py --ignore=tests/unit/proxy/management_helpers/test_audit_logs_proxy.py + workers: 4 + reruns: 0 + timeout-minutes: 20 + job-timeout-minutes: 60 + + - shard: proxy-feature-endpoints + artifact-name: proxy-feature-endpoints + test-path: >- + tests/unit/proxy/guardrails --ignore=tests/unit/proxy/google_endpoints/test_gemini_agents_endpoints.py --ignore=tests/unit/proxy/google_endpoints/test_google_endpoint_routing.py --ignore=tests/unit/proxy/google_endpoints/test_google_gemini_proxy_request.py --ignore=tests/unit/proxy/public_endpoints/test_blog_posts_endpoint.py tests/unit/proxy/anthropic_endpoints tests/unit/proxy/google_endpoints - tests/unit/proxy/openai_files_endpoint - tests/unit/proxy/batches_endpoints tests/unit/proxy/container_endpoints tests/unit/proxy/fine_tuning_endpoints tests/unit/proxy/vector_store_files_endpoints @@ -192,11 +321,9 @@ jobs: tests/unit/proxy/ocr_endpoints tests/unit/proxy/search_endpoints tests/unit/proxy/vector_store_endpoints - tests/unit/proxy/agent_endpoints tests/unit/proxy/a2a tests/unit/proxy/credential_endpoints tests/unit/proxy/discovery_endpoints - tests/unit/proxy/health_endpoints tests/unit/proxy/shutdown tests/unit/proxy/public_endpoints tests/unit/proxy/prompts @@ -207,7 +334,7 @@ jobs: tests/unit/proxy/config_resolvers tests/unit/proxy/utils workers: 4 - reruns: 2 + reruns: 0 timeout-minutes: 20 job-timeout-minutes: 60 @@ -215,7 +342,7 @@ jobs: artifact-name: proxy-server test-path: "tests/unit/proxy/proxy_server" workers: 4 - reruns: 2 + reruns: 0 timeout-minutes: 60 job-timeout-minutes: 100 @@ -247,7 +374,7 @@ jobs: tests/unit/proxy/management_endpoints/test_roi_calculator_endpoints.py tests/unit/proxy/roi_calculator workers: 4 - reruns: 2 + reruns: 0 timeout-minutes: 20 job-timeout-minutes: 60 @@ -281,7 +408,7 @@ jobs: --ignore=tests/unit/proxy/test_update_spend.py --ignore=tests/unit/proxy/test_zero_cost_model_budget_bypass.py workers: 4 - reruns: 2 + reruns: 0 timeout-minutes: 20 job-timeout-minutes: 60 @@ -290,7 +417,7 @@ jobs: test-path: >- tests/unit/caching workers: 2 - reruns: 2 + reruns: 0 timeout-minutes: 20 job-timeout-minutes: 60 @@ -299,7 +426,7 @@ jobs: test-path: >- tests/unit/litellm_proxy_extras workers: 2 - reruns: 2 + reruns: 0 timeout-minutes: 20 job-timeout-minutes: 60 @@ -309,16 +436,25 @@ jobs: tests/unit/enterprise/integrations tests/unit/enterprise/proxy/auth tests/unit/enterprise/proxy/guardrails - tests/unit/enterprise/proxy/hooks tests/unit/enterprise/proxy/management_endpoints tests/unit/enterprise/proxy/test_audit_logging_endpoints.py tests/unit/enterprise/proxy/test_liteadmin.py tests/unit/enterprise/enterprise_callbacks/test_prometheus_logging_callbacks.py workers: 4 - reruns: 2 + reruns: 0 timeout-minutes: 20 job-timeout-minutes: 60 + - shard: enterprise-managed-files + artifact-name: enterprise-managed-files + test-path: >- + tests/unit/enterprise/proxy/hooks + workers: 4 + reruns: 0 + timeout-minutes: 20 + job-timeout-minutes: 60 + dist: load + - shard: responses-caching-types artifact-name: responses-caching-types test-path: >- @@ -326,7 +462,7 @@ jobs: tests/unit/types --ignore=tests/unit/responses/mcp workers: 2 - reruns: 2 + reruns: 0 timeout-minutes: 20 job-timeout-minutes: 60 @@ -353,9 +489,11 @@ jobs: job-timeout-minutes: 60 uses: ./.github/workflows/_test-unit-base.yml with: + rust-bridge-artifact: ${{ needs.rust-bridge.outputs.artifact }} test-path: ${{ matrix.test-path }} workers: ${{ matrix.workers }} reruns: ${{ matrix.reruns }} timeout-minutes: ${{ matrix.timeout-minutes }} job-timeout-minutes: ${{ matrix.job-timeout-minutes }} + dist: ${{ matrix.dist || 'loadscope' }} artifact-name: ${{ matrix.artifact-name }}