Skip to content

perf(lint): skip and cache base gate passes, parallelize make lint, skip redundant prisma generate - #32000

Merged
mateo-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_pre_commit_lint_speedups
Jul 3, 2026
Merged

perf(lint): skip and cache base gate passes, parallelize make lint, skip redundant prisma generate#32000
mateo-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_pre_commit_lint_speedups

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

All runs on the same machine, same worktree, with the same staged one-line litellm docstring change (the typical "I touched litellm code and want to commit" scenario)

Before, at the branch point:

$ time make pre-commit
...
OK: every rule is within its basedpyright limit or no higher than base (154645 errors total)
...
make pre-commit 2>&1  124.56s user 16.51s system 122% cpu 1:55.56 total

After, with this change:

$ time make pre-commit
...
OK: every rule is within its basedpyright limit (154645 errors total)
...
make pre-commit 2>&1  62.47s user 10.69s system 105% cpu 1:09.12 total

The basedpyright gate goes from always paying a second full pass over a merge-base worktree to skipping it whenever head is within every ceiling. When the base pass is genuinely needed (a rule over its limit), the new cache makes only the first run pay for it; forced that path by temporarily setting reportAny's limit to 0 and piping the same saved head JSON through the gate twice:

$ time (uv run --no-sync python scripts/type_check_gate.py --base origin/litellm_internal_staging < bp_head.json)
OK: every rule is within its basedpyright limit or no higher than base (154645 errors total)
55.00s user 10.26s system 145% cpu 44.881 total

$ time (uv run --no-sync python scripts/type_check_gate.py --base origin/litellm_internal_staging < bp_head.json)
OK: every rule is within its basedpyright limit or no higher than base (154645 errors total)
5.11s user 1.62s system 99% cpu 6.785 total

$ ls "$(git rev-parse --git-common-dir)/litellm-lint-cache/"
basedpyright-base-8f364cc7ef8c4250.json

Both verdicts identical; the warm run's remaining seconds are parsing the 114MB head JSON from stdin, which every run pays. The cache lives in the git common dir so all worktrees off the same clone share it, and it is keyed by merge-base commit, pyrightconfig.json, and uv.lock, so a rebase, a config change, or a dependency bump each invalidate it

Prisma client regeneration is now stamped and skipped when the schema and prisma version are unchanged:

$ time uv run --no-sync python scripts/prisma_generate_if_needed.py
Prisma client already generated for litellm/proxy/schema.prisma (prisma 0.11.0); skipping prisma generate
0.03s user 0.02s system 50% cpu 0.103 total

against ~6s for the unconditional prisma generate it replaces. On an ungenerated or reinstalled client (fresh venv, prisma version bump, schema edit) it still generates and re-stamps

The failure paths were also exercised end to end, no mocks, to confirm the skip and cache cannot swallow a genuine breach. For the ruff gate: one real ANN001 violation staged and its limit set to the pre-change count; the gate fails through the new early-out with exact blame and lists the introduced violation

$ make lint-gate
FAIL: strict-rule totals exceed their limit (base origin/litellm_internal_staging):
  ANN001: total 2877 over limit 2876 (this change added 1)
    litellm/_e2e_fail_probe.py:1
make: *** [lint-gate] Error 1

For the basedpyright gate: one real type error (len(1), reportArgumentType) added and that rule's limit set to the base count of 1999; the base counts come from the warm cache (the cache file's mtime is identical before and after the run, so no second basedpyright pass ran) and the verdict is the correct failure

$ make lint-basedpyright
FAIL: basedpyright errors exceed the per-rule limit:
  reportArgumentType: total 2000 over limit 1999 (this change added 1)
BREACHED RULES: reportArgumentType 2000/1999 (+1)
make: *** [lint-basedpyright] Error 1

The prisma regenerate paths were exercised the same way: deleting the stamp triggers a real prisma generate (5.5s) and re-stamps, a matching stamp with a missing generated client.py (the reinstalled-package case) still regenerates, and the following run skips in 0.05s

Type

🚄 Infrastructure

Changes

make pre-commit's dominant cost was scripts/type_check_gate.py unconditionally running basedpyright a second time over a detached worktree at the merge-base, on every invocation, even when the verdict could not depend on it. A rule can only breach when its head count is over its limit, so the gate now short-circuits before the base pass when nothing is over ceiling; this is the same early-out scripts/type_discipline_gate.py already had, now also applied to scripts/ruff_strict_gate.py which paid a smaller version of the same cost (a worktree add per run)

