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:
parent
eb49f987de
commit
416da066eb
@ -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]:
|
||||
|
||||
@ -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 #####################
|
||||
|
||||
@ -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):
|
||||
"""
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user