fix(main.py): handle tool being a pydantic object (#13274)

* fix(main.py): handle tool being a pydantic object

Fixes https://github.com/BerriAI/litellm/issues/13064

* fix(prompt_templates/common_utils.py): fix unpack defs deepcopy issue

Fixes https://github.com/BerriAI/litellm/issues/13151

* fix(utils.py): handle tools is none
This commit is contained in:
Krish Dholakia 2025-08-04 23:44:02 -07:00 committed by GitHub
parent eb49f987de
commit 416da066eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 151 additions and 11 deletions

View File

@ -519,25 +519,25 @@ def unpack_defs(schema: dict, defs: dict) -> None:
}
# Use iterative approach with queue to avoid recursion
# Each item in queue is (node, parent_container, key/index, active_defs, seen_ids)
# Each item in queue is (node, parent_container, key/index, active_defs, ref_chain)
queue: deque[
tuple[Any, Union[dict, list, None], Union[str, int, None], dict, set]
] = deque([(schema, None, None, root_defs, set())])
while queue:
node, parent, key, active_defs, seen = queue.popleft()
# Avoid infinite loops on self-referential schemas
if id(node) in seen:
continue
seen = seen.copy() # Create new set for this branch
seen.add(id(node))
node, parent, key, active_defs, ref_chain = queue.popleft()
# ----------------------------- dict -----------------------------
if isinstance(node, dict):
# --- Case 1: this node *is* a reference ---
if "$ref" in node:
ref_name = node["$ref"].split("/")[-1]
# Check for circular reference in the resolution chain
if ref_name in ref_chain:
# Circular reference detected - leave as-is to prevent infinite recursion
continue
target_schema = active_defs.get(ref_name)
# Unknown reference leave untouched
if target_schema is None:
@ -563,8 +563,12 @@ def unpack_defs(schema: dict, defs: dict) -> None:
schema.update(resolved)
resolved = schema
# Add to ref chain to track circular references
new_ref_chain = ref_chain.copy()
new_ref_chain.add(ref_name)
# Add resolved node to queue for further processing
queue.append((resolved, parent, key, child_defs, seen))
queue.append((resolved, parent, key, child_defs, new_ref_chain))
continue
# --- Case 2: regular dict process its values ---
@ -577,13 +581,13 @@ def unpack_defs(schema: dict, defs: dict) -> None:
# Add all dict values to queue
for k, v in node.items():
queue.append((v, node, k, current_defs, seen))
queue.append((v, node, k, current_defs, ref_chain))
# ---------------------------- list ------------------------------
elif isinstance(node, list):
# Add all list items to queue
for idx, item in enumerate(node):
queue.append((item, node, idx, active_defs, seen))
queue.append((item, node, idx, active_defs, ref_chain))
def _get_image_mime_type_from_url(url: str) -> Optional[str]:

View File

@ -107,6 +107,7 @@ from litellm.utils import (
supports_httpx_timeout,
token_counter,
validate_and_fix_openai_messages,
validate_and_fix_openai_tools,
validate_chat_completion_tool_choice,
)
@ -965,6 +966,7 @@ def completion( # type: ignore # noqa: PLR0915
raise ValueError("model param not passed in.")
# validate messages
messages = validate_and_fix_openai_messages(messages=messages)
tools = validate_and_fix_openai_tools(tools=tools)
# validate tool_choice
tool_choice = validate_chat_completion_tool_choice(tool_choice=tool_choice)
######### unpacking kwargs #####################

View File

@ -6604,6 +6604,19 @@ def validate_and_fix_openai_messages(messages: List):
new_messages.append(cleaned_message)
return validate_chat_completion_user_messages(messages=new_messages)
def validate_and_fix_openai_tools(tools: Optional[List]) -> Optional[List[dict]]:
"""
Ensure tools is List[dict] and not List[BaseModel]
"""
new_tools = []
if tools is None:
return tools
for tool in tools:
if isinstance(tool, BaseModel):
new_tools.append(tool.model_dump())
elif isinstance(tool, dict):
new_tools.append(tool)
return new_tools
def cleanup_none_field_in_message(message: AllMessageValues):
"""

View File

@ -603,3 +603,54 @@ def test_openai_deepresearch_model_bridge():
)
print("response: ", response)
def test_openai_tool_calling():
from pydantic import BaseModel
from typing import Any, Literal
class OpenAIFunction(BaseModel):
description: Optional[str] = None
name: str
parameters: Optional[dict[str, Any]] = None
class OpenAITool(BaseModel):
type: Literal["function"]
function: OpenAIFunction
completion_params = {
"model": "openai/gpt-4.1",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "What is TSLA stock price at today?"}
],
}
],
"stream": False,
"temperature": 0.5,
"stop": None,
"max_tokens": 1600,
"tools": [
OpenAITool(
type="function",
function=OpenAIFunction(
description="Get the current stock price for a given ticker symbol.",
name="get_stock_price",
parameters={
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "The stock ticker symbol, e.g. AAPL for Apple Inc.",
}
},
"required": ["ticker"],
},
),
)
],
}
response = litellm.completion(**completion_params)

View File

@ -435,3 +435,73 @@ def test_convert_gemini_messages():
message=message,
last_message_with_tool_calls=last_message_with_tool_calls,
)
def test_bedrock_tools_unpack_defs():
"""
Test that the unpack_defs method handles nested $ref inside anyOf items correctly
"""
from litellm.litellm_core_utils.prompt_templates.factory import _bedrock_tools_pt
circularRefSchema = {
"type": "object",
"properties": {
"type": {"type": "string", "enum": ["doc"]},
"content": {"type": "array", "items": {"$ref": "#/$defs/node"}},
},
"required": ["type", "content"],
"additionalProperties": False,
"$defs": {
"node": {
"type": "object",
"anyOf": [
{
"type": "object",
"properties": {
"type": {"type": "string", "enum": ["bulletList"]},
"content": {
"type": "array",
"items": {"$ref": "#/$defs/listItem"},
},
},
"required": ["type"],
"additionalProperties": True,
},
{
"type": "object",
"properties": {
"type": {"type": "string", "enum": ["orderedList"]},
"content": {
"type": "array",
"items": {"$ref": "#/$defs/listItem"},
},
},
"required": ["type"],
"additionalProperties": True,
},
],
},
"listItem": {
"type": "object",
"properties": {
"type": {"type": "string", "enum": ["listItem"]},
"content": {"type": "array", "items": {"$ref": "#/$defs/node"}},
},
"required": ["type"],
"additionalProperties": True,
},
},
}
tools = [
{
"type": "function",
"function": {
"name": "json_schema",
"description": "Process the content using json schema validation",
"parameters": circularRefSchema,
},
}
]
_bedrock_tools_pt(tools=tools)