Tool Calling
Both the Chat Completions and Responses APIs support function/tool calling — the model can decide to invoke functions you define, and you return the results.
Chat Completions Tool Calling
GPT-5.6 models require reasoning_effort="none" when using Chat tools. Use Respond to combine reasoning with tool calls; GPT-6 Astra tools also require the Responses API.
Defining Tools
Wrap your function schema in a Tool:
using UniLM
using JSON
weather_tool = Tool(
func=FunctionSignature(
name="get_weather",
description="Get current weather for a location",
parameters=Dict(
"type" => "object",
"properties" => Dict(
"location" => Dict("type" => "string", "description" => "City name"),
"unit" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"])
),
"required" => ["location"]
)
)
)
println("Tool type: ", weather_tool.type)
println("Function name: ", weather_tool.func.name)
println("Tool JSON:")
println(JSON.json(JSON.lower(weather_tool)))Tool type: function
Function name: get_weather
Tool JSON:
{"function":{"name":"get_weather","description":"Get current weather for a location","parameters":{"properties":{"location":{"description":"City name","type":"string"},"unit":{"enum":["celsius","fahrenheit"],"type":"string"}},"required":["location"],"type":"object"}},"type":"function"}Strict Function Calling
Pass strict=true to make the API guarantee that tool-call arguments conform to your schema (no extra keys, all required fields present). A strict schema must set additionalProperties => false on every object and mark every property as required — the API rejects strict-invalid schemas with a 400. Omitting strict (the default) sends no flag at all: the request body is identical to previous UniLM versions.
strict_tool = Tool(
func=FunctionSignature(
name="get_weather",
description="Get current weather for a location",
parameters=Dict(
"type" => "object",
"properties" => Dict(
"location" => Dict("type" => "string", "description" => "City name")
),
"required" => ["location"],
"additionalProperties" => false
),
strict=true
)
)
println(JSON.json(strict_tool)){"function":{"name":"get_weather","description":"Get current weather for a location","parameters":{"additionalProperties":false,"properties":{"location":{"description":"City name","type":"string"}},"required":["location"],"type":"object"},"strict":true},"type":"function"}Making Tool-Enabled Requests
chat = Chat(model="gpt-5.2", tools=[weather_tool])
push!(chat, Message(Val(:system), "You are a helpful assistant with access to weather data."))
push!(chat, Message(Val(:user), "What's the weather in Paris?"))
println("Chat has ", length(chat.tools), " tool(s) registered")
println("Request body:")
println(JSON.json(chat))Chat has 1 tool(s) registered
Request body:
{"messages":[{"content":"You are a helpful assistant with access to weather data.","role":"system"},{"content":"What's the weather in Paris?","role":"user"}],"model":"gpt-5.2","parallel_tool_calls":false,"tools":[{"function":{"name":"get_weather","description":"Get current weather for a location","parameters":{"properties":{"location":{"description":"City name","type":"string"},"unit":{"enum":["celsius","fahrenheit"],"type":"string"}},"required":["location"],"type":"object"}},"type":"function"}]}Handling Tool Calls
When the model wants to call a function, the result message will have finish_reason == "tool_calls":
chat = Chat(
model="gpt-5.4-mini",
tools=[weather_tool],
tool_choice=UniLM.GPTToolChoice(func=:get_weather)
)
push!(chat, Message(Val(:system), "Use the provided tools to answer."))
push!(chat, Message(Val(:user), "What's the weather in Paris?"))
result = chatrequest!(chat)
if result isa LLMSuccess
println("Finish reason: ", result.message.finish_reason)
tc = result.message.tool_calls[1]
println("Function: ", tc.func.name)
println("Arguments: ", JSON.json(tc.func.arguments, 2))
else
println("Request failed — see result for details")
endRequest failed — see result for detailsWhen streaming (stream=true), pass on_tool_call to chatrequest! to be notified as each tool call completes — exactly once per call — instead of waiting for the final message. See the Streaming guide.
Gemini's chat path (generateContent) may omit FunctionCall.id. UniLM assigns such calls a synthetic positional id (unilm_call_1, unilm_call_2, …) so parallel tool results correlate correctly; synthetic ids never appear on the Gemini wire (the re-encoded request omits the id and correlates positionally, per the API contract). The unilm_call_ prefix is reserved. This applies to the chat surface only — the Interactions API always returns server-generated call ids.
Controlling Tool Choice
# Let the model decide
chat = Chat(tools=[weather_tool], tool_choice="auto", reasoning_effort="none")
# Force the model to use a tool
chat = Chat(tools=[weather_tool], tool_choice="required", reasoning_effort="none")
# Prevent tool use
chat = Chat(tools=[weather_tool], tool_choice="none", reasoning_effort="none")Responses API Tool Calling
The Responses API makes tool calling more ergonomic with dedicated types.
Function Tools
tool = function_tool(
"calculate",
"Evaluate a math expression",
parameters=Dict(
"type" => "object",
"properties" => Dict(
"expression" => Dict("type" => "string")
),
"required" => ["expression"]
),
strict=true
)
println("Tool: ", tool.name, " (strict=", tool.strict, ")")
println("JSON: ", JSON.json(JSON.lower(tool)))Tool: calculate (strict=true)
JSON: {"description":"Evaluate a math expression","name":"calculate","parameters":{"properties":{"expression":{"type":"string"}},"required":["expression"],"type":"object"},"strict":true,"type":"function"}weather_fn = function_tool(
"get_weather",
"Get current weather for a location",
parameters=Dict(
"type" => "object",
"properties" => Dict(
"location" => Dict("type" => "string", "description" => "City name"),
"unit" => Dict("type" => "string", "enum" => ["celsius", "fahrenheit"])
),
"required" => ["location"]
)
)
result = respond("What's the weather in Tokyo? Use celsius.", tools=[weather_fn], model="gpt-5.4-mini")
calls = function_calls(result)
if !isempty(calls)
println("Function: ", calls[1]["name"])
println("Arguments: ", JSON.json(JSON.parse(calls[1]["arguments"]), 2))
else
println("No function calls — ", output_text(result))
endNo function calls — Error: KeyError: key "OPENAI_API_KEY" not foundWeb Search
The model can search the web — no function implementation needed:
ws = web_search(context_size="high")
println("Web search tool type: ", typeof(ws))
println("Context size: ", ws.search_context_size)Web search tool type: WebSearchTool
Context size: highresult = respond(
"What is the latest stable release of the Julia programming language?",
tools=[web_search()],
model="gpt-5.4-mini"
)
if result isa ResponseSuccess
println(output_text(result))
else
println("Request failed — ", output_text(result))
endRequest failed — Error: KeyError: key "OPENAI_API_KEY" not foundFile Search
Search over your uploaded vector stores:
result = respond(
"Find the error handling policy",
tools=[file_search(["vs_store_id_123"], max_results=5)]
)Combining Tools
Mix different tool types freely:
tools = [
web_search(),
function_tool("save_summary", "Save a summary to the database",
parameters=Dict(
"type" => "object",
"properties" => Dict(
"title" => Dict("type" => "string"),
"content" => Dict("type" => "string")
)
)
)
]
println("Number of tools: ", length(tools))
for t in tools
println(" - ", typeof(t))
endNumber of tools: 2
- WebSearchTool
- FunctionToolTool Choice, Tool Results & Hosted Tools
Constrain which tool the model may call with the tool_choice= builders (tool_choice_function, tool_choice_hosted, tool_choice_allowed, tool_choice_mcp, tool_choice_custom):
r = Respond(input="What's the weather?",
tools=[function_tool("get_weather", "Get weather",
parameters=Dict("type" => "object",
"properties" => Dict("location" => Dict("type" => "string"))))],
tool_choice=tool_choice_function("get_weather"))
println(r.tool_choice)Dict{Symbol, Any}(:type => "function", :name => "get_weather")Return a tool's output on the next turn with tool_result:
respond(Respond(; previous_response_id=r1.response.id,
input=[tool_result("call_abc", "get_weather", "72F and sunny")]))Gemini Interactions adds server-side hosted tools — see the Agentic Workflows guide for gemini_google_search and friends. When Gemini returns tool calls, the provider's opaque reasoning token is preserved on ToolCall.thought_signature and echoed automatically on the next turn.
Automated Tool Loop
Instead of manually handling tool calls, use tool_loop! (Chat Completions) or tool_loop (Responses API) for automatic dispatch:
Chat Completions
ct = CallableTool(weather_tool, (name, args) -> "22C, sunny in $(args["location"])")
println("Callable tool wrapping: ", ct.tool.func.name)Callable tool wrapping: get_weatherchat = Chat(model="gpt-5.2", tools=[ct.tool])
push!(chat, Message(Val(:system), "You are a helpful assistant."))
push!(chat, Message(Val(:user), "What's the weather in Paris?"))
result = tool_loop!(chat; tools=[ct])
# result.completed == true when the model gives a text responseResponses API
ct = CallableTool(
function_tool("get_weather", "Get weather", parameters=Dict(...)),
(name, args) -> "22C, sunny")
result = tool_loop("What's the weather?"; tools=[ct])MCP Tool Integration
MCP servers expose tools that integrate directly with the tool loop via mcp_tools and mcp_tools_respond. See the MCP Guide for full details.
# Chat Completions + MCP
session = mcp_connect(`npx server`)
tools = mcp_tools(session)
chat = Chat(model="gpt-5.2", tools=map(t -> t.tool, tools))
push!(chat, Message(Val(:system), "You are a helpful assistant."))
push!(chat, Message(Val(:user), "Do something"))
result = tool_loop!(chat; tools)
# Responses API + MCP
tools = mcp_tools_respond(session)
result = tool_loop("Do something"; tools=tools)Inspecting the Result
tool_loop / tool_loop! return a ToolLoopResult: the final response, the list of tool_calls that ran (each a ToolCallOutcome), turns_used, whether it completed, and any llm_error.
result = tool_loop("What's the weather in Paris and Tokyo?"; tools=[ct])
if result.completed
println(output_text(result.response))
else
# completed=false means it hit max_turns or an llm_error before a final text answer
println("Stopped after $(result.turns_used) turns: ", result.llm_error)
end
for oc in result.tool_calls # one ToolCallOutcome per executed tool call
status = oc.success ? "ok" : "error: $(oc.error)"
println(oc.tool_name, oc.arguments, " -> ", status)
endSee Also
Tool,FunctionSignature— Chat Completions tool typesFunctionTool,WebSearchTool,FileSearchTool— Responses API tool typesfunction_tool,web_search,file_search— convenience constructorsCallableTool,ToolCallOutcome,ToolLoopResult— tool loop typestool_loop!,tool_loop— automated tool dispatch- MCP Guide — MCP server integration