litellm/tests/local_testing/test_function_setup.py
ryan-crabbe-berri e9d40a8f73 test: enforce F811 so a duplicate definition cannot silently replace the first
A name bound twice keeps only the second binding. In `tests/` that is nearly
always a repeated import, harmless but misleading, and the same rule is what
catches the cases that are not harmless: a local that shadows an import the
module still calls, and a second `def test_x` that quietly replaces the first.

311 of the 344 sites were repeated imports and came out with ruff's own fix.
The remaining 33 needed a decision. Four modules imported a name they never
used because a local definition below already shadowed it. Two comprehensions
bound `call` over `unittest.mock.call`, which those modules import and use.
One test rebound the two module handles its nested reload closure had captured.
One class attribute shadowed an unused `status` import.

The load-test fixtures move to a conftest, which is how pytest is meant to share
them, so the test module no longer imports three fixture names it never calls.
The nine `prisma_client` parameters keep a narrow `noqa`: pytest resolves that
fixture by name before the body runs, so the parameter never shadows anything.
2026-08-21 12:06:19 -07:00

211 lines
6.3 KiB
Python

# What is this?
## Unit tests for the 'function_setup()' function
import sys, os
import traceback
from dotenv import load_dotenv
load_dotenv()
import io
sys.path.insert(
0, os.path.abspath("../..")
) # Adds the parent directory to the system path
import pytest, uuid
from litellm.utils import function_setup, Rules
from litellm.litellm_core_utils.prompt_templates.factory import (
THOUGHT_SIGNATURE_SEPARATOR,
)
from datetime import datetime
def test_empty_content():
"""
Make a chat completions request with empty content -> expect this to work
"""
rules_obj = Rules()
def completion():
pass
function_setup(
original_function="completion",
rules_obj=rules_obj,
start_time=datetime.now(),
messages=[],
litellm_call_id=str(uuid.uuid4()),
)
def test_thought_signature_removal_for_non_gemini():
"""
Test that thought signatures are removed from tool call IDs when sending to non-Gemini models
"""
rules_obj = Rules()
# Create messages with thought signatures (as would come from Gemini)
messages = [
{"role": "user", "content": "What's the weather?"},
{
"role": "assistant",
"tool_calls": [
{
"id": f"call_123{THOUGHT_SIGNATURE_SEPARATOR}sig1",
"type": "function",
"function": {
"name": "get_weather",
"arguments": '{"location": "SF"}',
},
}
],
},
{
"role": "tool",
"tool_call_id": f"call_123{THOUGHT_SIGNATURE_SEPARATOR}sig1",
"content": "Sunny, 72°F",
},
]
# Call function_setup with OpenAI model (non-Gemini)
logging_obj, kwargs = function_setup(
original_function="acompletion",
rules_obj=rules_obj,
start_time=datetime.now(),
model="gpt-4",
messages=messages,
litellm_call_id=str(uuid.uuid4()),
custom_llm_provider="openai",
)
# Verify thought signatures were removed
processed_messages = kwargs["messages"]
assert processed_messages[1]["tool_calls"][0]["id"] == "call_123"
assert processed_messages[2]["tool_call_id"] == "call_123"
assert (
THOUGHT_SIGNATURE_SEPARATOR not in processed_messages[1]["tool_calls"][0]["id"]
)
assert THOUGHT_SIGNATURE_SEPARATOR not in processed_messages[2]["tool_call_id"]
def test_thought_signature_preserved_for_gemini():
"""
Test that thought signatures are preserved when sending to Gemini models
"""
rules_obj = Rules()
# Create messages with thought signatures
messages = [
{"role": "user", "content": "What's the weather?"},
{
"role": "assistant",
"tool_calls": [
{
"id": f"call_456{THOUGHT_SIGNATURE_SEPARATOR}sig2",
"type": "function",
"function": {
"name": "get_weather",
"arguments": '{"location": "NYC"}',
},
}
],
},
{
"role": "tool",
"tool_call_id": f"call_456{THOUGHT_SIGNATURE_SEPARATOR}sig2",
"content": "Rainy, 65°F",
},
]
# Call function_setup with Gemini model
logging_obj, kwargs = function_setup(
original_function="acompletion",
rules_obj=rules_obj,
start_time=datetime.now(),
model="gemini-1.5-pro",
messages=messages,
litellm_call_id=str(uuid.uuid4()),
custom_llm_provider="vertex_ai",
)
# Verify thought signatures were preserved (messages should be unchanged)
processed_messages = kwargs["messages"]
assert THOUGHT_SIGNATURE_SEPARATOR in processed_messages[1]["tool_calls"][0]["id"]
assert THOUGHT_SIGNATURE_SEPARATOR in processed_messages[2]["tool_call_id"]
def test_thought_signature_removal_with_multiple_tool_calls():
"""
Test that thought signatures are removed from multiple tool calls
"""
rules_obj = Rules()
messages = [
{"role": "user", "content": "Get weather and time"},
{
"role": "assistant",
"tool_calls": [
{
"id": f"call_1{THOUGHT_SIGNATURE_SEPARATOR}sig1",
"type": "function",
"function": {"name": "get_weather", "arguments": "{}"},
},
{
"id": f"call_2{THOUGHT_SIGNATURE_SEPARATOR}sig2",
"type": "function",
"function": {"name": "get_time", "arguments": "{}"},
},
],
},
{
"role": "tool",
"tool_call_id": f"call_1{THOUGHT_SIGNATURE_SEPARATOR}sig1",
"content": "Sunny",
},
{
"role": "tool",
"tool_call_id": f"call_2{THOUGHT_SIGNATURE_SEPARATOR}sig2",
"content": "3:00 PM",
},
]
logging_obj, kwargs = function_setup(
original_function="acompletion",
rules_obj=rules_obj,
start_time=datetime.now(),
model="claude-3-opus",
messages=messages,
litellm_call_id=str(uuid.uuid4()),
custom_llm_provider="anthropic",
)
processed_messages = kwargs["messages"]
# Check all tool call IDs are cleaned
assert processed_messages[1]["tool_calls"][0]["id"] == "call_1"
assert processed_messages[1]["tool_calls"][1]["id"] == "call_2"
assert processed_messages[2]["tool_call_id"] == "call_1"
assert processed_messages[3]["tool_call_id"] == "call_2"
def test_messages_without_tool_calls_unchanged():
"""
Test that messages without tool calls pass through unchanged
"""
rules_obj = Rules()
messages = [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there!"},
]
logging_obj, kwargs = function_setup(
original_function="acompletion",
rules_obj=rules_obj,
start_time=datetime.now(),
model="gpt-4",
messages=messages,
litellm_call_id=str(uuid.uuid4()),
custom_llm_provider="openai",
)
# Messages should be unchanged
assert kwargs["messages"] == messages