When the base pass is needed (some rule over its limit, or --update ratcheting), its per-rule counts are memoized to /litellm-lint-cache/. The base tree at a given commit is immutable, so the counts are a pure function of the merge-base commit plus the environment, captured in the cache key as sha256 fingerprints of pyrightconfig.json and uv.lock. Stale entries are pruned on write, corrupt or misshapen entries fall back to recomputing, writes are atomic (tmp + rename), and an empty result is never stored since that is the signature of a crashed pass rather than a clean tree, and caching it would blame the next run for every error in the repo. The tmp scratch is dot-prefixed and pid-suffixed so the stale-entry prune glob (now restricted to committed .json entries) can never unlink another run's in-flight scratch, which would otherwise crash a concurrent lint from a sibling worktree with FileNotFoundError

make lint previously ran its seven checks sequentially. They are independent, so lint now runs setup once (env sync, Prisma client, base fetch) and then fans the checks out through a $(MAKE) -j sub-make; the sub-make empties the per-check setup prerequisites (LINT_DEP_INSTALL, LINT_DEP_BASE) so seven concurrent checks don't race seven uv syncs and git fetches, while standalone targets like make lint-gate keep their defaults and behave as before. --output-sync=target is added only when the running make supports it (Apple's make 3.81 does not; it degrades gracefully)

prisma generate (about 6s) ran unconditionally in lint-install and in the pre-commit API-type drift block. The new scripts/prisma_generate_if_needed.py stamps sha256(schema.prisma) plus the prisma package version under the venv prefix and skips generation when the stamp matches and the generated client actually exists; any schema edit, prisma upgrade, venv recreation, or missing client regenerates. It deliberately never imports the prisma package, because once generated the package re-exports the whole client on import, which costs more than the generate being skipped

CI is unaffected in behavior: test-linting.yml invokes the gate scripts directly and gets the same verdicts (its runners are fresh, so the cache is simply cold), and it keeps its own unconditional prisma generate. The gates' new early-out also shaves the same base-pass cost off the CI lint job whenever a PR stays within ceilings

New tests cover the over-ceiling early-out for both gates (including the default limit for unbudgeted rules), cache key sensitivity to the merge-base and each fingerprint, round-trip plus corrupt/misshapen/missing cache handling, pruning of other branch points' entries while sparing a concurrent run's in-flight scratch, the scratch name staying invisible to the prune glob, hit-without-recompute and compute-once-then-hit behavior via an injected compute function, the never-cache-an-empty-pass rule, and the prisma stamp/skip decision logic


Note

Low Risk
Changes are confined to local lint orchestration and dev tooling; gate pass/fail logic is preserved with explicit early-outs and cache fallbacks, and CI behavior is unchanged when caches are cold.

Overview
Speeds up local make lint and make pre-commit without changing gate verdicts when CI runs the same scripts.

scripts/type_check_gate.py and scripts/ruff_strict_gate.py now skip the expensive merge-base worktree scan when every rule is at or under its budget (over_ceiling early-out). When a base pass is still required, basedpyright base per-rule counts are disk-cached under the git common dir (litellm-lint-cache/), keyed by merge-base, pyrightconfig.json, and uv.lock, with safe concurrent writes and no caching of empty (crashed) results.

make lint runs lint-install and lint-fetch-base once, then fans out lint-checks in parallel via $(MAKE) -j, clearing per-target uv sync / fetch deps so parallel jobs do not race setup.

scripts/prisma_generate_if_needed.py replaces unconditional prisma generate in lint-install and pre_commit_lint.sh, regenerating only when schema hash, prisma version, or missing client.py demand it.

Tests cover ceiling short-circuit, cache behavior, prisma stamp logic, and existing gate semantics.

Reviewed by Cursor Bugbot for commit 9689595. Bugbot is set up for automated code reviews on this repo. Configure here.

…kip redundant prisma generate

