mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
Merge branch 'litellm_internal_staging' into litellm_model_update_null_clear
This commit is contained in:
commit
6f48d92ba5
5 changed files with 63 additions and 271 deletions
129
.github/workflows/report-rust-release-wheel.yml
vendored
129
.github/workflows/report-rust-release-wheel.yml
vendored
|
|
@ -1,129 +0,0 @@
|
|||
name: Report LiteLLM Rust release wheel
|
||||
|
||||
on: # zizmor: ignore[dangerous-triggers] reporter executes no PR code and consumes no PR artifacts or outputs
|
||||
workflow_run:
|
||||
workflows:
|
||||
- LiteLLM Rust
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.pull_requests[0].number || github.event.workflow_run.id }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
report-release-wheel:
|
||||
name: report release wheel
|
||||
if: >-
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.path == '.github/workflows/test-rust.yml' &&
|
||||
github.event.workflow_run.head_repository.full_name == github.repository &&
|
||||
github.event.workflow_run.pull_requests[0].number != null
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Link release wheel report on PR
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
env:
|
||||
COMMENT_MARKER: "<!-- litellm-release-wheel-size -->"
|
||||
with:
|
||||
script: |
|
||||
const marker = process.env.COMMENT_MARKER;
|
||||
const workflowRun = context.payload.workflow_run;
|
||||
const allowedConclusions = new Set([
|
||||
"action_required",
|
||||
"cancelled",
|
||||
"failure",
|
||||
"neutral",
|
||||
"skipped",
|
||||
"stale",
|
||||
"startup_failure",
|
||||
"success",
|
||||
"timed_out",
|
||||
]);
|
||||
if (
|
||||
!allowedConclusions.has(workflowRun.conclusion) ||
|
||||
workflowRun.event !== "pull_request" ||
|
||||
workflowRun.path !== ".github/workflows/test-rust.yml" ||
|
||||
workflowRun.head_repository?.full_name !==
|
||||
`${context.repo.owner}/${context.repo.repo}` ||
|
||||
workflowRun.pull_requests?.length !== 1
|
||||
) {
|
||||
throw new Error("unexpected source workflow");
|
||||
}
|
||||
const pullRequest = workflowRun.pull_requests[0];
|
||||
const pullRequestNumber = pullRequest.number;
|
||||
const headSha = workflowRun.head_sha;
|
||||
const runId = workflowRun.id;
|
||||
if (
|
||||
!Number.isSafeInteger(pullRequestNumber) ||
|
||||
pullRequestNumber <= 0 ||
|
||||
!Number.isSafeInteger(runId) ||
|
||||
runId <= 0 ||
|
||||
!/^[0-9a-f]{40}$/.test(headSha) ||
|
||||
pullRequest.head?.sha !== headSha
|
||||
) {
|
||||
throw new Error("invalid source workflow metadata");
|
||||
}
|
||||
const runUrl =
|
||||
`${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` +
|
||||
`/actions/runs/${runId}`;
|
||||
const result =
|
||||
workflowRun.conclusion === "success"
|
||||
? "successfully"
|
||||
: `with \`${workflowRun.conclusion}\``;
|
||||
const body = [
|
||||
marker,
|
||||
"## LiteLLM Rust workflow",
|
||||
"",
|
||||
`Workflow completed ${result} for \`${headSha}\``,
|
||||
"",
|
||||
`[View workflow run](${runUrl})`,
|
||||
].join("\n");
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequestNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
const existing = comments.find(
|
||||
(comment) =>
|
||||
comment.user?.login === "github-actions[bot]" &&
|
||||
comment.body?.startsWith(marker),
|
||||
);
|
||||
const currentPullRequest = (
|
||||
await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pullRequestNumber,
|
||||
})
|
||||
).data;
|
||||
if (
|
||||
currentPullRequest.state !== "open" ||
|
||||
currentPullRequest.head.repo?.full_name !==
|
||||
`${context.repo.owner}/${context.repo.repo}` ||
|
||||
currentPullRequest.head.sha !== headSha
|
||||
) {
|
||||
core.info("source workflow no longer matches the current pull request head");
|
||||
return;
|
||||
}
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body,
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequestNumber,
|
||||
body,
|
||||
});
|
||||
}
|
||||
111
.github/workflows/test-rust.yml
vendored
111
.github/workflows/test-rust.yml
vendored
|
|
@ -7,6 +7,7 @@ on:
|
|||
- ".cargo/**"
|
||||
- "pyproject.toml"
|
||||
- "rust-toolchain.toml"
|
||||
- ".github/actions/setup-uv-with-retries/**"
|
||||
- ".github/scripts/smoke_test_native_wheel.py"
|
||||
- ".github/scripts/verify_linux_native_wheel.py"
|
||||
- "tests/test_litellm/rust_bridge/native_route_wheel_test.py"
|
||||
|
|
@ -22,6 +23,7 @@ on:
|
|||
- ".cargo/**"
|
||||
- "pyproject.toml"
|
||||
- "rust-toolchain.toml"
|
||||
- ".github/actions/setup-uv-with-retries/**"
|
||||
- ".github/scripts/smoke_test_native_wheel.py"
|
||||
- ".github/scripts/verify_linux_native_wheel.py"
|
||||
- "tests/test_litellm/rust_bridge/native_route_wheel_test.py"
|
||||
|
|
@ -34,102 +36,89 @@ concurrency:
|
|||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
rust-checks:
|
||||
name: rustfmt, clippy, test
|
||||
rust-lint:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
defaults:
|
||||
run:
|
||||
working-directory: litellm-rust
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Rust
|
||||
run: rustup toolchain install
|
||||
- run: rustup toolchain install --no-self-update
|
||||
|
||||
- name: Cache Cargo registry and target
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
- run: cargo fmt --check
|
||||
|
||||
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
litellm-rust/target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('rust-toolchain.toml', 'litellm-rust/Cargo.lock') }}
|
||||
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('rust-toolchain.toml', '.cargo/**', 'litellm-rust/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-
|
||||
${{ runner.os }}-cargo-${{ github.job }}-
|
||||
|
||||
- name: Check Rust formatting
|
||||
run: cargo fmt --check
|
||||
- run: cargo clippy --workspace --all-targets --locked -- -D warnings
|
||||
|
||||
- name: Run Clippy
|
||||
run: cargo clippy --workspace --all-targets --locked -- -D warnings
|
||||
- run: cargo clippy -p litellm-core --all-targets --features bedrock-auth --locked -- -D warnings
|
||||
|
||||
- name: Run Clippy with Bedrock auth
|
||||
run: cargo clippy -p litellm-core --all-targets --features bedrock-auth --locked -- -D warnings
|
||||
- run: cargo clippy -p litellm-ai-gateway --all-targets --all-features --locked -- -D warnings
|
||||
|
||||
- name: Run Clippy with all gateway features
|
||||
run: cargo clippy -p litellm-ai-gateway --all-targets --all-features --locked -- -D warnings
|
||||
|
||||
- name: Run Rust tests
|
||||
run: cargo test --workspace --locked
|
||||
|
||||
- name: Run core tests with Bedrock auth
|
||||
run: cargo test -p litellm-core --features bedrock-auth --locked
|
||||
|
||||
# Not --all-features: python-config links libpython, which this job does not install.
|
||||
- name: Run gateway tests with the server feature
|
||||
run: cargo test -p litellm-ai-gateway --features server --locked
|
||||
|
||||
release-wheel:
|
||||
name: release wheel
|
||||
rust-test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up uv
|
||||
uses: ./.github/actions/setup-uv-with-retries
|
||||
- uses: ./.github/actions/setup-uv-with-retries
|
||||
with:
|
||||
version: "0.10.9"
|
||||
|
||||
- name: Set up Rust
|
||||
run: rustup toolchain install
|
||||
- run: rustup toolchain install --no-self-update
|
||||
|
||||
- name: Build release wheel
|
||||
run: uv build --wheel --out-dir dist
|
||||
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
litellm-rust/target
|
||||
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('rust-toolchain.toml', '.cargo/**', 'litellm-rust/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-${{ github.job }}-
|
||||
|
||||
- name: Build panic contract wheel
|
||||
run: >-
|
||||
- run: cargo test --workspace --locked
|
||||
working-directory: litellm-rust
|
||||
|
||||
- run: cargo test -p litellm-core --features bedrock-auth --locked
|
||||
working-directory: litellm-rust
|
||||
|
||||
- run: cargo test -p litellm-ai-gateway --features server --locked
|
||||
working-directory: litellm-rust
|
||||
|
||||
- run: uv build --wheel --out-dir dist
|
||||
|
||||
- run: python .github/scripts/verify_linux_native_wheel.py dist/*.whl
|
||||
env:
|
||||
RELEASE_WHEEL_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- run: python tests/test_litellm/rust_bridge/native_route_wheel_test.py dist/*.whl
|
||||
|
||||
- run: >-
|
||||
uv build --wheel --out-dir panic-dist
|
||||
--config-setting "maturin.build-args=--features panic-test,extension-module"
|
||||
|
||||
- name: Smoke-test native panic unwinding
|
||||
run: python .github/scripts/smoke_test_native_wheel.py panic-dist/*.whl
|
||||
|
||||
- name: Verify stripped native extension
|
||||
env:
|
||||
RELEASE_WHEEL_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
run: python .github/scripts/verify_linux_native_wheel.py dist/*.whl
|
||||
|
||||
- name: Test native route wheel
|
||||
run: python tests/test_litellm/rust_bridge/native_route_wheel_test.py dist/*.whl
|
||||
- run: python .github/scripts/smoke_test_native_wheel.py panic-dist/*.whl
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ Don't hesitate to use values in .env to get needed API keys and other secrets, a
|
|||
|
||||
Python max line length is 120, not 88
|
||||
|
||||
When you fix violations gated by `ruff-strict-budget.json`, `type-discipline-budget.json`, or `basedpyright-code-budget.json`, run `make lint-budget-update` and commit the lowered limits so the ceilings ratchet down instead of leaving stale headroom. It measures the working tree, so it must contain exactly the fixes you're committing
|
||||
Never edit or commit `ruff-strict-budget.json`, `type-discipline-budget.json`, `basedpyright-code-budget.json`, or `test-quality-budget.json` on a PR branch, and don't run `make lint-budget-update` there. A scheduled Devin automation lowers the limits on `litellm_internal_staging` in its own PR by exactly what landed since the last ratchet, so concurrent PRs don't fight over the same `"limit"` lines. If your branch already carries a budget edit, drop it before opening the PR
|
||||
|
||||
`make check` (f.k.a. `make pre-commit`, which still works identically as an alias) saves its complete output to a log file in .git (overwriting previous logs) and prints that path as its first and last output lines. To inspect a run, read or grep that log instead of re-running the multi-minute checks just to see a different slice
|
||||
|
||||
|
|
|
|||
|
|
@ -10,17 +10,12 @@ base.
|
|||
|
||||
Every rule is seeded at exactly its count on the day the gate landed, so the
|
||||
suite's existing debt is grandfathered and any net-new violation trips the gate
|
||||
immediately. ``--update`` ratchets a limit down by the violations this branch
|
||||
fixed relative to its branch point (the merge-base), so the ceilings only ever
|
||||
fall. Base counts are measured with the *current* checker, so a rule introduced
|
||||
on this branch is counted at the base too and ratchets like every other one.
|
||||
|
||||
Only ever falling is not the same as always falling, so the gate enforces the
|
||||
second half: a branch that clears violations and leaves the ceiling above its
|
||||
new count fails, naming the rules and telling the author to run
|
||||
``make lint-budget-update``. Without that, a removed violation could come back
|
||||
later under a ceiling nobody lowered. Drift already in the base is never
|
||||
blamed, so this fires only on the branch that did the clearing.
|
||||
immediately. ``--update`` ratchets a limit down by the violations fixed relative
|
||||
to ``--base``, so the ceilings only ever fall. Base counts are measured with the
|
||||
*current* checker, so a rule introduced on this branch is counted at the base too
|
||||
and ratchets like every other one. The ratchet runs as a scheduled automation
|
||||
against litellm_internal_staging, not on PR branches, so concurrent PRs never
|
||||
race to edit the same limit.
|
||||
|
||||
The deliberate difference from its sibling: this gate has no headroom anywhere.
|
||||
Type discipline seeded LIT010/LIT011 at 1.5x to leave room for an in-flight
|
||||
|
|
@ -144,21 +139,6 @@ def over_ceiling(head: Mapping[str, int], budget: Mapping[str, Mapping[str, int]
|
|||
)
|
||||
|
||||
|
||||
def unratcheted(
|
||||
head: Mapping[str, int],
|
||||
base: Mapping[str, int],
|
||||
budget: Mapping[str, Mapping[str, int]],
|
||||
) -> tuple[Breach, ...]:
|
||||
"""Rules this branch cleared without lowering the ceiling behind them. Requires
|
||||
both `head < base`, so drift already in the base is never blamed on this change,
|
||||
and `head < limit`, so a ceiling already at the count is left alone."""
|
||||
return tuple(sorted(
|
||||
Breach(rule, head.get(rule, 0), spec["limit"], head.get(rule, 0) - base.get(rule, 0))
|
||||
for rule, spec in budget.items()
|
||||
if head.get(rule, 0) < base.get(rule, 0) and head.get(rule, 0) < spec["limit"]
|
||||
))
|
||||
|
||||
|
||||
def evaluate(
|
||||
head: Mapping[str, int],
|
||||
base: Mapping[str, int],
|
||||
|
|
@ -198,38 +178,15 @@ def introduced(
|
|||
return tuple(v for v in violations if v.line in changed.get(v.file, frozenset()))
|
||||
|
||||
|
||||
def touches_measured_tree(base_point: str) -> bool:
|
||||
"""Whether this branch changed anything that can move a count. A branch that
|
||||
touches neither the test tree nor the checker cannot have cleared a violation,
|
||||
so the base scan is skipped and the gate stays cheap on the common change."""
|
||||
changed: Final = _run(
|
||||
["git", "diff", "--name-only", base_point, "--", TARGET, str(CHECKER.relative_to(REPO_ROOT))]
|
||||
)
|
||||
return bool(changed.strip())
|
||||
|
||||
|
||||
def cmd_check(base: str) -> None:
|
||||
budget: Final = json.loads(BUDGET_PATH.read_text())
|
||||
head: Final = head_violations()
|
||||
head_counts: Final = count_by_rule(head)
|
||||
base_point: Final = resolve_base_point(base)
|
||||
if not over_ceiling(head_counts, budget) and not touches_measured_tree(base_point):
|
||||
if not over_ceiling(head_counts, budget):
|
||||
print(f"OK: every TQ rule is within its test-suite ceiling (base {base})")
|
||||
return
|
||||
base_point: Final = resolve_base_point(base)
|
||||
base_at_point: Final = base_counts(base_point)
|
||||
stale: Final = unratcheted(head_counts, base_at_point, budget)
|
||||
if stale:
|
||||
print(f"FAIL: TQ-rule limits were left above the count this branch reached (base {base}):")
|
||||
for breach in stale:
|
||||
print(
|
||||
f" {breach.rule}: this branch cleared {-breach.added} down to {breach.total}, "
|
||||
f"but the limit is still {breach.cap}"
|
||||
)
|
||||
print(
|
||||
"Run `make lint-budget-update` and commit the lowered limits, so the "
|
||||
"violations you cleared cannot come back under a ceiling nobody moved."
|
||||
)
|
||||
raise SystemExit(1)
|
||||
breaches: Final = evaluate(head_counts, base_at_point, budget)
|
||||
if not breaches:
|
||||
print(f"OK: every TQ rule is within its test-suite ceiling (base {base})")
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
"""Tests for scripts/test_quality_gate.py.
|
||||
|
||||
The gate's whole value is that it blames a change only for what it adds, that a limit
|
||||
can never rise, and that a limit cannot stay above a count the branch pushed below it.
|
||||
All three live in pure functions, so they are tested directly: `evaluate` for the blame
|
||||
rule, `ratcheted_budget` for the one-way ratchet, `unratcheted` for the ceiling a branch
|
||||
left behind, and `parse_changed_lines` for the diff scan that turns a breach into
|
||||
file:line.
|
||||
The gate's whole value is that it blames a change only for what it adds and that a
|
||||
limit can never rise. Both live in pure functions, so they are tested directly:
|
||||
`evaluate` for the blame rule, `ratcheted_budget` for the one-way ratchet, and
|
||||
`parse_changed_lines` for the diff scan that turns a breach into file:line.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
|
|
@ -73,29 +71,6 @@ def test_ratchet_lowers_a_rule_introduced_on_this_branch_like_any_other():
|
|||
assert updated["TQ001"]["limit"] == 4
|
||||
|
||||
|
||||
def test_a_branch_that_cleared_violations_must_lower_the_ceiling():
|
||||
stale = gate.unratcheted({"TQ001": 6}, {"TQ001": 10}, _BUDGET)
|
||||
assert [(b.rule, b.total, b.cap, b.added) for b in stale] == [("TQ001", 6, 10, -4)]
|
||||
|
||||
|
||||
def test_headroom_already_in_the_base_is_not_blamed_on_this_branch():
|
||||
assert gate.unratcheted({"TQ001": 6}, {"TQ001": 6}, _BUDGET) == ()
|
||||
|
||||
|
||||
def test_a_branch_that_cleared_down_to_the_ceiling_exactly_is_clean():
|
||||
assert gate.unratcheted({"TQ001": 10}, {"TQ001": 12}, _BUDGET) == ()
|
||||
|
||||
|
||||
def test_a_branch_that_added_violations_is_not_a_ratchet_finding():
|
||||
assert gate.unratcheted({"TQ001": 14}, {"TQ001": 10}, _BUDGET) == ()
|
||||
|
||||
|
||||
def test_the_ratchet_finding_survives_the_update_that_answers_it():
|
||||
cleared = {"TQ001": 6}
|
||||
updated = gate.ratcheted_budget(_BUDGET, cleared, {"TQ001": 10})
|
||||
assert gate.unratcheted(cleared, {"TQ001": 10}, updated) == ()
|
||||
|
||||
|
||||
def test_parse_changed_lines_groups_hunks_under_their_own_file():
|
||||
diff = (
|
||||
"diff --git a/tests/a.py b/tests/a.py\n"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue