From 416da066eb08de2eb242c00852e95ac00f16d5f3 Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Mon, 4 Aug 2025 23:44:02 -0700 Subject: [PATCH] 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 --- .../prompt_templates/common_utils.py | 26 ++++--- litellm/main.py | 2 + litellm/utils.py | 13 ++++ tests/llm_translation/test_openai.py | 51 ++++++++++++++ ...llm_core_utils_prompt_templates_factory.py | 70 +++++++++++++++++++ 5 files changed, 151 insertions(+), 11 deletions(-) diff --git a/litellm/litellm_core_utils/prompt_templates/common_utils.py b/litellm/litellm_core_utils/prompt_templates/common_utils.py index 827d28598e..9ba547b360 100644 --- a/litellm/litellm_core_utils/prompt_templates/common_utils.py +++ b/litellm/litellm_core_utils/prompt_templates/common_utils.py @@ -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]: diff --git a/litellm/main.py b/litellm/main.py index a6583ab059..108abbde84 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -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 ##################### diff --git a/litellm/utils.py b/litellm/utils.py index a9695e14c0..36910740e6 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -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): """ diff --git a/tests/llm_translation/test_openai.py b/tests/llm_translation/test_openai.py index 98707cbc1e..f5de082ded 100644 --- a/tests/llm_translation/test_openai.py +++ b/tests/llm_translation/test_openai.py @@ -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) diff --git a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py index 5cc30f3918..0cfb9f87d1 100644 --- a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py +++ b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py @@ -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)