| name | tools |
| description | Use when implementing function calling, tool use, or agents with LLMs - unified tool API works across OpenAI, Anthropic, Google, and Ollama with consistent tool definition and execution patterns |
Function Calling and Tool Use
Installation
uv add llmring
pip install llmring
Provider SDKs (install what you need):
uv add openai>=1.0
uv add anthropic>=0.67
uv add google-genai
uv add ollama>=0.4
API Overview
This skill covers:
- Tool definition format (JSON Schema)
tools parameter in LLMRequest
tool_choice parameter for controlling tool selection
tool_calls in LLMResponse
- Multi-turn tool execution pattern
- Tool result messages
Quick Start
from llmring import LLMRing, LLMRequest, Message
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
}
}
}]
async with LLMRing() as service:
request = LLMRequest(
model="tool-user",
messages=[Message(role="user", content="What's the weather in NYC?")],
tools=tools
)
response = await service.chat(request)
if response.tool_calls:
tool_call = response.tool_calls[0]
print(f"Tool: {tool_call['function']['name']}")
print(f"Args: {tool_call['function']['arguments']}")
Complete API Documentation
Tool Definition Format
Tools use JSON Schema format for function definitions.
Structure:
tool = {
"type": "function",
"function": {
"name": str,
"description": str,
"parameters": {
"type": "object",
"properties": {
"param_name": {
"type": str,
"description": str,
"enum": List[str]
}
},
"required": List[str]
}
}
}
Example:
get_weather_tool = {
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name, e.g. 'New York' or 'London'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
}
}
}
LLMRequest with Tools
Parameters:
tools (list, optional): List of available tools
tool_choice (str/dict, optional): Control tool selection
"auto": Model decides whether to use tools (default)
"none": Force no tool use
"required": Force tool use (OpenAI) / "any" (Anthropic equivalent)
{"type": "function", "function": {"name": "tool_name"}}: Force specific tool
Note: Provider differences - OpenAI uses "required", Anthropic uses "any" for the same behavior. LLMRing handles the translation automatically.
Example:
from llmring import LLMRequest, Message
request = LLMRequest(
model="tool-user",
messages=[Message(role="user", content="What's the weather?")],
tools=[get_weather_tool]
)
request = LLMRequest(
model="tool-user",
messages=[Message(role="user", content="Check NYC weather")],
tools=[get_weather_tool],
tool_choice="required"
)
request = LLMRequest(
model="tool-user",
messages=[Message(role="user", content="Get weather")],
tools=[get_weather_tool, search_tool],
tool_choice={
"type": "function",
"function": {"name": "get_weather"}
}
)
request = LLMRequest(
model="tool-user",
messages=[Message(role="user", content="Just chat")],
tools=[get_weather_tool],
tool_choice="none"
)
Tool Calls in Response
When the model wants to call a tool, response.tool_calls is populated.
Structure:
tool_call = {
"id": str,
"type": "function",
"function": {
"name": str,
"arguments": str
}
}
Example:
response = await service.chat(request)
if response.tool_calls:
for tool_call in response.tool_calls:
tool_name = tool_call["function"]["name"]
tool_args = json.loads(tool_call["function"]["arguments"])
tool_id = tool_call["id"]
print(f"Tool: {tool_name}")
print(f"Arguments: {tool_args}")
Tool Result Messages
After executing a tool, send the result back with role="tool".
Structure:
tool_result = Message(
role="tool",
content: str,
tool_call_id: str
)
Example:
import json
from llmring import Message
tool_result = {"temperature": 72, "condition": "sunny"}
result_message = Message(
role="tool",
content=json.dumps(tool_result),
tool_call_id=tool_call["id"]
)
Complete Tool Execution Pattern
import json
from llmring import LLMRing, LLMRequest, Message
def get_weather(location: str, unit: str = "fahrenheit") -> dict:
"""Mock weather function."""
return {
"location": location,
"temperature": 72,
"unit": unit,
"condition": "sunny"
}
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
}
}
}]
async with LLMRing() as service:
messages = [
Message(role="user", content="What's the weather in San Francisco?")
]
request = LLMRequest(model="tool-user",
response = await service.chat(request)
messages.append(Message(
role="assistant",
content=response.content or "",
tool_calls=response.tool_calls
))
if response.tool_calls:
for tool_call in response.tool_calls:
args = json.loads(tool_call["function"]["arguments"])
result = get_weather(**args)
messages.append(Message(
role="tool",
content=json.dumps(result),
tool_call_id=tool_call["id"]
))
request = LLMRequest(model="tool-user",
response = await service.chat(request)
print(response.content)
Common Patterns
Multiple Tools
from llmring import LLMRing, LLMRequest, Message
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
}
}
},
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the web for information",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"}
},
"required": ["query"]
}
}
}
]
async with LLMRing() as service:
request = LLMRequest(
model="tool-user",
messages=[Message(role="user", content="Weather in Paris and latest news")],
tools=tools
)
response = await service.chat(request)
if response.tool_calls:
print(f"Model wants to call {len(response.tool_calls)} tools")
Tool Execution Loop
import json
from llmring import LLMRing, LLMRequest, Message
FUNCTIONS = {
"get_weather": lambda location: {"temp": 72, "condition": "sunny"},
"search_web": lambda query: {"results": ["Result 1", "Result 2"]}
}
async with LLMRing() as service:
messages = [
Message(role="user", content="What's the weather in NYC?")
]
tools = [...]
while True:
request = LLMRequest(model="tool-user",
response = await service.chat(request)
messages.append(Message(
role="assistant",
content=response.content or "",
tool_calls=response.tool_calls
))
if not response.tool_calls:
break
for tool_call in response.tool_calls:
func_name = tool_call["function"]["name"]
args = json.loads(tool_call["function"]["arguments"])
result = FUNCTIONS[func_name](**args)
messages.append(Message(
role="tool",
content=json.dumps(result),
tool_call_id=tool_call["id"]
))
print(response.content)
Parallel Tool Calls
Some models can call multiple tools in parallel:
import json
from llmring import LLMRing, LLMRequest, Message
async with LLMRing() as service:
request = LLMRequest(
model="tool-user",
messages=[Message(role="user", content="Weather in NYC and Paris")],
tools=tools
)
response = await service.chat(request)
if response.tool_calls:
for tool_call in response.tool_calls:
print(f"Tool: {tool_call['function']['name']}")
print(f"Args: {tool_call['function']['arguments']}")
Streaming with Tools
from llmring import LLMRing, LLMRequest, Message
async with LLMRing() as service:
request = LLMRequest(
model="tool-user",
messages=[Message(role="user", content="What's the weather?")],
tools=tools
)
tool_calls_accumulated = []
async for chunk in service.chat_stream(request):
print(chunk.delta, end="", flush=True)
if chunk.tool_calls:
tool_calls_accumulated = chunk.tool_calls
if tool_calls_accumulated:
print("\nModel wants to call tools")
Error Handling in Tools
import json
from llmring import LLMRing, LLMRequest, Message
def execute_tool_safely(func_name: str, args: dict) -> dict:
"""Execute tool with error handling."""
try:
result = FUNCTIONS[func_name](**args)
return {"success": True, "data": result}
except Exception as e:
return {"success": False, "error": str(e)}
async with LLMRing() as service:
for tool_call in response.tool_calls:
func_name = tool_call["function"]["name"]
args = json.loads(tool_call["function"]["arguments"])
result = execute_tool_safely(func_name, args)
messages.append(Message(
role="tool",
content=json.dumps(result),
tool_call_id=tool_call["id"]
))
Provider Support
| Provider | Tool Support | Notes |
|---|
| OpenAI | Native | Full support, parallel calls |
| Anthropic | Native | Full support |
| Google | Native | Full support |
| Ollama | Prompt-based | Tools via prompt engineering |
Note: Ollama uses prompt-based tool calling. LLMRing handles the adaptation automatically.
Common Mistakes
Wrong: Not Including Assistant Message with tool_calls
if response.tool_calls:
for tool_call in response.tool_calls:
result = execute_tool(tool_call)
messages.append(Message(role="tool", content=result))
Right: Include Assistant Message
messages.append(Message(
role="assistant",
content=response.content or "",
tool_calls=response.tool_calls
))
for tool_call in response.tool_calls:
result = execute_tool(tool_call)
messages.append(Message(
role="tool",
content=json.dumps(result),
tool_call_id=tool_call["id"]
))
Wrong: Forgetting tool_call_id
messages.append(Message(
role="tool",
content=json.dumps(result)
))
Right: Include tool_call_id
messages.append(Message(
role="tool",
content=json.dumps(result),
tool_call_id=tool_call["id"]
))
Wrong: Tool Result Not JSON String
result = {"temperature": 72}
messages.append(Message(
role="tool",
content=result,
tool_call_id=tool_id
))
Right: JSON String
result = {"temperature": 72}
messages.append(Message(
role="tool",
content=json.dumps(result),
tool_call_id=tool_id
))
Wrong: Poor Tool Descriptions
{
"name": "get_data",
"description": "Gets data",
"parameters": {...}
}
Right: Clear Descriptions
{
"name": "get_weather",
"description": "Get the current weather conditions for a specific city, including temperature and general conditions",
"parameters": {...}
}
Best Practices
- Clear tool descriptions: Model uses descriptions to decide when to call tools
- Include tool_calls in assistant message: Required for proper conversation flow
- Always include tool_call_id: Links results to requests
- Use JSON strings for results: Tool content must be string, not dict
- Handle errors gracefully: Return error info as JSON to let model respond
- Validate tool arguments: Parse and validate before executing
- Loop until done: Continue conversation until no more tool calls
Related Skills
llmring-chat - Basic chat without tools
llmring-streaming - Streaming tool calls
llmring-structured - Combine tools with structured output
llmring-lockfile - Configure models for tool use
llmring-providers - Provider-specific tool features