From ddd0d973f67e268b3d71896f139c64f573eeb9ba Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 17:33:44 +0000 Subject: [PATCH] chore: move config and docs off the repo root --- .../skills}/litellm-setup/SKILL.md | 0 .devcontainer/devcontainer.json | 57 -------------- .devcontainer/post-create.sh | 17 ----- .env.example | 32 -------- .git-blame-ignore-revs | 40 ---------- .gitattributes | 2 - .github/CODEOWNERS | 2 +- CONTRIBUTING.md => .github/CONTRIBUTING.md | 6 +- SECURITY.md => .github/SECURITY.md | 0 codecov.yaml => .github/codecov.yml | 0 .grype.yaml => .github/grype.yaml | 0 .../linters/ruff-strict.toml | 4 +- .../linters/ruff-tests.toml | 2 +- osv-scanner.toml => .github/osv-scanner.toml | 0 {.semgrep => .github/semgrep}/rules/README.md | 4 +- .../python/reliability/unbounded-memory.yml | 0 .../rules/python/unbounded-memory.yml | 0 .../rules/security/no-claude-directory.yml | 0 .github/workflows/conventional-commits.yml | 4 +- .github/workflows/cost-map-guard.yml | 2 +- .github/workflows/cost-map-pins-recheck.yml | 6 +- .github/workflows/image-scan.yml | 4 +- .github/workflows/osv-scan.yml | 2 +- .github/workflows/spend-logs-guard.yml | 2 +- .github/workflows/test-linting.yml | 6 +- .npmrc | 5 -- AGENTS.md | 3 +- Makefile | 4 +- README.md | 2 +- ARCHITECTURE.md => docs/ARCHITECTURE.md | 2 +- .../request_log_indexes.py | 2 +- .../litellm_proxy_extras/utils.py | 8 +- litellm-proxy-extras/migration_runbook.md | 6 +- litellm/litellm_core_utils/litellm_logging.py | 6 +- litellm/proxy/_types.py | 2 +- litellm/proxy/client/README.md | 2 +- litellm/proxy/db/create_views.py | 2 +- .../spend_logs_partition_manager.py | 2 +- litellm/proxy/utils.py | 2 +- litellm/utils.py | 2 +- pyproject.toml | 76 +++++++++++++++++++ pyrightconfig.json | 18 ----- render.yaml | 14 ---- ruff.toml | 58 -------------- scripts/check_type_discipline.py | 2 +- {ci_cd => scripts/ci_cd}/TEST_KEY_PATTERNS.md | 0 {ci_cd => scripts/ci_cd}/baseline_db.py | 2 +- {ci_cd => scripts/ci_cd}/check_file_length.py | 0 {ci_cd => scripts/ci_cd}/check_files_match.py | 0 {ci_cd => scripts/ci_cd}/cost_map_guard.py | 4 +- {ci_cd => scripts/ci_cd}/cost_map_pins.json | 0 {ci_cd => scripts/ci_cd}/run_migration.py | 4 +- .../ci_cd}/security_scans_readme.md | 0 {ci_cd => scripts/ci_cd}/spend_logs_guard.py | 0 .../db}/backfill_daily_tool_spend.sql | 2 +- .../db}/backfill_key_total_spend.sql | 4 +- ...ckfill_key_total_spend_from_spend_logs.sql | 4 +- {db_scripts => scripts/db}/create_views.py | 0 {db_scripts => scripts/db}/migrate_keys.py | 0 .../db}/partition_spend_logs.sql | 2 +- .../strip_vector_store_internal_metadata.py | 0 .../db}/unpartition_spend_logs.sql | 2 +- .../db}/update_unassigned_teams.py | 0 {.githooks => scripts/githooks}/commit-msg | 0 scripts/install_git_hooks.sh | 8 +- scripts/pre_commit_lint.sh | 14 ++-- scripts/ruff_strict_gate.py | 12 +-- scripts/type_check_gate.py | 17 +++-- terraform/provider/README.md | 2 +- tests/code_coverage_tests/check_licenses.py | 2 +- .../check_migrations_no_data_rewrites.py | 2 +- .../dependency_license_cache.json | 0 tests/e2e/AGENTS.md | 2 +- .../migrations/test_db_schema_migration.py | 4 +- .../migrations/test_request_log_indexes.py | 2 +- .../test_request_log_indexes_at_boot.py | 2 +- tests/typing/pyrightconfig.json | 2 +- ...st_strip_vector_store_internal_metadata.py | 2 +- .../test_litellm_proxy_extras_utils.py | 2 +- .../unit/proxy/proxy_server_config.yaml | 0 tests/unit/proxy/test_route_llm_request.py | 2 +- tests/unit/test_cost_map_guard.py | 4 +- tests/unit/test_default_branch.py | 2 +- tests/unit/test_git_hooks.py | 4 +- tests/unit/test_lint_workflow_diff_gates.py | 4 +- tests/unit/test_pre_commit_lint.py | 12 +-- tests/unit/test_ruff_strict_gate.py | 24 +++--- tests/unit/test_spend_logs_guard.py | 2 +- tests/unit/test_type_check_gate.py | 19 ++++- tests/unit/test_utils_get_model_info.py | 6 +- .../unit/whitelisted_bedrock_models.txt | 0 .../src/lib/http/.gitattributes | 1 + ui/litellm-dashboard/src/lib/http/schema.d.ts | 2 +- 93 files changed, 223 insertions(+), 366 deletions(-) rename {skills => .agents/skills}/litellm-setup/SKILL.md (100%) delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .devcontainer/post-create.sh delete mode 100644 .env.example delete mode 100644 .git-blame-ignore-revs delete mode 100644 .gitattributes rename CONTRIBUTING.md => .github/CONTRIBUTING.md (90%) rename SECURITY.md => .github/SECURITY.md (100%) rename codecov.yaml => .github/codecov.yml (100%) rename .grype.yaml => .github/grype.yaml (100%) rename ruff-strict.toml => .github/linters/ruff-strict.toml (97%) rename ruff-tests.toml => .github/linters/ruff-tests.toml (98%) rename osv-scanner.toml => .github/osv-scanner.toml (100%) rename {.semgrep => .github/semgrep}/rules/README.md (80%) rename {.semgrep => .github/semgrep}/rules/python/reliability/unbounded-memory.yml (100%) rename {.semgrep => .github/semgrep}/rules/python/unbounded-memory.yml (100%) rename {.semgrep => .github/semgrep}/rules/security/no-claude-directory.yml (100%) delete mode 100644 .npmrc rename ARCHITECTURE.md => docs/ARCHITECTURE.md (99%) delete mode 100644 pyrightconfig.json delete mode 100644 render.yaml delete mode 100644 ruff.toml rename {ci_cd => scripts/ci_cd}/TEST_KEY_PATTERNS.md (100%) rename {ci_cd => scripts/ci_cd}/baseline_db.py (97%) rename {ci_cd => scripts/ci_cd}/check_file_length.py (100%) rename {ci_cd => scripts/ci_cd}/check_files_match.py (100%) rename {ci_cd => scripts/ci_cd}/cost_map_guard.py (98%) rename {ci_cd => scripts/ci_cd}/cost_map_pins.json (100%) rename {ci_cd => scripts/ci_cd}/run_migration.py (99%) rename {ci_cd => scripts/ci_cd}/security_scans_readme.md (100%) rename {ci_cd => scripts/ci_cd}/spend_logs_guard.py (100%) rename {db_scripts => scripts/db}/backfill_daily_tool_spend.sql (98%) rename {db_scripts => scripts/db}/backfill_key_total_spend.sql (93%) rename {db_scripts => scripts/db}/backfill_key_total_spend_from_spend_logs.sql (96%) rename {db_scripts => scripts/db}/create_views.py (100%) rename {db_scripts => scripts/db}/migrate_keys.py (100%) rename {db_scripts => scripts/db}/partition_spend_logs.sql (99%) rename {db_scripts => scripts/db}/strip_vector_store_internal_metadata.py (100%) rename {db_scripts => scripts/db}/unpartition_spend_logs.sql (98%) rename {db_scripts => scripts/db}/update_unassigned_teams.py (100%) rename {.githooks => scripts/githooks}/commit-msg (100%) rename license_cache.json => tests/code_coverage_tests/dependency_license_cache.json (100%) rename proxy_server_config.yaml => tests/unit/proxy/proxy_server_config.yaml (100%) rename whitelisted_bedrock_models.txt => tests/unit/whitelisted_bedrock_models.txt (100%) create mode 100644 ui/litellm-dashboard/src/lib/http/.gitattributes diff --git a/skills/litellm-setup/SKILL.md b/.agents/skills/litellm-setup/SKILL.md similarity index 100% rename from skills/litellm-setup/SKILL.md rename to .agents/skills/litellm-setup/SKILL.md diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 50253186c01..00000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh deleted file mode 100644 index 78f857d55d6..00000000000 --- a/.devcontainer/post-create.sh +++ /dev/null @@ -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" diff --git a/.env.example b/.env.example deleted file mode 100644 index dc1fd5a6ccb..00000000000 --- a/.env.example +++ /dev/null @@ -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" diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs deleted file mode 100644 index a0943cff53d..00000000000 --- a/.git-blame-ignore-revs +++ /dev/null @@ -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 diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 5c9061f52ac..00000000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -*.ipynb linguist-vendored -ui/litellm-dashboard/src/lib/http/schema.d.ts linguist-generated \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2826098fd1f..5e9754df260 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,6 @@ /model_prices_and_context_window.json @mateo-berri @ryan-crabbe-berri @kerry-berri @joshua-berri /litellm/model_prices_and_context_window_backup.json @mateo-berri @ryan-crabbe-berri @kerry-berri @joshua-berri -/ci_cd/cost_map_pins.json @mateo-berri @ryan-crabbe-berri @kerry-berri @joshua-berri +/scripts/ci_cd/cost_map_pins.json @mateo-berri @ryan-crabbe-berri @kerry-berri @joshua-berri /helm/ @yassin-berriai /terraform/ @yassin-berriai /.github/workflows/zizmor.yml @yuneng-berri diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 90% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md index 426cf986d4c..6297cd9a7c0 100644 --- a/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -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 diff --git a/SECURITY.md b/.github/SECURITY.md similarity index 100% rename from SECURITY.md rename to .github/SECURITY.md diff --git a/codecov.yaml b/.github/codecov.yml similarity index 100% rename from codecov.yaml rename to .github/codecov.yml diff --git a/.grype.yaml b/.github/grype.yaml similarity index 100% rename from .grype.yaml rename to .github/grype.yaml diff --git a/ruff-strict.toml b/.github/linters/ruff-strict.toml similarity index 97% rename from ruff-strict.toml rename to .github/linters/ruff-strict.toml index e9db4d95cc8..406306ae809 100644 --- a/ruff-strict.toml +++ b/.github/linters/ruff-strict.toml @@ -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. diff --git a/ruff-tests.toml b/.github/linters/ruff-tests.toml similarity index 98% rename from ruff-tests.toml rename to .github/linters/ruff-tests.toml index d75f10b9605..a3bf8744700 100644 --- a/ruff-tests.toml +++ b/.github/linters/ruff-tests.toml @@ -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 diff --git a/osv-scanner.toml b/.github/osv-scanner.toml similarity index 100% rename from osv-scanner.toml rename to .github/osv-scanner.toml diff --git a/.semgrep/rules/README.md b/.github/semgrep/rules/README.md similarity index 80% rename from .semgrep/rules/README.md rename to .github/semgrep/rules/README.md index 0dbb77cdd48..315976f1eac 100644 --- a/.semgrep/rules/README.md +++ b/.github/semgrep/rules/README.md @@ -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:** diff --git a/.semgrep/rules/python/reliability/unbounded-memory.yml b/.github/semgrep/rules/python/reliability/unbounded-memory.yml similarity index 100% rename from .semgrep/rules/python/reliability/unbounded-memory.yml rename to .github/semgrep/rules/python/reliability/unbounded-memory.yml diff --git a/.semgrep/rules/python/unbounded-memory.yml b/.github/semgrep/rules/python/unbounded-memory.yml similarity index 100% rename from .semgrep/rules/python/unbounded-memory.yml rename to .github/semgrep/rules/python/unbounded-memory.yml diff --git a/.semgrep/rules/security/no-claude-directory.yml b/.github/semgrep/rules/security/no-claude-directory.yml similarity index 100% rename from .semgrep/rules/security/no-claude-directory.yml rename to .github/semgrep/rules/security/no-claude-directory.yml diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml index 4ae59ed581f..f8d416ca487 100644 --- a/.github/workflows/conventional-commits.yml +++ b/.github/workflows/conventional-commits.yml @@ -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 diff --git a/.github/workflows/cost-map-guard.yml b/.github/workflows/cost-map-guard.yml index f26c649aad2..b0ac841fd15 100644 --- a/.github/workflows/cost-map-guard.yml +++ b/.github/workflows/cost-map-guard.yml @@ -49,7 +49,7 @@ 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" + uv run --no-project python scripts/ci_cd/cost_map_guard.py --base "$MERGE_BASE" --head "$HEAD_SHA" --head-ref "$HEAD_REF" - name: Check whether the pull request changes the cost map id: changed env: diff --git a/.github/workflows/cost-map-pins-recheck.yml b/.github/workflows/cost-map-pins-recheck.yml index f2572efbb31..98c3371bcf2 100644 --- a/.github/workflows/cost-map-pins-recheck.yml +++ b/.github/workflows/cost-map-pins-recheck.yml @@ -8,7 +8,7 @@ on: paths: - model_prices_and_context_window.json - litellm/model_prices_and_context_window_backup.json - - ci_cd/cost_map_pins.json + - scripts/ci_cd/cost_map_pins.json permissions: {} @@ -28,8 +28,8 @@ jobs: BEFORE_SHA: ${{ github.event.before }} AFTER_SHA: ${{ github.event.after }} FORCED: ${{ github.event.forced }} - COST_MAP_PATHS: '["model_prices_and_context_window.json", "litellm/model_prices_and_context_window_backup.json", "ci_cd/cost_map_pins.json"]' - PINS_PATHS: '["ci_cd/cost_map_pins.json"]' + COST_MAP_PATHS: '["model_prices_and_context_window.json", "litellm/model_prices_and_context_window_backup.json", "scripts/ci_cd/cost_map_pins.json"]' + PINS_PATHS: '["scripts/ci_cd/cost_map_pins.json"]' run: | if [[ "$BEFORE_SHA" =~ ^0+$ || "$FORCED" == true ]] \ || ! gh api "repos/${GITHUB_REPOSITORY}/compare/${BEFORE_SHA}...${AFTER_SHA}?per_page=1" \ diff --git a/.github/workflows/image-scan.yml b/.github/workflows/image-scan.yml index 6e13b49eb15..9cf00b76ea8 100644 --- a/.github/workflows/image-scan.yml +++ b/.github/workflows/image-scan.yml @@ -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 diff --git a/.github/workflows/osv-scan.yml b/.github/workflows/osv-scan.yml index 5016957bd6f..90c0712b2ff 100644 --- a/.github/workflows/osv-scan.yml +++ b/.github/workflows/osv-scan.yml @@ -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 diff --git a/.github/workflows/spend-logs-guard.yml b/.github/workflows/spend-logs-guard.yml index bdba95f6a5d..37703437c03 100644 --- a/.github/workflows/spend-logs-guard.yml +++ b/.github/workflows/spend-logs-guard.yml @@ -42,4 +42,4 @@ 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" + uv run --no-project --with pydantic==2.14.1 python scripts/ci_cd/spend_logs_guard.py --base "$MERGE_BASE" --head "$HEAD_SHA" --approver kerry-berri --reviews "$RUNNER_TEMP/reviews.json" diff --git a/.github/workflows/test-linting.yml b/.github/workflows/test-linting.yml index fd1d2973951..7544aad8f60 100644 --- a/.github/workflows/test-linting.yml +++ b/.github/workflows/test-linting.yml @@ -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] diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 7999681cc35..00000000000 --- a/.npmrc +++ /dev/null @@ -1,5 +0,0 @@ -# Supply-chain hardening -# Packages needing lifecycle scripts: npm rebuild -ignore-scripts=true -# Protects local npm install only — npm ci (used in CI) ignores this -min-release-age=3 diff --git a/AGENTS.md b/AGENTS.md index d8de447be90..b249e2c8b1d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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] # ` 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 # `. `# 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 # `. `# 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 diff --git a/Makefile b/Makefile index c256d273d8a..78d594b1ecd 100644 --- a/Makefile +++ b/Makefile @@ -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: diff --git a/README.md b/README.md index 31a93bf8c67..7b30c2507f0 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/ARCHITECTURE.md b/docs/ARCHITECTURE.md similarity index 99% rename from ARCHITECTURE.md rename to docs/ARCHITECTURE.md index f457720c58d..0e4f402d67a 100644 --- a/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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) --- diff --git a/litellm-proxy-extras/litellm_proxy_extras/request_log_indexes.py b/litellm-proxy-extras/litellm_proxy_extras/request_log_indexes.py index 4c856c741d3..0991ab270f3 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/request_log_indexes.py +++ b/litellm-proxy-extras/litellm_proxy_extras/request_log_indexes.py @@ -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. """ diff --git a/litellm-proxy-extras/litellm_proxy_extras/utils.py b/litellm-proxy-extras/litellm_proxy_extras/utils.py index 8b19576d86e..8d316e42ca5 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/utils.py +++ b/litellm-proxy-extras/litellm_proxy_extras/utils.py @@ -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." ) diff --git a/litellm-proxy-extras/migration_runbook.md b/litellm-proxy-extras/migration_runbook.md index b1e9236e520..e571422be8b 100644 --- a/litellm-proxy-extras/migration_runbook.md +++ b/litellm-proxy-extras/migration_runbook.md @@ -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:** diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index d88c8876ae0..07049edf382 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -3651,7 +3651,7 @@ class Logging(LiteLLMLoggingBaseClass): def success_handler( self, - result: object = None, # heterogeneous response object; varies by call type (ANN401 ignored, see ruff-strict.toml) + result: object = None, # heterogeneous response object; varies by call type (ANN401 ignored, see .github/linters/ruff-strict.toml) start_time: datetime.datetime | None = None, end_time: datetime.datetime | None = None, cache_hit: bool | None = None, @@ -4169,7 +4169,7 @@ class Logging(LiteLLMLoggingBaseClass): async def async_success_handler( self, - result: object = None, # heterogeneous response object; varies by call type (ANN401 ignored, see ruff-strict.toml) + result: object = None, # heterogeneous response object; varies by call type (ANN401 ignored, see .github/linters/ruff-strict.toml) start_time: datetime.datetime | None = None, end_time: datetime.datetime | None = None, cache_hit: bool | None = None, @@ -4187,7 +4187,7 @@ class Logging(LiteLLMLoggingBaseClass): async def _async_success_handler_body( self, - result: object = None, # heterogeneous response object; varies by call type (ANN401 ignored, see ruff-strict.toml) + result: object = None, # heterogeneous response object; varies by call type (ANN401 ignored, see .github/linters/ruff-strict.toml) start_time: datetime.datetime | None = None, end_time: datetime.datetime | None = None, cache_hit: bool | None = None, diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index abf61a909b2..0de2bdc34e1 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -3004,7 +3004,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, diff --git a/litellm/proxy/client/README.md b/litellm/proxy/client/README.md index 9a7e067c792..d5c76428e3b 100644 --- a/litellm/proxy/client/README.md +++ b/litellm/proxy/client/README.md @@ -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 diff --git a/litellm/proxy/db/create_views.py b/litellm/proxy/db/create_views.py index d51e39596c6..fd5c2356373 100644 --- a/litellm/proxy/db/create_views.py +++ b/litellm/proxy/db/create_views.py @@ -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. diff --git a/litellm/proxy/db/db_transaction_queue/spend_logs_partition_manager.py b/litellm/proxy/db/db_transaction_queue/spend_logs_partition_manager.py index 049a6ecbeee..f916d53a63a 100644 --- a/litellm/proxy/db/db_transaction_queue/spend_logs_partition_manager.py +++ b/litellm/proxy/db/db_transaction_queue/spend_logs_partition_manager.py @@ -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. """ diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 97c327cbe4e..0b6c0e7da2d 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -5982,7 +5982,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, ) diff --git a/litellm/utils.py b/litellm/utils.py index 7f7c6ffbc4d..e48baab0542 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -1279,7 +1279,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]]: diff --git a/pyproject.toml b/pyproject.toml index 4d47f851466..3ccf462124a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -485,3 +485,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"] diff --git a/pyrightconfig.json b/pyrightconfig.json deleted file mode 100644 index 820c4758b46..00000000000 --- a/pyrightconfig.json +++ /dev/null @@ -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" -} diff --git a/render.yaml b/render.yaml deleted file mode 100644 index b61385c4d90..00000000000 --- a/render.yaml +++ /dev/null @@ -1,14 +0,0 @@ -services: - - type: web - name: openai-proxy - runtime: image - image: - url: ghcr.io/berriai/litellm:main-stable - envVars: - - key: PORT - value: 4000 - - key: LITELLM_MASTER_KEY - generateValue: true - numInstances: 1 - healthCheckPath: /health/liveliness - autoDeploy: true diff --git a/ruff.toml b/ruff.toml deleted file mode 100644 index f7624f07f51..00000000000 --- a/ruff.toml +++ /dev/null @@ -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"] diff --git a/scripts/check_type_discipline.py b/scripts/check_type_discipline.py index 52992281c99..81ab4c03615 100644 --- a/scripts/check_type_discipline.py +++ b/scripts/check_type_discipline.py @@ -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: `. 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] # ` instead. LIT010 Variable assignment without a `Final` declaration. Every local and module-level diff --git a/ci_cd/TEST_KEY_PATTERNS.md b/scripts/ci_cd/TEST_KEY_PATTERNS.md similarity index 100% rename from ci_cd/TEST_KEY_PATTERNS.md rename to scripts/ci_cd/TEST_KEY_PATTERNS.md diff --git a/ci_cd/baseline_db.py b/scripts/ci_cd/baseline_db.py similarity index 97% rename from ci_cd/baseline_db.py rename to scripts/ci_cd/baseline_db.py index ecc080abedd..3fb9203b6a7 100644 --- a/ci_cd/baseline_db.py +++ b/scripts/ci_cd/baseline_db.py @@ -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" diff --git a/ci_cd/check_file_length.py b/scripts/ci_cd/check_file_length.py similarity index 100% rename from ci_cd/check_file_length.py rename to scripts/ci_cd/check_file_length.py diff --git a/ci_cd/check_files_match.py b/scripts/ci_cd/check_files_match.py similarity index 100% rename from ci_cd/check_files_match.py rename to scripts/ci_cd/check_files_match.py diff --git a/ci_cd/cost_map_guard.py b/scripts/ci_cd/cost_map_guard.py similarity index 98% rename from ci_cd/cost_map_guard.py rename to scripts/ci_cd/cost_map_guard.py index c9bcf6e3278..1ce49907b44 100644 --- a/ci_cd/cost_map_guard.py +++ b/scripts/ci_cd/cost_map_guard.py @@ -2,7 +2,7 @@ Every pull request whose diff against its merge base touches one of the two cost map files or the pins file gets the file checks: the files parse, the backup copy matches the root file, and every capability pinned in -ci_cd/cost_map_pins.json (a value verified against a live provider call where the provider's model listing says +scripts/ci_cd/cost_map_pins.json (a value verified against a live provider call where the provider's model listing says otherwise) still holds in the tree that merging the pull request into its base branch produces, so a branch cut before a pin was added cannot lower it, and a pull request that deletes the pins file fails too. A pull request that leaves all three untouched skips them, since merging it keeps the base branch's copies and its head tree only carries @@ -27,7 +27,7 @@ from typing import Final COST_MAP_PATH: Final = "model_prices_and_context_window.json" BACKUP_PATH: Final = "litellm/model_prices_and_context_window_backup.json" -PINS_PATH: Final = "ci_cd/cost_map_pins.json" +PINS_PATH: Final = "scripts/ci_cd/cost_map_pins.json" PINS_PATHS: Final = ( "litellm/scripts/ci_cd/cost_map_pins.json", "scripts/ci_cd/cost_map_pins.json", diff --git a/ci_cd/cost_map_pins.json b/scripts/ci_cd/cost_map_pins.json similarity index 100% rename from ci_cd/cost_map_pins.json rename to scripts/ci_cd/cost_map_pins.json diff --git a/ci_cd/run_migration.py b/scripts/ci_cd/run_migration.py similarity index 99% rename from ci_cd/run_migration.py rename to scripts/ci_cd/run_migration.py index 84d529d2a99..b1dfc8b43a9 100644 --- a/ci_cd/run_migration.py +++ b/scripts/ci_cd/run_migration.py @@ -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( diff --git a/ci_cd/security_scans_readme.md b/scripts/ci_cd/security_scans_readme.md similarity index 100% rename from ci_cd/security_scans_readme.md rename to scripts/ci_cd/security_scans_readme.md diff --git a/ci_cd/spend_logs_guard.py b/scripts/ci_cd/spend_logs_guard.py similarity index 100% rename from ci_cd/spend_logs_guard.py rename to scripts/ci_cd/spend_logs_guard.py diff --git a/db_scripts/backfill_daily_tool_spend.sql b/scripts/db/backfill_daily_tool_spend.sql similarity index 98% rename from db_scripts/backfill_daily_tool_spend.sql rename to scripts/db/backfill_daily_tool_spend.sql index 309b9dbe0ff..9c65be20ada 100644 --- a/db_scripts/backfill_daily_tool_spend.sql +++ b/scripts/db/backfill_daily_tool_spend.sql @@ -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'; diff --git a/db_scripts/backfill_key_total_spend.sql b/scripts/db/backfill_key_total_spend.sql similarity index 93% rename from db_scripts/backfill_key_total_spend.sql rename to scripts/db/backfill_key_total_spend.sql index 634a2ac70ce..8c3d75ef5e2 100644 --- a/db_scripts/backfill_key_total_spend.sql +++ b/scripts/db/backfill_key_total_spend.sql @@ -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 diff --git a/db_scripts/backfill_key_total_spend_from_spend_logs.sql b/scripts/db/backfill_key_total_spend_from_spend_logs.sql similarity index 96% rename from db_scripts/backfill_key_total_spend_from_spend_logs.sql rename to scripts/db/backfill_key_total_spend_from_spend_logs.sql index b725437e359..b76a5d1fb6d 100644 --- a/db_scripts/backfill_key_total_spend_from_spend_logs.sql +++ b/scripts/db/backfill_key_total_spend_from_spend_logs.sql @@ -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, diff --git a/db_scripts/create_views.py b/scripts/db/create_views.py similarity index 100% rename from db_scripts/create_views.py rename to scripts/db/create_views.py diff --git a/db_scripts/migrate_keys.py b/scripts/db/migrate_keys.py similarity index 100% rename from db_scripts/migrate_keys.py rename to scripts/db/migrate_keys.py diff --git a/db_scripts/partition_spend_logs.sql b/scripts/db/partition_spend_logs.sql similarity index 99% rename from db_scripts/partition_spend_logs.sql rename to scripts/db/partition_spend_logs.sql index 750c1a934aa..217a7463b54 100644 --- a/db_scripts/partition_spend_logs.sql +++ b/scripts/db/partition_spend_logs.sql @@ -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; diff --git a/db_scripts/strip_vector_store_internal_metadata.py b/scripts/db/strip_vector_store_internal_metadata.py similarity index 100% rename from db_scripts/strip_vector_store_internal_metadata.py rename to scripts/db/strip_vector_store_internal_metadata.py diff --git a/db_scripts/unpartition_spend_logs.sql b/scripts/db/unpartition_spend_logs.sql similarity index 98% rename from db_scripts/unpartition_spend_logs.sql rename to scripts/db/unpartition_spend_logs.sql index 2555eca212b..3f2b9c768b0 100644 --- a/db_scripts/unpartition_spend_logs.sql +++ b/scripts/db/unpartition_spend_logs.sql @@ -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. -- diff --git a/db_scripts/update_unassigned_teams.py b/scripts/db/update_unassigned_teams.py similarity index 100% rename from db_scripts/update_unassigned_teams.py rename to scripts/db/update_unassigned_teams.py diff --git a/.githooks/commit-msg b/scripts/githooks/commit-msg similarity index 100% rename from .githooks/commit-msg rename to scripts/githooks/commit-msg diff --git a/scripts/install_git_hooks.sh b/scripts/install_git_hooks.sh index cccc8d6afc4..35d5ce4c968 100755 --- a/scripts/install_git_hooks.sh +++ b/scripts/install_git_hooks.sh @@ -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 < `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" diff --git a/scripts/ruff_strict_gate.py b/scripts/ruff_strict_gate.py index 61ae0236b53..248a76cea1d 100644 --- a/scripts/ruff_strict_gate.py +++ b/scripts/ruff_strict_gate.py @@ -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)]) diff --git a/scripts/type_check_gate.py b/scripts/type_check_gate.py index 2a9f1c1229f..874fda013be 100644 --- a/scripts/type_check_gate.py +++ b/scripts/type_check_gate.py @@ -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) diff --git a/terraform/provider/README.md b/terraform/provider/README.md index 56b95f8916a..b5ff0e5e8f8 100644 --- a/terraform/provider/README.md +++ b/terraform/provider/README.md @@ -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 diff --git a/tests/code_coverage_tests/check_licenses.py b/tests/code_coverage_tests/check_licenses.py index 81213e9ce79..6898d245803 100644 --- a/tests/code_coverage_tests/check_licenses.py +++ b/tests/code_coverage_tests/check_licenses.py @@ -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: diff --git a/tests/code_coverage_tests/check_migrations_no_data_rewrites.py b/tests/code_coverage_tests/check_migrations_no_data_rewrites.py index 5c694702c6e..48c2c468b9f 100644 --- a/tests/code_coverage_tests/check_migrations_no_data_rewrites.py +++ b/tests/code_coverage_tests/check_migrations_no_data_rewrites.py @@ -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. diff --git a/license_cache.json b/tests/code_coverage_tests/dependency_license_cache.json similarity index 100% rename from license_cache.json rename to tests/code_coverage_tests/dependency_license_cache.json diff --git a/tests/e2e/AGENTS.md b/tests/e2e/AGENTS.md index aadce82ec71..a1aa2ed10bb 100644 --- a/tests/e2e/AGENTS.md +++ b/tests/e2e/AGENTS.md @@ -51,7 +51,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 `, 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(, 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 diff --git a/tests/integration/database/migrations/test_db_schema_migration.py b/tests/integration/database/migrations/test_db_schema_migration.py index 70498a3bcbd..e7d65d65f8c 100644 --- a/tests/integration/database/migrations/test_db_schema_migration.py +++ b/tests/integration/database/migrations/test_db_schema_migration.py @@ -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 `.\n\n" + "`python scripts/ci_cd/run_migration.py `.\n\n" + drift ) else: diff --git a/tests/integration/database/migrations/test_request_log_indexes.py b/tests/integration/database/migrations/test_request_log_indexes.py index 5741c85470b..fc28f8fd2f3 100644 --- a/tests/integration/database/migrations/test_request_log_indexes.py +++ b/tests/integration/database/migrations/test_request_log_indexes.py @@ -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" diff --git a/tests/integration/database/test_request_log_indexes_at_boot.py b/tests/integration/database/test_request_log_indexes_at_boot.py index e72477de901..86323c49d47 100644 --- a/tests/integration/database/test_request_log_indexes_at_boot.py +++ b/tests/integration/database/test_request_log_indexes_at_boot.py @@ -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" diff --git a/tests/typing/pyrightconfig.json b/tests/typing/pyrightconfig.json index 0910ca64f5b..15fb7d86e70 100644 --- a/tests/typing/pyrightconfig.json +++ b/tests/typing/pyrightconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../pyrightconfig.json", + "extends": "../../pyproject.toml", "include": ["openai_sdk_compat.py"], "exclude": [], "extraPaths": ["../.."], diff --git a/tests/unit/db_scripts/test_strip_vector_store_internal_metadata.py b/tests/unit/db_scripts/test_strip_vector_store_internal_metadata.py index 05b055f75a1..1adca1ffbeb 100644 --- a/tests/unit/db_scripts/test_strip_vector_store_internal_metadata.py +++ b/tests/unit/db_scripts/test_strip_vector_store_internal_metadata.py @@ -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 diff --git a/tests/unit/litellm_proxy_extras/test_litellm_proxy_extras_utils.py b/tests/unit/litellm_proxy_extras/test_litellm_proxy_extras_utils.py index 02bdf3429ea..1fa1606259f 100644 --- a/tests/unit/litellm_proxy_extras/test_litellm_proxy_extras_utils.py +++ b/tests/unit/litellm_proxy_extras/test_litellm_proxy_extras_utils.py @@ -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.""" diff --git a/proxy_server_config.yaml b/tests/unit/proxy/proxy_server_config.yaml similarity index 100% rename from proxy_server_config.yaml rename to tests/unit/proxy/proxy_server_config.yaml diff --git a/tests/unit/proxy/test_route_llm_request.py b/tests/unit/proxy/test_route_llm_request.py index 75a17d49d3a..28e2ca4758d 100644 --- a/tests/unit/proxy/test_route_llm_request.py +++ b/tests/unit/proxy/test_route_llm_request.py @@ -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, ( diff --git a/tests/unit/test_cost_map_guard.py b/tests/unit/test_cost_map_guard.py index 0a19903da59..f83a776de4d 100644 --- a/tests/unit/test_cost_map_guard.py +++ b/tests/unit/test_cost_map_guard.py @@ -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: @@ -378,7 +378,7 @@ def _commit(repo: Path, cost_map: dict[str, object], message: str, pins: str | N text = _serialize(cost_map) pins_file = repo / guard.PINS_PATH if pins is not None or not pins_file.exists(): - pins_file.parent.mkdir(exist_ok=True) + pins_file.parent.mkdir(parents=True, exist_ok=True) pins_file.write_text(NO_PINS if pins is None else pins) (repo / guard.COST_MAP_PATH).write_text(text) (repo / guard.BACKUP_PATH).parent.mkdir(exist_ok=True) diff --git a/tests/unit/test_default_branch.py b/tests/unit/test_default_branch.py index 577b1126976..3ed842b3b8e 100644 --- a/tests/unit/test_default_branch.py +++ b/tests/unit/test_default_branch.py @@ -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, diff --git a/tests/unit/test_git_hooks.py b/tests/unit/test_git_hooks.py index b130fa7b04a..c143f7f353e 100644 --- a/tests/unit/test_git_hooks.py +++ b/tests/unit/test_git_hooks.py @@ -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( diff --git a/tests/unit/test_lint_workflow_diff_gates.py b/tests/unit/test_lint_workflow_diff_gates.py index 5e6ae9d285a..1739690addd 100644 --- a/tests/unit/test_lint_workflow_diff_gates.py +++ b/tests/unit/test_lint_workflow_diff_gates.py @@ -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}) diff --git a/tests/unit/test_pre_commit_lint.py b/tests/unit/test_pre_commit_lint.py index ef84ab9fbc8..10e528db73b 100644 --- a/tests/unit/test_pre_commit_lint.py +++ b/tests/unit/test_pre_commit_lint.py @@ -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") == [] diff --git a/tests/unit/test_ruff_strict_gate.py b/tests/unit/test_ruff_strict_gate.py index 852ebef7436..1a964727d43 100644 --- a/tests/unit/test_ruff_strict_gate.py +++ b/tests/unit/test_ruff_strict_gate.py @@ -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") diff --git a/tests/unit/test_spend_logs_guard.py b/tests/unit/test_spend_logs_guard.py index eb54099cf8b..8760c970f2d 100644 --- a/tests/unit/test_spend_logs_guard.py +++ b/tests/unit/test_spend_logs_guard.py @@ -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) diff --git a/tests/unit/test_type_check_gate.py b/tests/unit/test_type_check_gate.py index 76003a3d296..62f637e7d16 100644 --- a/tests/unit/test_type_check_gate.py +++ b/tests/unit/test_type_check_gate.py @@ -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 = [] diff --git a/tests/unit/test_utils_get_model_info.py b/tests/unit/test_utils_get_model_info.py index cde0a8978c0..148b37e6204 100644 --- a/tests/unit/test_utils_get_model_info.py +++ b/tests/unit/test_utils_get_model_info.py @@ -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 diff --git a/whitelisted_bedrock_models.txt b/tests/unit/whitelisted_bedrock_models.txt similarity index 100% rename from whitelisted_bedrock_models.txt rename to tests/unit/whitelisted_bedrock_models.txt diff --git a/ui/litellm-dashboard/src/lib/http/.gitattributes b/ui/litellm-dashboard/src/lib/http/.gitattributes new file mode 100644 index 00000000000..3a253eb381e --- /dev/null +++ b/ui/litellm-dashboard/src/lib/http/.gitattributes @@ -0,0 +1 @@ +schema.d.ts linguist-generated diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 29cc8782aa5..0266c03c619 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -31476,7 +31476,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; /**