Tool Dispatch
View SourceOpenResponses supports two kinds of tools: external (implemented in your client) and hosted (implemented on the server). Both follow the same request format.
Defining tools in a request
Tools are defined as JSON Schema function descriptions:
{
"model": "gpt-4o",
"input": [{"role": "user", "content": "Search for recent news about Elixir."}],
"tools": [
{
"type": "function",
"name": "web_search",
"description": "Search the web for recent information.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query"
}
},
"required": ["query"]
}
}
]
}External tool flow
When the model calls a tool that is not registered as a hosted tool, the call appears in the response output and you are responsible for executing it and submitting the result.
Step 1 — Model calls the tool
The response (or stream) includes a function_call item:
{
"output": [
{
"type": "function_call",
"id": "fc_01",
"call_id": "call_abc123",
"name": "web_search",
"arguments": "{\"query\": \"Elixir news 2026\"}"
}
]
}Step 2 — Execute the tool in your code
arguments = Jason.decode!(function_call["arguments"])
result = MyApp.WebSearch.search(arguments["query"])Step 3 — Submit the result
Send a follow-up request with the previous response ID and a function_call_output item:
{
"model": "gpt-4o",
"previous_response_id": "resp_01",
"input": [
{
"type": "function_call_output",
"call_id": "call_abc123",
"output": "Top results: ElixirConf 2026 announced..."
}
]
}OpenResponses reconstructs the full conversation context from previous_response_id and continues from where it left off.
Hosted tools
Hosted tools execute on the server. The loop calls them automatically without involving the client. This is ideal for tools that are safe, fast, and don't require user interaction.
Implementing a hosted tool
Implement the OpenResponses.Tool behaviour:
defmodule MyApp.Tools.TimeZone do
@behaviour OpenResponses.Tool
@impl OpenResponses.Tool
def execute(%{"timezone" => tz}, _context) do
case DateTime.now(tz) do
{:ok, dt} -> {:ok, Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S %Z")}
{:error, _} -> {:error, "Unknown timezone: #{tz}"}
end
end
endexecute/2 receives:
arguments— the parsed JSON arguments from the model (a map with string keys)context— a map with request context, includingresponse_id
Return {:ok, string_result} or {:error, reason}.
Registering hosted tools
In config/config.exs:
config :open_responses, :hosted_tools, %{
"get_time" => MyApp.Tools.TimeZone,
"search_docs" => MyApp.Tools.DocSearch,
"run_sql" => MyApp.Tools.SQL
}The key is the tool name the model uses in function_call. When a model emits a call for a registered name, OpenResponses dispatches it internally, appends the result, and continues the agentic loop — the client never sees the intermediate step.
Controlling tool use
tool_choice
Force or prevent tool use with the tool_choice field:
| Value | Behaviour |
|---|---|
"auto" (default) | Model decides whether to call a tool |
"required" | Model must call at least one tool |
"none" | Model may not call any tools |
{"type": "function", "function": {"name": "my_tool"}} | Model must call this specific tool |
{
"model": "gpt-4o",
"tool_choice": "required",
"tools": [...],
"input": [...]
}allowed_tools
Restrict which tools the model may call, regardless of what is defined in tools:
{
"model": "gpt-4o",
"tools": [{"name": "search"}, {"name": "calculator"}, {"name": "email"}],
"allowed_tools": ["search", "calculator"],
"input": [...]
}Calls to tools not in allowed_tools are rejected by OpenResponses.ToolPolicy before dispatch.
Error handling
If a hosted tool returns {:error, reason}, the error is formatted as a string and submitted back to the model as a function_call_output. The model can then handle the error gracefully in its response.
If a tool raises an exception, the loop catches it, submits an error output, and continues.