make pre-commit paid for a full second basedpyright pass over a merge-base
worktree on every run even when no rule was over its ceiling, re-generated an
unchanged Prisma client, and ran seven independent checks sequentially. The
basedpyright and ruff strict gates now skip the base pass when head is within
every limit (the same early-out type_discipline_gate already had), the
basedpyright base counts are cached under the git common dir keyed by
merge-base commit, pyrightconfig.json, and uv.lock, prisma generate only runs
when the schema or prisma version changed, and make lint fans its checks out
through a parallel sub-make after a single setup phase
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR significantly cuts make pre-commit wall time (~115s → ~70s) and make lint cost by applying three independent optimizations: an over-ceiling early-out in both gate scripts that skips the expensive base-worktree pass whenever no rule exceeds its limit, a disk cache for base counts keyed by merge-base commit + config fingerprints so the base pass is paid at most once per branch point, and a stamp-based skip for prisma generate that avoids a ~6s regeneration when schema and package version are unchanged.

  • Gate early-outs (scripts/ruff_strict_gate.py, scripts/type_check_gate.py): both gates now return immediately when no rule is over its ceiling; the ruff gate's early-out correctly mirrors evaluate (which also ignores unbudgeted rules), while the pyright gate's early-out applies DEFAULT_LIMIT to unbudgeted rules, consistent with its evaluate.
  • Base-count cache (scripts/type_check_gate.py): atomic tmp+rename writes, dot-prefixed pid-suffixed scratch to survive concurrent prune passes, never-cache-empty-result guard, and comprehensive test coverage for cache key sensitivity, round-trips, corrupt entries, and prune behavior.
  • Parallel make lint (Makefile): setup (env sync, Prisma, base fetch) runs once; the seven check targets then fan out through a sub-make with -j; --output-sync=target is added only when the running make supports it, with a graceful fallback for Apple make 3.81.

Confidence Score: 5/5

All changes are confined to build tooling and lint scripts; production code is untouched and CI behavior is unchanged.

The early-outs are provably equivalent to the full evaluation paths, the cache design handles concurrent writers, corrupt entries, and empty-result suppression correctly, and all new logic is covered by thorough tests exercising the failure paths end-to-end.

No files require special attention.

Important Files Changed

Filename Overview
Makefile Adds parallelized lint execution via sub-make with -j, introduces LINT_DEP_* variables to avoid redundant setup in parallel sub-targets, swaps prisma generate for the new stamping script, and adds output-sync graceful degradation for older make versions.
scripts/prisma_generate_if_needed.py New script that skips prisma generate when schema hash + prisma version stamp matches and the generated client.py exists; correctly invalidates on schema edit, prisma upgrade, or missing client.
scripts/ruff_strict_gate.py Adds over_ceiling early-out that skips the base worktree scan when no budgeted rule is over its limit; behavior is consistent with evaluate which also only iterates over budgeted rules.
scripts/type_check_gate.py Adds over_ceiling early-out (checking unbudgeted rules against DEFAULT_LIMIT, consistent with evaluate) and a disk-based cache for base counts keyed by merge-base commit + pyrightconfig.json + uv.lock fingerprints; scratch files are dot-prefixed and pid-suffixed to avoid prune glob collisions with concurrent writers.
scripts/pre_commit_lint.sh One-line swap of prisma generate for prisma_generate_if_needed.py in the API-type drift block; existing error handling is preserved.
tests/test_litellm/test_type_check_gate.py Comprehensive tests for over_ceiling (including DEFAULT_LIMIT for unbudgeted rules), cache key sensitivity, round-trip/corrupt/misshapen cache reads, prune behavior (spares in-flight scratch, deletes other branch-point entries), and never-cache-empty-pass rule.
tests/test_litellm/test_ruff_strict_gate.py Adds tests for ruff over_ceiling, including unbudgeted-rule ignorance and multi-rule independence; consistent with the gate's policy of ignoring unbudgeted rules.
tests/test_litellm/test_prisma_generate_if_needed.py Tests stamp uniqueness, skip logic requiring both matching stamp and generated client, and the client-absent case forcing regeneration.

Reviews (3): Last reviewed commit: "fix(lint): keep the base-cache scratch f..." | Re-trigger Greptile

