ci: split slow unit shards and build the Rust bridge once per run (#44622)

* ci: split slow unit shards and build the Rust bridge once per run

* ci: key the Rust bridge cache on source files only

* ci: keep the unit setup ceiling unchanged with the shared Rust bridge

* ci: fall back to the Cargo cache when the Rust bridge artifact is missing

* ci: keep reruns on enterprise-routing for the prompt caching flake

---------

Co-authored-by: yuneng <yuneng@berri.ai>
This commit is contained in:
devin-ai-integration[bot] 2026-10-05 23:43:01 +00:00 • committed by GitHub
parent c29b42a32b
commit 169af2f883
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 213 additions and 40 deletions

19
.github/actions/rust-bridge/action.yml vendored Normal file
View file

@ -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

View file

@ -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

View file

@ -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
# "<shard> / 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 "<shard> / 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 }}