chore: move config and docs off the repo root (#50966)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Yassin Kortam 2026-11-11 18:29:35 -08:00 • committed by GitHub
parent 45a31852a3
commit b0e389503f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
91 changed files with 297 additions and 508 deletions

View file

@ -1,57 +0,0 @@
{
"name": "Python 3.11",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/python:3.11-bookworm",
// https://github.com/devcontainers/images/tree/main/src/python
// https://mcr.microsoft.com/en-us/product/devcontainers/python/tags
// "build": {
// "dockerfile": "Dockerfile",
// "context": ".."
// },
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "lts"
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
"settings": {},
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"GitHub.copilot",
"GitHub.copilot-chat",
"ms-python.autopep8"
]
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [4000],
"containerEnv": {
"LITELLM_LOG": "DEBUG"
},
// Use 'portsAttributes' to set default properties for specific forwarded ports.
// More info: https://containers.dev/implementors/json_reference/#port-attributes
"portsAttributes": {
"4000": {
"label": "LiteLLM Server",
"onAutoForward": "notify"
}
},
// More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "litellm",
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "bash ./.devcontainer/post-create.sh"
}

View file

@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -e
echo "[post-create] Installing uv"
curl -LsSf https://astral.sh/uv/0.10.9/install.sh | env UV_NO_MODIFY_PATH=1 sh
export PATH="$HOME/.local/bin:$PATH"
echo "[post-create] Installing Python dependencies (uv)"
uv sync --frozen --group proxy-dev --extra proxy
echo "[post-create] Generating Prisma client"
uv run --no-sync prisma generate
echo "[post-create] Installing npm dependencies"
cd ui/litellm-dashboard && npm ci
echo "[post-create] Done"

View file

@ -1,32 +0,0 @@
# OpenAI
OPENAI_API_KEY = ""
OPENAI_BASE_URL = ""
# Cohere
COHERE_API_KEY = ""
# OpenRouter
OR_SITE_URL = ""
OR_APP_NAME = "LiteLLM Example app"
OR_API_KEY = ""
# Azure API base URL
AZURE_API_BASE = ""
# Azure API version
AZURE_API_VERSION = ""
# Azure API key
AZURE_API_KEY = ""
# Replicate
REPLICATE_API_KEY = ""
REPLICATE_API_TOKEN = ""
# Anthropic
ANTHROPIC_API_KEY = ""
# Infisical
INFISICAL_TOKEN = ""
# Novita AI
NOVITA_API_KEY = ""
# INFINITY
INFINITY_API_KEY = ""
# Development Configs
# Generate one with: echo "LITELLM_MASTER_KEY=sk-$(openssl rand -hex 32)"
LITELLM_MASTER_KEY = ""
DATABASE_URL = "postgresql://llmproxy:dbpassword9090@db:5432/litellm"
STORE_MODEL_IN_DB = "True"

View file

@ -1,40 +0,0 @@
# Add the commit hash of any commit you want to ignore in `git blame` here.
# One commit hash per line.
#
# The GitHub Blame UI will use this file automatically!
#
# Run this command to always ignore formatting commits in `git blame`
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# Update pydantic code to fix warnings (GH-3600)
876840e9957bc7e9f7d6a2b58c4d7c53dad16481
# style(ui): run prettier --write across the dashboard (#29622)
7edf3a9cb55548b143df1692f4ed7c4681d7fcf7
# style: reformat litellm/ with ruff format (#31317)
17bfd415aeb5a57fb646b5cc67da1c730aa7c50b
# style: unify ruff format width on 120 (#31518)
48b5a5a0cc5a694a11219416ee0b6eb6e620e74e
# refactor(imports): move collections.abc names out of typing (#35495)
397e8e4918777e4e60a7f5e88699e0a9a7dabb3d
# refactor(lint): apply every safe ruff autofix and zero 28 strict-rule budgets (#35495)
b604e2b20c6db2099085a2f0e59b7e99e87eed6f
# refactor(logging): drop redundant !s conversion flags from f-strings (#35546)
7b2d3440cba3160277470f7a0180098ae9b87864
# perf: build log messages lazily so filtered-out log records cost nothing (#35703)
c9887a1f94bc1e7e4bdfe64d640f0509a0bc19dd
# feat(lint): enforce Final on locals and freeze function parameters (#35807)
2708620d6a599cc73c1950a942d26ac26a7ed3d4
# chore(lint): remove litellm/types from the ruff lint exclusion (#35926)
4e32a8bf6a1e1af1e04b67c759841ccef44b2235
# chore(lint): strip inert type: ignore comments and zero LIT009/LIT010/LIT011 headroom (#35928)
338e411103ad5d7003e97f34f04fa36bca542dbe

2
.gitattributes vendored
View file

@ -1,2 +0,0 @@
*.ipynb linguist-vendored
ui/litellm-dashboard/src/lib/http/schema.d.ts linguist-generated

View file

