Back to Blog
5 min read

Tool Choice Control: Fine-Tuning When and How AI Uses Tools

Controlling when and how the model uses tools is crucial for building predictable AI applications. Let’s explore the tool_choice parameter and advanced control patterns.

Tool Choice Options

from openai import OpenAI
import json

client = OpenAI()

tools = [
    {
        "type": "function",
        "function": {
            "name": "search_database",
            "description": "Search the product database",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string"}
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "create_order",
            "description": "Create a new order",
            "parameters": {
                "type": "object",
                "properties": {
                    "product_id": {"type": "string"},
                    "quantity": {"type": "integer"}
                },
                "required": ["product_id", "quantity"]
            }
        }
    }
]

# Option 1: "auto" - Model decides (default)
response = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[{"role": "user", "content": "Hello!"}],
    tools=tools,
    tool_choice="auto"  # Model may or may not use tools
)

# Option 2: "none" - Never use tools
response = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[{"role": "user", "content": "Search for laptops"}],
    tools=tools,
    tool_choice="none"  # Model will respond without tools
)

# Option 3: "required" - Must use a tool
response = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[{"role": "user", "content": "What products do you have?"}],
    tools=tools,
    tool_choice="required"  # Model must call at least one tool
)

# Option 4: Specific function - Force a particular tool
response = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[{"role": "user", "content": "I want to buy something"}],
    tools=tools,
    tool_choice={
        "type": "function",
        "function": {"name": "search_database"}
    }
)

Dynamic Tool Selection

from typing import List, Optional, Literal
from enum import Enum

class ConversationState(Enum):
    BROWSING = "browsing"
    SELECTING = "selecting"
    CHECKOUT = "checkout"
    SUPPORT = "support"

class DynamicToolSelector:
    """Select appropriate tools and control based on conversation state"""

    def __init__(self):
        self.all_tools = self._define_all_tools()

    def _define_all_tools(self) -> dict:
        return {
            "search_database": {
                "type": "function",
                "function": {
                    "name": "search_database",
                    "description": "Search products",
                    "parameters": {
                        "type": "object",
                        "properties": {"query": {"type": "string"}},
                        "required": ["query"]
                    }
                }
            },
            "get_product_details": {
                "type": "function",
                "function": {
                    "name": "get_product_details",
                    "description": "Get product details",
                    "parameters": {
                        "type": "object",
                        "properties": {"product_id": {"type": "string"}},
                        "required": ["product_id"]
                    }
                }
            },
            "add_to_cart": {
                "type": "function",
                "function": {
                    "name": "add_to_cart",
                    "description": "Add item to cart",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "product_id": {"type": "string"},
                            "quantity": {"type": "integer"}
                        },
                        "required": ["product_id", "quantity"]
                    }
                }
            },
            "process_payment": {
                "type": "function",
                "function": {
                    "name": "process_payment",
                    "description": "Process payment",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "amount": {"type": "number"},
                            "payment_method": {"type": "string"}
                        },
                        "required": ["amount", "payment_method"]
                    }
                }
            },
            "contact_support": {
                "type": "function",
                "function": {
                    "name": "contact_support",
                    "description": "Contact human support",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "issue": {"type": "string"},
                            "priority": {"type": "string"}
                        },
                        "required": ["issue"]
                    }
                }
            }
        }

    def get_tools_for_state(self, state: ConversationState) -> tuple:
        """Return (tools, tool_choice) based on state"""

        if state == ConversationState.BROWSING:
            tools = [
                self.all_tools["search_database"],
                self.all_tools["get_product_details"]
            ]
            return tools, "auto"

        elif state == ConversationState.SELECTING:
            tools = [
                self.all_tools["get_product_details"],
                self.all_tools["add_to_cart"]
            ]
            return tools, "auto"

        elif state == ConversationState.CHECKOUT:
            tools = [
                self.all_tools["process_payment"]
            ]
            # Force payment processing during checkout
            return tools, {"type": "function", "function": {"name": "process_payment"}}

        elif state == ConversationState.SUPPORT:
            tools = [
                self.all_tools["contact_support"]
            ]
            return tools, "required"  # Must escalate

        return [], "none"

# Usage
selector = DynamicToolSelector()

def chat_with_state(message: str, state: ConversationState) -> str:
    tools, tool_choice = selector.get_tools_for_state(state)

    response = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=[{"role": "user", "content": message}],
        tools=tools if tools else None,
        tool_choice=tool_choice if tools else None
    )

    return response

Safety Controls

class ToolSafetyController:
    """Control tool usage based on safety policies"""

    def __init__(self):
        self.dangerous_tools = {"delete_data", "process_payment", "send_email"}
        self.require_confirmation = {"create_order", "update_profile"}
        self.rate_limits = {}

    def filter_tools(self, tools: List[dict], user_permissions: List[str]) -> List[dict]:
        """Filter tools based on user permissions"""
        return [
            tool for tool in tools
            if self._user_can_use(tool["function"]["name"], user_permissions)
        ]

    def _user_can_use(self, tool_name: str, permissions: List[str]) -> bool:
        """Check if user has permission for this tool"""
        if tool_name in self.dangerous_tools:
            return f"admin:{tool_name}" in permissions
        return True

    def get_tool_choice(self, tool_name: str, already_confirmed: bool) -> dict:
        """Determine appropriate tool choice with safety checks"""

        if tool_name in self.dangerous_tools:
            if not already_confirmed:
                return "none"  # Require explicit confirmation

        if tool_name in self.require_confirmation:
            return {
                "type": "function",
                "function": {"name": f"confirm_{tool_name}"}
            }

        return {"type": "function", "function": {"name": tool_name}}

    def check_rate_limit(self, tool_name: str, user_id: str) -> bool:
        """Check if tool usage is within rate limits"""
        key = f"{user_id}:{tool_name}"
        # Implementation depends on your rate limiting system
        return True

# Confirmation flow
def execute_with_confirmation(message: str, tools: List[dict],
                              pending_action: dict = None) -> dict:
    """Execute tools with confirmation for sensitive actions"""

    if pending_action:
        # User is confirming a pending action
        if "yes" in message.lower() or "confirm" in message.lower():
            # Execute the pending action
            result = execute_tool(pending_action["tool_call"])
            return {"status": "executed", "result": result}
        else:
            return {"status": "cancelled"}

    response = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=[{"role": "user", "content": message}],
        tools=tools,
        tool_choice="auto"
    )

    message = response.choices[0].message

    if message.tool_calls:
        call = message.tool_calls[0]

        # Check if this requires confirmation
        if call.function.name in ["process_payment", "delete_data"]:
            return {
                "status": "pending_confirmation",
                "action": {
                    "name": call.function.name,
                    "args": json.loads(call.function.arguments),
                    "tool_call": call
                },
                "message": f"Are you sure you want to {call.function.name}?"
            }

        # Execute directly
        result = execute_tool(call)
        return {"status": "executed", "result": result}

    return {"status": "response", "content": message.content}

Workflow Control

class WorkflowController:
    """Control tool usage through a defined workflow"""

    def __init__(self):
        self.workflow = {
            "start": {"allowed_tools": ["search_database"], "next": "found_results"},
            "found_results": {"allowed_tools": ["get_details", "search_database"], "next": "viewing_product"},
            "viewing_product": {"allowed_tools": ["add_to_cart", "search_database"], "next": "cart_updated"},
            "cart_updated": {"allowed_tools": ["checkout", "continue_shopping"], "next": "checkout"},
            "checkout": {"allowed_tools": ["process_payment"], "next": "complete"}
        }
        self.current_state = "start"

    def get_allowed_tools(self, all_tools: List[dict]) -> List[dict]:
        """Get tools allowed in current state"""
        allowed_names = self.workflow[self.current_state]["allowed_tools"]
        return [t for t in all_tools if t["function"]["name"] in allowed_names]

    def transition(self, executed_tool: str):
        """Move to next state based on executed tool"""
        next_state = self.workflow[self.current_state].get("next")
        if next_state:
            self.current_state = next_state

    def can_proceed(self) -> bool:
        """Check if workflow allows proceeding"""
        return self.current_state != "complete"

# Usage
workflow = WorkflowController()

while workflow.can_proceed():
    allowed = workflow.get_allowed_tools(all_tools)

    response = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=[{"role": "user", "content": user_input}],
        tools=allowed,
        tool_choice="required" if len(allowed) == 1 else "auto"
    )

    if response.choices[0].message.tool_calls:
        tool_name = response.choices[0].message.tool_calls[0].function.name
        workflow.transition(tool_name)

Tool choice control is essential for building AI applications that are predictable, safe, and aligned with your business logic. Use these patterns to guide model behavior exactly as needed.

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.