From 20d4dcc4ed01e44db25c3057eea65786fb38eb6b Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Fri, 26 Jun 2026 17:42:05 -0700 Subject: [PATCH 1/4] fix(mcp): stop logging tool-call input in MCP client (#31393) The MCP client logged the full tool arguments (and prompt arguments) at INFO on every call, so caller input such as user queries, model names, and instructions landed in the proxy application logs and any downstream log aggregator Log only the tool or prompt name and drop the arguments from these INFO lines (cherry picked from commit 7acc0157dffed1521dad610a5807ddd207fa9015) --- litellm/experimental_mcp_client/client.py | 4 +- .../test_mcp_client.py | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/litellm/experimental_mcp_client/client.py b/litellm/experimental_mcp_client/client.py index c6d427e7f09..63be47b64f8 100644 --- a/litellm/experimental_mcp_client/client.py +++ b/litellm/experimental_mcp_client/client.py @@ -555,7 +555,7 @@ async def call_tool( Call an MCP Tool. """ verbose_logger.info( - f"MCP client calling tool '{call_tool_request_params.name}' with arguments: {call_tool_request_params.arguments}" + f"MCP client calling tool '{call_tool_request_params.name}'" ) async def on_progress( @@ -664,7 +664,7 @@ async def get_prompt( ) -> GetPromptResult: """Fetch a prompt definition from the MCP server.""" verbose_logger.info( - f"MCP client fetching prompt '{get_prompt_request_params.name}' with arguments: {get_prompt_request_params.arguments}" + f"MCP client fetching prompt '{get_prompt_request_params.name}'" ) async def _get_prompt_operation(session: ClientSession): diff --git a/tests/test_litellm/experimental_mcp_client/test_mcp_client.py b/tests/test_litellm/experimental_mcp_client/test_mcp_client.py index c9e500b4a5b..5cf062ab8ac 100644 --- a/tests/test_litellm/experimental_mcp_client/test_mcp_client.py +++ b/tests/test_litellm/experimental_mcp_client/test_mcp_client.py @@ -543,5 +543,51 @@ async def _op(session): assert result == "done" +def _all_logged_messages(mock_logger): + return " ".join( + str(call.args[0]) + for level in ("info", "debug", "warning", "error", "exception") + for call in getattr(mock_logger, level).call_args_list + if call.args + ) + + +@pytest.mark.asyncio +async def test_call_tool_does_not_log_arguments(): + from mcp.types import CallToolRequestParams + + secret = "ssn-123-45-6789" + client = MCPClient(server_url="http://test-server") + client.run_with_session = AsyncMock(return_value=MagicMock()) + params = CallToolRequestParams( + name="search_tool", arguments={"input": secret, "model": "gpt-5-mini"} + ) + + with patch.object(mcp_client_module, "verbose_logger") as mock_logger: + await client.call_tool(params) + + logged = _all_logged_messages(mock_logger) + assert "search_tool" in logged + assert secret not in logged + assert "gpt-5-mini" not in logged + + +@pytest.mark.asyncio +async def test_get_prompt_does_not_log_arguments(): + from mcp.types import GetPromptRequestParams + + secret = "ssn-987-65-4321" + client = MCPClient(server_url="http://test-server") + client.run_with_session = AsyncMock(return_value=MagicMock()) + params = GetPromptRequestParams(name="my_prompt", arguments={"input": secret}) + + with patch.object(mcp_client_module, "verbose_logger") as mock_logger: + await client.get_prompt(params) + + logged = _all_logged_messages(mock_logger) + assert "my_prompt" in logged + assert secret not in logged + + if __name__ == "__main__": pytest.main([__file__]) From 319e588ed894b8a401cc0a5bd6ea52be164d925e Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 2 Jul 2026 16:53:35 -0700 Subject: [PATCH 2/4] =?UTF-8?q?bump:=20version=201.89.5=20=E2=86=92=201.89?= =?UTF-8?q?.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1a9845bd636..051186e1aa5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "litellm" -version = "1.89.5" +version = "1.89.6" description = "Library to easily interface with LLM API providers" readme = "README.md" requires-python = ">=3.10, <3.14" @@ -261,7 +261,7 @@ source-exclude = [ profile = "black" [tool.commitizen] -version = "1.89.5" +version = "1.89.6" version_files = [ "pyproject.toml:^version", ] From 30dce2ba3a1408cc43b023144d461b8b051ff478 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 2 Jul 2026 16:53:47 -0700 Subject: [PATCH 3/4] chore: refresh uv.lock for 1.89.6 --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index faaa5e5e741..113894d312b 100644 --- a/uv.lock +++ b/uv.lock @@ -9,7 +9,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-06-27T00:53:21.064066Z" +exclude-newer = "2026-06-29T23:53:46.228718Z" exclude-newer-span = "P3D" [manifest] @@ -3294,7 +3294,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.89.5" +version = "1.89.6" source = { editable = "." } dependencies = [ { name = "aiohttp" }, From ff544cfe15f0f80eeea7b5c34220e8b6b961b3f8 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 2 Jul 2026 16:54:12 -0700 Subject: [PATCH 4/4] chore(ci): sync GitHub Actions with default branch --- .github/workflows/_test-unit-base.yml | 2 +- .github/workflows/check-ui-api-types.yml | 4 +- .github/workflows/close_low_quality_prs.yml | 92 ++++++++++ .github/workflows/codeql.yml | 6 +- .github/workflows/codspeed.yml | 4 +- .github/workflows/conventional-commits.yml | 46 +++++ .github/workflows/create-release.yml | 63 ++++++- .github/workflows/guard-fork-dependencies.yml | 2 +- .github/workflows/guard-main-branch.yml | 4 +- .github/workflows/image-scan.yml | 65 +++++++ .github/workflows/mutation-test.yml | 2 +- .github/workflows/osv-scan.yml | 44 +++++ .github/workflows/test-code-quality.yml | 2 +- .github/workflows/test-linting.yml | 79 ++++++-- .github/workflows/test-litellm-ui-build.yml | 8 +- .github/workflows/test-mcp.yml | 4 +- .github/workflows/test-model-map.yaml | 2 +- .github/workflows/test-rust.yml | 65 +++++++ .github/workflows/test-semgrep.yml | 2 +- .github/workflows/test-unit-core-utils.yml | 2 +- .github/workflows/test-unit-documentation.yml | 4 +- .../test-unit-enterprise-routing.yml | 2 +- .github/workflows/test-unit-integrations.yml | 2 +- .github/workflows/test-unit-llm-providers.yml | 2 +- .github/workflows/test-unit-misc.yml | 8 +- .github/workflows/test-unit-proxy-auth.yml | 2 +- .../workflows/test-unit-proxy-endpoints.yml | 14 +- .github/workflows/test-unit-proxy-infra.yml | 3 +- .github/workflows/test-unit-proxy-legacy.yml | 4 +- .../test-unit-responses-caching-types.yml | 2 +- .github/workflows/test_server_root_path.yml | 9 +- .github/workflows/triage_issue_with_llm.yml | 96 ++++++++++ .github/workflows/triage_reconsider.yml | 172 ++++++++++++++++++ .github/workflows/triage_rollout_heads_up.yml | 92 ++++++++++ .github/workflows/zizmor.yml | 13 +- 35 files changed, 866 insertions(+), 57 deletions(-) create mode 100644 .github/workflows/close_low_quality_prs.yml create mode 100644 .github/workflows/conventional-commits.yml create mode 100644 .github/workflows/image-scan.yml create mode 100644 .github/workflows/osv-scan.yml create mode 100644 .github/workflows/test-rust.yml create mode 100644 .github/workflows/triage_issue_with_llm.yml create mode 100644 .github/workflows/triage_reconsider.yml create mode 100644 .github/workflows/triage_rollout_heads_up.yml diff --git a/.github/workflows/_test-unit-base.yml b/.github/workflows/_test-unit-base.yml index a42b2f8f9df..25c6d4a7019 100644 --- a/.github/workflows/_test-unit-base.yml +++ b/.github/workflows/_test-unit-base.yml @@ -73,7 +73,7 @@ jobs: - name: Install dependencies run: | - uv sync --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router + .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router - name: Generate Prisma client env: diff --git a/.github/workflows/check-ui-api-types.yml b/.github/workflows/check-ui-api-types.yml index eeb5545b15e..439126aa1ee 100644 --- a/.github/workflows/check-ui-api-types.yml +++ b/.github/workflows/check-ui-api-types.yml @@ -46,7 +46,7 @@ jobs: ${{ runner.os }}-uv- - name: Install backend dependencies - run: uv sync --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router + run: .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router - name: Generate Prisma client env: @@ -54,7 +54,7 @@ jobs: run: uv run --no-sync prisma generate --schema litellm/proxy/schema.prisma - name: Set up Node.js - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: "20" cache: "npm" diff --git a/.github/workflows/close_low_quality_prs.yml b/.github/workflows/close_low_quality_prs.yml new file mode 100644 index 00000000000..2401be84000 --- /dev/null +++ b/.github/workflows/close_low_quality_prs.yml @@ -0,0 +1,92 @@ +name: Close Low-Quality PRs + +# Auto-close any open PR (including drafts, regardless of age) authored by an +# external OSS contributor that Greptile reviewed with a confidence score +# below 4/5. Closures are explained in a comment that tells the contributor +# to push fixes and open a fresh PR (since OSS authors cannot reopen a PR +# closed by a bot/maintainer) or comment `@agent-shin reconsider` to have +# Agent Shin re-evaluate. +# +# Manual one-off run: +# gh workflow run "Close Low-Quality PRs" -f close=true +# +# Dry-run preview (no PRs are touched): +# gh workflow run "Close Low-Quality PRs" -f close=false + +on: + schedule: + # Daily at 09:00 UTC. Pairs well with the stale-issue workflow at midnight. + - cron: "0 9 * * *" + workflow_dispatch: + inputs: + close: + description: "Actually close matching PRs (false = dry run)." + required: false + default: "false" + type: choice + options: + - "true" + - "false" + min_age_days: + description: "Minimum PR age in days (default 0 = no age filter)." + required: false + default: "0" + min_score: + description: "Greptile score below which a PR is closed (1-5)." + required: false + default: "4" + limit: + description: "Maximum number of PRs to close in a single run." + required: false + default: "25" + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + close-low-quality-prs: + if: github.repository == 'BerriAI/litellm' + runs-on: ubuntu-latest + steps: + - name: Checkout triage script + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + sparse-checkout: .github/scripts + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Run low-quality PR closer + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Scheduled runs are ALWAYS dry-run, even when AGENT_SHIN_ENABLED is + # "true", so the team can QA the closer's verdicts in step summaries + # before any contributor sees a PR closed. Real closures only happen + # on manual workflow_dispatch with close=true (and the variable set). + CLOSE_FLAG: ${{ github.event.inputs.close || 'false' }} + AGENT_SHIN_ENABLED: ${{ vars.AGENT_SHIN_ENABLED }} + MIN_AGE_DAYS: ${{ github.event.inputs.min_age_days || '0' }} + MIN_SCORE: ${{ github.event.inputs.min_score || '4' }} + LIMIT: ${{ github.event.inputs.limit || '25' }} + run: | + set -euo pipefail + ARGS=( + --repo "${{ github.repository }}" + --min-age-days "${MIN_AGE_DAYS}" + --min-score "${MIN_SCORE}" + --limit "${LIMIT}" + ) + if [ "${AGENT_SHIN_ENABLED:-false}" != "true" ]; then + echo "::notice::AGENT_SHIN_ENABLED is not 'true' -> forcing dry-run regardless of close input." + elif [ "${GITHUB_EVENT_NAME:-}" = "workflow_dispatch" ] && [ "${CLOSE_FLAG}" = "true" ]; then + ARGS+=(--close) + echo "::notice::Running in close-on-fail mode." + else + echo "::notice::AGENT_SHIN_ENABLED is true but this trigger is dry-run (scheduled event or close=false)." + fi + python3 .github/scripts/close_low_quality_prs.py "${ARGS[@]}" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index babe3b62933..d3a165a11da 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -43,14 +43,14 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@ebcb5b36ded6beda4ceefea6a8bc4cc885255bb3 # v3 + uses: github/codeql-action/init@ebcb5b36ded6beda4ceefea6a8bc4cc885255bb3 # v3.34.1 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} config-file: ./.github/codeql/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ebcb5b36ded6beda4ceefea6a8bc4cc885255bb3 # v3 + uses: github/codeql-action/analyze@ebcb5b36ded6beda4ceefea6a8bc4cc885255bb3 # v3.34.1 with: category: "/language:${{ matrix.language }}" output: sarif-results @@ -77,7 +77,7 @@ jobs: output: sarif-results/python.sarif - name: Upload SARIF - uses: github/codeql-action/upload-sarif@ebcb5b36ded6beda4ceefea6a8bc4cc885255bb3 # v3 + uses: github/codeql-action/upload-sarif@ebcb5b36ded6beda4ceefea6a8bc4cc885255bb3 # v3.34.1 with: sarif_file: sarif-results category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 17efbf90339..49f1d906069 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -21,7 +21,7 @@ concurrency: jobs: benchmarks: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -48,6 +48,8 @@ jobs: uv run --frozen --no-default-groups --with pytest==8.3.5 --with pytest-codspeed==4.3.0 + --with "mcp>=1.26.0,<2.0" + --with "a2a-sdk>=1.1.0,<2.0" pytest -p pytest_codspeed.plugin tests/benchmarks/ diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml new file mode 100644 index 00000000000..69ade24d028 --- /dev/null +++ b/.github/workflows/conventional-commits.yml @@ -0,0 +1,46 @@ +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. +# +# See https://www.conventionalcommits.org/en/v1.0.0/ + +on: + pull_request: + types: [opened, edited, reopened, synchronize, labeled, unlabeled] + +permissions: + pull-requests: read + +jobs: + lint-pr-title: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - name: Check title against Conventional Commits + uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + # Must mirror the type list in .githooks/commit-msg. + types: | + feat + fix + docs + style + refactor + perf + test + build + ci + chore + revert + requireScope: false + subjectPattern: ^(?![A-Z]).+$ + subjectPatternError: | + The subject "{subject}" must start with a lowercase character. + # Allow merges/reverts that GitHub generates automatically. + ignoreLabels: | + ignore-semantic-pull-request diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index a726a921a2b..0ad84cd3ceb 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -52,6 +52,22 @@ jobs: // are stable maintenance releases, not pre-releases. const isPrerelease = /(?:rc|nightly|alpha|beta|[-.]dev)/i.test(tag); + // A stable release should only claim the repo "latest" badge when its + // version is >= the current latest. Otherwise a backport (e.g. 1.84.6) + // would steal "latest" from a newer line (e.g. 1.88.1). + const versionKey = (rawTag) => { + const m = String(rawTag).match(/^v?(\d+)\.(\d+)\.(\d+)/); + if (!m) return null; + const maintenance = String(rawTag).match(/(?:\.post|\.patch\.)(\d+)/i); + return [Number(m[1]), Number(m[2]), Number(m[3]), maintenance ? Number(maintenance[1]) : 0]; + }; + const isAtLeast = (a, b) => { + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return a[i] > b[i]; + } + return true; + }; + const cosignSection = [ `## Verify Docker Image Signature`, ``, @@ -90,10 +106,44 @@ jobs: ].join('\n'); try { + let makeLatest = "false"; + const newVersion = versionKey(tag); + if (!isPrerelease && newVersion) { + let latestVersion = null; + try { + const latest = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + }); + latestVersion = versionKey(latest.data.tag_name); + } catch (error) { + if (error.status !== 404) throw error; + } + makeLatest = (!latestVersion || isAtLeast(newVersion, latestVersion)) ? "true" : "false"; + } + + try { + await github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `refs/tags/${tag}`, + sha: commitHash, + }); + } catch (error) { + if (error.status !== 422) throw error; + const existing = await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `tags/${tag}`, + }); + if (existing.data.object.sha !== commitHash) { + throw new Error(`Tag ${tag} already exists at ${existing.data.object.sha}, expected ${commitHash}`); + } + } + const response = await github.rest.repos.createRelease({ draft: true, generate_release_notes: true, - target_commitish: commitHash, name: tag, owner: context.repo.owner, prerelease: isPrerelease, @@ -106,10 +156,21 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, release_id: response.data.id, + tag_name: tag, body: updatedBody, draft: false, }); + if (!isPrerelease) { + await github.rest.repos.updateRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: response.data.id, + tag_name: tag, + make_latest: makeLatest, + }); + } + } catch (error) { core.setFailed(error.message); } diff --git a/.github/workflows/guard-fork-dependencies.yml b/.github/workflows/guard-fork-dependencies.yml index bf7282688ef..f4cbdd63cdf 100644 --- a/.github/workflows/guard-fork-dependencies.yml +++ b/.github/workflows/guard-fork-dependencies.yml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" paths: - "uv.lock" diff --git a/.github/workflows/guard-main-branch.yml b/.github/workflows/guard-main-branch.yml index 1c1ce0de079..21aad18d298 100644 --- a/.github/workflows/guard-main-branch.yml +++ b/.github/workflows/guard-main-branch.yml @@ -31,12 +31,12 @@ jobs: echo "PR head repo: $HEAD_REPO" echo "PR head branch: $HEAD_REF" if [ "$HEAD_REPO" != "$BASE_REPO" ]; then - echo "::error::PRs to main must originate from the canonical repository ($BASE_REPO), not a fork ($HEAD_REPO). External contributors should open PRs against the 'litellm_oss_branch' branch instead." + echo "::error::PRs to main must originate from the canonical repository ($BASE_REPO), not a fork ($HEAD_REPO). External contributors should open PRs against the 'litellm_oss_staging' branch instead." exit 1 fi if [ "$HEAD_REF" = "litellm_internal_staging" ] || [[ "$HEAD_REF" == litellm_hotfix_?* ]]; then echo "Allowed source branch." exit 0 fi - echo "::error::PRs to main must originate from 'litellm_internal_staging' or a 'litellm_hotfix_*' branch. Got: '$HEAD_REF'. If this is a contribution, retarget the PR against 'litellm_oss_branch' instead." + echo "::error::PRs to main must originate from 'litellm_internal_staging' or a 'litellm_hotfix_*' branch. Got: '$HEAD_REF'. If this is a contribution, retarget the PR against 'litellm_oss_staging' instead." exit 1 diff --git a/.github/workflows/image-scan.yml b/.github/workflows/image-scan.yml new file mode 100644 index 00000000000..90ede5a653f --- /dev/null +++ b/.github/workflows/image-scan.yml @@ -0,0 +1,65 @@ +name: Image Scan + +on: + pull_request: + branches: + - main + - litellm_internal_staging + - litellm_oss_branch + - "litellm_**" + paths: + - docker/Dockerfile.non_root + - uv.lock + - ui/litellm-dashboard/package-lock.json + - .github/workflows/image-scan.yml + schedule: + - cron: "41 6 * * *" + workflow_dispatch: + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + image-scan: + name: image-scan + runs-on: ubuntu-latest + if: >- + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository + timeout-minutes: 30 + permissions: + contents: read + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + + - name: Download Grype v0.114.0 + run: | + curl -fsSL --retry 3 -o "$RUNNER_TEMP/grype.tar.gz" \ + https://github.com/anchore/grype/releases/download/v0.114.0/grype_0.114.0_linux_amd64.tar.gz + echo "edda0968d8827daab01d32b3cd7de192ae0915005e7bbfcfef9e68e79bc43343 $RUNNER_TEMP/grype.tar.gz" | sha256sum -c - + tar xzf "$RUNNER_TEMP/grype.tar.gz" -C "$RUNNER_TEMP" grype + chmod +x "$RUNNER_TEMP/grype" + + # Dockerfile.non_root is the rootless variant we ship. The other + # Dockerfiles share the same wolfi base and apk set, so OS-layer coverage + # is the same; matrix-scan if those variants ever diverge. + - name: Build runtime image + run: docker build -f docker/Dockerfile.non_root -t litellm-image-scan:${{ github.sha }} . + + # Scans the whole shipped artifact: OS/apk plus every language package + # baked into the image, including ones no lockfile declares (e.g. prisma's + # vendored node engine) that osv-scan cannot see. osv-scan stays the fast + # source-level gate; this is the customer's-eye-view backstop. Credential- + # free OSS, run as a pinned, checksum-verified binary; no GitHub Action + # dependency and no vendor SaaS callout. + - name: Scan image for fixable HIGH/CRITICAL CVEs + run: | + "$RUNNER_TEMP/grype" litellm-image-scan:${{ github.sha }} \ + --only-fixed \ + --fail-on high \ + --output table diff --git a/.github/workflows/mutation-test.yml b/.github/workflows/mutation-test.yml index 8094ca57467..183f12f969c 100644 --- a/.github/workflows/mutation-test.yml +++ b/.github/workflows/mutation-test.yml @@ -55,7 +55,7 @@ jobs: - name: Install dependencies run: | - uv sync --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router + .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router - name: Generate Prisma client env: diff --git a/.github/workflows/osv-scan.yml b/.github/workflows/osv-scan.yml new file mode 100644 index 00000000000..31104002dab --- /dev/null +++ b/.github/workflows/osv-scan.yml @@ -0,0 +1,44 @@ +name: OSV Scan + +on: + pull_request: + branches: + - main + - litellm_internal_staging + - litellm_oss_staging + - "litellm_**" + schedule: + - cron: "23 6 * * *" + workflow_dispatch: + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + osv-scan: + name: osv-scan + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + + - name: Download osv-scanner v2.3.8 + run: | + curl -fsSL --retry 3 -o "$RUNNER_TEMP/osv-scanner" \ + https://github.com/google/osv-scanner/releases/download/v2.3.8/osv-scanner_linux_amd64 + echo "bc98e15319ed0d515e3f9235287ba53cdc5535d576d24fd573978ecfe9ab92dc $RUNNER_TEMP/osv-scanner" | sha256sum -c - + chmod +x "$RUNNER_TEMP/osv-scanner" + + - name: Scan lockfiles + run: | + "$RUNNER_TEMP/osv-scanner" scan source \ + --config osv-scanner.toml \ + -L uv.lock \ + -L ui/litellm-dashboard/package-lock.json diff --git a/.github/workflows/test-code-quality.yml b/.github/workflows/test-code-quality.yml index 4f09857eb1b..872a1799d98 100644 --- a/.github/workflows/test-code-quality.yml +++ b/.github/workflows/test-code-quality.yml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" permissions: diff --git a/.github/workflows/test-linting.yml b/.github/workflows/test-linting.yml index b5e45a38cf9..6deb28c95c7 100644 --- a/.github/workflows/test-linting.yml +++ b/.github/workflows/test-linting.yml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" permissions: @@ -14,11 +14,15 @@ permissions: jobs: lint: runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 15 steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + # Check out the PR head, not the default refs/pull/N/merge: the merge ref + # folds in newer base commits, which the diff-based gates (ruff delta, + # Any-discipline) would otherwise blame on this branch. with: + ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 clean: true persist-credentials: false @@ -44,13 +48,27 @@ jobs: - name: Install dependencies run: | - uv sync --frozen + uv sync --frozen --group proxy-dev - - name: Check Black formatting + # basedpyright resolves Prisma's generated client (litellm/proxy/schema.prisma) + # only after `prisma generate` writes prisma/client.py et al. Without this the + # DB wrappers typed against the generated client would degrade to Unknown. + - name: Generate Prisma client + env: + PRISMA_BINARY_CACHE_DIR: ${{ runner.temp }}/prisma-cache run: | - cd litellm - uv run --no-sync black --check --exclude '/enterprise/' . - cd .. + uv run --no-sync prisma generate --schema litellm/proxy/schema.prisma + + - name: Check ruff format + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + git diff --name-only "$BASE_SHA"...HEAD -- 'litellm/**/*.py' | grep -v '^litellm/enterprise/' > "$RUNNER_TEMP/ruff_format_files.txt" || true + if [ ! -s "$RUNNER_TEMP/ruff_format_files.txt" ]; then + echo "No changed litellm Python files to check with ruff format." + exit 0 + fi + xargs uv run --no-sync ruff format --check --exclude '/enterprise/' < "$RUNNER_TEMP/ruff_format_files.txt" - name: Debug - Check file state run: | @@ -67,15 +85,27 @@ jobs: uv run --no-sync ruff check . cd .. + - name: Check strict-rule budget (delta vs base) + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + uv run --no-sync python scripts/ruff_strict_gate.py --base "$BASE_SHA" + + - name: Check type-discipline budget (mutable collections / casts / type guards / kwargs / unexplained suppressions, delta vs base) + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + uv run --no-sync python scripts/type_discipline_gate.py --base "$BASE_SHA" + - name: Print OpenAI version run: | uv run --no-sync python -c "import openai; print(f'OpenAI version: {openai.__version__}')" - - name: Run MyPy type checking + - name: Check basedpyright budget (delta vs base) + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | - cd litellm - uv run --no-sync mypy . - cd .. + (uv run --no-sync basedpyright --outputjson || true) | uv run --no-sync python scripts/type_check_gate.py --base "$BASE_SHA" - name: Check for circular imports run: | @@ -87,6 +117,33 @@ jobs: run: | uv run --no-sync python -c "from litellm import *" || (echo '🚨 import failed, this means you introduced unprotected imports! 🚨'; exit 1) + # Intentionally NON-GATING. This job turns red when a *-budget.json ceiling is + # raised (or a rule/budget is dropped) so a loosening is obvious in review, but it + # must be kept OUT of the branch-protection required-checks list so a justified + # bump can still be merged by a human who has seen and accepted the red. + budget-ratchet: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Ratchet check (budgets may only decrease; non-gating) + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + python scripts/budget_ratchet_check.py --base "$BASE_SHA" + secret-scan: runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/.github/workflows/test-litellm-ui-build.yml b/.github/workflows/test-litellm-ui-build.yml index 68497b10dbb..ce8d8cb9c95 100644 --- a/.github/workflows/test-litellm-ui-build.yml +++ b/.github/workflows/test-litellm-ui-build.yml @@ -7,7 +7,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" jobs: @@ -25,7 +25,7 @@ jobs: persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: "20" cache: "npm" @@ -77,7 +77,7 @@ jobs: - name: Setup Node.js if: steps.changed.outputs.has_files == 'true' - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: "20" cache: "npm" @@ -111,4 +111,4 @@ jobs: if: ${{ !cancelled() && steps.changed.outputs.has_files == 'true' }} run: | npx eslint . -f json -o "$RUNNER_TEMP/lint-report.json" || true - node scripts/check-lint-budgets.mjs "$RUNNER_TEMP/lint-report.json" eslint-budgets.json + node scripts/check-lint-budgets.mjs "$RUNNER_TEMP/lint-report.json" eslint-budgets.json --check eslint-metrics.json diff --git a/.github/workflows/test-mcp.yml b/.github/workflows/test-mcp.yml index 2ae60951afc..5b5290880c1 100644 --- a/.github/workflows/test-mcp.yml +++ b/.github/workflows/test-mcp.yml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" permissions: @@ -39,7 +39,7 @@ jobs: - name: Install dependencies run: | uv lock --check - uv sync --frozen --group proxy-dev --extra proxy --extra semantic-router + .github/scripts/uv_sync_with_retries.sh --frozen --group proxy-dev --extra proxy --extra semantic-router - name: Run MCP tests run: | diff --git a/.github/workflows/test-model-map.yaml b/.github/workflows/test-model-map.yaml index 49821fca3a8..b2170d9f6a4 100644 --- a/.github/workflows/test-model-map.yaml +++ b/.github/workflows/test-model-map.yaml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" permissions: diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml new file mode 100644 index 00000000000..13e1dc4ad5e --- /dev/null +++ b/.github/workflows/test-rust.yml @@ -0,0 +1,65 @@ +name: LiteLLM Rust + +on: + push: + paths: + - "litellm-rust/**" + - ".github/workflows/test-rust.yml" + pull_request: + branches: + - main + - litellm_internal_staging + - litellm_oss_staging + - "litellm_**" + paths: + - "litellm-rust/**" + - ".github/workflows/test-rust.yml" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + rust-checks: + name: rustfmt, clippy, test + 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 + with: + persist-credentials: false + + - name: Set up Rust + run: | + rustup toolchain install stable --profile minimal --component clippy,rustfmt + rustup default stable + + - name: Cache Cargo registry and target + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + litellm-rust/target + key: ${{ runner.os }}-cargo-${{ hashFiles('litellm-rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Check Rust formatting + run: cargo fmt --check + + - name: Run Clippy + run: cargo clippy --workspace --all-targets --locked -- -D warnings + + - name: Run Rust tests + run: cargo test --workspace --locked diff --git a/.github/workflows/test-semgrep.yml b/.github/workflows/test-semgrep.yml index 2ba23e44da8..f0dcb9887be 100644 --- a/.github/workflows/test-semgrep.yml +++ b/.github/workflows/test-semgrep.yml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" permissions: diff --git a/.github/workflows/test-unit-core-utils.yml b/.github/workflows/test-unit-core-utils.yml index da1267756cd..d6d6353238f 100644 --- a/.github/workflows/test-unit-core-utils.yml +++ b/.github/workflows/test-unit-core-utils.yml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" permissions: diff --git a/.github/workflows/test-unit-documentation.yml b/.github/workflows/test-unit-documentation.yml index b2a8640223a..4cef791a9b3 100644 --- a/.github/workflows/test-unit-documentation.yml +++ b/.github/workflows/test-unit-documentation.yml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" permissions: @@ -54,7 +54,7 @@ jobs: - name: Install dependencies run: | - uv sync --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router + .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router - name: Generate Prisma client env: diff --git a/.github/workflows/test-unit-enterprise-routing.yml b/.github/workflows/test-unit-enterprise-routing.yml index ffc09dd8f94..13136c968d1 100644 --- a/.github/workflows/test-unit-enterprise-routing.yml +++ b/.github/workflows/test-unit-enterprise-routing.yml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" permissions: diff --git a/.github/workflows/test-unit-integrations.yml b/.github/workflows/test-unit-integrations.yml index b316ad5dfdf..c95ed4e7c24 100644 --- a/.github/workflows/test-unit-integrations.yml +++ b/.github/workflows/test-unit-integrations.yml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" permissions: diff --git a/.github/workflows/test-unit-llm-providers.yml b/.github/workflows/test-unit-llm-providers.yml index 2a1912ce92d..df78564ab0c 100644 --- a/.github/workflows/test-unit-llm-providers.yml +++ b/.github/workflows/test-unit-llm-providers.yml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" permissions: diff --git a/.github/workflows/test-unit-misc.yml b/.github/workflows/test-unit-misc.yml index 9add77ff424..7c3b195f0ad 100644 --- a/.github/workflows/test-unit-misc.yml +++ b/.github/workflows/test-unit-misc.yml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" permissions: @@ -22,16 +22,22 @@ jobs: uses: ./.github/workflows/_test-unit-base.yml with: test-path: >- + tests/test_litellm/batches tests/test_litellm/secret_managers tests/test_litellm/a2a_protocol tests/test_litellm/anthropic_interface tests/test_litellm/completion_extras tests/test_litellm/containers tests/test_litellm/experimental_mcp_client + tests/test_litellm/models + tests/test_litellm/repositories tests/test_litellm/images tests/test_litellm/interactions + tests/test_litellm/ocr tests/test_litellm/passthrough + tests/test_litellm/sandbox tests/test_litellm/vector_stores + tests/test_litellm/videos tests/test_litellm/test_*.py workers: 2 reruns: 2 diff --git a/.github/workflows/test-unit-proxy-auth.yml b/.github/workflows/test-unit-proxy-auth.yml index 99882066a8e..97dfaed6e81 100644 --- a/.github/workflows/test-unit-proxy-auth.yml +++ b/.github/workflows/test-unit-proxy-auth.yml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" permissions: diff --git a/.github/workflows/test-unit-proxy-endpoints.yml b/.github/workflows/test-unit-proxy-endpoints.yml index 0a9513ec024..cbb36eebdb9 100644 --- a/.github/workflows/test-unit-proxy-endpoints.yml +++ b/.github/workflows/test-unit-proxy-endpoints.yml @@ -5,14 +5,12 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" workflow_dispatch: permissions: contents: read - id-token: write - pull-requests: write concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -20,6 +18,10 @@ concurrency: jobs: proxy-endpoints: + permissions: + contents: read + id-token: write + pull-requests: write uses: ./.github/workflows/_test-unit-base.yml with: test-path: >- @@ -29,6 +31,8 @@ jobs: tests/test_litellm/proxy/anthropic_endpoints tests/test_litellm/proxy/google_endpoints tests/test_litellm/proxy/openai_files_endpoint + tests/test_litellm/proxy/batches_endpoints + tests/test_litellm/proxy/video_endpoints tests/test_litellm/proxy/response_api_endpoints tests/test_litellm/proxy/image_endpoints tests/test_litellm/proxy/vector_store_endpoints @@ -52,6 +56,10 @@ jobs: # is independent and its coverage artifact is uploaded separately. # See: https://www.notion.so/36c43b8acdab81ee845fd5365128a2fc proxy-server: + permissions: + contents: read + id-token: write + pull-requests: write uses: ./.github/workflows/_test-unit-base.yml with: test-path: tests/test_litellm/proxy/proxy_server diff --git a/.github/workflows/test-unit-proxy-infra.yml b/.github/workflows/test-unit-proxy-infra.yml index 336e53ee3d7..884d62289b9 100644 --- a/.github/workflows/test-unit-proxy-infra.yml +++ b/.github/workflows/test-unit-proxy-infra.yml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" permissions: @@ -29,6 +29,7 @@ jobs: tests/test_litellm/proxy/_experimental tests/test_litellm/proxy/experimental tests/test_litellm/proxy/common_utils + tests/test_litellm/proxy/logging_endpoints tests/test_litellm/proxy/test_*.py workers: 2 reruns: 2 diff --git a/.github/workflows/test-unit-proxy-legacy.yml b/.github/workflows/test-unit-proxy-legacy.yml index 5768551f9b0..8db218cd1fc 100644 --- a/.github/workflows/test-unit-proxy-legacy.yml +++ b/.github/workflows/test-unit-proxy-legacy.yml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" permissions: @@ -71,7 +71,7 @@ jobs: - name: Install dependencies run: | - uv sync --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router + .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router - name: Generate Prisma client env: diff --git a/.github/workflows/test-unit-responses-caching-types.yml b/.github/workflows/test-unit-responses-caching-types.yml index 13069be9e3a..2f177587997 100644 --- a/.github/workflows/test-unit-responses-caching-types.yml +++ b/.github/workflows/test-unit-responses-caching-types.yml @@ -5,7 +5,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" permissions: diff --git a/.github/workflows/test_server_root_path.yml b/.github/workflows/test_server_root_path.yml index 57ff746c9c8..ac363071d55 100644 --- a/.github/workflows/test_server_root_path.yml +++ b/.github/workflows/test_server_root_path.yml @@ -7,7 +7,7 @@ on: branches: - main - litellm_internal_staging - - litellm_oss_branch + - litellm_oss_staging - "litellm_**" jobs: @@ -32,17 +32,16 @@ jobs: df -h / - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Build Docker image - uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 #v6.14 + uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0 with: context: . file: ./docker/Dockerfile.non_root tags: litellm-test:${{ github.sha }} load: true - cache-from: type=gha - cache-to: type=gha,mode=max + push: false - name: Start LiteLLM container with SERVER_ROOT_PATH run: | diff --git a/.github/workflows/triage_issue_with_llm.yml b/.github/workflows/triage_issue_with_llm.yml new file mode 100644 index 00000000000..765453cf2c6 --- /dev/null +++ b/.github/workflows/triage_issue_with_llm.yml @@ -0,0 +1,96 @@ +name: Agent Shin — Issue triage + +# LLM-as-judge triage for external GitHub issues. +# +# DRY-RUN BY DEFAULT. See .github/workflows/triage_pr_with_llm.yml for the +# enablement procedure — same repo variable (`AGENT_SHIN_ENABLED=true`) +# unlocks the PR and issue triage flows together. + +on: + issues: + types: [opened, reopened] + workflow_dispatch: + inputs: + issue_number: + description: "Issue number to triage manually." + required: true + close: + description: "If true and AGENT_SHIN_ENABLED=true, actually close on fail." + required: false + default: "false" + type: choice + options: + - "true" + - "false" + +permissions: + contents: read + issues: write + +jobs: + triage: + if: github.repository == 'BerriAI/litellm' + runs-on: ubuntu-latest + steps: + - name: Checkout triage script + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + sparse-checkout: .github/scripts + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Install LLM client + run: pip install --no-cache-dir --require-hashes -r .github/scripts/triage-requirements.txt + + - name: Run Agent Shin + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Only expose the LLM key when the bot is enabled or a collaborator + # triggers it manually, so an external user can't force paid LLM + # calls by churning issues while the bot is still in dry-run. + # The Python script calls the LLM whenever this var is set + # (regardless of `--close`); stripping `--close` doesn't suppress + # the API call, only the destructive side effects. + OPENAI_API_KEY: ${{ (vars.AGENT_SHIN_ENABLED == 'true' || github.event_name == 'workflow_dispatch') && secrets.OPENAI_API_KEY || '' }} + OPENAI_BASE_URL: ${{ vars.OPENAI_BASE_URL }} + TRIAGE_MODEL: ${{ vars.TRIAGE_MODEL }} + AGENT_SHIN_ENABLED: ${{ vars.AGENT_SHIN_ENABLED }} + DISPATCH_CLOSE: ${{ github.event.inputs.close }} + ISSUE_NUMBER: ${{ github.event.issue.number || github.event.inputs.issue_number }} + run: | + set -euo pipefail + ARGS=(--repo "${{ github.repository }}" --issue "${ISSUE_NUMBER}") + # Fail-safe gating: only the EXACT string "true" enables the + # destructive --close path. The workflow_dispatch input is a + # `choice` dropdown of "true"/"false" so the UI is constrained, + # but the API (`gh workflow run -f close=...`) accepts any + # string, and a `!= "false"` check would treat "True", "yes", + # "1", "TRUE", typos, and accidental whitespace as enabling + # closure. Mirror the Greptile closer's `= "true"` pattern. + if [ "${AGENT_SHIN_ENABLED:-false}" = "true" ] && [ "${DISPATCH_CLOSE:-false}" = "true" ]; then + ARGS+=(--close) + echo "::notice::Agent Shin is ENABLED and running in close-on-fail mode." + elif [ "${AGENT_SHIN_ENABLED:-false}" = "true" ]; then + echo "::notice::Agent Shin is ENABLED but this trigger is dry-run (workflow_dispatch close != 'true')." + else + echo "::notice::Agent Shin is in DRY-RUN mode (AGENT_SHIN_ENABLED is not 'true'). No comments will be posted; no issues will be closed." + fi + # Automatic `issues` events stay dry-run regardless until the team + # explicitly invokes workflow_dispatch with close=true. + if [ "${GITHUB_EVENT_NAME:-}" = "issues" ]; then + # filter out --close rather than substituting to "" (which would + # leave an empty positional arg that argparse rejects) + FILTERED=() + for arg in "${ARGS[@]}"; do + if [ "${arg}" != "--close" ]; then + FILTERED+=("${arg}") + fi + done + ARGS=("${FILTERED[@]}") + echo "::notice::issues trigger -> forcing dry-run." + fi + python3 .github/scripts/triage_with_llm.py "${ARGS[@]}" diff --git a/.github/workflows/triage_reconsider.yml b/.github/workflows/triage_reconsider.yml new file mode 100644 index 00000000000..f35f681d09a --- /dev/null +++ b/.github/workflows/triage_reconsider.yml @@ -0,0 +1,172 @@ +name: Agent Shin — reconsider + +# Comment-trigger workflow: when the PR/issue author (or an internal +# collaborator) comments `@agent-shin reconsider` on a CLOSED PR/issue, +# Agent Shin re-runs LLM-judge triage on the current title+body and: +# +# - on PASS: posts a "re-evaluated and reopened" comment + reopens. +# - on FAIL: posts a "still missing X" comment and leaves it closed, +# so the contributor can iterate again. +# +# This exists because GitHub does NOT let an external (non-write-access) +# OSS contributor reopen a PR/issue closed by a bot or maintainer. Without +# this comment trigger, a contributor whose PR Agent Shin auto-closed +# would have no path back into the review queue except opening a fresh PR +# (which loses the original PR's history). The bot, on the other hand, +# has write access via GH_TOKEN and can reopen on their behalf. +# +# DRY-RUN BY DEFAULT — gated on `vars.AGENT_SHIN_ENABLED == 'true'` just +# like the other Agent Shin workflows. The workflow also gates on the +# commenter being either the PR/issue author or an internal collaborator +# (OWNER/MEMBER/COLLABORATOR) so random commenters cannot DOS the LLM +# judge or force a reopen. + +on: + issue_comment: + types: [created] + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + reconsider: + if: | + github.repository == 'BerriAI/litellm' + && contains(github.event.comment.body, '@agent-shin reconsider') + runs-on: ubuntu-latest + steps: + - name: Authorize commenter + # Only the PR/issue author OR an internal collaborator may trigger + # a reconsider. Outside random commenters could otherwise spam the + # phrase to burn LLM budget or, if a fail-open bug were ever + # introduced, force a reopen on someone else's behalf. + # + # We expose the authorization decision as a step output and gate + # every subsequent (potentially destructive) step on it. A `run:` + # step with `exit 0` would NOT stop the job — only `if:` gating + # on a known-true output is safe here. + id: auth + env: + COMMENTER: ${{ github.event.comment.user.login }} + AUTHOR: ${{ github.event.issue.user.login }} + ASSOCIATION: ${{ github.event.comment.author_association }} + run: | + set -euo pipefail + if [ "${COMMENTER}" = "${AUTHOR}" ]; then + echo "::notice::Authorized: commenter is the PR/issue author." + echo "authorized=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + case "${ASSOCIATION}" in + OWNER|MEMBER|COLLABORATOR) + echo "::notice::Authorized: commenter is an internal collaborator (${ASSOCIATION})." + echo "authorized=true" >> "$GITHUB_OUTPUT" + ;; + *) + echo "::notice::Commenter '${COMMENTER}' (${ASSOCIATION}) is not authorized to trigger reconsider; skipping subsequent steps." + echo "authorized=false" >> "$GITHUB_OUTPUT" + ;; + esac + + - name: React 👀 to acknowledge the reconsider + # Add an eyes reaction to the triggering comment the moment we accept + # it, so the contributor gets instant feedback that the bot saw their + # `@agent-shin reconsider` before the slower triage steps run. Gated on + # AGENT_SHIN_ENABLED so dry-run leaves no visible trace. Best-effort: + # a reactions API hiccup must never fail the actual reconsider. + if: steps.auth.outputs.authorized == 'true' && vars.AGENT_SHIN_ENABLED == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMENT_ID: ${{ github.event.comment.id }} + run: | + set -euo pipefail + gh api --method POST \ + -H "Accept: application/vnd.github+json" \ + "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}/reactions" \ + -f content=eyes \ + || echo "::warning::failed to add 👀 reaction (non-fatal)" + + - name: Checkout triage script + if: steps.auth.outputs.authorized == 'true' + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + sparse-checkout: .github/scripts + persist-credentials: false + + - name: Set up Python + if: steps.auth.outputs.authorized == 'true' + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Install LLM client + if: steps.auth.outputs.authorized == 'true' + run: pip install --no-cache-dir --require-hashes -r .github/scripts/triage-requirements.txt + + - name: Run Agent Shin reconsider + if: steps.auth.outputs.authorized == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Only expose the LLM key when the bot is enabled, so a PR/issue + # author can't force paid LLM calls by spamming `@agent-shin + # reconsider` while the bot is still in dry-run. The Python script + # calls the LLM whenever this var is set (regardless of `--close`); + # stripping `--close` doesn't suppress the API call, only the + # destructive side effects. Mirror the gating used by every other + # Agent Shin workflow (triage_pr_with_llm.yml, review_gate.yml, ...). + OPENAI_API_KEY: ${{ vars.AGENT_SHIN_ENABLED == 'true' && secrets.OPENAI_API_KEY || '' }} + OPENAI_BASE_URL: ${{ vars.OPENAI_BASE_URL }} + TRIAGE_MODEL: ${{ vars.TRIAGE_MODEL }} + AGENT_SHIN_ENABLED: ${{ vars.AGENT_SHIN_ENABLED }} + # `issue_comment` events fire for both issues and PR comments. + # `issue.pull_request` is set iff this is a PR comment, so we use + # its presence to decide whether to invoke `--pr N` or `--issue N`. + IS_PR: ${{ github.event.issue.pull_request != null }} + NUMBER: ${{ github.event.issue.number }} + run: | + set -euo pipefail + if [ "${IS_PR}" = "true" ]; then + ARGS=(--repo "${{ github.repository }}" --pr "${NUMBER}" --reconsider) + else + ARGS=(--repo "${{ github.repository }}" --issue "${NUMBER}" --reconsider) + fi + # Reconsider's destructive actions (post comment + reopen) are + # gated on `--close`, mirroring the regular triage workflows. + # When AGENT_SHIN_ENABLED is not the EXACT string "true", we + # still run the script so its verdict + would-X action lands in + # the step summary for QA — but without `--close`, the script + # returns `would-reopen` / `would-reconsider-still-failing` + # instead of touching GitHub state. + # + # Use the positive `= "true"` gate (not `!= "true" -> exit`) so + # the workflow guardrails in + # tests/test_litellm/test_github_triage_workflows.py see the + # canonical fail-safe enable pattern. Unknown values like + # "True", "yes", "1", or typos fall through to the dry-run + # branch, which is the safe default. + if [ "${AGENT_SHIN_ENABLED:-false}" = "true" ]; then + ARGS+=(--close) + echo "::notice::Agent Shin reconsider ENABLED — running real triage (close=true)." + else + echo "::notice::AGENT_SHIN_ENABLED is not 'true' -> reconsider stays in dry-run (no comment, no reopen)." + fi + python3 .github/scripts/triage_with_llm.py "${ARGS[@]}" + + - name: React 👍 when the reconsider finishes + # Once the reconsider run has completed successfully, add a thumbs-up so + # the contributor sees the bot is done (the 👀 stays, signalling + # seen -> handled). `success()` keeps this from firing if the run + # errored, and the AGENT_SHIN_ENABLED gate keeps dry-run inert. + if: success() && steps.auth.outputs.authorized == 'true' && vars.AGENT_SHIN_ENABLED == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMENT_ID: ${{ github.event.comment.id }} + run: | + set -euo pipefail + gh api --method POST \ + -H "Accept: application/vnd.github+json" \ + "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}/reactions" \ + -f content=+1 \ + || echo "::warning::failed to add 👍 reaction (non-fatal)" diff --git a/.github/workflows/triage_rollout_heads_up.yml b/.github/workflows/triage_rollout_heads_up.yml new file mode 100644 index 00000000000..903960151e2 --- /dev/null +++ b/.github/workflows/triage_rollout_heads_up.yml @@ -0,0 +1,92 @@ +name: Agent Shin — rollout heads-up (one-shot) + +# Fires the 7-day heads-up comment on every open external PR/issue that the +# new triage bot would auto-close. The real sweep is a deliberate one-shot: +# trigger it at rollout via a manual `workflow_dispatch` with `dry_run=false`. +# The script is idempotent (skips items that already carry the +# `` marker), so a re-run is harmless. +# +# The automatic push trigger runs DRY-RUN only, so merging the script to +# `litellm_internal_staging` never posts a comment; it just confirms the +# workflow is wired up. Posting real comments requires the manual dispatch, +# which is also the only trigger that exposes `OPENAI_API_KEY`. The heads-up +# is intentionally NOT gated on `AGENT_SHIN_ENABLED`: it has to warn +# contributors while that flag is still off, ahead of the flip that turns on +# auto-closing. +# +# The workflow is a thin shell over `.github/scripts/triage_rollout_heads_up.py`. +# Dry-run vs. real run differ in EXACTLY one CLI flag (`--close`), added only +# on a manual dispatch with `dry_run=false`. + +on: + push: + branches: + - litellm_internal_staging + paths: + # The presence of this script on staging IS the rollout merge marker. + # Editing the file later would re-fire the workflow; that's safe because + # the script skips PRs/issues that already have the heads-up marker. + - ".github/scripts/triage_rollout_heads_up.py" + workflow_dispatch: + inputs: + dry_run: + description: "Dry run (true = preview only, false = actually post comments)." + required: false + default: "true" + type: choice + options: + - "true" + - "false" + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + heads-up: + if: github.repository == 'BerriAI/litellm' + runs-on: ubuntu-latest + steps: + - name: Checkout triage scripts + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + sparse-checkout: .github/scripts + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Install LLM client + run: pip install --no-cache-dir --require-hashes -r .github/scripts/triage-requirements.txt + + - name: Run heads-up sweep + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Only the manual dispatch (the real-run trigger) needs the LLM key. + # The automatic push trigger runs dry-run and never posts, so it gets + # no key. Mirrors the sibling triage workflows, which expose the key + # only on an enabled/dispatched run rather than unconditionally. + OPENAI_API_KEY: ${{ github.event_name == 'workflow_dispatch' && secrets.OPENAI_API_KEY || '' }} + OPENAI_BASE_URL: ${{ vars.OPENAI_BASE_URL }} + TRIAGE_MODEL: ${{ vars.TRIAGE_MODEL }} + # The real run is a deliberate manual dispatch with dry_run=false. + # Use the EXACT "false" comparison so any unexpected input value + # fail-closes to dry-run (mirrors the AGENT_SHIN_ENABLED pattern in + # the sibling workflows). The automatic push trigger always stays + # dry-run, so merging the script never posts. + DRY_RUN_INPUT: ${{ github.event.inputs.dry_run }} + run: | + set -euo pipefail + ARGS=(--repo "${{ github.repository }}") + if [ "${GITHUB_EVENT_NAME:-}" = "workflow_dispatch" ] && [ "${DRY_RUN_INPUT:-true}" = "false" ]; then + ARGS+=(--close) + echo "::notice::Manual rollout dispatch with dry_run=false -> heads-up comments WILL be posted." + elif [ "${GITHUB_EVENT_NAME:-}" = "workflow_dispatch" ]; then + echo "::notice::Manual dispatch in dry-run mode -> previewing only, no comments will be posted." + else + echo "::notice::Automatic push trigger -> dry-run preview only. Fire the real rollout sweep with a manual workflow_dispatch (dry_run=false)." + fi + python3 .github/scripts/triage_rollout_heads_up.py "${ARGS[@]}" diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 9a1e899fed5..db79fe43038 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -2,9 +2,9 @@ name: GitHub Actions Security Analysis on: push: - branches: [main] + branches: [main, litellm_internal_staging] pull_request: - branches: [main] + branches: [main, litellm_internal_staging] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -18,9 +18,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 permissions: - security-events: write contents: read - actions: read steps: - name: Checkout repository uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 @@ -28,4 +26,9 @@ jobs: persist-credentials: false - name: Run zizmor - uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + with: + version: "1.24.1" + min-severity: medium + advanced-security: false + annotations: true