@ -276,15 +276,15 @@ When the provider reports its own usage or spend numbers, compare the proxy's nu
Provider work (a new provider, a new parameter, a new usage or cost field) is the most common kind of contribution, and it is where PRs get reworked most often. Follow these rules
**Preserve zero prices.** In [`model_prices_and_context_window.json`](model_prices_and_context_window.json), an explicit `0.0` means that the priced operation is free. A missing field or `null` means no rate is specified, so the relevant calculator may use a fallback. For example, when no batch input rate is configured, batch cache writes use `cache_creation_input_token_cost` if set, otherwise the regular input rate, then apply the batch discount. A zero ordinary input rate does not make separately priced cache reads or writes free. Test `rate is None` before substituting a fallback, since `row.get("cache_creation_input_token_cost") or fallback` would discard a valid zero. Speech keeps character pricing ahead of token pricing even when the character rate is zero. If neither rate is configured, speech cost calculation raises an error. Its model lookup preserves unset rates instead of treating compatibility defaults as explicit zero prices. Perplexity keeps a configured legacy search rate ahead of the current search rate, including zero. When registering custom prices, `None` leaves an existing rate unchanged and `0.0` overrides it
**Preserve zero prices.** In [`model_prices_and_context_window.json`](../model_prices_and_context_window.json), an explicit `0.0` means that the priced operation is free. A missing field or `null` means no rate is specified, so the relevant calculator may use a fallback. For example, when no batch input rate is configured, batch cache writes use `cache_creation_input_token_cost` if set, otherwise the regular input rate, then apply the batch discount. A zero ordinary input rate does not make separately priced cache reads or writes free. Test `rate is None` before substituting a fallback, since `row.get("cache_creation_input_token_cost") or fallback` would discard a valid zero. Speech keeps character pricing ahead of token pricing even when the character rate is zero. If neither rate is configured, speech cost calculation raises an error. Its model lookup preserves unset rates instead of treating compatibility defaults as explicit zero prices. Perplexity keeps a configured legacy search rate ahead of the current search rate, including zero. When registering custom prices, `None` leaves an existing rate unchanged and `0.0` overrides it
**Find the existing machinery before you add plumbing.** LiteLLM already has generic paths that move data between the endpoints, the provider transformations, and cost tracking. A second, hand-rolled path for the same data is a common reason a provider PR gets rewritten before merge. [`ARCHITECTURE.md`](ARCHITECTURE.md) shows where the layers live, and the decision records in [`docs/adrs/`](docs/adrs/AGENTS.md) explain why they are shaped that way. For example, [the ADR on provider usage fields](docs/adrs/adr_001_provider_usage_fields_ride_as_usage_extras.md) covers how provider-specific usage fields reach cost tracking. If you cannot find a mechanism for what you need, say so in the PR or ask in [#pr-review on Slack](https://join.slack.com/t/litellmossslack/shared_invite/zt-3o7nkuyfr-p_kbNJj8taRfXGgQI1~YyA) before you build one
**Find the existing machinery before you add plumbing.** LiteLLM already has generic paths that move data between the endpoints, the provider transformations, and cost tracking. A second, hand-rolled path for the same data is a common reason a provider PR gets rewritten before merge. [`ARCHITECTURE.md`](../docs/ARCHITECTURE.md) shows where the layers live, and the decision records in [`docs/adrs/`](../docs/adrs/AGENTS.md) explain why they are shaped that way. For example, [the ADR on provider usage fields](../docs/adrs/adr_001_provider_usage_fields_ride_as_usage_extras.md) covers how provider-specific usage fields reach cost tracking. If you cannot find a mechanism for what you need, say so in the PR or ask in [#pr-review on Slack](https://join.slack.com/t/litellmossslack/shared_invite/zt-3o7nkuyfr-p_kbNJj8taRfXGgQI1~YyA) before you build one
**Bill a new provider usage field from the usage object.** Leave the field on the usage object as an extra and read it in that provider's `cost_calculator.py`. A provider without one gets a new `cost_calculator.py` and an arm for it in `cost_per_token` (`litellm/cost_calculator.py`), which picks the calculator by provider. If the provider's transform builds `Usage` from named fields, as the Anthropic, Gemini, and Bedrock Converse ones do, pass the new field there too. Streamed requests rebuild usage in `ChunkProcessor.calculate_usage` (`litellm/litellm_core_utils/streaming_chunk_builder_utils.py`), which does not copy extras yet the way the Responses-to-chat bridge does (minus a short exclusion list), so until it does, bill the field on streams by naming it there, as Anthropic's `inference_geo` is. A provider that reports its own charge for the request, as OpenRouter does in `usage.cost`, is billed at that charge from `_hidden_params` before any calculator runs, unless the deployment sets its own prices. When the field counts a built-in tool such as web search, add the provider to `get_cost_for_web_search_request` in `litellm/llms/__init__.py`, and add a gate to `litellm/litellm_core_utils/llm_cost_calc/tool_call_cost_tracking.py` only when no existing gate fires. Then show the field's cost in your proof on every endpoint it reaches, streaming included
**Do not change what a caller sees to make internals easier.** Each endpoint keeps the schema of the API it emulates. A `/v1/responses` caller gets `input_tokens` and a `/v1/chat/completions` caller gets `prompt_tokens`, whatever the provider sent and whatever cost tracking needs. A provider usage field that shows up as an extra key is fine, since it only adds a key, but renaming, removing, or reshaping a key the endpoint defines is not. Reshaping a public response to feed an internal consumer breaks every user of that endpoint
If your change makes an architecture decision, such as a new cross-cutting mechanism or a deliberate difference from a provider's API shape, add an ADR to [`docs/adrs/`](docs/adrs/AGENTS.md) in the same PR
If your change makes an architecture decision, such as a new cross-cutting mechanism or a deliberate difference from a provider's API shape, add an ADR to [`docs/adrs/`](../docs/adrs/AGENTS.md) in the same PR
## Building and Running Locally

View file

72
.github/codecov.yml vendored Normal file
View file

@ -0,0 +1,72 @@
codecov:
require_ci_to_pass: false # post coverage status even if CI has unrelated failures
notify:
wait_for_ci: false # post as soon as expected uploads arrive, don't wait on CI
ignore:
- "litellm-rust/**"
# Flagged uploads avoid overlapping sessions on reruns.
# Only the flags below reuse parent coverage, avoiding stale line maps elsewhere.
flag_management:
default_rules:
carryforward: false
individual_flags:
- name: unit
carryforward: true
- name: core-packaging
carryforward: true
- name: mcp-integration
carryforward: true
component_management:
individual_components:
- component_id: "Router"
paths:
- "router"
- component_id: "LLMs"
paths:
- "*/llms/*"
- component_id: "Caching"
paths:
- "*/caching/*"
- ".*redis.*"
- component_id: "litellm_logging"
paths:
- "*/integrations/*"
- ".*litellm_logging.*"
- component_id: "Proxy_Authentication"
paths:
- "*/proxy/auth/**"
- component_id: "Enterprise"
paths:
- "enterprise/**"
- component_id: "Batches"
paths:
- "*/proxy/batches_endpoints/**"
- "litellm/batches/**"
- "*/llms/*/batches/**"
- component_id: "Videos"
paths:
- "litellm/videos/**"
- "*/proxy/video_endpoints/**"
- "*/llms/*/videos/**"
- component_id: "Realtime"
paths:
- "litellm/realtime_api/**"
- "*/proxy/realtime_endpoints/**"
- "*/llms/*/realtime/**"
- "litellm/litellm_core_utils/realtime_streaming.py"
comment:
layout: "header, diff, flags, components" # show component info in the PR comment
coverage:
status:
project:
default:
target: auto
threshold: 0% # do not allow project coverage to drop
patch:
default:
target: auto
threshold: 0% # patch coverage should be 100%

View file

View file

@ -1,4 +1,4 @@
extend = "ruff.toml"
extend = "../../pyproject.toml"
[lint]
preview = true
@ -6,7 +6,7 @@ select = ["ANN", "ASYNC230", "B004", "B006", "B008", "B009", "B010", "B018", "B0
extend-select = []
# Overrides the inherited list: rules this gate enforces itself must NOT be external here,
# so this config's RUF100 flags their stale `# noqa` directives. What remains external is
# only what other tooling enforces: every base ruff.toml rule this select list doesn't
# only what other tooling enforces: every base pyproject.toml rule this select list doesn't
# re-enable (all of the default E/F families plus T20/PGH004/RUF008/RUF009, minus the
# strict-selected F401 and RUF100; F4 is split out so stale F401 noqas stay detectable),
# plus upstream litellm's ruff config.

View file

@ -1,4 +1,4 @@
# Lint config for the test tree, which ruff.toml excludes from `ruff check`.
# Lint config for the test tree, which pyproject.toml excludes from `ruff check`.
#
# Every rule here catches a test that cannot fail. Rules land one at a time, each
# with its existing violations already fixed, so this list never needs a budget

View file

@ -5,13 +5,13 @@ Add custom rule YAML files here. Semgrep loads all `.yml`/`.yaml` files under th
**Run only custom rules (CI / fail on findings):**
```bash
semgrep scan --config .semgrep/rules . --error
semgrep scan --config .github/semgrep/rules . --error
```
**Run with registry + custom rules:**
```bash
semgrep scan --config auto --config .semgrep/rules .
semgrep scan --config auto --config .github/semgrep/rules .
```
**Layout:**

View file

@ -3,7 +3,7 @@ name: Conventional PR Title
# Squash-merge replaces the merge commit subject with the PR title, so
# enforcing Conventional Commits at the PR-title level is what actually gates
# the commits that land on the default branch. The local commit-msg hook
# (.githooks/commit-msg) is a best-effort assist; this workflow is the gate.
# (scripts/githooks/commit-msg) is a best-effort assist; this workflow is the gate.
#
# See https://www.conventionalcommits.org/en/v1.0.0/
@ -28,7 +28,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Must mirror the type list in .githooks/commit-msg.
# Must mirror the type list in scripts/githooks/commit-msg.
types: |
feat
fix

View file

@ -49,7 +49,12 @@ jobs:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
uv run --no-project python ci_cd/cost_map_guard.py --base "$MERGE_BASE" --head "$HEAD_SHA" --head-ref "$HEAD_REF"
guard=scripts/ci_cd/cost_map_guard.py
# TODO: Drop the legacy path after this lands on main.
if [ ! -f "$guard" ]; then
guard=ci_cd/cost_map_guard.py
fi
uv run --no-project python "$guard" --base "$MERGE_BASE" --head "$HEAD_SHA" --head-ref "$HEAD_REF"
- name: Check whether the pull request changes the cost map
id: changed
env:

View file

@ -29,7 +29,7 @@ on:
- ui/litellm-dashboard/package-lock.json
- ui/nginx.conf
- .github/workflows/image-scan.yml
- .grype.yaml
- .github/grype.yaml
schedule:
- cron: "41 6 * * *"
workflow_dispatch:
@ -201,7 +201,7 @@ jobs:
GRYPE_MATCH_PYTHON_USING_CPES: "true"
run: |
"$RUNNER_TEMP/grype" "$IMAGE" \
--config .grype.yaml \
--config .github/grype.yaml \
--only-fixed \
--fail-on high \
--output table

View file

@ -40,7 +40,7 @@ jobs:
- name: Scan lockfiles
run: |
"$RUNNER_TEMP/osv-scanner" scan source \
--config osv-scanner.toml \
--config .github/osv-scanner.toml \
-L uv.lock \
-L ui/litellm-dashboard/package-lock.json \
-L vscode-extension/package-lock.json

View file

@ -42,4 +42,9 @@ jobs:
MERGE_BASE: ${{ steps.revisions.outputs.merge_base }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
uv run --no-project --with pydantic==2.14.1 python ci_cd/spend_logs_guard.py --base "$MERGE_BASE" --head "$HEAD_SHA" --approver kerry-berri --reviews "$RUNNER_TEMP/reviews.json"
guard=scripts/ci_cd/spend_logs_guard.py
# TODO: Drop the legacy path after this lands on main.
if [ ! -f "$guard" ]; then
guard=ci_cd/spend_logs_guard.py
fi
uv run --no-project --with pydantic==2.14.1 python "$guard" --base "$MERGE_BASE" --head "$HEAD_SHA" --approver kerry-berri --reviews "$RUNNER_TEMP/reviews.json"

View file

@ -145,7 +145,7 @@ jobs:
- name: Run Ruff linting (test tree)
if: steps.changes.outputs.decision != 'skip'
run: |
uv run --no-sync ruff check --config ruff-tests.toml tests
uv run --no-sync ruff check --config .github/linters/ruff-tests.toml tests
- name: Check strict ruff rules (delta vs merge-base counts)
if: steps.changes.outputs.decision != 'skip'
@ -191,7 +191,7 @@ jobs:
- name: Check tests/e2e basedpyright (zero errors)
if: steps.changes.outputs.decision != 'skip'
run: |
if git diff --name-only --diff-filter=ACMRD "$GATE_BASE_SHA" HEAD -- ':(glob)tests/e2e/**/*.py' ':(glob)tests/e2e_harness/**/*.py' pyrightconfig.json | grep -q .; then
if git diff --name-only --diff-filter=ACMRD "$GATE_BASE_SHA" HEAD -- ':(glob)tests/e2e/**/*.py' ':(glob)tests/e2e_harness/**/*.py' pyproject.toml | grep -q .; then
uv run --no-sync basedpyright tests/e2e tests/e2e_harness
else
echo "No changed tests/e2e Python files; skipping."
@ -703,7 +703,7 @@ jobs:
version: "0.10.9"
- name: Run Semgrep (custom rules)
run: uv tool run --from 'semgrep==1.157.0' semgrep scan --config .semgrep/rules . --error
run: uv tool run --from 'semgrep==1.157.0' semgrep scan --config .github/semgrep/rules . --error
lint-passed:
name: lint passed
needs: [python, secret-scan, ui, ui-api-types, code-quality, ci-coverage, semgrep]

5
.npmrc
View file

@ -1,5 +0,0 @@
# Supply-chain hardening
# Packages needing lifecycle scripts: npm rebuild <pkg>
ignore-scripts=true
# Protects local npm install only — npm ci (used in CI) ignores this
min-release-age=3

View file

@ -1,7 +1,6 @@
Do not write comments unless they are any of:
- absolutely necessary to explain some very complex business logic (in which case, keep it concise and clear)
- used as an input for tools to read and act on. For example:
- entries in `.git-blame-ignore-revs` saying which commit is excluded from git blame
- a lint or type checker suppression like `# mutable-ok` or `# pyright: ignore[reportArgumentType] # <reason>` when introducing a truly unavoidable violation
- a TODO or FIXME
- Not great to have those, but if it's unavoidable, make sure to include a strong, concise reason for why it's there or, better yet, link to a GitHub issue for the follow-up work
@ -69,7 +68,7 @@ If you're trying to create a new function that relies on untyped stuff, instead
If you get an LIT001 fail, refactor the code to follow functional programming best practices rather than introducing mutable data structures. For example, build values in one shot with comprehensions or generators wrapped in `tuple()` / `MappingProxyType()` / `frozenset()` instead of seeding an empty `list`/`dict`/`set` and mutating it over time. Ideally, `# mutable-ok` is never used. Reach for it only as a genuine last resort when an immutable rewrite is truly impossible, and always pair it with a real reason
Every lint or type suppression must name the exact rule inside brackets and carry a reason comment, e.g. `# pyright: ignore[reportArgumentType] # stubs lack async overload` or `# noqa: TID251 # <reason>`. `# type: ignore` is banned (LIT009): pyrightconfig.json sets `enableTypeIgnoreComments` to false, so it silently does nothing
Every lint or type suppression must name the exact rule inside brackets and carry a reason comment, e.g. `# pyright: ignore[reportArgumentType] # stubs lack async overload` or `# noqa: TID251 # <reason>`. `# type: ignore` is banned (LIT009): `[tool.pyright]` in pyproject.toml sets `enableTypeIgnoreComments` to false, so it silently does nothing
Commit and push your work when you're done without asking

View file

@ -126,7 +126,7 @@ install-hooks:
./scripts/install_git_hooks.sh
# Formatting
# Wrap width is ruff.toml's single source of truth (line-length = 120), shared by the
# Wrap width is pyproject.toml's single source of truth (line-length = 120), shared by the
# formatter and the import sorter so there's no 88-vs-120 split to reconcile.
format: install-dev
cd litellm && $(UV_RUN) ruff format --exclude '/enterprise/' . && cd ..
@ -165,7 +165,7 @@ lint-format-check-changed: $(LINT_DEP_INSTALL) $(LINT_DEP_BASE)
lint-ruff: $(LINT_DEP_INSTALL)
$(UV_RUN) python scripts/check_mcp_operation_boundary.py
cd litellm && $(UV_RUN) ruff check . && cd ..
$(UV_RUN) ruff check --config ruff-tests.toml tests
$(UV_RUN) ruff check --config .github/linters/ruff-tests.toml tests
# faster linter for developing ...
# inspiration from:

View file

@ -6,7 +6,7 @@
</p>
<p align="center">Open Source AI Gateway for 100+ LLMs. Self-hosted. Enterprise-ready. Call any LLM in OpenAI format.</p>
<p align="center">
<a href="https://render.com/deploy?repo=https://github.com/BerriAI/litellm" target="_blank" rel="nofollow"><img src="https://render.com/images/deploy-to-render-button.svg" alt="Deploy to Render" height="40"></a>
<a href="https://render.com/deploy?repo=https://github.com/BerriAI/litellm&amp;blueprintPath=litellm/deploy/render/render.yaml" target="_blank" rel="nofollow"><img src="https://render.com/images/deploy-to-render-button.svg" alt="Deploy to Render" height="40"></a>
<a href="https://railway.com/deploy/RhvhdC?referralCode=7mRv9K&utm_medium=integration&utm_source=template&utm_campaign=generic"><img src="https://railway.com/button.svg" alt="Deploy on Railway" height="40"></a>
<a href="https://console.aws.amazon.com/cloudshell/home" target="_blank" rel="nofollow"><img src="./.github/deploy-on-aws.png" alt="Deploy on AWS" height="40"></a>
<a href="https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2FBerriAI%2Fterraform-google-litellm&cloudshell_git_branch=v1.105.0&cloudshell_workspace=examples%2Fdefault&cloudshell_tutorial=TUTORIAL.md&cloudshell_image=gcr.io/ds-artifacts-cloudshell/deploystack_custom_image&shellonly=true" target="_blank" rel="nofollow"><img src="./.github/deploy-on-gcp.png" alt="Deploy on GCP" height="40"></a>
@ -725,7 +725,7 @@ make test-unit # Run unit tests
make format-check # Check formatting only
```
For detailed contributing guidelines, see [CONTRIBUTING.md](CONTRIBUTING.md).
For detailed contributing guidelines, see [CONTRIBUTING.md](.github/CONTRIBUTING.md).
> **📖 Contributing to documentation?** The LiteLLM docs have moved to a separate repository: [BerriAI/litellm-docs](https://github.com/BerriAI/litellm-docs). Please open doc PRs there. Docs are served at [docs.litellm.ai](https://docs.litellm.ai).

View file

@ -1,164 +0,0 @@
codecov:
require_ci_to_pass: false # post coverage status even if CI has unrelated failures
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 tier. 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
# was not re-uploaded. Required because the same commit can receive the
# push-triggered workflows more than once (re-runs / branches cut at the same
# SHA); flagless overlapping sessions made Codecov drop the largest files.
flag_management:
default_rules:
carryforward: true
# Dead flags no CI job uploads anymore, and flags whose job is path-gated on
# PRs: their carried-forward sessions were measured against old revisions, and
# the stale line maps mark comment lines of since-edited files as missed,
# sinking patch coverage on unrelated PRs.
individual_flags:
- name: proxy-mgmt-behavior
carryforward: false
- name: security
carryforward: false
- name: proxy-db-schema-migration
carryforward: false
- name: circleci
carryforward: false
- name: core-utils
carryforward: false
- name: enterprise-routing
carryforward: false
- name: integrations
carryforward: false
- name: llm-vertex-ai
carryforward: false
- name: llm-other-providers
carryforward: false
- name: llm-openai-meta
carryforward: false
- name: misc
carryforward: false
- name: misc-dirs
carryforward: false
- name: proxy-auth
carryforward: false
- name: proxy-hooks-client
carryforward: false
- name: proxy-endpoints
carryforward: false
- name: proxy-feature-endpoints
carryforward: false
- name: proxy-server
carryforward: false
- name: mcp-elicitation
carryforward: false
- name: proxy-infra
carryforward: false
- name: proxy-infra-root
carryforward: false
- name: proxy-infra-root-a-p
carryforward: false
- name: proxy-infra-root-q-z
carryforward: false
- name: proxy-subprocess
carryforward: false
- name: caching-local
carryforward: false
- name: proxy-extras
carryforward: false
- name: enterprise-package
carryforward: false
- name: enterprise-managed-files
carryforward: false
- name: responses-caching-types
carryforward: false
- name: lens-python-310
carryforward: false
- name: lens-postgres
carryforward: false
- name: proxy-db-key-generation
carryforward: false
- name: proxy-db-auth-checks
carryforward: false
- name: proxy-db-jwt-and-keys
carryforward: false
- name: proxy-db-proxy-utils
carryforward: false
- name: proxy-db-proxy-server-core
carryforward: false
- name: proxy-db-proxy-runtime
carryforward: false
- name: proxy-db-mcp-oauth
carryforward: false
- name: proxy-db-custom-logging
carryforward: false
- name: proxy-db-logging-misc
carryforward: false
- name: proxy-db-db-and-spend
carryforward: false
- name: proxy-db-guardrails-hooks
carryforward: false
- name: proxy-db-budgets
carryforward: false
- name: proxy-db-endpoints-and-responses
carryforward: false
- name: roi-postgres
carryforward: false
- name: redis-compat
carryforward: false
component_management:
individual_components:
- component_id: "Router"
paths:
- "router"
- component_id: "LLMs"
paths:
- "*/llms/*"
- component_id: "Caching"
paths:
- "*/caching/*"
- ".*redis.*"
- component_id: "litellm_logging"
paths:
- "*/integrations/*"
- ".*litellm_logging.*"
- component_id: "Proxy_Authentication"
paths:
- "*/proxy/auth/**"
- component_id: "Enterprise"
paths:
- "enterprise/**"
- component_id: "Batches"
paths:
- "*/proxy/batches_endpoints/**"
- "litellm/batches/**"
- "*/llms/*/batches/**"
- component_id: "Videos"
paths:
- "litellm/videos/**"
- "*/proxy/video_endpoints/**"
- "*/llms/*/videos/**"
- component_id: "Realtime"
paths:
- "litellm/realtime_api/**"
- "*/proxy/realtime_endpoints/**"
- "*/llms/*/realtime/**"
- "litellm/litellm_core_utils/realtime_streaming.py"
comment:
layout: "header, diff, flags, components" # show component info in the PR comment
coverage:
status:
project:
default:
target: auto
threshold: 0% # do not allow project coverage to drop
patch:
default:
target: auto
threshold: 0% # patch coverage should be 100%

View file

@ -2,7 +2,7 @@
This document helps contributors understand where to make changes in LiteLLM.
For why a layer is shaped the way it is, read the architecture decision records in [`docs/adrs/`](docs/adrs/AGENTS.md) (Python SDK and proxy) and [`litellm-rust/docs/adrs/`](litellm-rust/docs/adrs/AGENTS.md) (Rust core)
For why a layer is shaped the way it is, read the architecture decision records in [`docs/adrs/`](adrs/AGENTS.md) (Python SDK and proxy) and [`litellm-rust/docs/adrs/`](../litellm-rust/docs/adrs/AGENTS.md) (Rust core)
---

View file

@ -4,7 +4,7 @@ background, once it serves).
A migration cannot build them: a plain `CREATE INDEX` blocks spend-log inserts for the
whole build, and `CREATE INDEX CONCURRENTLY` is refused on a partitioned parent
(db_scripts/partition_spend_logs.sql). `REQUEST_LOG_INDEXES` is the one list to extend;
(scripts/db/partition_spend_logs.sql). `REQUEST_LOG_INDEXES` is the one list to extend;
names match what Prisma derives from the `@@index` declarations in schema.prisma, so an
index a database already has is recognized and never rebuilt.
"""

View file

@ -147,7 +147,7 @@ _SPEND_LOGS_PK_CLAUSE_RE = re.compile(
)
PARTITIONED_SPEND_LOGS_PUSH_ERROR = (
"LiteLLM_SpendLogs is a partitioned table (see db_scripts/partition_spend_logs.sql), "
"LiteLLM_SpendLogs is a partitioned table (see scripts/db/partition_spend_logs.sql), "
'so its primary key must include the partition key ("startTime"). `prisma db push` '
"reconciles the database against schema.prisma, which declares the unpartitioned "
'primary key ("request_id"), and Postgres rejects that rewrite with: unique '
@ -179,7 +179,7 @@ def _without_spend_logs_pk_clauses(statement: str) -> str | None:
def filter_partitioned_spend_logs_diff(diff_sql: str) -> str:
"""Drop statements from a `prisma migrate diff` script that fight the
SpendLogs partitioning runbook (db_scripts/partition_spend_logs.sql): the
SpendLogs partitioning runbook (scripts/db/partition_spend_logs.sql): the
primary-key rewrite on "LiteLLM_SpendLogs", which Postgres rejects on a
partitioned table, and drops of runbook artifacts such as
"LiteLLM_SpendLogs_legacy"."""
@ -679,7 +679,7 @@ class ProxyExtrasDBManager:
"""True when the connected database's LiteLLM_SpendLogs is a
partitioned table in Prisma's target schema (the `schema` URL param,
falling back to Prisma's default target, public), i.e. the operator
ran db_scripts/partition_spend_logs.sql. Returns False when psycopg is
ran scripts/db/partition_spend_logs.sql. Returns False when psycopg is
unavailable or the database cannot be reached, preserving the
pre-existing behavior in those cases."""
database_url = os.getenv("DATABASE_URL")
@ -692,7 +692,7 @@ class ProxyExtrasDBManager:
logger.warning(
"psycopg is not installed; skipping the LiteLLM_SpendLogs "
"partition check. If this table is partitioned (see "
"db_scripts/partition_spend_logs.sql), schema reconciliation "
"scripts/db/partition_spend_logs.sql), schema reconciliation "
"will try to rewrite its primary key and fail. Install the "
"litellm[extra_proxy] extra, which now includes psycopg."
)

View file

@ -43,7 +43,7 @@ brew install postgresql@14 # macOS
export PATH="/opt/homebrew/opt/postgresql@14/bin:$PATH"
# Run migration
uv run --with testing.postgresql python ci_cd/run_migration.py "your_migration_name"
uv run --with testing.postgresql python scripts/ci_cd/run_migration.py "your_migration_name"
```
## What It Does
@ -89,7 +89,7 @@ When the guard fires:
4. Only if the drops are genuinely intentional, re-run with the flag:
```bash
uv run --with testing.postgresql python ci_cd/run_migration.py "your_migration_name" --allow-destructive
uv run --with testing.postgresql python scripts/ci_cd/run_migration.py "your_migration_name" --allow-destructive
```
> **AI AGENTS / ASSISTANTS:** Do **not** automatically re-run the command with `--allow-destructive`. If the guard fires while you are driving the runbook for a human, stop, show them the error, and wait for their explicit confirmation before passing the flag. Auto-passing `--allow-destructive` is the exact failure mode this guard exists to prevent.
@ -98,7 +98,7 @@ When the guard fires:
**Missing testing module:**
```bash
uv run --with testing.postgresql python ci_cd/run_migration.py "your_migration_name"
uv run --with testing.postgresql python scripts/ci_cd/run_migration.py "your_migration_name"
```
**initdb not found:**

View file

@ -3034,7 +3034,7 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase):
)
use_spend_logs_partitioning: bool | None = Field(
None,
description="If True and LiteLLM_SpendLogs has been converted to a range-partitioned table (db_scripts/partition_spend_logs.sql), retention cleanup drops expired partitions instead of deleting rows, and pre-creates upcoming partitions. Default is False.",
description="If True and LiteLLM_SpendLogs has been converted to a range-partitioned table (scripts/db/partition_spend_logs.sql), retention cleanup drops expired partitions instead of deleting rows, and pre-creates upcoming partitions. Default is False.",
)
maximum_spend_logs_cleanup_batch_size: int | None = Field(
None,

View file

@ -294,7 +294,7 @@ keys = client.keys.list(
## Contributing
Contributions are welcome! Please check out our [contributing guidelines](../../CONTRIBUTING.md) for details.
Contributions are welcome! Please check out our [contributing guidelines](../../../.github/CONTRIBUTING.md) for details.
## License

View file

@ -51,7 +51,7 @@ async def create_view_tolerating_race(db: SupportsExecuteRaw, view_name: str, dd
async def create_missing_views(db: SupportsRawQueries) -> None:
"""
--------------------------------------------------
NOTE: Copy of `litellm/db_scripts/create_views.py`.
NOTE: Copy of `scripts/db/create_views.py`.
--------------------------------------------------
Checks if the LiteLLM_VerificationTokenView and MonthlyGlobalSpend exists in the user's db.

View file

@ -9,7 +9,7 @@ to the OS immediately, with no tombstones and no vacuum.
This manager only acts when use_spend_logs_partitioning is enabled in
general_settings AND the table is already partitioned (set up via the
db_scripts/partition_spend_logs.sql runbook). Without both, the cleanup job
scripts/db/partition_spend_logs.sql runbook). Without both, the cleanup job
keeps the batched-DELETE path, so existing deployments are untouched.
"""

View file

@ -6006,7 +6006,7 @@ class PrismaClient:
missing_views: Final = expected_views_set - ret_view_names_set
verbose_proxy_logger.warning(
"\n\n\x1b[93mNot all views exist in db, needed for UI 'Usage' tab. Missing=%s.\nRun 'create_views.py' from https://github.com/BerriAI/litellm/tree/main/db_scripts to create missing views.\x1b[0m\n",
"\n\n\x1b[93mNot all views exist in db, needed for UI 'Usage' tab. Missing=%s.\nRun 'create_views.py' from https://github.com/BerriAI/litellm/tree/main/scripts/db to create missing views.\x1b[0m\n",
missing_views,
)

View file

@ -1289,7 +1289,7 @@ def function_setup(
original_function: str,
rules_obj: Rules,
start_time: datetime.datetime,
*args: Any, # positional passthrough to the wrapped LLM call (ANN401 ignored, see ruff-strict.toml)
*args: Any, # positional passthrough to the wrapped LLM call (ANN401 ignored, see .github/linters/ruff-strict.toml)
is_async_call: bool = True,
**kwargs: Any, # kwargs-ok: forwarded to Logging()/callbacks, varies per call_type
) -> tuple[LiteLLMLoggingObject, dict[str, Any]]:

View file

@ -488,3 +488,79 @@ pytest_add_cli_args = [
[tool.coverage.run]
source = ["litellm"]
relative_files = true
[tool.ruff]
lint.ignore = ["F405", "E402", "F403"]
# The second group is the strict gate's graduates: rules the codebase already has zero
# violations of, so they hard-fail here instead of being counted by the strict gate.
# That gives editors and `ruff check --fix` the diagnostic, which the gate script cannot.
lint.extend-select = [
"T20", "PGH004", "RUF008", "RUF009", "RUF100",
"B004", "B018", "B021", "B033", "FURB136", "FURB168", "FURB188", "I001", "PERF402", "PIE790",
"PIE800", "PLC0208", "PLR0124", "PLR0402", "PLR0206", "PLR1704", "PLR1711", "PLR1730", "PLR2044",
"PLW0133", "PYI030", "PYI041", "PYI064", "RET501", "RUF010", "RUF022", "RUF023", "RUF051", "S113",
"SIM114", "SIM118", "SIM201", "SIM211", "SIM222", "TC005", "UP006", "UP007", "UP008",
"UP012", "UP018", "UP024", "UP032", "UP034", "UP035", "UP036", "UP037", "UP045",
"C404", "C419", "TID253",
]
# RUF100 (unused-noqa) only knows the rules enabled in THIS config, so it would strip
# `# noqa` directives that protect rules enforced elsewhere. List those codes as external
# so RUF100 leaves their directives alone: the strict gate (.github/linters/ruff-strict.toml) and upstream
# litellm's own ruff config both rely on suppressions this config can't see.
lint.external = [
# Enforced by the strict-rule gate (scripts/ruff_strict_gate.py + .github/linters/ruff-strict.toml).
# Family entries whose every strict rule graduated into extend-select above (FURB), and
# standalone graduated codes (I001, RUF010, RUF022, RUF023, RUF051), are dropped so this
# config's RUF100 polices their directives itself.
"ANN", "ASYNC230", "B", "C", "D202", "D403", "D417", "D419", "DTZ", "EXE", "LOG015", "N999", "PERF",
"PIE", "PL", "PYI", "RET", "RUF012", "RUF015", "RUF019",
"RUF046", "RUF059", "S110", "S112", "SIM", "TC", "TID251", "TRY", "UP",
# Enforced by upstream litellm's ruff config, but not run in this repo's CI
"PLC0415", "E402", "BLE001", "ARG002", "S102", "S324", "S606", "D401", "F403", "F405",
]
line-length = 120
extend-exclude = ["litellm/proxy/discovery_endpoints/skill_scanner_adapter"]
format.exclude = ["**/enterprise/**"]
# Was the top-level `exclude`. Scoped to lint so `ruff format` still formats these paths
# (Black did) while `ruff check` keeps skipping them.
lint.exclude = ["litellm/__init__.py", "litellm/proxy/example_config_yaml/*", "tests/*"]
[tool.ruff.lint.flake8-tidy-imports]
banned-module-level-imports = ["prisma"]
[tool.ruff.lint.per-file-ignores]
"litellm/main.py" = ["F401"]
"litellm/types/caching.py" = ["F401"]
"litellm/types/integrations/slack_alerting.py" = ["F401"]
"litellm/types/llms/custom_http.py" = ["F401"]
"litellm/types/llms/openai.py" = ["F401"]
"litellm/types/proxy/management_endpoints/scim_v2.py" = ["F401"]
"litellm/types/responses/main.py" = ["F401"]
"litellm/utils.py" = ["F401"]
"litellm/proxy/proxy_server.py" = ["F401"]
"litellm/caching/__init__.py" = ["F401"]
"litellm/llms/anthropic/chat/__init__.py" = ["F401"]
"litellm/llms/azure_ai/embed/__init__.py" = ["F401"]
"litellm/llms/azure_ai/rerank/__init__.py" = ["F401"]
"litellm/llms/bedrock/chat/__init__.py" = ["F401"]
"litellm/proxy/utils.py" = ["F401"]
[tool.pyright]
include = ["litellm", "litellm_cli"]
ignore = []
exclude = ["**/node_modules", "**/__pycache__", "tests/e2e/claude_code", "tests/e2e_harness/claude_code", "tests/e2e/ui", "litellm/types/utils.py", "litellm/proxy/_types.py", "litellm/proxy/discovery_endpoints/skill_scanner_adapter"]
pythonVersion = "3.12"
typeCheckingMode = "strict"
enableTypeIgnoreComments = false
reportMissingImports = false
reportPrivateImportUsage = false
reportExplicitAny = "error"
reportAny = "error"
[[tool.pyright.executionEnvironments]]
root = "tests/e2e_harness"
extraPaths = ["tests/e2e", "tests/e2e/batches", "tests/e2e/guardrails", "tests/e2e/perf", "tests/e2e/logging"]

View file

@ -1,18 +0,0 @@
{
"include": ["litellm", "litellm_cli"],
"ignore": [],
"exclude": ["**/node_modules", "**/__pycache__", "tests/e2e/claude_code", "tests/e2e_harness/claude_code", "tests/e2e/ui", "litellm/types/utils.py", "litellm/proxy/_types.py", "litellm/proxy/discovery_endpoints/skill_scanner_adapter"],
"executionEnvironments": [
{
"root": "tests/e2e_harness",
"extraPaths": ["tests/e2e", "tests/e2e/batches", "tests/e2e/guardrails", "tests/e2e/perf", "tests/e2e/logging"]
}
],
"pythonVersion": "3.12",
"typeCheckingMode": "strict",
"enableTypeIgnoreComments": false,
"reportMissingImports": false,
"reportPrivateImportUsage": false,
"reportExplicitAny": "error",
"reportAny": "error"
}

View file

@ -1,58 +0,0 @@
lint.ignore = ["F405", "E402", "F403"]
# The second group is the strict gate's graduates: rules the codebase already has zero
# violations of, so they hard-fail here instead of being counted by the strict gate.
# That gives editors and `ruff check --fix` the diagnostic, which the gate script cannot.
lint.extend-select = [
"T20", "PGH004", "RUF008", "RUF009", "RUF100",
"B004", "B018", "B021", "B033", "FURB136", "FURB168", "FURB188", "I001", "PERF402", "PIE790",
"PIE800", "PLC0208", "PLR0124", "PLR0402", "PLR0206", "PLR1704", "PLR1711", "PLR1730", "PLR2044",
"PLW0133", "PYI030", "PYI041", "PYI064", "RET501", "RUF010", "RUF022", "RUF023", "RUF051", "S113",
"SIM114", "SIM118", "SIM201", "SIM211", "SIM222", "TC005", "UP006", "UP007", "UP008",
"UP012", "UP018", "UP024", "UP032", "UP034", "UP035", "UP036", "UP037", "UP045",
"C404", "C419", "TID253",
]
# RUF100 (unused-noqa) only knows the rules enabled in THIS config, so it would strip
# `# noqa` directives that protect rules enforced elsewhere. List those codes as external
# so RUF100 leaves their directives alone: the strict gate (ruff-strict.toml) and upstream
# litellm's own ruff config both rely on suppressions this config can't see.
lint.external = [
# Enforced by the strict-rule gate (scripts/ruff_strict_gate.py + ruff-strict.toml).
# Family entries whose every strict rule graduated into extend-select above (FURB), and
# standalone graduated codes (I001, RUF010, RUF022, RUF023, RUF051), are dropped so this
# config's RUF100 polices their directives itself.
"ANN", "ASYNC230", "B", "C", "D202", "D403", "D417", "D419", "DTZ", "EXE", "LOG015", "N999", "PERF",
"PIE", "PL", "PYI", "RET", "RUF012", "RUF015", "RUF019",
"RUF046", "RUF059", "S110", "S112", "SIM", "TC", "TID251", "TRY", "UP",
# Enforced by upstream litellm's ruff config, but not run in this repo's CI
"PLC0415", "E402", "BLE001", "ARG002", "S102", "S324", "S606", "D401", "F403", "F405",
]
line-length = 120
extend-exclude = ["litellm/proxy/discovery_endpoints/skill_scanner_adapter"]
format.exclude = ["**/enterprise/**"]
# Was the top-level `exclude`. Scoped to lint so `ruff format` still formats these paths
# (Black did) while `ruff check` keeps skipping them.
lint.exclude = ["litellm/__init__.py", "litellm/proxy/example_config_yaml/*", "tests/*"]
[lint.flake8-tidy-imports]
banned-module-level-imports = ["prisma"]
[lint.per-file-ignores]
"litellm/main.py" = ["F401"]
"litellm/types/caching.py" = ["F401"]
"litellm/types/integrations/slack_alerting.py" = ["F401"]
"litellm/types/llms/custom_http.py" = ["F401"]
"litellm/types/llms/openai.py" = ["F401"]
"litellm/types/proxy/management_endpoints/scim_v2.py" = ["F401"]
"litellm/types/responses/main.py" = ["F401"]
"litellm/utils.py" = ["F401"]
"litellm/proxy/proxy_server.py" = ["F401"]
"litellm/caching/__init__.py" = ["F401"]
"litellm/llms/anthropic/chat/__init__.py" = ["F401"]
"litellm/llms/azure_ai/embed/__init__.py" = ["F401"]
"litellm/llms/azure_ai/rerank/__init__.py" = ["F401"]
"litellm/llms/bedrock/chat/__init__.py" = ["F401"]
"litellm/proxy/utils.py" = ["F401"]

View file

@ -32,7 +32,7 @@ LIT008 `**kwargs` parameter. The keyword contract is erased and everything it c
syntax. Declare explicit keyword params, or accept one frozen payload. `*args`,
by contrast, is fine when typed (it's just a tuple). Suppress: `# kwargs-ok: <reason>`.
LIT009 `# type: ignore` in any shape (bare, with codes, with a reason).
pyrightconfig.json sets enableTypeIgnoreComments to false, so basedpyright
[tool.pyright] in pyproject.toml sets enableTypeIgnoreComments to false, so basedpyright
never honors it: the comment is inert dead syntax that suppresses nothing.
Use `# pyright: ignore[ruleName] # <reason>` instead.
LIT010 Variable assignment without a `Final` declaration. Every local and module-level

View file

@ -7,7 +7,7 @@ def create_baseline():
"""Create baseline migration in deploy/migrations"""
try:
# Get paths
root_dir = Path(__file__).parent.parent
root_dir = Path(__file__).resolve().parents[2]
deploy_dir = root_dir / "deploy"
migrations_dir = deploy_dir / "migrations"
schema_path = root_dir / "schema.prisma"

View file

@ -96,7 +96,7 @@ def _default_base_branch(root_dir: Path) -> str:
result: Final = subprocess.run(
[
sys.executable,
str(Path(__file__).resolve().parents[1] / "scripts" / "default_branch.py"),
str(Path(__file__).resolve().parents[1] / "default_branch.py"),
"--repo-root",
str(root_dir),
"--branch",
@ -238,7 +238,7 @@ def create_migration(
skip_freshness_check (bool): Skip the "branch is up to date" check.
Only for intentional migrations against an older base.
"""
root_dir = Path(__file__).parent.parent
root_dir = Path(__file__).resolve().parents[2]
if skip_freshness_check:
print(

View file

@ -26,7 +26,7 @@
-- such buckets keep the writer's numbers and skip the backfill's.
--
-- Usage:
-- psql "$DATABASE_URL" -v cutover="'2026-07-25T00:00:00Z'" -f db_scripts/backfill_daily_tool_spend.sql
-- psql "$DATABASE_URL" -v cutover="'2026-07-25T00:00:00Z'" -f scripts/db/backfill_daily_tool_spend.sql
SET TIME ZONE 'UTC';

View file

@ -22,14 +22,14 @@
-- budget_id links to a LiteLLM_BudgetTable row whose budget_duration IS
-- NOT NULL (a linked budget resets the key's spend each period too). For
-- those keys this script only recovers the current period;
-- db_scripts/backfill_key_total_spend_from_spend_logs.sql is an optional
-- scripts/db/backfill_key_total_spend_from_spend_logs.sql is an optional
-- follow-up that rebuilds the earlier periods from LiteLLM_SpendLogs.
--
-- 3. No proxy restart is needed. The proxy picks up the corrected values on
-- its next read of each key.
--
-- Usage:
-- psql "$DATABASE_URL" -f db_scripts/backfill_key_total_spend.sql
-- psql "$DATABASE_URL" -f scripts/db/backfill_key_total_spend.sql
UPDATE "LiteLLM_VerificationToken"
SET total_spend = spend

View file

@ -1,4 +1,4 @@
-- Optional follow-up to db_scripts/backfill_key_total_spend.sql. Run that
-- Optional follow-up to scripts/db/backfill_key_total_spend.sql. Run that
-- script first; this one rebuilds earlier budget periods for the keys it
-- can only partially fix: keys whose spend resets each period, because their own
-- budget_duration IS NOT NULL or because their budget_id links to a
@ -38,7 +38,7 @@
-- its next read of each key.
--
-- Usage:
-- psql "$DATABASE_URL" -f db_scripts/backfill_key_total_spend_from_spend_logs.sql
-- psql "$DATABASE_URL" -f scripts/db/backfill_key_total_spend_from_spend_logs.sql
-- Active keys whose spend resets (own budget_duration, or a linked
-- LiteLLM_BudgetTable row with one). Rebuild from LiteLLM_SpendLogs,

View file

@ -32,7 +32,7 @@
-- The spend-log cleanup job then verifies the table is partitioned and reclaims
-- disk by dropping expired partitions instead of deleting rows. It also
-- pre-creates upcoming partitions on each run. To roll back, see
-- db_scripts/unpartition_spend_logs.sql.
-- scripts/db/unpartition_spend_logs.sql.
BEGIN;

View file

@ -1,4 +1,4 @@
-- Rolls back db_scripts/partition_spend_logs.sql: converts the native
-- Rolls back scripts/db/partition_spend_logs.sql: converts the native
-- range-partitioned "LiteLLM_SpendLogs" table back into a plain,
-- non-partitioned table matching the default LiteLLM schema.
--

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
# Install the repo's git hooks by pointing core.hooksPath at .githooks.
# Install the repo's git hooks by pointing core.hooksPath at scripts/githooks.
#
# Idempotent: re-running just reaffirms the config and refreshes chmod bits.
# Run from anywhere inside the repo.
@ -13,7 +13,7 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
fi
repo_root=$(git rev-parse --show-toplevel)
hooks_dir="$repo_root/.githooks"
hooks_dir="$repo_root/scripts/githooks"
if [ ! -d "$hooks_dir" ]; then
echo "install_git_hooks: $hooks_dir does not exist" >&2
@ -24,11 +24,11 @@ fi
# filesystems sometimes lose the exec bit; this normalizes it.
chmod +x "$hooks_dir"/* 2>/dev/null || true
git config core.hooksPath .githooks
git config core.hooksPath scripts/githooks
cat <<EOF
✓ Git hooks installed.
core.hooksPath = .githooks
core.hooksPath = scripts/githooks
active hooks: $(ls "$hooks_dir" | tr '\n' ' ')
These hooks enforce Conventional Commits.

View file

@ -13,9 +13,9 @@
# - tests/e2e and tests/e2e_harness Python
# -> `make lint-e2e-basedpyright` (test-linting.yml's e2e type-check step)
# + raw HTTP client ban (test-linting.yml's check_e2e_no_raw_requests)
# - tests/ Python, ruff-tests.toml, scripts/check_test_quality.py,
# - tests/ Python, .github/linters/ruff-tests.toml, scripts/check_test_quality.py,
# scripts/test_quality_gate.py
# -> ruff over ruff-tests.toml + `make lint-test-quality` (test-linting.yml's
# -> ruff over .github/linters/ruff-tests.toml + `make lint-test-quality` (test-linting.yml's
# test-tree ruff and test-quality gate steps)
# - dashboard -> prettier + eslint + lint budgets (test-linting.yml's ui job)
# - spec_pattern -> regenerate the lazy OpenAPI snapshot and dashboard API types, fail on drift
@ -98,7 +98,7 @@ existing_files() {
litellm_py_pattern='^litellm/.*\.py$'
e2e_py_pattern='^tests/e2e(_harness)?/.*\.py$'
test_tree_pattern='^(tests/.*\.py|ruff-tests\.toml|scripts/(check_test_quality|test_quality_gate)\.py)$'
test_tree_pattern='^(tests/.*\.py|\.github/linters/ruff-tests\.toml|pyproject\.toml|scripts/(check_test_quality|test_quality_gate)\.py)$'
spec_pattern='^(litellm/.*|enterprise/.*|pyproject\.toml|uv\.lock|ui/litellm-dashboard/(scripts/gen-api-types\.mjs|package\.json|package-lock\.json|src/lib/http/schema\.d\.ts))$'
ui_prettier_pattern='^ui/litellm-dashboard/.*\.(js|jsx|ts|tsx|mjs|cjs|json|css|scss|md|mdx|yml|yaml|html)$'
ui_eslint_pattern='^ui/litellm-dashboard/.*\.(js|jsx|ts|tsx|mjs|cjs)$'
@ -145,7 +145,7 @@ if [ -n "$staged" ]; then
}
warn_skipped "Python lint (make lint)" "$litellm_py_pattern" "$litellm_py_files"
warn_skipped "tests/e2e checks (basedpyright + raw HTTP client ban)" "$e2e_py_pattern" "$e2e_py_files"
warn_skipped "test-tree lint (ruff-tests.toml + test-quality gate)" "$test_tree_pattern" "$test_tree_files"
warn_skipped "test-tree lint (.github/linters/ruff-tests.toml + test-quality gate)" "$test_tree_pattern" "$test_tree_files"
warn_skipped "dashboard lint (prettier + eslint + lint budgets)" "$ui_prettier_pattern" "$ui_prettier_changed"
warn_skipped "dashboard API-type sync (npm run gen:api)" "$spec_pattern" "$spec_files"
fi
@ -299,8 +299,8 @@ if [ -n "$spec_files" ]; then
fi
if [ -n "$test_tree_files" ] && [ -z "$litellm_py_files" ]; then
echo "check: linting the test tree (ruff check --config ruff-tests.toml tests)"
uv run --no-sync ruff check --config ruff-tests.toml tests \
echo "check: linting the test tree (ruff check --config .github/linters/ruff-tests.toml tests)"
uv run --no-sync ruff check --config .github/linters/ruff-tests.toml tests \
|| { echo "✗ Test-tree ruff failed. Fix the errors above, then re-run make check." >&2; status=1; }
echo "check: checking the test-quality gate (make lint-test-quality)"
make lint-test-quality \
@ -332,7 +332,7 @@ summary_item() {
echo "check: summary"
summary_item "Python lint (make lint)" "$litellm_py_files" "no litellm/ Python files in scope"
summary_item "tests/e2e checks (basedpyright + raw HTTP client ban)" "$e2e_py_files" "no tests/e2e Python files in scope"
summary_item "test-tree lint (ruff-tests.toml + test-quality gate)" "$test_tree_files" \
summary_item "test-tree lint (.github/linters/ruff-tests.toml + test-quality gate)" "$test_tree_files" \
"no tests/ Python files or test-tree lint inputs in scope"
summary_item "dashboard lint (prettier + eslint + lint budgets)" "$ui_prettier_changed$ui_eslint_changed" "no dashboard files in scope"
summary_item "dashboard API-type sync (npm run gen:api)" "$spec_files" "no litellm/, enterprise/, dependency, or generator files in scope"

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Delta-vs-base gate for the strict ruff rules in ruff-strict.toml.
"""Delta-vs-base gate for the strict ruff rules in .github/linters/ruff-strict.toml.
Each rule is counted across the whole tree at HEAD and at the merge-base with
the branch this change merges into, and the gate fails only when a rule grew
@ -37,8 +37,8 @@ from lint_base_counts import (
)
REPO_ROOT: Final = Path(__file__).resolve().parent.parent
STRICT_CONFIG: Final = REPO_ROOT / "ruff-strict.toml"
BASE_CONFIG: Final = REPO_ROOT / "ruff.toml"
STRICT_CONFIG: Final = REPO_ROOT / ".github/linters/ruff-strict.toml"
BASE_CONFIG: Final = REPO_ROOT / "pyproject.toml"
TARGET: Final = "litellm"
_HUNK: Final = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
@ -100,8 +100,10 @@ def base_counts(ref: str) -> dict[str, int]:
try:
_run(["git", "worktree", "add", "--detach", str(worktree), ref])
shutil.copy(BASE_CONFIG, worktree / BASE_CONFIG.name)
shutil.copy(STRICT_CONFIG, worktree / STRICT_CONFIG.name)
items: Final = _ruff_json(worktree, worktree / STRICT_CONFIG.name)
strict_config: Final = worktree / STRICT_CONFIG.relative_to(REPO_ROOT)
strict_config.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(STRICT_CONFIG, strict_config)
items: Final = _ruff_json(worktree, strict_config)
return dict(Counter(item["code"] for item in items))
finally:
_run(["git", "worktree", "remove", "--force", str(worktree)])

View file

@ -34,7 +34,7 @@ was one forgotten env line away from an 80-second crash. The base pass is a
second basedpyright run over a detached worktree at the merge-base, under the
same environment so import resolution matches, and scripts/lint_base_counts.py
spares it whenever it can: the per-rule counts are cached under the repo's git
common dir keyed by merge-base commit, ``pyrightconfig.json``, ``uv.lock``,
common dir keyed by merge-base commit, ``pyproject.toml``, ``uv.lock``,
the Prisma schema, and the dependency-group set, and on a disk-cache miss the
artifact publish-lint-base-counts.yml uploaded for the merge-base is
downloaded through the ``gh`` CLI (``--emit-counts-dir`` is the publisher's
@ -62,6 +62,11 @@ from pathlib import Path
from types import MappingProxyType
from typing import Final
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
from lint_base_counts import (
Checker,
base_counts_cached,
@ -72,7 +77,7 @@ from lint_base_counts import (
)
REPO_ROOT = Path(__file__).resolve().parent.parent
PYRIGHT_CONFIG = REPO_ROOT / "pyrightconfig.json"
PYRIGHT_CONFIG = REPO_ROOT / "pyproject.toml"
UV_LOCK = REPO_ROOT / "uv.lock"
# The one environment every basedpyright pass measures in. The group set is
@ -140,12 +145,12 @@ def node_options_with_heap(base_env: Mapping[str, str]) -> str:
return f"{base_env.get('NODE_OPTIONS', '')} {NODE_HEAP_OPTION}".strip()
def typecheck_python_version() -> str | None:
def typecheck_python_version(config_path: Path = PYRIGHT_CONFIG) -> str | None:
"""The interpreter version to build the owned env with, read from
pyrightconfig's `pythonVersion` so the packages installed for basedpyright
to see always come from the same version it type-checks against."""
try:
config = json.loads(PYRIGHT_CONFIG.read_text())
config: Final = tomllib.loads(config_path.read_text()).get("tool", {}).get("pyright", {})
except (OSError, ValueError):
return None
version: Final = config.get("pythonVersion") if isinstance(config, dict) else None
@ -214,6 +219,8 @@ def run_basedpyright(cwd: Path = REPO_ROOT, env_dir: Path = TYPECHECK_ENV_DIR) -
[
str(bin_dir / "basedpyright"),
"--outputjson",
"--project",
str(cwd / PYRIGHT_CONFIG.name),
"--pythonpath",
str(bin_dir / "python"),
],
@ -250,7 +257,7 @@ def base_counts(ref: str) -> dict[str, int]:
config is copied in so the base is judged by today's rules, and the run uses
the head environment's basedpyright (on PATH) so imports resolve the same."""
with _temp_worktree(ref) as worktree:
shutil.copy(PYRIGHT_CONFIG, worktree / "pyrightconfig.json")
shutil.copy(PYRIGHT_CONFIG, worktree / PYRIGHT_CONFIG.name)
return count_basedpyright(run_basedpyright(worktree), root=worktree)

View file

@ -226,7 +226,7 @@ make test
### Contributing
Contributions are welcome! Please read our [contributing guidelines](CONTRIBUTING.md) first.
Contributions are welcome! Please read our [contributing guidelines](../../.github/CONTRIBUTING.md) first.
## License

View file

@ -87,7 +87,7 @@ class LicenseChecker:
self.authorized_packages = self._parse_authorized_packages()
# Initialize cache
self.cache_file = Path("license_cache.json")
self.cache_file = Path(__file__).with_name("dependency_license_cache.json")
self.license_cache: Dict[str, str] = {}
if self.cache_file.exists():
with open(self.cache_file) as f:

View file

@ -27,7 +27,7 @@ table without a default is still free on every version.
An index on a request-log table cannot ship as a migration at all. A plain `CREATE
INDEX` blocks writes to the table until the build finishes, and `CREATE INDEX
CONCURRENTLY` is refused by Postgres on a partitioned parent, which LiteLLM_SpendLogs
is wherever the operator ran db_scripts/partition_spend_logs.sql. The migration job builds
is wherever the operator ran scripts/db/partition_spend_logs.sql. The migration job builds
those indexes after `migrate deploy`, concurrently and per partition, from the list in
litellm_proxy_extras/request_log_indexes.py, so that list is where a new one goes.

View file

@ -50,7 +50,7 @@ Each subdirectory under `tests/e2e/` is one suite, scoped to an endpoint family
- `claude_code/` - the Claude Code compatibility matrix: drives the real `claude` CLI (and HTTP probes) against a proxy for each feature x provider cell, reporting tagged-union outcomes via the `compat_result` fixture. Ships its own driver/builder/publisher, covered by `tests/e2e_harness/claude_code/`. The HTTP probes ride the shared transport (`ProxyClient.count_tokens` / `ProxyClient.messages`). The CLI-driving path stays bespoke. `pr_gate_version_resolver.py` only forwards to `cli_harness/npm_version_resolver.py`, because the Buildkite runner images still run that path and install @latest without it
- `cli_harness/` - no tests, only the plumbing every suite that drives a real coding-agent CLI shares: `npm_version_resolver.py` picks the CLI release to install (the newest npm release at least three days old, run by CI as `python tests/e2e/cli_harness/npm_version_resolver.py <npm package>`, stdlib only), `lockdown.py` gives the CLI subprocess an allowlisted env and a fresh empty HOME so it never sees the runner's credentials or dotfiles, and `rate_limiter.py` is the cross-process token bucket per upstream that keeps parallel CLI runs under one request rate. Covered by `tests/e2e_harness/cli_harness/`. Code only one CLI suite needs stays in that suite
- `codex/` - the Codex compatibility suite: drives the real `codex exec --json` CLI through the gateway, one class per row (the command round trip, a file edit with the patch tool, a long session that follows a chain of files one command at a time, auto-compaction, where Codex reads a random value, compacts through the gateway and is asked for the value after the file is deleted, and an MCP tool, where Codex calls the tool of `mcp_server.py`, a stdio MCP server the test hands it with `add_mcp_server`, on a first and a resumed turn, and a sub-agent, where Codex's multi-agent v2 hands a helper a random codeword as its task with shell tools off so the helper can only learn it from the task, and the helper's own final reply is read from its rollout), each row parametrized over `SETUPS` in `setups.py`, one `pytest.param` per setup (GPT on Bedrock Mantle, Bedrock Runtime, Azure OpenAI and OpenAI, plus Claude on Bedrock). A row with one setup broken by a known bug parametrizes over `with_expected_failure(<setup id>, reason=...)`, which is `SETUPS` with a strict xfail on that one setup. The `setup` fixture registers that setup's deployment under a fresh name via `/model/new` and the `codex` fixture makes a key scoped to it, and both are deleted on teardown. `harness.py` gives each test a throwaway HOME and CODEX_HOME whose `config.toml` points a custom `responses` provider at the gateway and has Codex read its model list from the gateway's `/v1/models` (`model_catalog_url` on the provider), with the sandbox on (`workspace-write`) and approvals off, and runs the CLI under `cli_harness`'s env lockdown and rate limiter. `run.py` reads the JSONL events into a `CodexRun`: the commands with their output, the patched files, the MCP tool calls with the text they returned, the last agent message as `answer`, the usage and the errors, where an error's `status_code` is what lets the suite's 5xx rerun rule catch a turn Codex gave up on. `codex exec --json` prints nothing when Codex compacts, so `rollout.py` reads the compactions from the thread's rollout file under CODEX_HOME/sessions, and a compaction is remote when its replacement history kept the provider's `compaction` item. `subagents` reads the sub-agents a thread spawned from their own rollouts, whose `session_meta` names that thread as their parent, each with its final replies and whether it forked the parent's conversation (`forked_from_id`), so the sub-agent row can reject a helper that saw the codeword in inherited history instead of its task. `exec` takes `enable` and `disable`, Codex feature names it passes as `--enable` and `--disable`. `exec` and `resume` take `compact_after_tokens`, which forces Codex's remote compaction (`compaction_trigger` items over `/v1/responses`, the path Codex otherwise only takes for OpenAI and Azure) on the gateway provider and compacts once the context grows past that many tokens. A test passes on a random value the model can only learn through the gateway, never on the model's wording or the exact command it typed. It needs `codex` on PATH and fails without it. Covered by `tests/e2e_harness/codex/`
- `ui/` - the Admin UI browser suite: Playwright in TypeScript, driving the dashboard served by a live proxy on port 4000 (seeded postgres + mock LLM upstream, see its `run_e2e.sh`). It is a self-contained npm package with its own lockfile and does not use the Python harness, pytest markers, or the shared transport. The Python rules in this file (typed models, `Result` unions, basedpyright zero-error gate) do not apply inside it. Its only Python file, `fixtures/mock_llm_server/server.py`, is excluded from the e2e basedpyright gate via the root `pyrightconfig.json`. Its specs follow the dashboard's rule against testing what shadcn and Base UI already guarantee, in `ui/litellm-dashboard/AGENTS.md`
- `ui/` - the Admin UI browser suite: Playwright in TypeScript, driving the dashboard served by a live proxy on port 4000 (seeded postgres + mock LLM upstream, see its `run_e2e.sh`). It is a self-contained npm package with its own lockfile and does not use the Python harness, pytest markers, or the shared transport. The Python rules in this file (typed models, `Result` unions, basedpyright zero-error gate) do not apply inside it. Its only Python file, `fixtures/mock_llm_server/server.py`, is excluded from the e2e basedpyright gate via the `[tool.pyright]` in `pyproject.toml`. Its specs follow the dashboard's rule against testing what shadcn and Base UI already guarantee, in `ui/litellm-dashboard/AGENTS.md`
## MCP suite: real Datadog only

View file

@ -37,7 +37,7 @@ def test_schema_migration_in_sync():
if not any(migrations_dir.iterdir()):
pytest.fail(
"No existing migrations found. Run `python litellm/ci_cd/baseline_db_migration.py`."
"No existing migrations found. Run `python scripts/ci_cd/baseline_db.py`."
)
subprocess.run(
@ -67,7 +67,7 @@ def test_schema_migration_in_sync():
if drift.strip():
pytest.fail(
"Schema changes detected that no migration captures. Run "
"`python litellm/ci_cd/run_migration.py <migration_name>`.\n\n"
"`python scripts/ci_cd/run_migration.py <migration_name>`.\n\n"
+ drift
)
else:

View file

@ -40,7 +40,7 @@ requires_db: Final = pytest.mark.skipif(
REPO: Final = Path(__file__).resolve().parents[4]
PACKAGE: Final = REPO / "litellm-proxy-extras" / "litellm_proxy_extras"
PARTITION_SCRIPT: Final = REPO / "db_scripts" / "partition_spend_logs.sql"
PARTITION_SCRIPT: Final = REPO / "scripts" / "db" / "partition_spend_logs.sql"
API_KEY_INDEX_MIGRATION: Final = "20260823000000_add_spend_logs_api_key_starttime_index"
CALL_ID_INDEX_MIGRATION: Final = "20260831120001_spend_logs_litellm_call_id_index"
API_KEY_INDEX: Final = "LiteLLM_SpendLogs_api_key_startTime_idx"

View file

@ -21,7 +21,7 @@ pytestmark: Final = pytest.mark.timeout(240)
REPO_ROOT: Final = Path(__file__).resolve().parents[3]
PRISMA_DIR: Final = REPO_ROOT / "litellm-proxy-extras" / "litellm_proxy_extras"
PARTITION_SCRIPT: Final = REPO_ROOT / "db_scripts" / "partition_spend_logs.sql"
PARTITION_SCRIPT: Final = REPO_ROOT / "scripts" / "db" / "partition_spend_logs.sql"
SHIPPED_MIGRATIONS: Final = tuple(sorted(path.name for path in (PRISMA_DIR / "migrations").iterdir() if path.is_dir()))
API_KEY_INDEX_MIGRATION: Final = "20260823000000_add_spend_logs_api_key_starttime_index"
CALL_ID_INDEX_MIGRATION: Final = "20260831120001_spend_logs_litellm_call_id_index"

View file

@ -1,5 +1,5 @@
{
"extends": "../../pyrightconfig.json",
"extends": "../../pyproject.toml",
"include": ["openai_sdk_compat.py"],
"exclude": [],
"extraPaths": ["../.."],

View file

@ -8,7 +8,7 @@ from typing import Final
import httpx
import pytest
_SCRIPT: Final = Path(__file__).resolve().parents[3] / "db_scripts" / "strip_vector_store_internal_metadata.py"
_SCRIPT: Final = Path(__file__).resolve().parents[3] / "scripts" / "db" / "strip_vector_store_internal_metadata.py"
_spec: Final = importlib.util.spec_from_file_location("strip_vector_store_internal_metadata", _SCRIPT)
strip_script: Final = importlib.util.module_from_spec(_spec)
sys.modules[_spec.name] = strip_script

View file

@ -621,7 +621,7 @@ DROP TABLE "LiteLLM_SpendLogs_legacy";
class TestPartitionedSpendLogsDriftFilter:
"""A doc-partitioned LiteLLM_SpendLogs (db_scripts/partition_spend_logs.sql) has a
"""A doc-partitioned LiteLLM_SpendLogs (scripts/db/partition_spend_logs.sql) has a
composite primary key that schema.prisma cannot express, so `prisma migrate diff`
emits a primary-key rewrite that Postgres rejects, aborting the whole drift script
before its legitimate statements run."""

View file

@ -547,7 +547,7 @@ def test_e2e_proxy_config_opts_in_to_the_mock_params_its_suite_sends():
for param in GATED_MOCK_PARAM_NAMES
if f"{param}=" in source or f'"{param}"' in source
)
config = yaml.safe_load((repo_root / "proxy_server_config.yaml").read_text(encoding="utf-8"))
config = yaml.safe_load(Path(__file__).with_name("proxy_server_config.yaml").read_text(encoding="utf-8"))
general_settings = config.get("general_settings") or {}
assert not senders or general_settings.get(MOCK_TESTING_CONFIG_KEY) is True, (

View file

@ -11,7 +11,7 @@ from typing import Final
import pytest
ROOT: Final = Path(__file__).resolve().parents[2]
CI_CD: Final = ROOT / "ci_cd"
CI_CD: Final = ROOT / "scripts" / "ci_cd"
def _load(name: str) -> ModuleType:

View file

@ -136,7 +136,7 @@ def _freshness(repo: Path, *args: str) -> subprocess.CompletedProcess[str]:
sys.executable,
"-c",
"import sys; from pathlib import Path; "
"from ci_cd.run_migration import _check_branch_freshness; "
"from scripts.ci_cd.run_migration import _check_branch_freshness; "
"_check_branch_freshness(Path(sys.argv[1]), sys.argv[2] if len(sys.argv) > 2 else None)",
str(repo),
*args,

View file

@ -1,4 +1,4 @@
"""Tests for the repo's git hook scripts in ``.githooks/``.
"""Tests for the repo's git hook scripts in ``scripts/githooks/``.
The commit-msg hook enforces Conventional Commits. It is exercised as a
subprocess against representative valid / invalid inputs so that any future
@ -16,7 +16,7 @@ from pathlib import Path
import pytest
_REPO_ROOT = Path(__file__).resolve().parents[2]
_HOOKS_DIR = _REPO_ROOT / ".githooks"
_HOOKS_DIR = _REPO_ROOT / "scripts/githooks"
_COMMIT_MSG_HOOK = _HOOKS_DIR / "commit-msg"
pytestmark = pytest.mark.skipif(

View file

@ -248,14 +248,14 @@ def test_diff_gate_selects_top_level_and_nested_python_files_only(tmp_path: Path
(
"tests/e2e_harness/top_level_module.py",
"tests/e2e_harness/pkg/sub/nested_module.py",
"pyrightconfig.json",
"pyproject.toml",
),
)
def test_e2e_basedpyright_gate_also_fires_on_harness_python_and_pyrightconfig(tmp_path: Path, trigger: str) -> None:
selected = _changed_files_selected_by(
tmp_path,
_gate_rooted_at("tests/e2e/"),
(trigger, "tests/e2e_harness/notes.md", "elsewhere/pyrightconfig.json", "tests/e2e_harnessish/module.py"),
(trigger, "tests/e2e_harness/notes.md", "elsewhere/pyproject.toml", "tests/e2e_harnessish/module.py"),
)
assert selected == frozenset({trigger})

View file

@ -13,10 +13,10 @@ from tests._process_helpers import process_is_gone
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "scripts" / "pre_commit_lint.sh"
WHOLE_TREE_RUFF = "run --no-sync ruff check --config ruff-tests.toml tests"
TEST_TREE_RAN = "ran: test-tree lint (ruff-tests.toml + test-quality gate)"
WHOLE_TREE_RUFF = "run --no-sync ruff check --config .github/linters/ruff-tests.toml tests"
TEST_TREE_RAN = "ran: test-tree lint (.github/linters/ruff-tests.toml + test-quality gate)"
TEST_TREE_SKIPPED = (
"skipped: test-tree lint (ruff-tests.toml + test-quality gate) "
"skipped: test-tree lint (.github/linters/ruff-tests.toml + test-quality gate) "
"(no tests/ Python files or test-tree lint inputs in scope)"
)
@ -81,7 +81,7 @@ case "$*" in
*orjson*)
[ -n "${STUB_BARRIER_DIR:-}" ] && barrier_sync genapi "python dashboard"
;;
"run --no-sync ruff check --config ruff-tests.toml"*)
"run --no-sync ruff check --config .github/linters/ruff-tests.toml"*)
[ -n "${STUB_ARGS_DIR:-}" ] && echo "$*" >> "$STUB_ARGS_DIR/ruff_tests.args"
[ "${STUB_FAIL:-}" = "tests-ruff" ] && exit 1
;;
@ -497,7 +497,7 @@ def test_tests_only_change_runs_the_whole_test_tree_ruff_and_the_quality_gate(tm
@pytest.mark.parametrize(
"changed",
[
"ruff-tests.toml",
".github/linters/ruff-tests.toml",
"scripts/check_test_quality.py",
"scripts/test_quality_gate.py",
"tests/e2e/test_x.py",
@ -593,7 +593,7 @@ def test_partial_staging_warns_when_test_files_are_left_unstaged(tmp_path: Path)
(repo / "tests" / "test_a.py").write_text("def test_a() -> None:\n assert True\n")
proc = _run(repo, bin_dir, {"STUB_ARGS_DIR": str(args_dir)})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "SKIPPED test-tree lint (ruff-tests.toml + test-quality gate)" in proc.stdout
assert "SKIPPED test-tree lint (.github/linters/ruff-tests.toml + test-quality gate)" in proc.stdout
assert "tests/test_a.py" in proc.stdout
assert _recorded(args_dir, "ruff_tests.args") == []
assert _recorded(args_dir, "make.args") == []

View file

@ -7,7 +7,7 @@ import pytest
import yaml
_REPO_ROOT: Final = Path(__file__).resolve().parents[2]
_SKILLS_DIR: Final = _REPO_ROOT / "skills"
_SKILLS_DIR: Final = _REPO_ROOT / ".agents" / "skills"
_SKILL_DIRECTORIES: Final = tuple(sorted(skill_file.parent for skill_file in _SKILLS_DIR.glob("*/SKILL.md")))
_FULL_DEPTH_MAX_DEPTH: Final = 5
_FULL_DEPTH_SKIPPED_NAMES: Final = frozenset(("node_modules", ".git", "dist", "build", "__pycache__"))

View file

@ -50,19 +50,20 @@ def test_parse_changed_lines_handles_single_and_ranged_hunks(hunk):
def _lint_section(config_name: str) -> dict:
return tomllib.loads((_REPO_ROOT / config_name).read_text())["lint"]
config: Final = tomllib.loads((_REPO_ROOT / config_name).read_text())
return (config["tool"]["ruff"] if config_name == "pyproject.toml" else config)["lint"]
def _base_external() -> tuple[str, ...]:
return tuple(_lint_section("ruff.toml")["external"])
return tuple(_lint_section("pyproject.toml")["external"])
def _strict_external() -> tuple[str, ...]:
return tuple(_lint_section("ruff-strict.toml")["external"])
return tuple(_lint_section(".github/linters/ruff-strict.toml")["external"])
def _strict_selected() -> frozenset:
return frozenset(_lint_section("ruff-strict.toml")["select"])
return frozenset(_lint_section(".github/linters/ruff-strict.toml")["select"])
def _prefix_covered(code: str, prefixes: tuple[str, ...]) -> bool:
@ -70,7 +71,7 @@ def _prefix_covered(code: str, prefixes: tuple[str, ...]) -> bool:
def _selected_by_the_normal_config() -> frozenset:
return frozenset(_lint_section("ruff.toml")["extend-select"]) | _ENABLED_BY_RUFF_DEFAULTS
return frozenset(_lint_section("pyproject.toml")["extend-select"]) | _ENABLED_BY_RUFF_DEFAULTS
def _ruff_binary() -> str | None:
@ -110,7 +111,7 @@ def test_every_strict_gate_rule_is_protected_from_base_ruf100():
)
assert unprotected == frozenset(), (
f"`ruff check` deletes any `# noqa` naming {sorted(unprotected)} as unused, so suppressing "
"one of those strict-gate rules breaks lint. Cover them in ruff.toml's lint.external or "
"one of those strict-gate rules breaks lint. Cover them in pyproject.toml's lint.external or "
"enable them in its lint.extend-select."
)
@ -132,7 +133,7 @@ def test_every_selected_rule_keeps_stale_noqa_detection_somewhere():
assert shadowed == frozenset(), (
f"no config's RUF100 can ever report a stale `# noqa` for {sorted(shadowed)}: every config "
"that selects each of them also shadows it with an external entry. Narrow the external "
"entry in ruff.toml or ruff-strict.toml."
"entry in pyproject.toml or .github/linters/ruff-strict.toml."
)
@ -167,7 +168,7 @@ def test_every_base_owned_rule_is_external_or_selected_in_the_strict_config(all_
)
assert stranded == frozenset(), (
f"the strict gate's RUF100 reads a valid `# noqa` for {sorted(stranded)} as unused, the "
"spurious-breach trap ruff-strict.toml's external override exists to prevent. Cover them "
"spurious-breach trap .github/linters/ruff-strict.toml's external override exists to prevent. Cover them "
"there."
)
double_booked = frozenset(
@ -178,7 +179,7 @@ def test_every_base_owned_rule_is_external_or_selected_in_the_strict_config(all_
assert double_booked == frozenset(), (
f"{sorted(double_booked)} are selected by the strict config yet shadowed by its external "
"list, so their stale suppressions can never be reported. Narrow the external entry in "
"ruff-strict.toml."
".github/linters/ruff-strict.toml."
)
@ -225,8 +226,9 @@ def test_a_graduated_rule_can_still_be_suppressed_without_tripping_unused_noqa()
def test_editing_either_ruff_config_or_upgrading_ruff_rekeys_the_base_counts(tmp_path: Path) -> None:
strict: Final = tmp_path / "ruff-strict.toml"
base: Final = tmp_path / "ruff.toml"
strict: Final = tmp_path / ".github/linters/ruff-strict.toml"
base: Final = tmp_path / "pyproject.toml"
strict.parent.mkdir(parents=True)
strict.write_text("strict v1\n")
base.write_text("base v1\n")

View file

@ -7,7 +7,7 @@ from typing import Final
import pytest
_SCRIPT: Final = Path(__file__).resolve().parents[2] / "ci_cd" / "spend_logs_guard.py"
_SCRIPT: Final = Path(__file__).resolve().parents[2] / "scripts" / "ci_cd" / "spend_logs_guard.py"
_SPEC: Final = importlib.util.spec_from_file_location("spend_logs_guard", _SCRIPT)
assert _SPEC is not None and _SPEC.loader is not None
guard: Final = importlib.util.module_from_spec(_SPEC)

View file

@ -117,6 +117,7 @@ def test_run_basedpyright_pins_import_resolution_to_the_owned_env(tmp_path):
gate.run_basedpyright(cwd=tmp_path, env_dir=env_dir)
argv = captured.read_text().split()
assert argv[argv.index("--pythonpath") + 1] == str(env_dir / "bin" / "python")
assert argv[argv.index("--project") + 1] == str(tmp_path / "pyproject.toml")
def test_run_basedpyright_fails_loudly_on_a_crash_exit_code(tmp_path):
@ -173,14 +174,26 @@ def test_env_commands_sync_the_canonical_groups_then_generate_prisma():
def test_env_interpreter_pin_tracks_pyrightconfigs_python_version():
configured = json.loads((ROOT / "pyrightconfig.json").read_text())[
"pythonVersion"
]
configured = gate.tomllib.loads((ROOT / "pyproject.toml").read_text())["tool"]["pyright"]["pythonVersion"]
assert gate.typecheck_python_version() == configured
sync = gate.typecheck_env_commands()[0]
assert sync[sync.index("--python") + 1] == configured
def test_env_interpreter_pin_reads_project_toml(tmp_path):
config: Final = tmp_path / "pyproject.toml"
config.write_text('[tool.pyright]\npythonVersion = "3.12"\n')
assert gate.typecheck_python_version(config) == "3.12"
def test_env_interpreter_pin_handles_missing_empty_or_invalid_config(tmp_path):
config: Final = tmp_path / "pyproject.toml"
assert gate.typecheck_python_version(config) is None
for content in ("", "[tool.pyright", "[tool.pyright]\npythonVersion = 312\n"):
config.write_text(content)
assert gate.typecheck_python_version(config) is None
def test_ensure_env_targets_the_owned_dir_and_runs_sync_then_generate(tmp_path):
calls = []

View file

@ -172,7 +172,7 @@ def _enforce_bedrock_converse_models(
def _read_whitelisted_bedrock_models() -> tuple[str, ...]:
path: Final = Path(__file__).resolve().parents[2] / "whitelisted_bedrock_models.txt"
path: Final = Path(__file__).with_name("whitelisted_bedrock_models.txt")
return tuple(path.read_text().splitlines())
@ -191,7 +191,7 @@ def test_model_info_bedrock_converse(monkeypatch):
litellm.model_cost = litellm.get_model_cost_map(url="")
try:
# Load whitelist models from file
with open("whitelisted_bedrock_models.txt", "r") as file:
with open(Path(__file__).with_name("whitelisted_bedrock_models.txt"), "r") as file:
whitelist_models = [line.strip() for line in file.readlines()]
except FileNotFoundError:
pytest.skip("whitelisted_bedrock_models.txt not found")
@ -217,7 +217,7 @@ def test_model_info_bedrock_converse_enforcement(monkeypatch):
try:
# Load whitelist models from file
with open("whitelisted_bedrock_models.txt", "r") as file:
with open(Path(__file__).with_name("whitelisted_bedrock_models.txt"), "r") as file:
whitelist_models = [line.strip() for line in file.readlines()]
# Check for unwhitelisted models

View file

@ -0,0 +1 @@
schema.d.ts linguist-generated

View file

@ -31785,7 +31785,7 @@ export interface components {
use_spend_logs_ledger: boolean;
/**
* Use Spend Logs Partitioning
* @description If True and LiteLLM_SpendLogs has been converted to a range-partitioned table (db_scripts/partition_spend_logs.sql), retention cleanup drops expired partitions instead of deleting rows, and pre-creates upcoming partitions. Default is False.
* @description If True and LiteLLM_SpendLogs has been converted to a range-partitioned table (scripts/db/partition_spend_logs.sql), retention cleanup drops expired partitions instead of deleting rows, and pre-creates upcoming partitions. Default is False.
*/
use_spend_logs_partitioning?: boolean | null;
/**