mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-09 08:18:12 +00:00
Response transformation crashed with TypeError when counting tokens for multimodal (dict-shaped) embedding inputs, since encoding.encode() was called directly on the dict instead of its text parts. Also removed a dead, equally-buggy duplicate of the same loop in _transform_response. Separately, the embeddings guardrail handler silently skipped multimodal dict inputs instead of scanning them, letting prohibited text/image content bypass the configured guardrail. It now extracts and scans the text parts while leaving image parts untouched.
180 lines
6.3 KiB
Python
180 lines
6.3 KiB
Python
import ast
|
|
import os
|
|
|
|
|
|
def check_for_litellm_module_deletion(base_dir):
|
|
"""
|
|
Checks for code patterns that delete litellm modules from sys.modules
|
|
in the test_litellm directory.
|
|
|
|
Specifically looks for patterns like:
|
|
for module in list(sys.modules.keys()):
|
|
if module.startswith("litellm"):
|
|
del sys.modules[module]
|
|
"""
|
|
problematic_files = []
|
|
test_dir = os.path.join(base_dir, "test_litellm")
|
|
|
|
if not os.path.exists(test_dir):
|
|
print(f"Warning: Directory {test_dir} does not exist.")
|
|
return []
|
|
|
|
print(f"Checking directory: {test_dir}")
|
|
|
|
for root, _, files in os.walk(test_dir):
|
|
for file in files:
|
|
if file.endswith(".py"):
|
|
file_path = os.path.join(root, file)
|
|
try:
|
|
with open(file_path, "r") as f:
|
|
tree = ast.parse(f.read())
|
|
except SyntaxError:
|
|
print(f"Warning: Syntax error in file {file_path}")
|
|
continue
|
|
|
|
# Check for litellm module deletion patterns
|
|
if has_litellm_module_deletion(tree):
|
|
relative_path = os.path.relpath(file_path, base_dir)
|
|
problematic_files.append(relative_path)
|
|
print(f"Found litellm module deletion in: {relative_path}")
|
|
|
|
return problematic_files
|
|
|
|
|
|
def has_litellm_module_deletion(tree):
|
|
"""
|
|
Checks if the AST contains patterns that delete litellm modules from sys.modules.
|
|
|
|
Looks for:
|
|
1. Loops over sys.modules.keys()
|
|
2. Conditions checking if module startswith "litellm"
|
|
3. del sys.modules[module] statements
|
|
"""
|
|
|
|
class LiteLLMDeletionVisitor(ast.NodeVisitor):
|
|
def __init__(self):
|
|
self.has_sys_modules_loop = False
|
|
self.has_litellm_check = False
|
|
self.has_del_sys_modules = False
|
|
self.current_for_target = None
|
|
|
|
def visit_For(self, node):
|
|
# Check if we're looping over sys.modules.keys()
|
|
if (
|
|
isinstance(node.iter, ast.Call)
|
|
and isinstance(node.iter.func, ast.Attribute)
|
|
and isinstance(node.iter.func.value, ast.Attribute)
|
|
and isinstance(node.iter.func.value.value, ast.Name)
|
|
and node.iter.func.value.value.id == "sys"
|
|
and node.iter.func.value.attr == "modules"
|
|
and node.iter.func.attr == "keys"
|
|
):
|
|
|
|
self.has_sys_modules_loop = True
|
|
if isinstance(node.target, ast.Name):
|
|
self.current_for_target = node.target.id
|
|
|
|
# Check the body of the for loop
|
|
for stmt in node.body:
|
|
self.visit(stmt)
|
|
|
|
# Also check for list(sys.modules.keys()) pattern
|
|
elif (
|
|
isinstance(node.iter, ast.Call)
|
|
and isinstance(node.iter.func, ast.Name)
|
|
and node.iter.func.id == "list"
|
|
and len(node.iter.args) == 1
|
|
and isinstance(node.iter.args[0], ast.Call)
|
|
and isinstance(node.iter.args[0].func, ast.Attribute)
|
|
and isinstance(node.iter.args[0].func.value, ast.Attribute)
|
|
and isinstance(node.iter.args[0].func.value.value, ast.Name)
|
|
and node.iter.args[0].func.value.value.id == "sys"
|
|
and node.iter.args[0].func.value.attr == "modules"
|
|
and node.iter.args[0].func.attr == "keys"
|
|
):
|
|
|
|
self.has_sys_modules_loop = True
|
|
if isinstance(node.target, ast.Name):
|
|
self.current_for_target = node.target.id
|
|
|
|
# Check the body of the for loop
|
|
for stmt in node.body:
|
|
self.visit(stmt)
|
|
|
|
self.generic_visit(node)
|
|
|
|
def visit_If(self, node):
|
|
# Check for conditions like module.startswith("litellm")
|
|
if (
|
|
isinstance(node.test, ast.Call)
|
|
and isinstance(node.test.func, ast.Attribute)
|
|
and isinstance(node.test.func.value, ast.Name)
|
|
and node.test.func.value.id == self.current_for_target
|
|
and node.test.func.attr == "startswith"
|
|
and len(node.test.args) == 1
|
|
and isinstance(node.test.args[0], ast.Constant)
|
|
and node.test.args[0].value == "litellm"
|
|
):
|
|
|
|
self.has_litellm_check = True
|
|
|
|
# Check the body of the if statement
|
|
for stmt in node.body:
|
|
self.visit(stmt)
|
|
|
|
self.generic_visit(node)
|
|
|
|
def visit_Delete(self, node):
|
|
# Check for del sys.modules[module]
|
|
for target in node.targets:
|
|
if (
|
|
isinstance(target, ast.Subscript)
|
|
and isinstance(target.value, ast.Attribute)
|
|
and isinstance(target.value.value, ast.Name)
|
|
and target.value.value.id == "sys"
|
|
and target.value.attr == "modules"
|
|
and isinstance(target.slice, ast.Name)
|
|
and target.slice.id == self.current_for_target
|
|
):
|
|
|
|
self.has_del_sys_modules = True
|
|
|
|
self.generic_visit(node)
|
|
|
|
visitor = LiteLLMDeletionVisitor()
|
|
visitor.visit(tree)
|
|
|
|
return (
|
|
visitor.has_sys_modules_loop
|
|
and visitor.has_litellm_check
|
|
and visitor.has_del_sys_modules
|
|
)
|
|
|
|
|
|
def main():
|
|
"""
|
|
Main function to check for litellm module deletion patterns in test files.
|
|
"""
|
|
# local dir
|
|
# tests_dir = "../../tests/"
|
|
|
|
# ci/cd dir
|
|
tests_dir = "./tests/"
|
|
|
|
problematic_files = check_for_litellm_module_deletion(tests_dir)
|
|
|
|
if problematic_files:
|
|
print("\nERROR: Found files that delete litellm modules from sys.modules:")
|
|
for file_path in problematic_files:
|
|
print(f" - {file_path}")
|
|
|
|
raise Exception(
|
|
f"Found {len(problematic_files)} file(s) that delete litellm modules from sys.modules. "
|
|
f"This can cause import issues and test failures. Files: {problematic_files}"
|
|
)
|
|
else:
|
|
print("✓ No litellm module deletion patterns found in test_litellm directory.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|