diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 00000000000..e6afd7ff530
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,19 @@
+[http]
+# CI has seen transient crates.io failures from libcurl's HTTP/2 multiplexing
+# during `maturin` metadata resolution. Disable multiplexing and retry more
+# aggressively so editable `uv sync` builds are not failed by one flaky frame.
+multiplexing = false
+
+[net]
+retry = 5
+
+# PyO3 cdylib (`litellm-python-bridge`) links against the host interpreter's
+# symbols, which are not present at link time when building an extension module.
+# On macOS, tell the linker to resolve undefined `_Py*` symbols dynamically at
+# load time (the standard pyo3 extension-module flag) so the cdylib links without
+# a libpython on the link line.
+[target.x86_64-apple-darwin]
+rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"]
+
+[target.aarch64-apple-darwin]
+rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"]
diff --git a/.circleci/config.yml b/.circleci/config.yml
index abcdbf45187..f13e9bf66f1 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -190,6 +190,8 @@ jobs:
working_directory: ~/project
environment:
UV_PYTHON: "3.11"
+ CARGO_HTTP_MULTIPLEXING: "false"
+ CARGO_NET_RETRY: "5"
steps:
- checkout
- run:
@@ -205,6 +207,24 @@ jobs:
environment:
UV_HTTP_TIMEOUT: "300"
command: |
+ $rustupInit = Join-Path $env:TEMP "rustup-init.exe"
+ $rustupVersion = "1.28.2"
+ $rustupUrl = "https://static.rust-lang.org/rustup/archive/$rustupVersion/x86_64-pc-windows-msvc/rustup-init.exe"
+ Invoke-WebRequest -Uri $rustupUrl -OutFile $rustupInit
+ $rustupExpected = "88d8258dcf6ae4f7a80c7d1088e1f36fa7025a1cfd1343731b4ee6f385121fc0"
+ $rustupActual = (Get-FileHash -Path $rustupInit -Algorithm SHA256).Hash.ToLower()
+ if ($rustupActual -ne $rustupExpected) {
+ throw "rustup installer hash mismatch: expected $rustupExpected got $rustupActual"
+ }
+ & $rustupInit -y --profile minimal --default-toolchain stable
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+ }
+ Remove-Item $rustupInit
+ $cargoBin = Join-Path $HOME ".cargo\bin"
+ $env:Path = "$cargoBin;$env:Path"
+ rustc --version
+ cargo --version
$installer = Join-Path $env:TEMP "uv-install.ps1"
Invoke-WebRequest -Uri https://astral.sh/uv/0.10.9/install.ps1 -OutFile $installer
$expected = "d43ffff8d28e7d1e7d1831a212465f12b24a43c7f87f386e95e2a5915aee5d7d"
@@ -222,7 +242,20 @@ jobs:
if (-not (Select-String -Path $PROFILE -SimpleMatch $uvBin -Quiet)) {
Add-Content -Path $PROFILE -Value "`$env:Path = `"$uvBin;`$env:Path`""
}
- uv sync --frozen --group dev --python 3.11
+ if (-not (Select-String -Path $PROFILE -SimpleMatch $cargoBin -Quiet)) {
+ Add-Content -Path $PROFILE -Value "`$env:Path = `"$cargoBin;`$env:Path`""
+ }
+ for ($attempt = 1; $attempt -le 5; $attempt++) {
+ Write-Host "uv sync attempt $attempt/5"
+ uv sync --frozen --group dev --python 3.11
+ if ($LASTEXITCODE -eq 0) {
+ break
+ }
+ if ($attempt -eq 5) {
+ exit $LASTEXITCODE
+ }
+ Start-Sleep -Seconds 15
+ }
- run:
name: Run Windows-specific test
command: |
@@ -232,6 +265,9 @@ jobs:
environment:
UV_HTTP_TIMEOUT: "300"
command: |
+ $env:Path = "$HOME\.cargo\bin;$HOME\.local\bin;$env:Path"
+ cargo --version
+ Get-ChildItem -Path "litellm\rust_bridge" -Filter "_native*" -File -ErrorAction SilentlyContinue | Remove-Item -Force
uv build --wheel --out-dir dist
uv run --no-sync python tests/windows_tests/check_windows_wheel_install.py
@@ -1020,7 +1056,9 @@ jobs:
name: Run tests
command: |
mkdir -p test-results
- TEST_FILES=$(circleci tests glob "tests/ocr_tests/**/test_*.py")
+ TEST_FILES=$(printf "%s\n%s\n" \
+ "$(circleci tests glob "tests/ocr_tests/**/test_*.py")" \
+ "tests/test_litellm/ocr/test_rust_bridge.py")
echo "$TEST_FILES" | circleci tests run \
--verbose \
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
diff --git a/.dockerignore b/.dockerignore
index a487d2a859a..f3a80fee3e4 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -49,6 +49,10 @@ build/
*.egg-info/
.DS_Store
**/node_modules
+ui/litellm-dashboard/.next
+ui/litellm-dashboard/out
+litellm-rust/target/
+litellm/rust_bridge/_native*.so
*.log
.env
.env.local
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 23b520e2ad5..7e705ec4f8f 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -11,3 +11,9 @@
# style(ui): run prettier --write across the dashboard (#29622)
7edf3a9cb55548b143df1692f4ed7c4681d7fcf7
+
+# style: reformat litellm/ with ruff format (#31317)
+430b5b8f1b12dc261a49fda99ac5d1b22381a428
+
+# style: unify ruff format width on 120 (#31518)
+3dfbeabe626d203ac9de86024519d9a96c484ce4
diff --git a/.github/deploy-on-aws.png b/.github/deploy-on-aws.png
new file mode 100644
index 00000000000..06d41f2a5e0
Binary files /dev/null and b/.github/deploy-on-aws.png differ
diff --git a/.github/deploy-on-gcp.png b/.github/deploy-on-gcp.png
new file mode 100644
index 00000000000..e831a8c2e4e
Binary files /dev/null and b/.github/deploy-on-gcp.png differ
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 9658baeb89a..12ad124fa20 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,17 +1,17 @@
## Relevant issues
-
+
## Linear ticket
-
+
## Pre-Submission checklist
**Please complete all items before asking a LiteLLM maintainer to review your PR**
- [ ] I have added meaningful tests
-- [ ] My PR passes all unit tests on [`make test-unit`](https://docs.litellm.ai/docs/extras/contributing_code)
+- [ ] My PR passes all CI/CD checks (e.g., lint, format, unit tests)
- [ ] My PR's scope is as isolated as possible; it only solves 1 specific problem
- [ ] I have requested a Greptile review by commenting `@greptileai` and received a **Confidence Score of at least 4/5** before requesting a maintainer review
@@ -19,29 +19,13 @@
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on [Slack (#pr-review)](https://join.slack.com/t/litellmossslack/shared_invite/zt-3o7nkuyfr-p_kbNJj8taRfXGgQI1~YyA).
-## CI (LiteLLM team)
-
-> **CI status guideline:**
->
-> - 50-55 passing tests: main is stable with minor issues.
-> - 45-49 passing tests: acceptable but needs attention
-> - <= 40 passing tests: unstable; be careful with your merges and assess the risk.
-
-- [ ] **Branch creation CI run**
- Link:
-
-- [ ] **CI run for the last commit**
- Link:
-
-- [ ] **Merge / cherry-pick CI run**
- Links:
-
## Screenshots / Proof of Fix
-
+
## Type
diff --git a/.github/scripts/uv_sync_with_retries.sh b/.github/scripts/uv_sync_with_retries.sh
new file mode 100755
index 00000000000..85ed75af566
--- /dev/null
+++ b/.github/scripts/uv_sync_with_retries.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+max_attempts="${UV_SYNC_MAX_ATTEMPTS:-5}"
+delay_seconds="${UV_SYNC_RETRY_DELAY_SECONDS:-15}"
+
+export CARGO_HTTP_MULTIPLEXING="${CARGO_HTTP_MULTIPLEXING:-false}"
+export CARGO_NET_RETRY="${CARGO_NET_RETRY:-5}"
+
+if [[ "$#" -eq 0 ]]; then
+ echo "usage: $0 " >&2
+ exit 2
+fi
+
+for attempt in $(seq 1 "${max_attempts}"); do
+ echo "uv sync attempt ${attempt}/${max_attempts}"
+ status=0
+ if uv sync "$@"; then
+ exit 0
+ else
+ status=$?
+ fi
+
+ if [[ "${attempt}" -eq "${max_attempts}" ]]; then
+ echo "uv sync failed after ${max_attempts} attempts" >&2
+ exit "${status}"
+ fi
+
+ echo "uv sync failed; retrying in ${delay_seconds}s..."
+ sleep "${delay_seconds}"
+done
diff --git a/.github/workflows/_test-unit-base.yml b/.github/workflows/_test-unit-base.yml
index a42b2f8f9df..25c6d4a7019 100644
--- a/.github/workflows/_test-unit-base.yml
+++ b/.github/workflows/_test-unit-base.yml
@@ -73,7 +73,7 @@ jobs:
- name: Install dependencies
run: |
- uv sync --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router
+ .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router
- name: Generate Prisma client
env:
diff --git a/.github/workflows/check-ui-api-types.yml b/.github/workflows/check-ui-api-types.yml
index d8053c15683..439126aa1ee 100644
--- a/.github/workflows/check-ui-api-types.yml
+++ b/.github/workflows/check-ui-api-types.yml
@@ -46,7 +46,7 @@ jobs:
${{ runner.os }}-uv-
- name: Install backend dependencies
- run: uv sync --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router
+ run: .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router
- name: Generate Prisma client
env:
diff --git a/.github/workflows/guard-fork-dependencies.yml b/.github/workflows/guard-fork-dependencies.yml
index bf7282688ef..f4cbdd63cdf 100644
--- a/.github/workflows/guard-fork-dependencies.yml
+++ b/.github/workflows/guard-fork-dependencies.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
paths:
- "uv.lock"
diff --git a/.github/workflows/guard-main-branch.yml b/.github/workflows/guard-main-branch.yml
index 1c1ce0de079..21aad18d298 100644
--- a/.github/workflows/guard-main-branch.yml
+++ b/.github/workflows/guard-main-branch.yml
@@ -31,12 +31,12 @@ jobs:
echo "PR head repo: $HEAD_REPO"
echo "PR head branch: $HEAD_REF"
if [ "$HEAD_REPO" != "$BASE_REPO" ]; then
- echo "::error::PRs to main must originate from the canonical repository ($BASE_REPO), not a fork ($HEAD_REPO). External contributors should open PRs against the 'litellm_oss_branch' branch instead."
+ echo "::error::PRs to main must originate from the canonical repository ($BASE_REPO), not a fork ($HEAD_REPO). External contributors should open PRs against the 'litellm_oss_staging' branch instead."
exit 1
fi
if [ "$HEAD_REF" = "litellm_internal_staging" ] || [[ "$HEAD_REF" == litellm_hotfix_?* ]]; then
echo "Allowed source branch."
exit 0
fi
- echo "::error::PRs to main must originate from 'litellm_internal_staging' or a 'litellm_hotfix_*' branch. Got: '$HEAD_REF'. If this is a contribution, retarget the PR against 'litellm_oss_branch' instead."
+ echo "::error::PRs to main must originate from 'litellm_internal_staging' or a 'litellm_hotfix_*' branch. Got: '$HEAD_REF'. If this is a contribution, retarget the PR against 'litellm_oss_staging' instead."
exit 1
diff --git a/.github/workflows/image-scan.yml b/.github/workflows/image-scan.yml
new file mode 100644
index 00000000000..90ede5a653f
--- /dev/null
+++ b/.github/workflows/image-scan.yml
@@ -0,0 +1,65 @@
+name: Image Scan
+
+on:
+ pull_request:
+ branches:
+ - main
+ - litellm_internal_staging
+ - litellm_oss_branch
+ - "litellm_**"
+ paths:
+ - docker/Dockerfile.non_root
+ - uv.lock
+ - ui/litellm-dashboard/package-lock.json
+ - .github/workflows/image-scan.yml
+ schedule:
+ - cron: "41 6 * * *"
+ workflow_dispatch:
+
+permissions: {}
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ image-scan:
+ name: image-scan
+ runs-on: ubuntu-latest
+ if: >-
+ github.event_name != 'pull_request' ||
+ github.event.pull_request.head.repo.full_name == github.repository
+ timeout-minutes: 30
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+ with:
+ persist-credentials: false
+
+ - name: Download Grype v0.114.0
+ run: |
+ curl -fsSL --retry 3 -o "$RUNNER_TEMP/grype.tar.gz" \
+ https://github.com/anchore/grype/releases/download/v0.114.0/grype_0.114.0_linux_amd64.tar.gz
+ echo "edda0968d8827daab01d32b3cd7de192ae0915005e7bbfcfef9e68e79bc43343 $RUNNER_TEMP/grype.tar.gz" | sha256sum -c -
+ tar xzf "$RUNNER_TEMP/grype.tar.gz" -C "$RUNNER_TEMP" grype
+ chmod +x "$RUNNER_TEMP/grype"
+
+ # Dockerfile.non_root is the rootless variant we ship. The other
+ # Dockerfiles share the same wolfi base and apk set, so OS-layer coverage
+ # is the same; matrix-scan if those variants ever diverge.
+ - name: Build runtime image
+ run: docker build -f docker/Dockerfile.non_root -t litellm-image-scan:${{ github.sha }} .
+
+ # Scans the whole shipped artifact: OS/apk plus every language package
+ # baked into the image, including ones no lockfile declares (e.g. prisma's
+ # vendored node engine) that osv-scan cannot see. osv-scan stays the fast
+ # source-level gate; this is the customer's-eye-view backstop. Credential-
+ # free OSS, run as a pinned, checksum-verified binary; no GitHub Action
+ # dependency and no vendor SaaS callout.
+ - name: Scan image for fixable HIGH/CRITICAL CVEs
+ run: |
+ "$RUNNER_TEMP/grype" litellm-image-scan:${{ github.sha }} \
+ --only-fixed \
+ --fail-on high \
+ --output table
diff --git a/.github/workflows/mutation-test.yml b/.github/workflows/mutation-test.yml
index 8094ca57467..183f12f969c 100644
--- a/.github/workflows/mutation-test.yml
+++ b/.github/workflows/mutation-test.yml
@@ -55,7 +55,7 @@ jobs:
- name: Install dependencies
run: |
- uv sync --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router
+ .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router
- name: Generate Prisma client
env:
diff --git a/.github/workflows/osv-scan.yml b/.github/workflows/osv-scan.yml
index 9dd321f88db..31104002dab 100644
--- a/.github/workflows/osv-scan.yml
+++ b/.github/workflows/osv-scan.yml
@@ -5,13 +5,8 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
- paths:
- - uv.lock
- - ui/litellm-dashboard/package-lock.json
- - osv-scanner.toml
- - .github/workflows/osv-scan.yml
schedule:
- cron: "23 6 * * *"
workflow_dispatch:
diff --git a/.github/workflows/test-code-quality.yml b/.github/workflows/test-code-quality.yml
index 4f09857eb1b..872a1799d98 100644
--- a/.github/workflows/test-code-quality.yml
+++ b/.github/workflows/test-code-quality.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
permissions:
diff --git a/.github/workflows/test-linting.yml b/.github/workflows/test-linting.yml
index de7e1b68346..f093caef073 100644
--- a/.github/workflows/test-linting.yml
+++ b/.github/workflows/test-linting.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
permissions:
@@ -14,7 +14,7 @@ permissions:
jobs:
lint:
runs-on: ubuntu-latest
- timeout-minutes: 10
+ timeout-minutes: 15
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
@@ -50,11 +50,16 @@ jobs:
run: |
uv sync --frozen
- - name: Check Black formatting
+ - name: Check ruff format
+ env:
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
- cd litellm
- uv run --no-sync black --check --exclude '/enterprise/' .
- cd ..
+ git diff --name-only "$BASE_SHA"...HEAD -- 'litellm/**/*.py' | grep -v '^litellm/enterprise/' > "$RUNNER_TEMP/ruff_format_files.txt" || true
+ if [ ! -s "$RUNNER_TEMP/ruff_format_files.txt" ]; then
+ echo "No changed litellm Python files to check with ruff format."
+ exit 0
+ fi
+ xargs uv run --no-sync ruff format --check --exclude '/enterprise/' < "$RUNNER_TEMP/ruff_format_files.txt"
- name: Debug - Check file state
run: |
@@ -87,9 +92,11 @@ jobs:
run: |
uv run --no-sync python -c "import openai; print(f'OpenAI version: {openai.__version__}')"
- - name: Run basedpyright type checking
+ - name: Check basedpyright budget (delta vs base)
+ env:
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
- (uv run --no-sync basedpyright --outputjson || true) | uv run --no-sync python scripts/type_check_gate.py
+ (uv run --no-sync basedpyright --outputjson || true) | uv run --no-sync python scripts/type_check_gate.py --base "$BASE_SHA"
- name: Check for circular imports
run: |
diff --git a/.github/workflows/test-litellm-ui-build.yml b/.github/workflows/test-litellm-ui-build.yml
index b83119712a7..ce8d8cb9c95 100644
--- a/.github/workflows/test-litellm-ui-build.yml
+++ b/.github/workflows/test-litellm-ui-build.yml
@@ -7,7 +7,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
jobs:
@@ -111,4 +111,4 @@ jobs:
if: ${{ !cancelled() && steps.changed.outputs.has_files == 'true' }}
run: |
npx eslint . -f json -o "$RUNNER_TEMP/lint-report.json" || true
- node scripts/check-lint-budgets.mjs "$RUNNER_TEMP/lint-report.json" eslint-budgets.json
+ node scripts/check-lint-budgets.mjs "$RUNNER_TEMP/lint-report.json" eslint-budgets.json --check eslint-metrics.json
diff --git a/.github/workflows/test-mcp.yml b/.github/workflows/test-mcp.yml
index 2ae60951afc..5b5290880c1 100644
--- a/.github/workflows/test-mcp.yml
+++ b/.github/workflows/test-mcp.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
permissions:
@@ -39,7 +39,7 @@ jobs:
- name: Install dependencies
run: |
uv lock --check
- uv sync --frozen --group proxy-dev --extra proxy --extra semantic-router
+ .github/scripts/uv_sync_with_retries.sh --frozen --group proxy-dev --extra proxy --extra semantic-router
- name: Run MCP tests
run: |
diff --git a/.github/workflows/test-model-map.yaml b/.github/workflows/test-model-map.yaml
index 49821fca3a8..b2170d9f6a4 100644
--- a/.github/workflows/test-model-map.yaml
+++ b/.github/workflows/test-model-map.yaml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
permissions:
diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml
new file mode 100644
index 00000000000..13e1dc4ad5e
--- /dev/null
+++ b/.github/workflows/test-rust.yml
@@ -0,0 +1,65 @@
+name: LiteLLM Rust
+
+on:
+ push:
+ paths:
+ - "litellm-rust/**"
+ - ".github/workflows/test-rust.yml"
+ pull_request:
+ branches:
+ - main
+ - litellm_internal_staging
+ - litellm_oss_staging
+ - "litellm_**"
+ paths:
+ - "litellm-rust/**"
+ - ".github/workflows/test-rust.yml"
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ rust-checks:
+ name: rustfmt, clippy, test
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ defaults:
+ run:
+ working-directory: litellm-rust
+ env:
+ CARGO_TERM_COLOR: always
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+ with:
+ persist-credentials: false
+
+ - name: Set up Rust
+ run: |
+ rustup toolchain install stable --profile minimal --component clippy,rustfmt
+ rustup default stable
+
+ - name: Cache Cargo registry and target
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ litellm-rust/target
+ key: ${{ runner.os }}-cargo-${{ hashFiles('litellm-rust/Cargo.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-cargo-
+
+ - name: Check Rust formatting
+ run: cargo fmt --check
+
+ - name: Run Clippy
+ run: cargo clippy --workspace --all-targets --locked -- -D warnings
+
+ - name: Run Rust tests
+ run: cargo test --workspace --locked
diff --git a/.github/workflows/test-semgrep.yml b/.github/workflows/test-semgrep.yml
index 2ba23e44da8..f0dcb9887be 100644
--- a/.github/workflows/test-semgrep.yml
+++ b/.github/workflows/test-semgrep.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
permissions:
diff --git a/.github/workflows/test-unit-core-utils.yml b/.github/workflows/test-unit-core-utils.yml
index da1267756cd..d6d6353238f 100644
--- a/.github/workflows/test-unit-core-utils.yml
+++ b/.github/workflows/test-unit-core-utils.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
permissions:
diff --git a/.github/workflows/test-unit-documentation.yml b/.github/workflows/test-unit-documentation.yml
index b2a8640223a..4cef791a9b3 100644
--- a/.github/workflows/test-unit-documentation.yml
+++ b/.github/workflows/test-unit-documentation.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
permissions:
@@ -54,7 +54,7 @@ jobs:
- name: Install dependencies
run: |
- uv sync --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router
+ .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router
- name: Generate Prisma client
env:
diff --git a/.github/workflows/test-unit-enterprise-routing.yml b/.github/workflows/test-unit-enterprise-routing.yml
index ffc09dd8f94..13136c968d1 100644
--- a/.github/workflows/test-unit-enterprise-routing.yml
+++ b/.github/workflows/test-unit-enterprise-routing.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
permissions:
diff --git a/.github/workflows/test-unit-integrations.yml b/.github/workflows/test-unit-integrations.yml
index b316ad5dfdf..c95ed4e7c24 100644
--- a/.github/workflows/test-unit-integrations.yml
+++ b/.github/workflows/test-unit-integrations.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
permissions:
diff --git a/.github/workflows/test-unit-llm-providers.yml b/.github/workflows/test-unit-llm-providers.yml
index 2a1912ce92d..df78564ab0c 100644
--- a/.github/workflows/test-unit-llm-providers.yml
+++ b/.github/workflows/test-unit-llm-providers.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
permissions:
diff --git a/.github/workflows/test-unit-misc.yml b/.github/workflows/test-unit-misc.yml
index a7363ac3b43..133c135d97a 100644
--- a/.github/workflows/test-unit-misc.yml
+++ b/.github/workflows/test-unit-misc.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
permissions:
@@ -32,7 +32,9 @@ jobs:
tests/test_litellm/repositories
tests/test_litellm/images
tests/test_litellm/interactions
+ tests/test_litellm/ocr
tests/test_litellm/passthrough
+ tests/test_litellm/sandbox
tests/test_litellm/vector_stores
tests/test_litellm/test_*.py
workers: 2
diff --git a/.github/workflows/test-unit-proxy-auth.yml b/.github/workflows/test-unit-proxy-auth.yml
index 99882066a8e..97dfaed6e81 100644
--- a/.github/workflows/test-unit-proxy-auth.yml
+++ b/.github/workflows/test-unit-proxy-auth.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
permissions:
diff --git a/.github/workflows/test-unit-proxy-endpoints.yml b/.github/workflows/test-unit-proxy-endpoints.yml
index d9b6a348b60..d4f00050596 100644
--- a/.github/workflows/test-unit-proxy-endpoints.yml
+++ b/.github/workflows/test-unit-proxy-endpoints.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
workflow_dispatch:
diff --git a/.github/workflows/test-unit-proxy-infra.yml b/.github/workflows/test-unit-proxy-infra.yml
index 336e53ee3d7..884d62289b9 100644
--- a/.github/workflows/test-unit-proxy-infra.yml
+++ b/.github/workflows/test-unit-proxy-infra.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
permissions:
@@ -29,6 +29,7 @@ jobs:
tests/test_litellm/proxy/_experimental
tests/test_litellm/proxy/experimental
tests/test_litellm/proxy/common_utils
+ tests/test_litellm/proxy/logging_endpoints
tests/test_litellm/proxy/test_*.py
workers: 2
reruns: 2
diff --git a/.github/workflows/test-unit-proxy-legacy.yml b/.github/workflows/test-unit-proxy-legacy.yml
index 5768551f9b0..8db218cd1fc 100644
--- a/.github/workflows/test-unit-proxy-legacy.yml
+++ b/.github/workflows/test-unit-proxy-legacy.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
permissions:
@@ -71,7 +71,7 @@ jobs:
- name: Install dependencies
run: |
- uv sync --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router
+ .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router
- name: Generate Prisma client
env:
diff --git a/.github/workflows/test-unit-responses-caching-types.yml b/.github/workflows/test-unit-responses-caching-types.yml
index 13069be9e3a..2f177587997 100644
--- a/.github/workflows/test-unit-responses-caching-types.yml
+++ b/.github/workflows/test-unit-responses-caching-types.yml
@@ -5,7 +5,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
permissions:
diff --git a/.github/workflows/test_server_root_path.yml b/.github/workflows/test_server_root_path.yml
index 985653796c2..ac363071d55 100644
--- a/.github/workflows/test_server_root_path.yml
+++ b/.github/workflows/test_server_root_path.yml
@@ -7,7 +7,7 @@ on:
branches:
- main
- litellm_internal_staging
- - litellm_oss_branch
+ - litellm_oss_staging
- "litellm_**"
jobs:
diff --git a/.gitignore b/.gitignore
index 78951aee663..256962c787d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,12 +9,17 @@ litellm/proxy/myenv/*
litellm_uuid.txt
__pycache__/
*.pyc
+
+# Rust bridge build artifacts (compiled, platform-specific; regenerated by maturin/cargo)
+litellm/rust_bridge/_native*.so
+litellm/rust_bridge/_native*.pyd
+litellm-rust/target/
+
bun.lockb
**/.DS_Store
.aider*
litellm_results.jsonl
secrets.toml
-.gitignore
litellm/proxy/litellm_secrets.toml
litellm/proxy/api_log.json
litellm/proxy/_experimental/out/
@@ -37,7 +42,6 @@ litellm/tests/dynamo*.log
.vscode/settings.json
litellm/proxy/log.txt
proxy_server_config_@.yaml
-.gitignore
proxy_server_config_2.yaml
litellm/proxy/secret_managers/credentials.json
hosted_config.yaml
@@ -125,3 +129,6 @@ crash.*.log
# and should be committed.
.vscode
.pin_list.txt
+
+# pytest coverage data
+.coverage
diff --git a/CLAUDE.md b/CLAUDE.md
index 2070b6fcdd6..eb32c2cd6da 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -29,7 +29,7 @@ If you ever make public-facing PR descriptions, comments, issues, commit message
- don't use "—". Instead, reach for ";", ".", etc.
- don't use the pattern "It's not X, it's Y", "You're not X, you're Y", etc.
- don't use bulleted or numbered lists unless it would be nonsensical not to. Instead, prefer prose
-- don't add a trailing "." at the end of paragraphs (just like this file)
+- don't add a trailing "." at the end of paragraphs (just like this file). That means every paragraph, not just the last one (of the markdown file, PR description, GitHub comment, etc.). Rule of thumb: if you're adding new line(s) before the next sentence, don't add a "."
- don't use →. Instead, prefer not to use arrows, and if need be, use -> instead
Don't hesitate to use values in .env to get needed API keys and other secrets, as long as you never add them to conversation history, commit them, or include them in GitHub issues / PRs
@@ -54,7 +54,7 @@ When working on a PR, keep the PR description in sync with new commits being mad
Monkeypatching attributes of a class to do testing is an anti-pattern. Prefer dependency-injecting things into classes. That way, at unit test time, you can pass a mocked dependency in
-Do not put names of customers or customer company names in code, PRs, and issues. The codebase is public
+Do not put names of customers or customer company names in code, PR descriptions, issue bodies, etc. This means never mention literally any company name. Especially if you're about to say a sentence mentioning that the reason the PR exists was a feature/model/bug fix/etc. requested by a company. That's the indication that you should replace that company name with "the customer". e.g. not "Model request from Acme (Pylon #1234)" but "Model request from a customer (Pylon #1234)". This is because the codebase is public. The only exception is for publicly known providers or vendors such as OpenAI, Anthropic, AWS Bedrock, etc. only IF we're adding support for that provider/vendor in general and NOT if that PR or whatnot was a request by one of them, and they're actually one of our customers.
CI supply-chain safety: Never pipe a remote script into a shell (`curl ... | bash`, `wget ... | sh`); download the artifact to a file, verify its SHA-256 checksum, then install. Pin every external tool to a specific version with a full URL (not `latest` or `stable`). Verify checksums for all downloaded binaries, using the provider's official `.sha256` / `.sha256sum` sidecar when available. These rules apply to every download in CI
diff --git a/Dockerfile b/Dockerfile
index 4d55148ff89..b6fef1a21fc 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,12 +1,33 @@
+# syntax=docker/dockerfile:1.7
+
# Base image for building
-ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:31da6565f35af6401031c1d7aa91dc84ac76c5c48edd17fb90f0ed9e3173c7a9
+ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
# Runtime image
-ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:31da6565f35af6401031c1d7aa91dc84ac76c5c48edd17fb90f0ed9e3173c7a9
+ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
+# Pinned by digest like the other base images; bump explicitly on Node upgrades.
+ARG UI_BUILD_IMAGE=node:20.18-alpine3.20@sha256:3488b10bf958af7125a176419d2d8a9937d895bf124012aae811651988d2ffe6
FROM $UV_IMAGE AS uvbin
+# Admin UI builder. Pinned to the build platform so the architecture-independent
+# Next.js static export compiles once natively even in a multi-arch build,
+# instead of once per target arch under QEMU.
+FROM --platform=$BUILDPLATFORM $UI_BUILD_IMAGE AS ui-builder
+
+ENV NEXT_TELEMETRY_DISABLED=1 \
+ npm_config_fund=false \
+ npm_config_audit=false
+
+WORKDIR /ui
+
+COPY ui/litellm-dashboard/package.json ui/litellm-dashboard/package-lock.json ./
+RUN --mount=type=cache,target=/root/.npm npm ci --prefer-offline
+
+COPY ui/litellm-dashboard/ ./
+RUN npm run build
+
# Builder stage
FROM $LITELLM_BUILD_IMAGE AS builder
@@ -21,6 +42,7 @@ RUN apk add --no-cache \
gcc \
python3 \
python3-dev \
+ rust \
openssl \
openssl-dev \
nodejs \
@@ -47,7 +69,13 @@ RUN uv sync --frozen --no-install-project --no-install-workspace --no-default-gr
# Copy full source tree
COPY . .
-# Build Admin UI before final sync
+# Replace the committed UI bundle with the one built from this exact source.
+# Clearing first drops the committed bundle's content-hashed chunks that COPY
+# would otherwise leave behind alongside the fresh ones.
+RUN rm -rf litellm/proxy/_experimental/out
+COPY --from=ui-builder /ui/out/. litellm/proxy/_experimental/out/
+
+# Build Admin UI before final sync (applies the enterprise color override when present)
RUN sed -i 's/\r$//' docker/build_admin_ui.sh && chmod +x docker/build_admin_ui.sh && ./docker/build_admin_ui.sh
# Install project and workspace packages (fast - deps already cached)
diff --git a/Makefile b/Makefile
index 31fcb562b27..2f8d7f2d62d 100644
--- a/Makefile
+++ b/Makefile
@@ -20,13 +20,13 @@ help:
@echo " make install-test-deps - Install the full local test environment"
@echo " make install-helm-unittest - Install helm unittest plugin"
@echo " make install-hooks - Install git hooks (Conventional Commits + Branches)"
- @echo " make format - Apply Black code formatting"
- @echo " make format-check - Check Black code formatting (matches CI)"
- @echo " make lint - Run all linting (Ruff, basedpyright, Black check, circular imports, import safety)"
+ @echo " make format - Apply ruff format code formatting"
+ @echo " make format-check - Check ruff format code formatting (matches CI)"
+ @echo " make lint - Run all linting (Ruff, basedpyright, format check, circular imports, import safety)"
@echo " make lint-ruff - Run Ruff linting only"
@echo " make lint-basedpyright - Run basedpyright strict, gated by per-rule error counts"
@echo " make lint-basedpyright-budget-update - Re-capture the basedpyright per-rule budget (ratchet)"
- @echo " make lint-black - Check Black formatting (matches CI)"
+ @echo " make lint-format - Check ruff format formatting (matches CI)"
@echo " make lint-ruff-budget - Gate the codebase total of each strict ruff rule against its ceiling"
@echo " make lint-gate - Strict ruff gate in CI-parity mode (fetches staging, simulates the merge)"
@echo " make lint-ruff-budget-update - Re-capture per-rule baselines in ruff-strict-budget.json (ratchet)"
@@ -82,11 +82,13 @@ install-hooks:
./scripts/install_git_hooks.sh
# Formatting
+# Wrap width is ruff.toml's single source of truth (line-length = 120), shared by the
+# formatter, E501, and the import sorter so there's no 88-vs-120 split to reconcile.
format: install-dev
- cd litellm && $(UV_RUN) black . && cd ..
+ cd litellm && $(UV_RUN) ruff format --exclude '/enterprise/' . && cd ..
format-check: install-dev
- cd litellm && $(UV_RUN) black --check . && cd ..
+ cd litellm && $(UV_RUN) ruff format --check --exclude '/enterprise/' . && cd ..
# Linting targets
lint-ruff: install-dev
@@ -125,12 +127,13 @@ lint-ruff-FULL-dev: install-dev
else echo "No changed .py files to check."; fi
lint-basedpyright: install-dev
- ($(UV_RUN) basedpyright --outputjson || true) | $(UV_RUN) python scripts/type_check_gate.py
+ git fetch origin litellm_internal_staging
+ ($(UV_RUN) basedpyright --outputjson || true) | $(UV_RUN) python scripts/type_check_gate.py --base origin/litellm_internal_staging
lint-basedpyright-budget-update: install-dev
($(UV_RUN) basedpyright --outputjson || true) | $(UV_RUN) python scripts/type_check_gate.py --update
-lint-black: format-check
+lint-format: format-check
lint-strict-budget: install-dev
$(UV_RUN) python scripts/ruff_strict_gate.py
diff --git a/README.md b/README.md
index b26ad39eada..3d0f7282d7c 100644
--- a/README.md
+++ b/README.md
@@ -6,10 +6,10 @@
Open Source AI Gateway for 100+ LLMs. Self-hosted. Enterprise-ready. Call any LLM in OpenAI format.
-
-
-
-
+
+
+
+
@@ -406,6 +406,140 @@ You can use LiteLLM through either the Proxy Server or Python SDK. Both give you
Support for more providers. Missing a provider or LLM Platform, raise a [feature request](https://github.com/BerriAI/litellm/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.yml&title=%5BFeature%5D%3A+).
+### Deploy on AWS or GCP with Terraform
+
+Run the LiteLLM proxy as a production-ready componentized stack (gateway, backend, UI on separate services; managed Postgres + Redis + object store) using the published Terraform modules. Both modules are on the [public Terraform Registry](https://registry.terraform.io/namespaces/BerriAI) — no auth needed.
+
+#### AWS — ECS Fargate + Aurora + ElastiCache + ALB
+
+[](https://console.aws.amazon.com/cloudshell/home) — opens an in-browser shell, already authenticated to your AWS account. Once inside, run:
+
+```bash
+git clone https://github.com/BerriAI/litellm.git
+cd litellm/terraform/litellm/aws/examples/default
+cp terraform.tfvars.example terraform.tfvars # edit region/tenant/env
+terraform init && terraform apply
+```
+
+[Module page →](https://registry.terraform.io/modules/BerriAI/litellm/aws/latest)
+
+Or call the module from your own root config:
+
+```hcl
+# main.tf
+terraform {
+ required_version = ">= 1.6.0"
+ required_providers {
+ aws = { source = "hashicorp/aws", version = "~> 5.60" }
+ }
+}
+
+provider "aws" {
+ region = "us-west-2"
+}
+
+module "litellm" {
+ source = "BerriAI/litellm/aws"
+ version = "~> 1.89"
+
+ region = "us-west-2"
+ azs = ["us-west-2a", "us-west-2b"]
+ tenant = "acme"
+ env = "prod"
+
+ # Production: provide an ACM cert. Without one, set allow_plaintext_alb = true
+ # (dev/trial only).
+ # acm_certificate_arn = "arn:aws:acm:us-west-2:111122223333:certificate/..."
+ allow_plaintext_alb = true
+}
+
+output "litellm_url" {
+ value = module.litellm.alb_dns_name
+}
+```
+
+```bash
+terraform init
+terraform apply
+```
+
+Provider API keys live in AWS Secrets Manager; reference ARNs via `gateway_extra_secrets`. Full input list and architecture diagram on the [registry page](https://registry.terraform.io/modules/BerriAI/litellm/aws/latest?tab=inputs).
+
+#### GCP — Cloud Run + Cloud SQL + Memorystore + HTTPS LB
+
+[](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2FBerriAI%2Flitellm&cloudshell_workspace=terraform%2Flitellm%2Fgcp%2Fexamples%2Fdefault&cloudshell_tutorial=TUTORIAL.md&cloudshell_image=gcr.io/ds-artifacts-cloudshell/deploystack_custom_image&shellonly=true)
+
+Real 1-click. Opens Cloud Shell, clones this repo, and walks you through `terraform apply` via a built-in [DeployStack tutorial](./terraform/litellm/gcp/examples/default/TUTORIAL.md) — pick the project, the tutorial sets up the Artifact Registry remote repo, writes `terraform.tfvars` from your answers, and runs apply.
+
+[Module page →](https://registry.terraform.io/modules/BerriAI/litellm/google/latest)
+
+To call the module from your own config instead, Cloud Run can't pull from `ghcr.io` directly, so first set up a one-time Artifact Registry remote repo backed by GHCR:
+
+```bash
+gcloud artifacts repositories create litellm \
+ --location=us-central1 \
+ --repository-format=docker \
+ --mode=remote-repository \
+ --remote-docker-repo=https://ghcr.io \
+ --project=my-gcp-project
+```
+
+Then:
+
+```hcl
+# main.tf
+terraform {
+ required_version = ">= 1.6.0"
+ required_providers {
+ google = { source = "hashicorp/google", version = "~> 6.10" }
+ google-beta = { source = "hashicorp/google-beta", version = "~> 6.10" }
+ }
+}
+
+provider "google" { project = "my-gcp-project"; region = "us-central1" }
+provider "google-beta" { project = "my-gcp-project"; region = "us-central1" }
+
+module "litellm" {
+ source = "BerriAI/litellm/google"
+ version = "~> 1.89"
+
+ project_id = "my-gcp-project"
+ region = "us-central1"
+ tenant = "acme"
+ env = "prod"
+
+ # Replace my-gcp-project with your GCP project ID (same value as project_id above).
+ image_registry = "us-central1-docker.pkg.dev/my-gcp-project/litellm/berriai"
+
+ # Production: provide DNS already pointing at the LB IP for Google-managed certs.
+ # Without one, set allow_plaintext_lb = true (dev/trial only).
+ # lb_domains = ["proxy.example.com"]
+ allow_plaintext_lb = true
+}
+
+output "litellm_url" {
+ value = module.litellm.load_balancer_url
+}
+```
+
+```bash
+terraform init
+terraform apply
+```
+
+Provider API keys live in Secret Manager; reference resource IDs (e.g. `projects/my-gcp-project/secrets/openai-api-key`) via `gateway_extra_secrets`. Full input list and architecture diagram on the [registry page](https://registry.terraform.io/modules/BerriAI/litellm/google/latest?tab=inputs).
+
+#### Both stacks include
+
+- The full componentized split (gateway / backend / UI as independent services)
+- Managed Postgres (writer + reader) and Redis
+- Versioned object store for proxy state + file uploads
+- An auto-generated `LITELLM_MASTER_KEY` in your cloud's secret manager
+- A one-off migration job that runs `prisma migrate deploy` before the proxy starts
+- The same `proxy_config` surface as the [Helm chart](./helm/litellm/) — pass YAML as a typed map
+
+The Terraform modules live at [`terraform/litellm/aws/`](./terraform/litellm/aws/) and [`terraform/litellm/gcp/`](./terraform/litellm/gcp/) in this repo; the registry entries are read-only mirrors updated on each release.
+
### Run in Developer Mode
#### Services
1. Setup .env file in root
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 2cfdde8a517..667bdb073eb 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,5 +1,5 @@
-ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:31da6565f35af6401031c1d7aa91dc84ac76c5c48edd17fb90f0ed9e3173c7a9
-ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:31da6565f35af6401031c1d7aa91dc84ac76c5c48edd17fb90f0ed9e3173c7a9
+ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
+ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
FROM $UV_IMAGE AS uvbin
diff --git a/backend/routes/allowlist.py b/backend/routes/allowlist.py
index 3a280da9ea3..82e4cf9c841 100644
--- a/backend/routes/allowlist.py
+++ b/backend/routes/allowlist.py
@@ -86,6 +86,8 @@ BACKEND_PATH_PREFIXES: tuple[str, ...] = (
"/active/callbacks",
"/callbacks",
"/team_callback",
+ # Rust data-plane gateway → proxy control-plane API (logging today, auth later)
+ "/v1/rust_control_plane/",
# Alerting / email / IP allowlist
"/alerting/",
"/email/",
diff --git a/basedpyright-code-budget.json b/basedpyright-code-budget.json
index 7ba7656e407..f2b54e1f889 100644
--- a/basedpyright-code-budget.json
+++ b/basedpyright-code-budget.json
@@ -4,7 +4,7 @@
"slack": 2500
},
"reportArgumentType": {
- "baseline": 1934,
+ "baseline": 1814,
"slack": 180
},
"reportAssignmentType": {
@@ -69,7 +69,7 @@
},
"reportMatchNotExhaustive": {
"baseline": 1,
- "slack": 3
+ "slack": 0
},
"reportMissingParameterType": {
"baseline": 3933,
@@ -121,7 +121,7 @@
},
"reportReturnType": {
"baseline": 126,
- "slack": 13
+ "slack": 100
},
"reportTypedDictNotRequiredAccess": {
"baseline": 20,
@@ -157,7 +157,7 @@
},
"reportUnnecessaryComparison": {
"baseline": 683,
- "slack": 10
+ "slack": 100
},
"reportUnnecessaryContains": {
"baseline": 4,
diff --git a/codecov.yaml b/codecov.yaml
index 3baea13e2d3..f5acdd39136 100644
--- a/codecov.yaml
+++ b/codecov.yaml
@@ -3,6 +3,9 @@ codecov:
notify:
wait_for_ci: false # post as soon as expected uploads arrive, don't wait on CI
+ignore:
+ - "litellm-rust/**"
+
# Uploads are flagged per workflow/shard (GHA) or "circleci". carryforward makes
# a re-upload of a flag replace its prior session instead of accumulating a
# conflicting one, and lets a commit reuse a flag from its parent when that flag
diff --git a/docker/Dockerfile.database b/docker/Dockerfile.database
index e591a4a2adb..b3af953511d 100644
--- a/docker/Dockerfile.database
+++ b/docker/Dockerfile.database
@@ -1,12 +1,33 @@
+# syntax=docker/dockerfile:1.7
+
# Base image for building
-ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:3258be472764337fd13095bcbb3182da170243b5819fd67ad4c0754590588b31
+ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
# Runtime image
-ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:3258be472764337fd13095bcbb3182da170243b5819fd67ad4c0754590588b31
+ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
+# Pinned by digest like the other base images; bump explicitly on Node upgrades.
+ARG UI_BUILD_IMAGE=node:20.18-alpine3.20@sha256:3488b10bf958af7125a176419d2d8a9937d895bf124012aae811651988d2ffe6
FROM $UV_IMAGE AS uvbin
+# Admin UI builder. Pinned to the build platform so the architecture-independent
+# Next.js static export compiles once natively even in a multi-arch build,
+# instead of once per target arch under QEMU.
+FROM --platform=$BUILDPLATFORM $UI_BUILD_IMAGE AS ui-builder
+
+ENV NEXT_TELEMETRY_DISABLED=1 \
+ npm_config_fund=false \
+ npm_config_audit=false
+
+WORKDIR /ui
+
+COPY ui/litellm-dashboard/package.json ui/litellm-dashboard/package-lock.json ./
+RUN --mount=type=cache,target=/root/.npm npm ci --prefer-offline
+
+COPY ui/litellm-dashboard/ ./
+RUN npm run build
+
FROM $LITELLM_BUILD_IMAGE AS builder
WORKDIR /app
@@ -46,7 +67,13 @@ RUN uv sync --frozen --no-install-project --no-install-workspace --no-default-gr
# Copy full source tree
COPY . .
-# Build Admin UI before final sync
+# Replace the committed UI bundle with the one built from this exact source.
+# Clearing first drops the committed bundle's content-hashed chunks that COPY
+# would otherwise leave behind alongside the fresh ones.
+RUN rm -rf litellm/proxy/_experimental/out
+COPY --from=ui-builder /ui/out/. litellm/proxy/_experimental/out/
+
+# Build Admin UI before final sync (applies the enterprise color override when present)
RUN sed -i 's/\r$//' docker/build_admin_ui.sh && chmod +x docker/build_admin_ui.sh && ./docker/build_admin_ui.sh
# Install project and workspace packages (fast - deps already cached)
diff --git a/docker/Dockerfile.non_root b/docker/Dockerfile.non_root
index eafbd23fd90..c24cb9008f0 100644
--- a/docker/Dockerfile.non_root
+++ b/docker/Dockerfile.non_root
@@ -1,11 +1,32 @@
+# syntax=docker/dockerfile:1.7
+
# Base images
-ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:3258be472764337fd13095bcbb3182da170243b5819fd67ad4c0754590588b31
-ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:3258be472764337fd13095bcbb3182da170243b5819fd67ad4c0754590588b31
+ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
+ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
ARG PROXY_EXTRAS_SOURCE=published
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
+# Pinned by digest like the other base images; bump explicitly on Node upgrades.
+ARG UI_BUILD_IMAGE=node:20.18-alpine3.20@sha256:3488b10bf958af7125a176419d2d8a9937d895bf124012aae811651988d2ffe6
FROM $UV_IMAGE AS uvbin
+# Admin UI builder. Pinned to the build platform so the architecture-independent
+# Next.js static export compiles once natively even in a multi-arch build,
+# instead of once per target arch under QEMU.
+FROM --platform=$BUILDPLATFORM $UI_BUILD_IMAGE AS ui-builder
+
+ENV NEXT_TELEMETRY_DISABLED=1 \
+ npm_config_fund=false \
+ npm_config_audit=false
+
+WORKDIR /ui
+
+COPY ui/litellm-dashboard/package.json ui/litellm-dashboard/package-lock.json ./
+RUN --mount=type=cache,target=/root/.npm npm ci --prefer-offline
+
+COPY ui/litellm-dashboard/ ./
+RUN npm run build
+
FROM $LITELLM_BUILD_IMAGE AS builder
ARG PROXY_EXTRAS_SOURCE
WORKDIR /app
@@ -19,6 +40,7 @@ RUN for i in 1 2 3; do \
python3 \
python3-dev \
gcc \
+ rust \
bash \
coreutils \
curl \
@@ -52,6 +74,12 @@ RUN --mount=type=cache,target=/app/.cache/uv,id=litellm-uv-cache \
# Copy full source tree
COPY . .
+# Replace the committed UI bundle with the one built from this exact source.
+# Clearing first drops the committed bundle's content-hashed chunks that COPY
+# would otherwise leave behind alongside the fresh ones.
+RUN rm -rf litellm/proxy/_experimental/out
+COPY --from=ui-builder /ui/out/. litellm/proxy/_experimental/out/
+
# Set non-root flag for build time consistency
ENV LITELLM_NON_ROOT=true
diff --git a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py
index 8486e37384e..af4870bb1a5 100644
--- a/enterprise/litellm_enterprise/proxy/hooks/managed_files.py
+++ b/enterprise/litellm_enterprise/proxy/hooks/managed_files.py
@@ -13,7 +13,9 @@ from litellm import Router, verbose_logger
from litellm._uuid import uuid
from litellm.caching.caching import DualCache
from litellm.integrations.custom_logger import CustomLogger
-from litellm.litellm_core_utils.prompt_templates.common_utils import extract_file_data
+from litellm.litellm_core_utils.prompt_templates.common_utils import (
+ extract_file_metadata,
+)
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
from litellm.llms.base_llm.managed_resources.isolation import (
build_list_page,
@@ -981,9 +983,7 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
target_model_names_list: List[str],
) -> OpenAIFileObject:
## GET THE FILE TYPE FROM THE CREATE FILE REQUEST
- file_data = extract_file_data(create_file_request["file"])
-
- file_type = file_data["content_type"]
+ _, file_type = extract_file_metadata(create_file_request["file"])
output_file_id = file_objects[0].id
model_id = file_objects[0]._hidden_params.get("model_id")
diff --git a/enterprise/pyproject.toml b/enterprise/pyproject.toml
index b032942427c..66f6aeb7abc 100644
--- a/enterprise/pyproject.toml
+++ b/enterprise/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "litellm-enterprise"
-version = "0.1.43"
+version = "0.1.44"
description = "Package for LiteLLM Enterprise features"
readme = "README.md"
requires-python = ">=3.9"
@@ -26,7 +26,7 @@ required-version = ">=0.10.9"
module-root = ""
[tool.commitizen]
-version = "0.1.43"
+version = "0.1.44"
version_files = [
"pyproject.toml:^version",
"../pyproject.toml:litellm-enterprise==",
diff --git a/examples/lar1_ollama_config.yaml b/examples/lar1_ollama_config.yaml
new file mode 100644
index 00000000000..998cbf169b3
--- /dev/null
+++ b/examples/lar1_ollama_config.yaml
@@ -0,0 +1,45 @@
+model_list:
+ - model_name: agent-router
+ litellm_params:
+ model: ollama/qwen3.5:9b
+ api_base: http://127.0.0.1:11434
+ model_info:
+ id: cloud-smart
+ type: cloud-smart
+
+ - model_name: agent-router
+ litellm_params:
+ model: ollama/phi4-mini:latest
+ api_base: http://127.0.0.1:11434
+ model_info:
+ id: cloud-fast
+ type: cloud-fast
+
+ - model_name: agent-router
+ litellm_params:
+ model: ollama/llama3.2:3b
+ api_base: http://127.0.0.1:11434
+ model_info:
+ id: local
+ type: local
+
+ - model_name: agent-router
+ litellm_params:
+ model: ollama/lfm2.5-thinking:latest
+ api_base: http://127.0.0.1:11434
+ model_info:
+ id: deep
+ type: deep
+
+router_settings:
+ routing_strategy: lar1
+ routing_strategy_args:
+ confidence_threshold_low: 0.3
+ confidence_threshold_medium: 0.5
+ confidence_threshold_high: 0.7
+
+general_settings:
+ master_key: sk-lar1-demo
+
+litellm_settings:
+ set_verbose: true
diff --git a/gateway/Dockerfile b/gateway/Dockerfile
index 19c8a10fdfe..716b2fa09d1 100644
--- a/gateway/Dockerfile
+++ b/gateway/Dockerfile
@@ -1,5 +1,5 @@
-ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:31da6565f35af6401031c1d7aa91dc84ac76c5c48edd17fb90f0ed9e3173c7a9
-ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:31da6565f35af6401031c1d7aa91dc84ac76c5c48edd17fb90f0ed9e3173c7a9
+ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
+ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:c61ac6919b811ea53c4782d69f1fe05218ba3c25d53f01b6ab7892e621bd4370
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
FROM $UV_IMAGE AS uvbin
diff --git a/litellm-rust/.gitignore b/litellm-rust/.gitignore
new file mode 100644
index 00000000000..b83d22266ac
--- /dev/null
+++ b/litellm-rust/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/litellm-rust/ADDING_A_PROVIDER.md b/litellm-rust/ADDING_A_PROVIDER.md
new file mode 100644
index 00000000000..2fa81798605
--- /dev/null
+++ b/litellm-rust/ADDING_A_PROVIDER.md
@@ -0,0 +1,9 @@
+# Adding a provider / route to litellm-rust
+
+Three layers, same for every route (see `ocr` and `realtime` as references):
+
+1. **Transform contract (pure)** — `crates/core/src//transformation.rs`: a `…ProviderConfig` trait (URL build + request/response transforms) + types in `types.rs`. No network, env, or auth.
+2. **Provider config (pure)** — `crates/providers/src///transformation.rs`: implement that trait as a `const __CONFIG`, mirroring the Python provider tree. Add parity unit tests.
+3. **HTTP / transport (the host)** — `crates/providers/src/.rs` (e.g. `ocr.rs`, `realtime.rs`): the callable fn (`run_ocr`, `realtime`). It resolves the key, builds the auth header, builds URL + transforms via the config, then does the network call. This is the only layer allowed to do I/O.
+
+**Calling:** the host invokes the route fn — the Python bridge calls `run_ocr`; the `ai-gateway` server calls `realtime`. Register new modules in `lib.rs` / `mod.rs`, then run `cargo fmt && cargo clippy --workspace -- -D warnings && cargo test --workspace`.
diff --git a/litellm-rust/AGENTS.md b/litellm-rust/AGENTS.md
new file mode 100644
index 00000000000..86dd2c92744
--- /dev/null
+++ b/litellm-rust/AGENTS.md
@@ -0,0 +1,17 @@
+# AGENTS.md
+
+litellm-rust has exactly THREE crates. A crate is a LAYER, not a route. Routes (ocr, realtime, chat) and providers (mistral, openai) are MODULES inside the layers.
+
+## Crates
+
+| Crate | Role | Pure / I/O |
+|-------|------|------------|
+| litellm-core | Translation layer — types, route contracts (traits), provider transforms (modules under providers/), and the router. Builds requests/responses; no network. | Pure |
+| litellm-ai-gateway | Routes + host — the only crate that touches the network. HTTP/WebSocket I/O (modules under io/) plus the axum server binary (behind the `server` feature). | I/O |
+| litellm-python-bridge | PyO3 cdylib exposing Rust to the litellm Python SDK — a thin adapter over litellm-ai-gateway's I/O. | Binding |
+
+Dependency direction (acyclic): litellm-core ← litellm-ai-gateway ← litellm-python-bridge.
+
+Adding a crate: default to a MODULE. New crate ONLY on a real trigger — separate artifact (binary/cdylib), proc-macro, shared foundation, or publishable standalone. A new provider or route is none of these.
+
+Adding a crate fails crates/core/tests/workspace_crate_allowlist.rs until you update its allowlist and this file — intentional.
diff --git a/litellm-rust/CLAUDE.md b/litellm-rust/CLAUDE.md
new file mode 100644
index 00000000000..7c723e570ef
--- /dev/null
+++ b/litellm-rust/CLAUDE.md
@@ -0,0 +1,109 @@
+# CLAUDE.md
+
+This file defines the rules for Rust work in LiteLLM.
+
+## Crates (exactly three — see AGENTS.md)
+
+`litellm-core` describes work; `litellm-ai-gateway` executes it; `litellm-python-bridge`
+exposes it to the Python SDK. A crate is a **layer**, not a route — add modules, not crates.
+
+## Core Boundary
+
+`litellm-core` is the pure translation layer; the `litellm-ai-gateway` host executes work.
+
+Route-level Rust structure mirrors LiteLLM's Python responsibilities:
+- `core/src//` owns the route contract, shared types, and provider
+ template traits. For OCR, this means `core/src/ocr`.
+- `core/src/providers///transformation.rs` owns the
+ provider-specific transform. For Mistral OCR, this means
+ `core/src/providers/mistral/ocr/transformation.rs`.
+- Network execution lives in the host crate `ai-gateway` (`ai-gateway/src/io/`),
+ never inside `core`.
+
+Allowed in `core`:
+- Pure request transforms
+- Pure response transforms
+- Pure stream chunk normalization
+- Shared data types and validation errors
+- Deterministic token/cost helper logic
+
+Not allowed in `core`:
+- Network calls
+- Environment variable or secret reads
+- Filesystem access
+- Database or cache access
+- Provider SDK signing or auth flows
+- Logging callbacks, spend writes, or custom callbacks
+- Global mutable runtime state
+
+Python owns rollout state and fallback while Rust is being introduced. Rust
+paths must be off by default until parity tests prove equivalence with Python.
+
+## Production Bar
+
+Rust code in this workspace is held to a strict parity and robustness bar from
+the first PR:
+
+- Correctness parity is proven with tests. Do not rely on README claims or
+ manual inspection for a port that mirrors Python behavior.
+- Every provider transform must have unit tests for supported-parameter
+ filtering, request body shape, response normalization, missing/null fields,
+ and bad-input errors.
+- When Rust is exposed through Python, add Python tests that prove disabled,
+ enabled, and unavailable-bridge fallback behavior.
+- Avoid panics on user/provider input. Return typed errors and let the host map
+ them to Python exceptions or HTTP responses.
+- OCR handles documents that often contain personal data. Do not log document
+ contents, base64 payloads, provider response bodies, or secrets.
+- Error messages must be useful but data-minimized. Truncate or sanitize any
+ upstream body before it crosses a host boundary.
+- Treat empty or whitespace-only credentials, URLs, and config values as absent
+ at the host/config resolution layer.
+- Preserve Python output shape intentionally. If a field is always serialized as
+ `null` for Python parity, leave a short comment explaining that parity choice.
+
+## Host I/O Rules
+
+These rules apply when adding future crates or modules that execute network I/O,
+such as `ai-gateway`, router hosts, or standalone servers:
+
+- Set connect and full-request timeouts. No unbounded waits.
+- Reuse HTTP clients; do not construct clients per request.
+- Prefer rustls TLS for portable Python wheels and Linux images unless there is
+ a documented reason not to.
+- Add request IDs and structured tracing at the host layer, without logging OCR
+ document contents or secrets.
+- Do not echo raw upstream response bodies to callers. Sanitize and bound them.
+- Avoid `expect`/`unwrap` in server startup and request paths unless the panic is
+ impossible by construction and documented.
+
+## Constants
+
+Magic numbers and fixed strings go in a crate-level `constants.rs`, never
+hardcoded inline — the Rust mirror of Python's `litellm/constants.py`.
+
+- Each crate that needs them has `src/constants.rs` (declared `mod constants;`);
+ import from it (`use crate::constants::...`). Don't scatter `const` values at
+ the top of feature modules.
+- An env-overridable tunable still lives in `constants.rs` as its `DEFAULT_*`
+ value; the env read (with fallback to that default) happens at the host/config
+ resolution layer, not in `core`/`providers`.
+- Exception: a value that is purely local to one function and has no meaning
+ elsewhere may stay inline, but prefer `constants.rs` when in doubt.
+
+## Checks
+
+Run these before pushing Rust changes. The same checks run in GitHub Actions
+for changes under `litellm-rust/`.
+
+```bash
+cd litellm-rust
+cargo fmt --check
+# the ai-gateway binary + server code is behind the `server` feature
+cargo clippy -p litellm-ai-gateway --all-targets --features server -- -D warnings
+cargo clippy -p litellm-core -p litellm-python-bridge --all-targets -- -D warnings
+cargo test --workspace
+```
+
+When a Rust path is exposed through Python, add Python parity tests that compare
+the existing Python output with the Rust-backed output.
diff --git a/litellm-rust/Cargo.lock b/litellm-rust/Cargo.lock
new file mode 100644
index 00000000000..9bffe9f9ec6
--- /dev/null
+++ b/litellm-rust/Cargo.lock
@@ -0,0 +1,2006 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "axum"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "base64",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sha1",
+ "sync_wrapper",
+ "tokio",
+ "tokio-tungstenite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
+
+[[package]]
+name = "cc"
+version = "1.2.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "http"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "indoc"
+version = "2.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "litellm-ai-gateway"
+version = "0.1.0"
+dependencies = [
+ "axum",
+ "base64",
+ "futures-channel",
+ "futures-util",
+ "litellm-core",
+ "pyo3",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "sha2",
+ "subtle",
+ "tokio",
+ "tokio-tungstenite",
+]
+
+[[package]]
+name = "litellm-core"
+version = "0.1.0"
+dependencies = [
+ "rand 0.8.6",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "tokio",
+]
+
+[[package]]
+name = "litellm-python-bridge"
+version = "0.1.0"
+dependencies = [
+ "litellm-ai-gateway",
+ "litellm-core",
+ "pyo3",
+ "pyo3-async-runtimes",
+ "serde_json",
+ "tokio",
+]
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[package]]
+name = "memchr"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mio"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "portable-atomic"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "pyo3"
+version = "0.23.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872"
+dependencies = [
+ "cfg-if",
+ "indoc",
+ "libc",
+ "memoffset",
+ "once_cell",
+ "portable-atomic",
+ "pyo3-build-config",
+ "pyo3-ffi",
+ "pyo3-macros",
+ "unindent",
+]
+
+[[package]]
+name = "pyo3-async-runtimes"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "977dc837525cfd22919ba6a831413854beb7c99a256c03bf8624ad707e45810e"
+dependencies = [
+ "futures",
+ "once_cell",
+ "pin-project-lite",
+ "pyo3",
+ "tokio",
+]
+
+[[package]]
+name = "pyo3-build-config"
+version = "0.23.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb"
+dependencies = [
+ "once_cell",
+ "target-lexicon",
+]
+
+[[package]]
+name = "pyo3-ffi"
+version = "0.23.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d"
+dependencies = [
+ "libc",
+ "pyo3-build-config",
+]
+
+[[package]]
+name = "pyo3-macros"
+version = "0.23.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da"
+dependencies = [
+ "proc-macro2",
+ "pyo3-macros-backend",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.23.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "pyo3-build-config",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
+dependencies = [
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand 0.9.4",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.18",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rand"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
+[[package]]
+name = "rustls"
+version = "0.23.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+
+[[package]]
+name = "socket2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
+dependencies = [
+ "futures-util",
+ "log",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tungstenite",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "tungstenite"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "rand 0.8.6",
+ "rustls",
+ "rustls-pki-types",
+ "sha1",
+ "thiserror 1.0.69",
+ "utf-8",
+]
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unindent"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/litellm-rust/Cargo.toml b/litellm-rust/Cargo.toml
new file mode 100644
index 00000000000..5842ed5ba9b
--- /dev/null
+++ b/litellm-rust/Cargo.toml
@@ -0,0 +1,30 @@
+[workspace]
+members = [
+ "crates/core",
+ "crates/ai-gateway",
+ "crates/python-bridge",
+]
+resolver = "2"
+
+[workspace.package]
+edition = "2021"
+license = "MIT"
+repository = "https://github.com/BerriAI/litellm"
+
+[workspace.dependencies]
+litellm-core = { path = "crates/core" }
+litellm-ai-gateway = { path = "crates/ai-gateway", default-features = false }
+axum = "0.7"
+pyo3 = "0.23.5"
+pyo3-async-runtimes = { version = "0.23.0", features = ["tokio-runtime"] }
+rand = "0.8"
+reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls", "http2", "stream"] }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+sha2 = "0.10"
+subtle = "2"
+thiserror = "2.0"
+tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "net"] }
+tokio-tungstenite = { version = "0.24", default-features = false, features = ["connect", "rustls-tls-native-roots"] }
+futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
+base64 = "0.22"
diff --git a/litellm-rust/README.md b/litellm-rust/README.md
new file mode 100644
index 00000000000..1646c90ad76
--- /dev/null
+++ b/litellm-rust/README.md
@@ -0,0 +1,44 @@
+# LiteLLM Rust
+
+This workspace contains the staged Rust implementation for LiteLLM.
+
+Rust starts as a pure transform core used by the existing Python host. Python
+continues to own auth, configuration, network I/O, retries, routing, logging,
+callbacks, spend tracking, and customer plugins until each Rust path has parity
+coverage and production evidence.
+
+## Crates
+
+| Crate | Role | Pure / I/O |
+|-------|------|------------|
+| litellm-core | Translation layer — types, route contracts (traits), provider transforms (modules under providers/), and the router. Builds requests/responses; no network. | Pure |
+| litellm-ai-gateway | Routes + host — the only crate that touches the network. HTTP/WebSocket I/O (modules under io/) plus the axum server binary (behind the `server` feature). | I/O |
+| litellm-python-bridge | PyO3 cdylib exposing Rust to the litellm Python SDK — a thin adapter over litellm-ai-gateway's I/O. | Binding |
+
+Dependency direction (acyclic): litellm-core ← litellm-ai-gateway ← litellm-python-bridge.
+
+## Layout
+
+```text
+crates/
+ core/ Route contracts, shared pure types, errors, and templates.
+ src/ocr/
+ providers/ Provider-specific pure transforms.
+ src/mistral/ocr/transformation.rs
+ python-bridge/ PyO3 bridge for Python LiteLLM.
+```
+
+The folder shape should follow the Python provider tree:
+`providers/src///transformation.rs`. The bridge should expose
+one function per top-level route, starting with `ocr(payload)`.
+
+## Checks
+
+Run these before pushing Rust changes. GitHub Actions runs the same checks for
+changes under `litellm-rust/`.
+
+```bash
+cargo fmt --check
+cargo clippy --workspace --all-targets -- -D warnings
+cargo test --workspace
+```
diff --git a/litellm-rust/crates/ai-gateway/AGENTS.md b/litellm-rust/crates/ai-gateway/AGENTS.md
new file mode 100644
index 00000000000..d9e6e1adde5
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/AGENTS.md
@@ -0,0 +1,50 @@
+# ai-gateway — folder architecture
+
+The Axum server that fronts the Rust gateway. It owns transport + config + auth
+only; deployment selection lives in `core::router`, transforms in `core`/`providers`.
+
+```
+src/
+ main.rs # entrypoint: build AppState (router + master key), bind, serve
+ state.rs # AppState — shared Arc + master_key
+ gil.rs # GIL-activity tracker (records Python acquisitions)
+ auth/ # authentication as an axum extractor — added to handler args
+ mod.rs # RequireMasterKey: FromRequestParts, single master key (LITELLM_MASTER_KEY)
+ routes/ # one module per route, all matching the same template
+ AGENTS.md # ← the route template (read this before adding a route)
+ mod.rs # app(): merges every module's router()
+ health.rs # simple route (one file): router() + liveness/readiness
+ gil.rs # simple route (one file): router() + GET /health/gil
+ realtime/ # route with logic → axum surface + a no-axum service:
+ mod.rs # router() + handler + WS<->events adapter (the axum surface)
+ service.rs # business logic (select deployment, call provider) — no axum, testable
+ python/ # Python interop (feature: python-config) — load-time only
+ mod.rs, config.rs, AGENTS.md
+```
+
+## Rules
+
+- **Routes follow one template.** Each route module exposes
+ `pub fn router() -> Router`; `routes/mod.rs` only merges them. Simple
+ routes are one file; non-trivial routes are a folder (`handler`/`service`/
+ `transport`). See `routes/AGENTS.md`.
+- **Auth is an extractor.** Add `crate::auth::RequireMasterKey` to a handler's
+ args; it runs during extraction. Never re-implement the check per route.
+- **Handlers are thin.** A handler validates and delegates to its `service`. No
+ business logic, no provider calls, no transforms in handlers.
+- **State is shared and cheap to clone.** Long-lived handles live behind `Arc` in
+ `state.rs`; read env/config only in `main.rs` when building state.
+
+## Auth (interim)
+
+A single **master key** (`LITELLM_MASTER_KEY`), enforced by the
+`auth::RequireMasterKey` extractor: any caller presenting it as
+`Authorization: Bearer ` may invoke the gateway. Fails closed (500) when
+unset; constant-time compare. The server binds `127.0.0.1` by default (`HOST` to
+override). Full per-key auth + budgets/rate-limits are delegated to the Python
+proxy in a later phase. Health routes don't add the extractor (unauthenticated).
+
+## Python interop
+
+Anything that calls into Python lives in `python/` and is **load-time only** — see
+`python/AGENTS.md`. The realtime data path never takes the GIL.
diff --git a/litellm-rust/crates/ai-gateway/ARCHITECTURE.md b/litellm-rust/crates/ai-gateway/ARCHITECTURE.md
new file mode 100644
index 00000000000..733953bbdb3
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/ARCHITECTURE.md
@@ -0,0 +1,12 @@
+# ai-gateway architecture
+
+The Rust ai-gateway does LLM inference (realtime WebSocket). Spend tracking is an
+API callback: it POSTs each finished session to the LiteLLM proxy, which records
+spend and runs the usual callbacks.
+
+```mermaid
+flowchart LR
+ C[client] <--> G[Rust ai-gateway
LLM inference]
+ G <--> O[OpenAI realtime]
+ G -. spend tracking callback .-> P[litellm proxy]
+```
diff --git a/litellm-rust/crates/ai-gateway/Cargo.toml b/litellm-rust/crates/ai-gateway/Cargo.toml
new file mode 100644
index 00000000000..4055be36785
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/Cargo.toml
@@ -0,0 +1,43 @@
+[package]
+name = "litellm-ai-gateway"
+version = "0.1.0"
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[lib]
+name = "litellm_ai_gateway"
+
+[[bin]]
+name = "litellm-ai-gateway"
+path = "src/main.rs"
+required-features = ["server"]
+
+[dependencies]
+litellm-core.workspace = true
+# reqwest (rustls + json) is used by io/ocr and ships realtime logs to the
+# Python proxy callbacks API.
+reqwest.workspace = true
+# `sync` powers the bounded mpsc channel the realtime logger drains.
+tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net", "time", "sync"] }
+tokio-tungstenite.workspace = true
+futures-util.workspace = true
+serde_json.workspace = true
+base64.workspace = true
+axum = { workspace = true, features = ["ws"], optional = true }
+serde.workspace = true
+subtle = { workspace = true, optional = true }
+# sha2 hashes the master key into user_api_key_hash (matches the proxy's
+# SHA-256 hash_token) so the plaintext credential never enters a log payload.
+sha2 = { workspace = true, optional = true }
+pyo3 = { workspace = true, features = ["auto-initialize"], optional = true }
+
+[features]
+default = []
+server = ["dep:axum", "dep:subtle", "dep:sha2"]
+# Build the gateway's config from the proxy YAML via an embedded Python
+# interpreter (links libpython; requires `litellm` importable at runtime).
+python-config = ["dep:pyo3"]
+
+[dev-dependencies]
+futures-channel = "0.3"
diff --git a/litellm-rust/crates/ai-gateway/Dockerfile b/litellm-rust/crates/ai-gateway/Dockerfile
new file mode 100644
index 00000000000..adf6fca0741
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/Dockerfile
@@ -0,0 +1,86 @@
+# Multi-stage build for the LiteLLM Rust AI Gateway (realtime WebSocket proxy).
+#
+# Build context is the **repo root** so we can install `litellm` from this repo's
+# source (the gateway loads its model_list via litellm.proxy.read_model_list,
+# which is not in any PyPI release yet) AND build the rust workspace under
+# litellm-rust/.
+#
+# docker build -f litellm-rust/crates/ai-gateway/Dockerfile -t litellm-ai-gateway .
+#
+# No secrets live in this file. Runtime config (LITELLM_MASTER_KEY,
+# OPENAI_API_KEY referenced by config.yaml, etc.) is injected as environment
+# variables at deploy time.
+
+# ---- Chef -------------------------------------------------------------------
+# cargo-chef caches the dependency build so only the gateway crate recompiles on
+# a source-only change. python3-dev is present in every rust stage because the
+# `python-config` feature links libpython via pyo3 (even in the cook step).
+FROM rust:1.90-slim-bookworm AS chef
+ENV PYO3_PYTHON=python3.11
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ python3 python3-dev pkg-config libssl-dev clang \
+ && rm -rf /var/lib/apt/lists/* \
+ && cargo install cargo-chef --locked --version 0.1.77
+WORKDIR /build/litellm-rust
+
+# ---- Planner ----------------------------------------------------------------
+# Produce the dependency recipe from the rust workspace manifests + Cargo.lock.
+FROM chef AS planner
+COPY litellm-rust/ .
+RUN cargo chef prepare --recipe-path recipe.json
+
+# ---- Builder ----------------------------------------------------------------
+FROM chef AS builder
+# Cook (compile) just the dependencies first — this layer is cached and reused
+# whenever only gateway source changes.
+COPY --from=planner /build/litellm-rust/recipe.json recipe.json
+RUN cargo chef cook --locked --release \
+ -p litellm-ai-gateway --features python-config \
+ --recipe-path recipe.json
+# Now copy the real sources and build the gateway binary. Deps are already cooked
+# above, so this step only recompiles the gateway crate.
+COPY litellm-rust/ .
+RUN cargo build --locked --release -p litellm-ai-gateway --features python-config
+
+# ---- Runtime ----------------------------------------------------------------
+# python:3.11-slim-bookworm ships libpython3.11, matching the builder's PyO3
+# 3.11 ABI so the embedded interpreter links and imports cleanly.
+FROM python:3.11-slim-bookworm AS runtime
+
+# CA certificates for outbound TLS to the OpenAI realtime endpoint.
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+# Install litellm (with proxy extras) FROM THIS REPO'S SOURCE so
+# `import litellm.proxy.read_model_list` works — it is not on PyPI yet. Copy the
+# package + packaging metadata, then pip install the proxy extra.
+COPY pyproject.toml README.md LICENSE ./
+COPY litellm/ ./litellm/
+RUN pip install --no-cache-dir ".[proxy]"
+
+# The compiled gateway binary (pure-Rust realtime hot path; Python is load-time
+# only).
+COPY --from=builder /build/litellm-rust/target/release/litellm-ai-gateway /usr/local/bin/litellm-ai-gateway
+
+# Default config.yaml. A real deploy can override this (e.g. mount a Render
+# secret file at the same path) — never bake secrets into the image.
+COPY litellm-rust/crates/ai-gateway/config.yaml /app/config.yaml
+
+# Bind to all interfaces (Render routes to 0.0.0.0:$PORT) and load the model_list
+# from config.yaml via the embedded python config reader.
+ENV HOST=0.0.0.0 \
+ LITELLM_CONFIG_PATH=/app/config.yaml
+
+# Drop to a non-root user. The realtime hot path needs no root privileges, so
+# running unprivileged limits blast radius if the process is ever compromised.
+# The binary in /usr/local/bin is world-executable (COPY default mode 755); we
+# only need /app (and the config.yaml it reads) owned by the unprivileged user.
+RUN useradd --system --no-create-home --uid 10001 appuser \
+ && chown -R appuser:appuser /app
+USER appuser
+
+ENTRYPOINT ["/usr/local/bin/litellm-ai-gateway"]
diff --git a/litellm-rust/crates/ai-gateway/Dockerfile.dockerignore b/litellm-rust/crates/ai-gateway/Dockerfile.dockerignore
new file mode 100644
index 00000000000..030ee6a37c5
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/Dockerfile.dockerignore
@@ -0,0 +1,45 @@
+# Dockerfile-specific ignore-file for the Rust AI Gateway build.
+#
+# The build context is the repo root (so the image can pip install litellm from
+# source AND build the rust workspace). BuildKit honors `.dockerignore`
+# next to the Dockerfile and it takes precedence over the repo-root `.dockerignore`,
+# so this file shrinks the (large) repo-root context for THIS build only without
+# touching the root `.dockerignore` used by the main litellm images.
+#
+# Strategy: ignore everything, then re-include only what the build needs:
+# - litellm/ (pip install . needs the full package + proxy reader)
+# - litellm-rust/ (the rust workspace; Cargo.lock + crate sources)
+# - pyproject.toml / README.md / LICENSE (packaging metadata for pip install)
+*
+
+# --- re-include the build inputs ---
+!litellm/
+!litellm-rust/
+!pyproject.toml
+!README.md
+!LICENSE
+
+# --- prune heavy / irrelevant subpaths back out of the re-included trees ---
+# Rust build artifacts (huge; regenerated in the builder).
+**/target/
+# Python caches and compiled bytecode.
+**/__pycache__/
+**/*.pyc
+**/*.pyo
+**/.pytest_cache/
+**/.ruff_cache/
+**/.mypy_cache/
+# Node / UI build output bundled under the python package (not needed to import
+# litellm.proxy.read_model_list).
+**/node_modules/
+litellm/proxy/_experimental/out/
+# Tests, logs, and local scratch.
+**/tests/
+**/test/
+*.log
+log.txt
+*.tgz
+# VCS / editor / CI metadata that may live under re-included trees.
+**/.git/
+.git/
+**/.DS_Store
diff --git a/litellm-rust/crates/ai-gateway/README.md b/litellm-rust/crates/ai-gateway/README.md
new file mode 100644
index 00000000000..f913beff6d5
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/README.md
@@ -0,0 +1,198 @@
+# LiteLLM Rust AI Gateway
+
+A minimal Axum service that fronts OpenAI's realtime API. Clients open a
+WebSocket to `GET /v1/realtime`; the gateway authenticates, selects a deployment,
+dials OpenAI upstream, and splices the two sockets frame-by-frame.
+
+## Crates
+
+`litellm-rust` is exactly three crates (a crate is a **layer**, not a route):
+
+| Crate | Role | Pure / I/O |
+|-------|------|------------|
+| litellm-core | Translation layer — types, route contracts (traits), provider transforms (modules under `providers/`), and the router. Builds requests/responses; no network. | Pure |
+| litellm-ai-gateway | Routes + host — the only crate that touches the network. HTTP/WebSocket I/O (modules under `io/`) plus the Axum server binary (behind the `server` feature). | I/O |
+| litellm-python-bridge | PyO3 cdylib exposing Rust to the litellm Python SDK — a thin adapter over litellm-ai-gateway's I/O. | Binding |
+
+Dependency direction (acyclic): litellm-core ← litellm-ai-gateway ← litellm-python-bridge.
+
+- **Client endpoint:** `wss:///v1/realtime?model=` (WebSocket)
+- **Auth:** `Authorization: Bearer $LITELLM_MASTER_KEY` (fails closed if unset)
+- **Health:** `GET /health/readiness`, `GET /health/liveness`, `GET /health/gil`
+- **Request logs:** POSTed to a LiteLLM proxy at `/v1/rust_control_plane/logs` (see [Request logging](#request-logging))
+
+> **Realtime serving is pure Rust.** Python is used at **load time only** — to
+> read the config once at boot. The realtime hot path never touches Python.
+
+## Configuration (config.yaml)
+
+The gateway loads its `model_list` from a **config.yaml**, the same as the
+LiteLLM proxy. Point `LITELLM_CONFIG_PATH` at the file:
+
+```yaml
+# config.yaml
+model_list:
+ - model_name: gpt-realtime
+ litellm_params:
+ model: openai/gpt-realtime
+ api_key: os.environ/OPENAI_API_KEY
+```
+
+```bash
+LITELLM_CONFIG_PATH=./config.yaml ./litellm-ai-gateway
+```
+
+At boot the gateway calls into `litellm.proxy.read_model_list`, which reuses the
+**real proxy config reader** (`ProxyConfig.get_config`). That means everything
+the proxy supports in config.yaml works here too:
+
+- `include:` to merge in other config files,
+- `os.environ/VAR` secret references (resolved via the secret manager, never
+ inlined),
+- DB-stored models (when a database is configured).
+
+Secrets stay out of the config — reference them with `os.environ/...` and set
+the env var at deploy time. The shipped Docker image is built with the
+`python-config` feature and **bundles litellm**, so config loading works out of
+the box; the default baked config lives at `/app/config.yaml` and can be
+overridden at deploy time (e.g. a Render secret file mounted at the same path).
+
+### Environment variables
+
+| Var | Required | Default | Purpose |
+|---|---|---|---|
+| `LITELLM_CONFIG_PATH` | yes (config mode) | — | Path to the config.yaml the gateway loads its `model_list` from. The Docker image defaults this to `/app/config.yaml`. |
+| `LITELLM_MASTER_KEY` | yes | — | Bearer token clients must send. Unset ⇒ all `/v1/realtime` requests are rejected (fail closed). |
+| `OPENAI_API_KEY` | yes | — | Upstream OpenAI key. Referenced by config.yaml as `os.environ/OPENAI_API_KEY` for the gateway→OpenAI dial. |
+| `HOST` | no | `127.0.0.1` | **Set to `0.0.0.0` in any container/deploy** or external traffic is refused. |
+| `PORT` | no | `4001` | Listen port. Render and most PaaS inject this automatically. |
+| `LITELLM_PROXY_BASE_URL` | no | `http://localhost:4000` | LiteLLM proxy that request logs are POSTed to. See [Request logging](#request-logging). |
+
+> Secrets (`LITELLM_MASTER_KEY`, `OPENAI_API_KEY`) are never baked into the image
+> or `render.yaml` — inject them at deploy time only.
+
+### Lean env stand-in (fallback)
+
+If the binary is built **without** `python-config` (default features), or
+`LITELLM_CONFIG_PATH` is unset, the gateway falls back to a single-deployment
+stand-in built from the environment:
+
+| Var | Default | Purpose |
+|---|---|---|
+| `OPENAI_REALTIME_MODEL` | `gpt-realtime` | The single deployment's model name (also the `?model=` clients pass). |
+
+This mode links no libpython and needs no config file, but it only supports one
+hard-coded OpenAI deployment. **config.yaml is the recommended path** — use the
+stand-in only for the leanest possible build.
+
+## Request logging
+
+The gateway runs no spend logic. When a session ends it builds one
+`StandardLoggingPayload` and POSTs it to `{LITELLM_PROXY_BASE_URL}/v1/rust_control_plane/logs`
+(admin-only, bearer = `LITELLM_MASTER_KEY`), and the proxy replays it through its
+normal callbacks (spend logs, Langfuse, etc.). The POST is non-blocking: a bounded
+channel drained by a background worker, dropping with a counter if the proxy is
+down. It sends one payload per session. Both env vars are in the table above.
+
+Worker tuning, rarely needed: `LITELLM_LOG_CHANNEL_CAPACITY` (4096),
+`LITELLM_LOG_BATCH_SIZE` (256), `LITELLM_LOG_FLUSH_INTERVAL_MS` (500).
+
+## Build & run with Docker
+
+The image is built `--features python-config` and installs litellm **from this
+repo's source** (the config reader is newer than any PyPI release), so the build
+**context is the repo root**:
+
+```bash
+# from the repo root
+docker build -f litellm-rust/crates/ai-gateway/Dockerfile -t litellm-ai-gateway .
+
+docker run --rm -p 4001:4001 \
+ -e HOST=0.0.0.0 -e PORT=4001 \
+ -e LITELLM_MASTER_KEY=sk-local \
+ -e OPENAI_API_KEY=$OPENAI_API_KEY \
+ litellm-ai-gateway # LITELLM_CONFIG_PATH defaults to /app/config.yaml
+
+# smoke test
+curl -s -o /dev/null -w '%{http_code}\n' localhost:4001/health/readiness # -> 200
+curl -s -o /dev/null -w '%{http_code}\n' localhost:4001/v1/realtime # -> 401 (auth fails closed)
+```
+
+On boot you should see `loaded model_list from /app/config.yaml via python
+config reader` — that confirms the config path (not the env stand-in fallback).
+To use your own config, mount it over the default:
+
+```bash
+docker run --rm -p 4001:4001 \
+ -e HOST=0.0.0.0 -e LITELLM_MASTER_KEY=sk-local -e OPENAI_API_KEY=$OPENAI_API_KEY \
+ -v $(pwd)/my-config.yaml:/app/config.yaml:ro \
+ litellm-ai-gateway
+```
+
+### Cargo-only (no Docker)
+
+```bash
+# config.yaml mode — needs litellm importable in the active python env
+LITELLM_CONFIG_PATH=./crates/ai-gateway/config.yaml \
+ cargo run --release -p litellm-ai-gateway --features python-config
+
+# env stand-in mode — no python, no config
+cargo run --release -p litellm-ai-gateway
+```
+
+## Deploy on Render
+
+The service is a Docker **web service**; Render terminates TLS and supports
+WebSockets, so the public endpoint is `wss://.onrender.com/v1/realtime`.
+
+### Option A — Blueprint (`render.yaml`)
+
+`crates/ai-gateway/render.yaml` describes the service (Docker runtime,
+`healthCheckPath: /health/readiness`, repo-root `dockerContext: .`,
+`dockerfilePath: ./litellm-rust/crates/ai-gateway/Dockerfile`,
+`LITELLM_CONFIG_PATH: /app/config.yaml`). `LITELLM_MASTER_KEY` and
+`OPENAI_API_KEY` are `sync: false` — set them in the dashboard after the first
+deploy. To use a non-default model_list, mount a **Render Secret File** at
+`/app/config.yaml`. Point a Render Blueprint at this repo/branch and apply.
+
+### Option B — Render API
+
+```bash
+# create a Docker web service from this repo+branch, then set env vars:
+curl -X POST https://api.render.com/v1/services \
+ -H "Authorization: Bearer $RENDER_API_KEY" -H "Content-Type: application/json" \
+ -d '{
+ "type": "web_service", "name": "litellm-rust-ai-gateway",
+ "ownerId": "", "repo": "https://github.com/BerriAI/litellm",
+ "branch": "",
+ "serviceDetails": {
+ "env": "docker",
+ "envSpecificDetails": {
+ "dockerfilePath": "./litellm-rust/crates/ai-gateway/Dockerfile",
+ "dockerContext": "."
+ },
+ "healthCheckPath": "/health/readiness"
+ }
+ }'
+# then set env vars LITELLM_MASTER_KEY, OPENAI_API_KEY, HOST=0.0.0.0,
+# LITELLM_CONFIG_PATH=/app/config.yaml
+```
+
+Health check path **must** be `/health/readiness`. `autoDeploy` is off by default
+in the blueprint — trigger deploys manually (or flip it on) to pick up new commits.
+
+## Scaling
+
+Concurrency is what matters, not total connections: each in-flight session holds
+one client socket + one upstream socket. To scale, raise the instance count /
+enable autoscaling on the Render service (e.g. baseline 10, max 100). Each
+instance needs file descriptors for `2 × peak_concurrent_sessions` — raise
+`ulimit -n` if you push very high concurrency.
+
+## Latency note
+
+The gateway adds the cost of one extra hop: client→gateway, then a fresh
+gateway→OpenAI realtime handshake (TLS + WS upgrade + `session.created`). In
+benchmarks this is ~100–150 ms of added session-establishment time; first-audio
+and steady-state streaming add no measurable overhead. To minimize it, deploy the
+gateway in the Render region with the lowest RTT to OpenAI's realtime endpoint.
diff --git a/litellm-rust/crates/ai-gateway/benchmarks/realtime/README.md b/litellm-rust/crates/ai-gateway/benchmarks/realtime/README.md
new file mode 100644
index 00000000000..84e926af243
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/benchmarks/realtime/README.md
@@ -0,0 +1,55 @@
+# Realtime gateway benchmark — pool on/off
+
+Measures what the gateway adds over talking to OpenAI's realtime WebSocket
+directly, and what the pre-warmed connection pool removes. See
+`../../src/routes/realtime/README.md` for how the pool works.
+
+## Results
+
+5000 calls / 500 concurrency, gateway at 10 instances, pool ON
+(`REALTIME_POOL_SIZE=64`), upstream OpenAI `gpt-realtime`. Each leg run twice.
+Times in **ms**. Phases per connection: **dial** = TCP+TLS+WS upgrade,
+**session** = upgrade → `session.created` (the phase the pool removes),
+**1st-audio** = `response.create` → first audio delta (OpenAI inference),
+**total** = full wall-clock.
+
+| metric | Direct OpenAI | Gateway (pool ON) | Overhead (ms) | vs OpenAI |
+| ------------------ | ------------- | ----------------- | ------------- | ---------- |
+| success rate (%) | 99.8 | 99.8 | — | — |
+| dial p50 (ms) | 276 | 158 | −118 | **faster** |
+| session p50 (ms) | 7 | 0 | −7 | **faster** |
+| 1st-audio p50 (ms) | 440 | 664 | +224 | slower¹ |
+| total p50 (ms) | 816 | 1010 | +194 | slower¹ |
+| total p95 (ms) | 2152 | 1970 | −182 | **faster** |
+| total p99 (ms) | 2692 | 2610 | −82 | **faster** |
+
+The gateway is **faster than direct on 4 of 6 metrics**. The warm pool makes the
+**session phase sub-millisecond** at the median — ~76% of connects hit the pool,
+~70% had session < 1 ms. ¹ The two "slower" rows are not gateway overhead:
+`1st-audio` is OpenAI's own inference time (the gateway only relays it), which ran
+slower during the gateway legs and drags `total p50` with it.
+
+**Pool OFF** (control, `REALTIME_POOL_SIZE=0`): session p50 was **367 ms** — the
+fresh-dial overhead the pool removes.
+
+## Reproduce
+
+The load generator lives in a separate repo:
+**https://github.com/ishaan-berri/litellm-realtime-bench**
+
+```bash
+git clone https://github.com/ishaan-berri/litellm-realtime-bench
+cd litellm-realtime-bench && go build -o wsbench .
+
+# Direct to OpenAI (baseline)
+./wsbench -host api.openai.com -key "$OPENAI_API_KEY" -m gpt-realtime -n 5000 -c 500 -t 60
+
+# Through the gateway — run once with pool ON, once with REALTIME_POOL_SIZE=0
+./wsbench -host -key "$LITELLM_MASTER_KEY" -m gpt-realtime -n 5000 -c 500 -t 60
+```
+
+Run the gateway with the env stand-in (`OPENAI_REALTIME_MODEL=gpt-realtime`,
+`OPENAI_API_KEY`, `LITELLM_MASTER_KEY`, `REALTIME_POOL_SIZE`, `HOST=0.0.0.0`). At
+500 concurrency over N instances, size the pool to `≈ 500 / N` per instance (64 was
+used here for 10 instances). The bench repo's README covers running 500-concurrency
+legs from a hosted multi-vCPU runner. **Never commit keys — pass them via `-key`.**
diff --git a/litellm-rust/crates/ai-gateway/config.yaml b/litellm-rust/crates/ai-gateway/config.yaml
new file mode 100644
index 00000000000..ac598c220dd
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/config.yaml
@@ -0,0 +1,13 @@
+# Sample realtime config for the LiteLLM Rust AI Gateway.
+#
+# The gateway loads this model_list at boot via the embedded python config
+# reader (litellm.proxy.read_model_list), which reuses the proxy's own reader —
+# so include:, os.environ/ secrets, and DB-stored models all work here too.
+#
+# Secrets are referenced (never inlined) via os.environ/. A real deploy can
+# override this file (e.g. mount a Render secret file at LITELLM_CONFIG_PATH).
+model_list:
+ - model_name: gpt-realtime
+ litellm_params:
+ model: openai/gpt-realtime
+ api_key: os.environ/OPENAI_API_KEY
diff --git a/litellm-rust/crates/ai-gateway/render.yaml b/litellm-rust/crates/ai-gateway/render.yaml
new file mode 100644
index 00000000000..4170849f65d
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/render.yaml
@@ -0,0 +1,35 @@
+# Render blueprint for the LiteLLM Rust AI Gateway (realtime WebSocket proxy).
+#
+# Single instance for now (no autoscaling). The public endpoint is a
+# WebSocket served over TLS: wss://.onrender.com/v1/realtime
+#
+# Paths are relative to the **repo root** (Render's convention). The build
+# context is the repo root so the image can install litellm from source — the
+# gateway loads its model_list via litellm.proxy.read_model_list at boot.
+#
+# Secrets (LITELLM_MASTER_KEY, OPENAI_API_KEY) are marked sync: false — set
+# them in the Render dashboard or via the API, never inline here.
+services:
+ - type: web
+ name: litellm-rust-ai-gateway
+ runtime: docker
+ plan: standard
+ dockerfilePath: ./litellm-rust/crates/ai-gateway/Dockerfile
+ dockerContext: .
+ healthCheckPath: /health/readiness
+ numInstances: 1
+ envVars:
+ # The gateway loads its model_list from this config.yaml via the embedded
+ # python config reader. The image bakes a default config at /app/config.yaml;
+ # a real deploy can override it by mounting a Render secret file at this
+ # same path (Dashboard → Environment → Secret Files) — never inline secrets.
+ - key: LITELLM_CONFIG_PATH
+ value: /app/config.yaml
+ - key: HOST
+ value: 0.0.0.0
+ # Bearer token clients must send on /v1/realtime (fail closed if unset).
+ - key: LITELLM_MASTER_KEY
+ sync: false
+ # Referenced by config.yaml as os.environ/OPENAI_API_KEY for the upstream dial.
+ - key: OPENAI_API_KEY
+ sync: false
diff --git a/litellm-rust/crates/ai-gateway/src/auth/mod.rs b/litellm-rust/crates/ai-gateway/src/auth/mod.rs
new file mode 100644
index 00000000000..438a0513057
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/auth/mod.rs
@@ -0,0 +1,93 @@
+//! Gateway authentication, as an axum **extractor** (the idiomatic pattern —
+//! keeps handlers clean and auth testable).
+//!
+//! For now this is a single **master key**: any caller presenting it as
+//! `Authorization: Bearer ` may invoke the gateway. Per-key auth, budgets,
+//! and rate limits are delegated to the Python proxy in a later phase.
+//!
+//! A handler opts in by adding [`RequireMasterKey`] to its arguments; auth then
+//! runs during extraction, before the handler body. Routes never re-implement it.
+
+use axum::extract::FromRequestParts;
+use axum::http::header::AUTHORIZATION;
+use axum::http::request::Parts;
+use axum::http::StatusCode;
+use sha2::{Digest, Sha256};
+use subtle::ConstantTimeEq;
+
+use crate::state::AppState;
+
+/// SHA-256 hex digest of a token — the exact transform the Python proxy applies
+/// (`litellm.proxy.utils.hash_token`).
+///
+/// STRICT REQUIREMENT: a raw key (`LITELLM_MASTER_KEY`, a virtual key, …) must
+/// **never** leave this gateway in a log payload. Spend logs and every callback
+/// integration receive `user_api_key_hash`, so that field must be this hash, not
+/// the credential. Hashing here also means the value matches the key's hash in
+/// `LiteLLM_SpendLogs.api_key`, so realtime spend joins with the rest of LiteLLM.
+pub fn hash_token(token: &str) -> String {
+ let digest = Sha256::digest(token.as_bytes());
+ let mut hex = String::with_capacity(digest.len() * 2);
+ for byte in digest {
+ use std::fmt::Write;
+ let _ = write!(hex, "{byte:02x}");
+ }
+ hex
+}
+
+/// Extractor that requires the configured master key as a bearer token.
+///
+/// Rejections: `500` when no master key is configured (permanent
+/// misconfiguration, not a transient outage); `401` on a missing/incorrect
+/// token. The comparison is constant-time.
+pub struct RequireMasterKey;
+
+#[axum::async_trait]
+impl FromRequestParts for RequireMasterKey {
+ type Rejection = (StatusCode, String);
+
+ async fn from_request_parts(
+ parts: &mut Parts,
+ state: &AppState,
+ ) -> Result {
+ let Some(expected) = state.master_key.as_deref() else {
+ return Err((
+ StatusCode::INTERNAL_SERVER_ERROR,
+ "gateway auth not configured (set LITELLM_MASTER_KEY)".to_string(),
+ ));
+ };
+ let provided = parts
+ .headers
+ .get(AUTHORIZATION)
+ .and_then(|value| value.to_str().ok())
+ .and_then(|value| value.strip_prefix("Bearer "))
+ .map(str::trim);
+ match provided {
+ Some(token) if bool::from(token.as_bytes().ct_eq(expected.as_bytes())) => Ok(Self),
+ _ => Err((
+ StatusCode::UNAUTHORIZED,
+ "missing or invalid bearer token".to_string(),
+ )),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::hash_token;
+
+ #[test]
+ fn hash_token_matches_python_sha256_hexdigest() {
+ // Must equal hashlib.sha256("sk-1234".encode()).hexdigest() — the value
+ // the proxy stores in LiteLLM_SpendLogs.api_key.
+ assert_eq!(
+ hash_token("sk-1234"),
+ "88dc28d0f030c55ed4ab77ed8faf098196cb1c05df778539800c9f1243fe6b4b"
+ );
+ // 64 lowercase hex chars, and never the raw input.
+ let h = hash_token("sk-secret");
+ assert_eq!(h.len(), 64);
+ assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
+ assert_ne!(h, "sk-secret");
+ }
+}
diff --git a/litellm-rust/crates/ai-gateway/src/constants.rs b/litellm-rust/crates/ai-gateway/src/constants.rs
new file mode 100644
index 00000000000..109b648f5db
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/constants.rs
@@ -0,0 +1,30 @@
+//! Crate-level constants for the ai-gateway.
+//!
+//! Per `litellm-rust/CLAUDE.md`, magic numbers and fixed strings live here
+//! (the Rust mirror of Python's `litellm/constants.py`), not inline in feature
+//! modules. Env-overridable tunables keep their `DEFAULT_*` value here; the env
+//! read + fallback happens at the host/config layer.
+
+/// Default LiteLLM control-plane base URL for request-log egress when
+/// `LITELLM_PROXY_BASE_URL` is unset.
+pub(crate) const DEFAULT_PROXY_BASE_URL: &str = "http://localhost:4000";
+
+/// The logs ingest path appended to the proxy base. Not a tunable; it is the
+/// proxy's API contract (the rust-control-plane router on the Python proxy).
+pub(crate) const RUST_CONTROL_PLANE_LOGS_PATH: &str = "/v1/rust_control_plane/logs";
+
+/// Default bounded channel depth for the log-egress worker.
+/// Override: `LITELLM_LOG_CHANNEL_CAPACITY`.
+pub(crate) const DEFAULT_CHANNEL_CAPACITY: usize = 4096;
+
+/// Default max records POSTed per request to the control plane.
+/// Override: `LITELLM_LOG_BATCH_SIZE`.
+pub(crate) const DEFAULT_MAX_BATCH_SIZE: usize = 256;
+
+/// Default partial-batch flush cadence, in ms.
+/// Override: `LITELLM_LOG_FLUSH_INTERVAL_MS`.
+pub(crate) const DEFAULT_FLUSH_INTERVAL_MS: u64 = 500;
+
+/// Provider attributed to realtime sessions in the logging payload.
+#[cfg(feature = "server")]
+pub(crate) const DEFAULT_PROVIDER: &str = "openai";
diff --git a/litellm-rust/crates/ai-gateway/src/gil.rs b/litellm-rust/crates/ai-gateway/src/gil.rs
new file mode 100644
index 00000000000..c749f722c73
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/gil.rs
@@ -0,0 +1,58 @@
+//! GIL-activity tracking.
+//!
+//! Every acquisition of the Python GIL is recorded here so the `/health/gil`
+//! endpoint can report whether Python was touched recently. The design goal is
+//! that the GIL is acquired **only at load time** (config read) and never on the
+//! realtime hot path — polling this endpoint during traffic should show the
+//! count holding steady and `acquired_last_30s` falling to `false`.
+
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::time::{SystemTime, UNIX_EPOCH};
+
+/// Window (seconds) for the "recently acquired" signal.
+pub const RECENT_WINDOW_SECS: u64 = 30;
+
+static GIL_ACQUISITIONS: AtomicU64 = AtomicU64::new(0);
+/// Unix seconds of the last acquisition; `0` means "never".
+static LAST_GIL_UNIX_SECS: AtomicU64 = AtomicU64::new(0);
+
+fn now_unix_secs() -> u64 {
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|d| d.as_secs())
+ .unwrap_or(0)
+}
+
+/// Record that the GIL was just acquired. Call immediately before taking the GIL.
+///
+/// Only invoked under the `python-config` feature; without it the gateway never
+/// touches Python, so the recorder is unused (and the endpoint reports zero).
+#[cfg_attr(not(feature = "python-config"), allow(dead_code))]
+pub fn record_acquisition() {
+ GIL_ACQUISITIONS.fetch_add(1, Ordering::Relaxed);
+ LAST_GIL_UNIX_SECS.store(now_unix_secs(), Ordering::Relaxed);
+}
+
+/// Point-in-time view of GIL activity.
+pub struct GilSnapshot {
+ pub total_acquisitions: u64,
+ pub seconds_since_last: Option,
+ pub acquired_last_30s: bool,
+}
+
+/// Read the current GIL-activity snapshot.
+pub fn snapshot() -> GilSnapshot {
+ let total = GIL_ACQUISITIONS.load(Ordering::Relaxed);
+ let last = LAST_GIL_UNIX_SECS.load(Ordering::Relaxed);
+ let seconds_since_last = if last == 0 {
+ None
+ } else {
+ Some(now_unix_secs().saturating_sub(last))
+ };
+ let acquired_last_30s = seconds_since_last.is_some_and(|secs| secs <= RECENT_WINDOW_SECS);
+ GilSnapshot {
+ total_acquisitions: total,
+ seconds_since_last,
+ acquired_last_30s,
+ }
+}
diff --git a/litellm-rust/crates/ai-gateway/src/integrations/README.md b/litellm-rust/crates/ai-gateway/src/integrations/README.md
new file mode 100644
index 00000000000..16a162dac57
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/integrations/README.md
@@ -0,0 +1,127 @@
+# LiteLLM Rust integrations
+
+This directory contains Rust-native equivalents of LiteLLM integration hooks.
+The first supported surfaces are terminal custom loggers and pre/during-call
+custom guardrails.
+
+## File layout
+
+Every integration is a folder:
+
+- `mod.rs` contains the implementation, trait, runner, or adapter
+- `types.rs` contains the integration-local request, response, error, and future
+ types
+
+Do not add new flat integration files such as `custom_logger.rs`. Shared wire
+contracts that are used by multiple integrations can stay in
+`integrations/types.rs`.
+
+Call ordering and lifecycle timing live in `litellm-core/src/call_lifecycle`.
+Call-type modules, such as OCR, adapt their request and response shapes into
+that generic lifecycle runner.
+
+## CustomLogger
+
+Implement `CustomLogger` when Rust code needs to observe terminal success or
+failure events. Method names intentionally match Python `CustomLogger` names.
+
+```rust
+use litellm_ai_gateway::integrations::custom_logger::{
+ CallbackTiming, CallbackValue, CustomLogger, LogFuture, ModelCallDetails,
+};
+
+struct RecordingLogger;
+
+impl CustomLogger for RecordingLogger {
+ fn async_log_success_event<'a>(
+ &'a self,
+ model_call_details: &'a ModelCallDetails,
+ response_obj: &'a CallbackValue,
+ timing: CallbackTiming,
+ ) -> LogFuture<'a> {
+ Box::pin(async move {
+ let model = &model_call_details.model;
+ let provider = &model_call_details.custom_llm_provider;
+ let call_type = model_call_details.call_type.to_string();
+ let request_id = model_call_details.request_id.as_deref();
+ let response_object = &response_obj.object;
+ let duration = timing.end_time - timing.start_time;
+ let standard_payload = model_call_details.standard_logging_payload.as_ref();
+
+ Ok(())
+ })
+ }
+
+ fn async_log_failure_event<'a>(
+ &'a self,
+ model_call_details: &'a ModelCallDetails,
+ response_obj: Option<&'a CallbackValue>,
+ timing: CallbackTiming,
+ ) -> LogFuture<'a> {
+ Box::pin(async move {
+ let error = model_call_details.failure_error.as_ref();
+ let response_object = response_obj.map(|value| value.object.as_str());
+ let duration = timing.end_time - timing.start_time;
+
+ Ok(())
+ })
+ }
+}
+```
+
+Use `CustomLoggerRunner` to fan out terminal events to configured loggers. The
+runner is a no-op when no loggers are configured, which is the expected fast
+path for requests without callbacks.
+
+## CustomGuardrail
+
+Implement `CustomGuardrail` when Rust code needs to run pre-call or native
+during-call checks. Method names intentionally match Python `CustomGuardrail`
+entrypoints inherited from Python `CustomLogger`.
+
+```rust
+use litellm_ai_gateway::integrations::custom_guardrail::{
+ CustomGuardrail, GuardrailContext, GuardrailDecision, GuardrailEventHook,
+ GuardrailFuture, GuardrailRequest,
+};
+
+struct BlocklistedPromptGuardrail;
+
+impl CustomGuardrail for BlocklistedPromptGuardrail {
+ fn guardrail_name(&self) -> &str {
+ "blocklisted-prompt"
+ }
+
+ fn supported_event_hooks(&self) -> &[GuardrailEventHook] {
+ &[GuardrailEventHook::PreCall]
+ }
+
+ fn async_pre_call_hook<'a>(
+ &'a self,
+ _context: &'a GuardrailContext,
+ request: GuardrailRequest,
+ ) -> GuardrailFuture<'a> {
+ Box::pin(async move {
+ if request.data.to_string().contains("blocked phrase") {
+ return Ok(GuardrailDecision::Block(
+ litellm_ai_gateway::integrations::custom_guardrail::GuardrailError::blocked(
+ "blocked phrase detected",
+ ),
+ ));
+ }
+ Ok(GuardrailDecision::Allow(request))
+ })
+ }
+}
+```
+
+Use `CustomGuardrailRunner::run_pre_call` for `pre_call` guardrails and
+`CustomGuardrailRunner::run_during_call` for `during_call` guardrails. A
+`GuardrailDecision::Mask` continues with modified request data.
+`GuardrailDecision::Block` short-circuits the provider call.
+
+## Current boundary
+
+These are Rust-only primitives. Python callback and guardrail adapters are a
+separate layer that should implement these Rust traits instead of changing the
+runner interfaces.
diff --git a/litellm-rust/crates/ai-gateway/src/integrations/custom_guardrail/mod.rs b/litellm-rust/crates/ai-gateway/src/integrations/custom_guardrail/mod.rs
new file mode 100644
index 00000000000..e5d4ce3a708
--- /dev/null
+++ b/litellm-rust/crates/ai-gateway/src/integrations/custom_guardrail/mod.rs
@@ -0,0 +1,468 @@
+//! Rust mirror of Python `CustomGuardrail` entrypoints used by the proxy.
+//!
+//! This module is intentionally Rust-only: Python/PyO3 adapters are a later
+//! layer that should implement this trait rather than changing the runner.
+
+use std::future::Future;
+use std::sync::Arc;
+
+use crate::integrations::custom_logger::{
+ CallbackTiming, CallbackValue, CustomLoggerRunner, LoggingError, ModelCallDetails,
+};
+
+pub mod types;
+
+pub use types::{
+ GuardrailContext, GuardrailDecision, GuardrailDispatchReport, GuardrailError,
+ GuardrailEventHook, GuardrailFuture, GuardrailRequest,
+};
+
+pub trait CustomGuardrail: Send + Sync {
+ fn guardrail_name(&self) -> &str;
+
+ fn supported_event_hooks(&self) -> &[GuardrailEventHook];
+
+ /// Python 1:1 name: `async_pre_call_hook(user_api_key_dict, cache, data, call_type)`.
+ fn async_pre_call_hook<'a>(
+ &'a self,
+ _context: &'a GuardrailContext,
+ request: GuardrailRequest,
+ ) -> GuardrailFuture<'a> {
+ Box::pin(async move { Ok(GuardrailDecision::Allow(request)) })
+ }
+
+ /// Python 1:1 name: `async_moderation_hook(data, user_api_key_dict, call_type)`.
+ fn async_moderation_hook<'a>(
+ &'a self,
+ _context: &'a GuardrailContext,
+ request: GuardrailRequest,
+ ) -> GuardrailFuture<'a> {
+ Box::pin(async move { Ok(GuardrailDecision::Allow(request)) })
+ }
+}
+
+pub struct CustomGuardrailRunner {
+ guardrails: Vec>,
+}
+
+impl CustomGuardrailRunner {
+ pub fn new(guardrails: Vec>) -> Self {
+ Self { guardrails }
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.guardrails.is_empty()
+ }
+
+ pub async fn run_pre_call(
+ &self,
+ context: &GuardrailContext,
+ request: GuardrailRequest,
+ ) -> Result<(GuardrailRequest, GuardrailDispatchReport), GuardrailError> {
+ self.run_hook(GuardrailEventHook::PreCall, context, request)
+ .await
+ }
+
+ pub async fn run_during_call(
+ &self,
+ context: &GuardrailContext,
+ request: GuardrailRequest,
+ ) -> Result<(GuardrailRequest, GuardrailDispatchReport), GuardrailError> {
+ self.run_hook(GuardrailEventHook::DuringCall, context, request)
+ .await
+ }
+
+ pub async fn run_before_provider(
+ &self,
+ event_hook: GuardrailEventHook,
+ context: &GuardrailContext,
+ request: GuardrailRequest,
+ provider: F,
+ ) -> Result
+ where
+ F: FnOnce(GuardrailRequest) -> Fut,
+ Fut: Future