Comment thread scripts/type_check_gate.py
Comment thread scripts/type_check_gate.py
@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR cuts make pre-commit runtime roughly in half by eliminating redundant work across three areas: skipping the expensive basedpyright base-tree pass when no rule is already over its budget ceiling (valid because a rule can only breach if it is simultaneously over its limit AND higher than base), memoising the base pass result to disk when it is needed, and skipping prisma generate when the schema and package version are unchanged.

  • Makefile parallelisation: make lint now fans out its seven independent checks through a sub-make -j call after a single shared setup (env sync, Prisma client, base fetch), with --output-sync=target feature-detected for graceful degradation on Apple make 3.81.
  • Cache design: The base-tree cache lives in the git common dir (shared across worktrees), keyed by merge-base commit, pyrightconfig.json, and uv.lock fingerprints; atomic writes (tmp + rename) and prune-on-write prevent unbounded growth; empty results (crashed pass) are never stored.
  • Prisma stamp: prisma_generate_if_needed.py writes a sha256(schema)+version stamp under sys.prefix and skips generation when the stamp matches and prisma/client.py exists, reducing the unconditional ~6 s cost to ~0.1 s.

Confidence Score: 4/5

Safe to merge; changes are confined to local developer tooling and have no effect on production code paths or CI verdicts.

The correctness of the early-out and caching logic is well-reasoned and thoroughly tested. The one actionable note is that the stale-file prune glob in store_counts also matches .tmp scratch files, which could cause an unhandled FileNotFoundError from scratch.replace() if two lint invocations write to the shared cache simultaneously — benign in practice but slightly fragile.

scripts/type_check_gate.py — the stale-file pruning glob in store_counts warrants a quick look.

Important Files Changed

Filename Overview
scripts/type_check_gate.py Adds over_ceiling early-out before the expensive base worktree pass, plus a disk-memoised base_counts_cached. Logic is correct; minor robustness gap in stale-file pruning glob matching the .tmp scratch file.
scripts/ruff_strict_gate.py Adds the same over_ceiling short-circuit as type_check_gate.py. Correctly limited to budget rules (ruff has no DEFAULT_LIMIT concept), consistent with evaluate. No issues.
scripts/prisma_generate_if_needed.py New script that stamps sha256(schema)+prisma_version under sys.prefix and skips prisma generate when both the stamp and generated client are present. Logic is sound; stamp is invalidated on venv recreation, schema change, or version bump.
Makefile Parallelises the seven lint checks via a sub-make -j fan-out; setup prerequisites are zeroed for the sub-make to prevent seven concurrent uv sync/git fetch races. output-sync is feature-detected to degrade gracefully on Apple make 3.81.
scripts/pre_commit_lint.sh Replaces unconditional prisma generate with prisma_generate_if_needed.py in the API-type drift check block. Straightforward swap; no issues.
tests/test_litellm/test_type_check_gate.py Comprehensive new tests: over-ceiling edge cases (at-limit, over-limit, unbudgeted rules with DEFAULT_LIMIT), full cache round-trip, corrupt/misshapen cache resilience, prune-on-write, compute-once-then-hit, and never-cache-empty-pass. Good coverage.
tests/test_litellm/test_ruff_strict_gate.py Adds three tests for the new over_ceiling function covering at-limit, over-limit, missing-from-budget (correctly returns empty set), and cross-rule independence. All correct.
tests/test_litellm/test_prisma_generate_if_needed.py Tests stamp uniqueness, skip-requires-matching-stamp, and skip-requires-generated-client. client_is_generated() and main() are not directly unit-tested, but the testable logic is well covered.

Reviews (2): Last reviewed commit: "perf(lint): skip and cache base gate pas..." | Re-trigger Greptile

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Comment thread scripts/type_check_gate.py Outdated
The tmp+rename scratch in store_counts was named basedpyright-base-<hash>.json.tmp,
which the stale-entry prune glob (basedpyright-base-*) also matches, so a concurrent
lint run from another worktree sharing the same git common dir could unlink it between
write_text and replace and crash the gate with FileNotFoundError. The scratch is now
dot-prefixed so the glob can never see it, pid-suffixed so concurrent writers of the
same entry never share a scratch, and the prune glob is restricted to committed
*.json entries
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai


Generated by Claude Code

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 9689595. Configure here.

@mateo-berri
mateo-berri merged commit cf6fdac into litellm_internal_staging Jul 3, 2026
126 checks passed
@mateo-berri
mateo-berri deleted the litellm_pre_commit_lint_speedups branch July 3, 2026 02:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants