mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-02 04:21:34 +00:00
merge: mateo/translation-rule-hardening into translation-v2-providers
Clean merge (append-only Makefile/semgrep/ruff additions plus the two gate scripts). CLAUDE.md freshness and file-size gates now run in make lint-translation; verified against the merged tree in the next baseline pass.
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
# litellm/translation v2 effects boundary, encoded as deterministic checks
|
||||
# (06-quality-team.md, "algebraic effects for I/O and side effects"). Scoped
|
||||
# to the translation package; the rest of the codebase is unaffected.
|
||||
#
|
||||
# The translation core is pure: every ambient input (clock, ids, env, fs,
|
||||
# network, randomness) must arrive as a value through deps.TranslationDeps or
|
||||
# the injected engine.http.HttpPort. The two boundary modules that DEFINE
|
||||
# those ports are whitelisted by path; they declare the window, they do not
|
||||
# get to reach through it either today (both are pure), but a future port
|
||||
# default would live there and nowhere else.
|
||||
|
||||
rules:
|
||||
- id: translation-no-ambient-clock
|
||||
message: >
|
||||
ambient effect: reading the clock (time.*, datetime.now/utcnow/today)
|
||||
inside litellm/translation. Inject the timestamp via TranslationDeps
|
||||
(tenets: algebraic effects for I/O).
|
||||
severity: ERROR
|
||||
languages: [python]
|
||||
paths:
|
||||
include:
|
||||
- litellm/translation
|
||||
exclude:
|
||||
# boundary modules: the only place a port/default may live
|
||||
- litellm/translation/deps.py
|
||||
- litellm/translation/engine/http.py
|
||||
pattern-either:
|
||||
- pattern: time.time(...)
|
||||
- pattern: time.time_ns(...)
|
||||
- pattern: time.monotonic(...)
|
||||
- pattern: time.monotonic_ns(...)
|
||||
- pattern: time.perf_counter(...)
|
||||
- pattern: time.perf_counter_ns(...)
|
||||
- pattern: time.sleep(...)
|
||||
- pattern: datetime.datetime.now(...)
|
||||
- pattern: datetime.datetime.utcnow(...)
|
||||
- pattern: datetime.datetime.today(...)
|
||||
- pattern: datetime.date.today(...)
|
||||
- pattern: datetime.now(...)
|
||||
- pattern: datetime.utcnow(...)
|
||||
- pattern: datetime.today(...)
|
||||
- pattern: date.today(...)
|
||||
metadata:
|
||||
category: correctness
|
||||
tags: [python, translation-v2, algebraic-effects]
|
||||
confidence: HIGH
|
||||
|
||||
- id: translation-no-ambient-ids-or-randomness
|
||||
message: >
|
||||
ambient effect: generating ids or randomness (uuid.*, fastuuid,
|
||||
random.*, secrets.*, os.urandom) inside litellm/translation. Inject the
|
||||
generator via TranslationDeps (tenets: algebraic effects for I/O).
|
||||
severity: ERROR
|
||||
languages: [python]
|
||||
paths:
|
||||
include:
|
||||
- litellm/translation
|
||||
exclude:
|
||||
- litellm/translation/deps.py
|
||||
- litellm/translation/engine/http.py
|
||||
pattern-either:
|
||||
- pattern: uuid.uuid1(...)
|
||||
- pattern: uuid.uuid3(...)
|
||||
- pattern: uuid.uuid4(...)
|
||||
- pattern: uuid.uuid5(...)
|
||||
- pattern: import fastuuid
|
||||
- pattern: from fastuuid import $X
|
||||
- pattern: import random
|
||||
- pattern: from random import $X
|
||||
- pattern: import secrets
|
||||
- pattern: from secrets import $X
|
||||
- pattern: os.urandom(...)
|
||||
metadata:
|
||||
category: correctness
|
||||
tags: [python, translation-v2, algebraic-effects]
|
||||
confidence: HIGH
|
||||
|
||||
- id: translation-no-ambient-env
|
||||
message: >
|
||||
ambient effect: reading process environment (os.environ, os.getenv)
|
||||
inside litellm/translation. Env-derived flags enter as values on
|
||||
TranslationDeps, built once at the dispatch seam (tenets: algebraic
|
||||
effects for I/O; dependency injection, no global singletons).
|
||||
severity: ERROR
|
||||
languages: [python]
|
||||
paths:
|
||||
include:
|
||||
- litellm/translation
|
||||
exclude:
|
||||
- litellm/translation/deps.py
|
||||
- litellm/translation/engine/http.py
|
||||
pattern-either:
|
||||
- pattern: os.environ
|
||||
- pattern: os.environb
|
||||
- pattern: os.getenv(...)
|
||||
- pattern: os.putenv(...)
|
||||
- pattern: os.unsetenv(...)
|
||||
metadata:
|
||||
category: correctness
|
||||
tags: [python, translation-v2, algebraic-effects]
|
||||
confidence: HIGH
|
||||
|
||||
- id: translation-no-ambient-filesystem
|
||||
message: >
|
||||
ambient effect: touching the filesystem (open(), io.open(), pathlib
|
||||
read/write) inside litellm/translation. The package translates values;
|
||||
file content must arrive pre-read via TranslationDeps (tenets:
|
||||
algebraic effects for I/O).
|
||||
severity: ERROR
|
||||
languages: [python]
|
||||
paths:
|
||||
include:
|
||||
- litellm/translation
|
||||
exclude:
|
||||
- litellm/translation/deps.py
|
||||
- litellm/translation/engine/http.py
|
||||
pattern-either:
|
||||
- pattern: open(...)
|
||||
- pattern: io.open(...)
|
||||
- pattern: $P.read_text(...)
|
||||
- pattern: $P.write_text(...)
|
||||
- pattern: $P.read_bytes(...)
|
||||
- pattern: $P.write_bytes(...)
|
||||
- pattern: $P.open(...)
|
||||
- pattern: os.remove(...)
|
||||
- pattern: os.unlink(...)
|
||||
- pattern: os.makedirs(...)
|
||||
- pattern: os.mkdir(...)
|
||||
- pattern: shutil.$F(...)
|
||||
- pattern: tempfile.$F(...)
|
||||
metadata:
|
||||
category: correctness
|
||||
tags: [python, translation-v2, algebraic-effects]
|
||||
confidence: HIGH
|
||||
|
||||
- id: translation-no-ambient-network
|
||||
message: >
|
||||
ambient effect: a network client (requests/httpx/aiohttp/urllib/socket)
|
||||
inside litellm/translation. The ONLY I/O surface is the injected
|
||||
engine.http.HttpPort; the seam owns clients (tenets: algebraic effects
|
||||
for I/O; dependency injection).
|
||||
severity: ERROR
|
||||
languages: [python]
|
||||
paths:
|
||||
include:
|
||||
- litellm/translation
|
||||
exclude:
|
||||
- litellm/translation/deps.py
|
||||
- litellm/translation/engine/http.py
|
||||
pattern-either:
|
||||
- pattern: import requests
|
||||
- pattern: from requests import $X
|
||||
- pattern: from requests.$M import $X
|
||||
- pattern: import httpx
|
||||
- pattern: from httpx import $X
|
||||
- pattern: from httpx.$M import $X
|
||||
- pattern: import aiohttp
|
||||
- pattern: from aiohttp import $X
|
||||
- pattern: from aiohttp.$M import $X
|
||||
- pattern: import urllib
|
||||
- pattern: import urllib.request
|
||||
- pattern: from urllib import $X
|
||||
- pattern: from urllib.$M import $X
|
||||
- pattern: import socket
|
||||
- pattern: from socket import $X
|
||||
metadata:
|
||||
category: correctness
|
||||
tags: [python, translation-v2, algebraic-effects]
|
||||
confidence: HIGH
|
||||
@@ -0,0 +1,91 @@
|
||||
# litellm/translation composition-over-inheritance, encoded (06-quality-team.md
|
||||
# tenet: composition over inheritance). Scoped to the translation package.
|
||||
#
|
||||
# A provider is a module of pure functions; behavior is composed, never
|
||||
# inherited. The only legitimate bases are structural/declarative ones:
|
||||
# Protocol ports, ABC port definitions, pydantic wire models (BaseModel and
|
||||
# the package's single-level WireModel base), NamedTuple/TypedDict records,
|
||||
# and the Enum family. errors.py is excluded by path: it is the one module
|
||||
# allowed to define error machinery, including Exception subclasses if the
|
||||
# seam contract ever needs them.
|
||||
#
|
||||
# Engine semantics this encoding relies on (verified against semgrep
|
||||
# 1.157.0): `class $C($B): ...` matches any class with at least one
|
||||
# positional base and binds $B to the FIRST base only. So the first rule
|
||||
# fully checks single-base classes, and the second rule closes the
|
||||
# multi-base hole by banning multiple inheritance outright -- which is what
|
||||
# the tenet wants anyway. A justified exception (e.g. a str+Enum mixin on
|
||||
# py3.10) carries an inline `# nosemgrep: <rule-id>` with a reason.
|
||||
#
|
||||
# First-run measurement (2026-06-12): 25 classes with bases in the tree
|
||||
# (HttpPort(Protocol); WireModel(BaseModel); 23x single-base WireModel
|
||||
# subclasses), zero findings -- false-positive rate 0, rule enabled.
|
||||
|
||||
rules:
|
||||
- id: translation-no-inheritance
|
||||
message: >
|
||||
inheritance: class with a non-declarative base inside
|
||||
litellm/translation. Compose pure functions instead of subclassing;
|
||||
allowed bases are Protocol/ABC ports, pydantic wire models
|
||||
(BaseModel/WireModel), NamedTuple/TypedDict, and Enum kinds (tenets:
|
||||
composition over inheritance).
|
||||
severity: ERROR
|
||||
languages: [python]
|
||||
paths:
|
||||
include:
|
||||
- litellm/translation
|
||||
exclude:
|
||||
# the one module allowed to define error machinery (Exception bases)
|
||||
- litellm/translation/errors.py
|
||||
patterns:
|
||||
- pattern: |
|
||||
class $C($B):
|
||||
...
|
||||
- metavariable-pattern:
|
||||
metavariable: $B
|
||||
patterns:
|
||||
- pattern-not: Protocol
|
||||
- pattern-not: typing.Protocol
|
||||
- pattern-not: Protocol[...]
|
||||
- pattern-not: typing.Protocol[...]
|
||||
- pattern-not: ABC
|
||||
- pattern-not: abc.ABC
|
||||
- pattern-not: BaseModel
|
||||
- pattern-not: pydantic.BaseModel
|
||||
- pattern-not: WireModel
|
||||
- pattern-not: NamedTuple
|
||||
- pattern-not: typing.NamedTuple
|
||||
- pattern-not: TypedDict
|
||||
- pattern-not: typing.TypedDict
|
||||
- pattern-not: Enum
|
||||
- pattern-not: enum.Enum
|
||||
- pattern-not: StrEnum
|
||||
- pattern-not: enum.StrEnum
|
||||
- pattern-not: IntEnum
|
||||
- pattern-not: enum.IntEnum
|
||||
- pattern-not: IntFlag
|
||||
- pattern-not: enum.IntFlag
|
||||
metadata:
|
||||
category: correctness
|
||||
tags: [python, translation-v2, composition-over-inheritance]
|
||||
confidence: HIGH
|
||||
|
||||
- id: translation-no-multiple-inheritance
|
||||
message: >
|
||||
multiple inheritance inside litellm/translation. One declarative base
|
||||
at most; compose behavior with functions instead of mixins (tenets:
|
||||
composition over inheritance).
|
||||
severity: ERROR
|
||||
languages: [python]
|
||||
paths:
|
||||
include:
|
||||
- litellm/translation
|
||||
exclude:
|
||||
- litellm/translation/errors.py
|
||||
pattern: |
|
||||
class $C($B1, $B2):
|
||||
...
|
||||
metadata:
|
||||
category: correctness
|
||||
tags: [python, translation-v2, composition-over-inheritance]
|
||||
confidence: HIGH
|
||||
@@ -167,6 +167,8 @@ lint-translation: install-test-deps
|
||||
uv tool run --from 'pyright==1.1.406' pyright --project litellm/translation
|
||||
uv tool run --from 'semgrep==1.157.0' semgrep scan --config .semgrep/rules litellm/translation --error -q
|
||||
$(UV_RUN) lint-imports
|
||||
$(UV_RUN) python scripts/check_translation_file_sizes.py
|
||||
$(UV_RUN) python scripts/check_translation_claude_md.py
|
||||
LITELLM_LOCAL_MODEL_COST_MAP=True $(UV_RUN) pytest tests/test_litellm/translation --tb=short -q
|
||||
|
||||
test-unit-root: install-test-deps
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
# module-global rebinding (PLW0603). Inherits the repo config; ruff's
|
||||
# hierarchical discovery applies this file to this subtree automatically,
|
||||
# so the repo-wide `ruff check` enforces it with no extra wiring.
|
||||
#
|
||||
# Match/union hygiene (tenet: tagged unions + match over branching on
|
||||
# types): ruff 0.15 has no use-match-over-isinstance rule, so exhaustiveness
|
||||
# stays with pyright (reportMatchNotExhaustive + assert_never). What IS
|
||||
# deterministic here: E721 (no `type(x) ==` branching), SIM101 (no
|
||||
# duplicated isinstance chains), FURB168 (no isinstance None checks).
|
||||
|
||||
extend = "../../ruff.toml"
|
||||
preview = true
|
||||
@@ -15,6 +21,9 @@ extend-select = [
|
||||
"PLR0915",
|
||||
"ANN",
|
||||
"PLW0603",
|
||||
"E721",
|
||||
"SIM101",
|
||||
"FURB168",
|
||||
]
|
||||
|
||||
[lint.mccabe]
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""CLAUDE.md structural-freshness gate for litellm/translation
|
||||
(06-quality-team.md tenet: deliberate file/folder structure with a
|
||||
hand-written CLAUDE.md mapping it).
|
||||
|
||||
Structural only, no prose judgment:
|
||||
- litellm/translation/CLAUDE.md must exist
|
||||
- every real directory under the package (any depth) must be mentioned as
|
||||
``<name>/`` somewhere in CLAUDE.md
|
||||
- every ``<name>/`` token in CLAUDE.md's tree code block must be a real
|
||||
directory (no phantom dirs)
|
||||
|
||||
Run: python scripts/check_translation_claude_md.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PACKAGE = Path(__file__).resolve().parent.parent / "litellm" / "translation"
|
||||
CLAUDE_MD = PACKAGE / "CLAUDE.md"
|
||||
DIR_TOKEN = re.compile(r"([A-Za-z_][\w.]*)/")
|
||||
|
||||
|
||||
def is_tooling_dir(path: Path) -> bool:
|
||||
parts = path.relative_to(PACKAGE).parts
|
||||
return any(part == "__pycache__" or part.startswith(".") for part in parts)
|
||||
|
||||
|
||||
def real_dirs() -> frozenset[str]:
|
||||
return frozenset(
|
||||
p.name
|
||||
for p in PACKAGE.rglob("*")
|
||||
if p.is_dir() and not is_tooling_dir(p)
|
||||
)
|
||||
|
||||
|
||||
def tree_block_lines(text: str) -> tuple[str, ...]:
|
||||
blocks = re.findall(r"^```\n(.*?)^```$", text, re.DOTALL | re.MULTILINE)
|
||||
if not blocks:
|
||||
return ()
|
||||
return tuple(line.split("#", 1)[0] for line in blocks[0].splitlines())
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not CLAUDE_MD.is_file():
|
||||
print(
|
||||
"CLAUDE.md freshness: litellm/translation/CLAUDE.md is missing "
|
||||
"(tenets: deliberate structure, hand-written CLAUDE.md at the root)"
|
||||
)
|
||||
return 1
|
||||
text = CLAUDE_MD.read_text(encoding="utf-8")
|
||||
dirs = real_dirs()
|
||||
|
||||
unmentioned = sorted(d for d in dirs if f"{d}/" not in text)
|
||||
claimed = frozenset(
|
||||
m for line in tree_block_lines(text) for m in DIR_TOKEN.findall(line)
|
||||
)
|
||||
phantoms = sorted(claimed - dirs - {"translation"})
|
||||
|
||||
if not unmentioned and not phantoms:
|
||||
print(
|
||||
f"CLAUDE.md freshness OK: {len(dirs)} directories all mapped, "
|
||||
"no phantom entries"
|
||||
)
|
||||
return 0
|
||||
for d in unmentioned:
|
||||
print(
|
||||
f"CLAUDE.md freshness: directory '{d}/' exists but is not in "
|
||||
"litellm/translation/CLAUDE.md; map it (tenets: deliberate "
|
||||
"structure, CLAUDE.md kept true)"
|
||||
)
|
||||
for d in phantoms:
|
||||
print(
|
||||
f"CLAUDE.md freshness: CLAUDE.md's tree names '{d}/' but no such "
|
||||
"directory exists under litellm/translation; prune it (tenets: "
|
||||
"deliberate structure, CLAUDE.md kept true)"
|
||||
)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,50 @@
|
||||
"""No-monster-files gate for litellm/translation (06-quality-team.md tenet:
|
||||
no monster files or god objects).
|
||||
|
||||
Hard line cap per .py file. 720 = 1.3x today's largest file (ir.py, 555
|
||||
lines, the deliberate home of every IR union) so the current tree passes
|
||||
with ~30% headroom; everything else is under 360. Raising the cap is a
|
||||
reviewed decision, not a drive-by edit.
|
||||
|
||||
Run: python scripts/check_translation_file_sizes.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MAX_LINES = 720
|
||||
PACKAGE = Path(__file__).resolve().parent.parent / "litellm" / "translation"
|
||||
|
||||
|
||||
def line_count(path: Path) -> int:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
return sum(1 for _ in handle)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
counts = sorted(
|
||||
((line_count(p), p) for p in PACKAGE.rglob("*.py")), reverse=True
|
||||
)
|
||||
offenders = [(n, p) for n, p in counts if n > MAX_LINES]
|
||||
if not offenders:
|
||||
biggest, where = counts[0]
|
||||
print(
|
||||
f"translation file sizes OK: {len(counts)} files, "
|
||||
f"largest {where.relative_to(PACKAGE.parent.parent)} at {biggest} "
|
||||
f"lines (cap {MAX_LINES})"
|
||||
)
|
||||
return 0
|
||||
print(
|
||||
f"monster file: {len(offenders)} file(s) over the {MAX_LINES}-line cap "
|
||||
"(tenets: no monster files or god objects). Split along the package "
|
||||
"map in litellm/translation/CLAUDE.md:"
|
||||
)
|
||||
for n, p in offenders:
|
||||
print(f" {p.relative_to(PACKAGE.parent.parent)}: {n} lines")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user