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")
end
Request failed — see result for details
Streaming tool calls

When 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 calls without wire ids

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))
end
No function calls — Error: KeyError: key "OPENAI_API_KEY" not found

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: high
result = 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))
end
Request failed — Error: KeyError: key "OPENAI_API_KEY" not found

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))
end
Number of tools: 2
  - WebSearchTool
  - FunctionTool

Tool 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_weather
chat = 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 response

Responses 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)
end

See Also