From b7d38b5475f81849c990f6a31592606e6d2ef139 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 20:44:16 +0000 Subject: [PATCH] fix: preserve relocated script readers and SDK env scan scope --- litellm/scripts/classify-issue.test.ts | 4 +-- litellm/scripts/classify-issue.ts | 4 +-- litellm/scripts/issue-labels.ts | 2 +- tests/documentation_tests/test_env_keys.py | 15 ++++++++--- tests/integration/_support/safeguards_rig.py | 2 +- tests/unit/test_env_key_doc_gate.py | 28 ++++++++++++++++++++ 6 files changed, 46 insertions(+), 9 deletions(-) diff --git a/litellm/scripts/classify-issue.test.ts b/litellm/scripts/classify-issue.test.ts index 96236dde4da..5d3113f4f17 100644 --- a/litellm/scripts/classify-issue.test.ts +++ b/litellm/scripts/classify-issue.test.ts @@ -24,7 +24,7 @@ import { type Schema, } from "./classify-issue"; import { MANIFEST, NAMESPACES } from "./issue-labels"; -import schemaJson from "../.github/prompts/issue-classifier.schema.json"; +import schemaJson from "../../.github/prompts/issue-classifier.schema.json"; const schema = schemaJson as Schema; const routes = routesOf(schema); @@ -133,7 +133,7 @@ describe("sections", () => { test("the known headings are exactly the field labels of the two issue forms", async () => { const labels = await Promise.all( ["bug_report.yml", "feature_request.yml"].map(async (file) => { - const form = Bun.YAML.parse(await Bun.file(`${import.meta.dir}/../.github/ISSUE_TEMPLATE/${file}`).text()) as { + const form = Bun.YAML.parse(await Bun.file(`${import.meta.dir}/../../.github/ISSUE_TEMPLATE/${file}`).text()) as { readonly body: readonly { readonly attributes?: { readonly label?: string } }[]; }; return form.body.flatMap((field) => (field.attributes?.label === undefined ? [] : [field.attributes.label.trim()])); diff --git a/litellm/scripts/classify-issue.ts b/litellm/scripts/classify-issue.ts index 7b72b29e711..3dd3e20cec7 100644 --- a/litellm/scripts/classify-issue.ts +++ b/litellm/scripts/classify-issue.ts @@ -376,8 +376,8 @@ export function readConfig( if (import.meta.main) { const { token, apiBase, apiKey, ...config } = readConfig(process.env, new Date()); - const prompt = await Bun.file(`${import.meta.dir}/../.github/prompts/issue-classifier.md`).text(); - const schema = (await Bun.file(`${import.meta.dir}/../.github/prompts/issue-classifier.schema.json`).json()) as Schema; + const prompt = await Bun.file(`${import.meta.dir}/../../.github/prompts/issue-classifier.md`).text(); + const schema = (await Bun.file(`${import.meta.dir}/../../.github/prompts/issue-classifier.schema.json`).json()) as Schema; const verdict = await classifyIssue(githubApi(token), litellmClient(apiBase, apiKey), config, prompt, schema); if (verdict === null) { console.error(`#${config.issueNumber}: edit ignored, the issue is already classified or older than the edit window`); diff --git a/litellm/scripts/issue-labels.ts b/litellm/scripts/issue-labels.ts index a39f4efa160..fe2fa770fb7 100644 --- a/litellm/scripts/issue-labels.ts +++ b/litellm/scripts/issue-labels.ts @@ -1,4 +1,4 @@ -import manifest from "../.github/issue-labels.json"; +import manifest from "../../.github/issue-labels.json"; export const NAMESPACES = ["domain", "provider", "kind", "priority", "lift", "needs"] as const; export type Namespace = (typeof NAMESPACES)[number]; diff --git a/tests/documentation_tests/test_env_keys.py b/tests/documentation_tests/test_env_keys.py index 2a7da58f39c..f78119d20c7 100644 --- a/tests/documentation_tests/test_env_keys.py +++ b/tests/documentation_tests/test_env_keys.py @@ -1,6 +1,7 @@ import os import re from collections.abc import Iterator +from typing import Final # Define the base directory for the litellm repository and documentation path repo_base = "./litellm" # Change this to your actual path @@ -76,6 +77,10 @@ SKIP_DIRS = { "build", } +NON_SDK_SOURCE_DIRS: Final = frozenset( + {"cli", "deploy", "enterprise", "packaging", "proxy-extras", "runtime", "rust", "scripts", "ui", "vscode-extension"} +) + def extract_env_keys(source: str) -> frozenset[str]: """Return every documentable env var name read by the given Python source.""" @@ -87,14 +92,18 @@ def extract_env_keys(source: str) -> frozenset[str]: def collect_env_keys(base_dir: str) -> frozenset[str]: """Return every documentable env var name read anywhere under ``base_dir``.""" return frozenset( - key for file_path in _files_with_suffix(base_dir, (".py",)) for key in extract_env_keys(_read_text(file_path)) + key + for file_path in _files_with_suffix(base_dir, (".py",), NON_SDK_SOURCE_DIRS) + for key in extract_env_keys(_read_text(file_path)) ) -def _files_with_suffix(base_dir: str, suffixes: tuple[str, ...]) -> Iterator[str]: +def _files_with_suffix( + base_dir: str, suffixes: tuple[str, ...], skip_root_dirs: frozenset[str] = frozenset() +) -> Iterator[str]: for root, dirs, files in os.walk(base_dir): # Skip dependency/venv directories - prevents picking up env vars from installed packages - dirs[:] = [d for d in dirs if d not in SKIP_DIRS] + dirs[:] = [d for d in dirs if d not in SKIP_DIRS and (root != base_dir or d not in skip_root_dirs)] yield from (os.path.join(root, name) for name in files if name.endswith(suffixes)) diff --git a/tests/integration/_support/safeguards_rig.py b/tests/integration/_support/safeguards_rig.py index c8748988be8..f0acf956e5e 100644 --- a/tests/integration/_support/safeguards_rig.py +++ b/tests/integration/_support/safeguards_rig.py @@ -160,7 +160,7 @@ def dispatch(scripts: Scripts) -> Responder: return Reply(body=_MODEL_DISCOVERY_BODY) if request.target.startswith(TAVILY_TARGET): return tavily_reply(request) - script: Final = litellm.scripts.lookup(request.body) + script: Final = scripts.lookup(request.body) if script is None: return Reply( status=500, body=json.dumps({"error": {"message": f"no script for {request.target}"}}).encode() diff --git a/tests/unit/test_env_key_doc_gate.py b/tests/unit/test_env_key_doc_gate.py index efc615a6ea7..74b1f9f2867 100644 --- a/tests/unit/test_env_key_doc_gate.py +++ b/tests/unit/test_env_key_doc_gate.py @@ -12,6 +12,9 @@ than in the central reference table still counts as documented. import importlib.util import sys from pathlib import Path +from typing import Final + +import pytest _REPO_ROOT = Path(__file__).resolve().parents[2] _MODULE_PATH = _REPO_ROOT / "tests" / "documentation_tests" / "test_env_keys.py" @@ -188,3 +191,28 @@ def test_only_documentation_pages_are_scanned_for_mentions(tmp_path: Path) -> No (tmp_path / "notes.txt").write_text("QSTASH_ALPHA\n", encoding="utf-8") (tmp_path / "example.py").write_text('get_secret("QSTASH_BRAVO")\n', encoding="utf-8") assert gate.collect_documented_keys(str(tmp_path)) == frozenset() + + +@pytest.mark.parametrize( + "directory", + ("cli", "deploy", "enterprise", "packaging", "proxy-extras", "runtime", "rust", "scripts", "ui", "vscode-extension"), +) +def test_relocated_sources_are_excluded_without_hiding_nested_sdk_sources(tmp_path: Path, directory: str) -> None: + source_dir, docs_dir = _write_tree(tmp_path, 'os.getenv("SDK_DOCUMENTED_KEY")', "SDK_DOCUMENTED_KEY") + relocated: Final = Path(source_dir) / directory + relocated.mkdir() + (relocated / "tool.py").write_text('os.getenv("NON_SDK_UNDOCUMENTED_KEY")', encoding="utf-8") + assert gate.undocumented_env_keys(source_dir, docs_dir) == frozenset() + + sdk_source: Final = Path(source_dir) / "llms" / directory + sdk_source.mkdir(parents=True) + (sdk_source / "provider.py").write_text('os.getenv("SDK_UNDOCUMENTED_KEY")', encoding="utf-8") + assert gate.undocumented_env_keys(source_dir, docs_dir) == {"SDK_UNDOCUMENTED_KEY"} + + +def test_source_exclusions_do_not_hide_documentation_pages(tmp_path: Path) -> None: + source_dir, docs_dir = _write_tree(tmp_path, 'os.getenv("SDK_DOCUMENTED_KEY")', "") + docs: Final = Path(docs_dir) / "scripts" + docs.mkdir() + (docs / "settings.md").write_text("SDK_DOCUMENTED_KEY", encoding="utf-8") + assert gate.undocumented_env_keys(source_dir, docs_dir) == frozenset()