Tool Dispatch

View Source

OpenResponses 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
end

execute/2 receives:

  • arguments — the parsed JSON arguments from the model (a map with string keys)
  • context — a map with request context, including response_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:

ValueBehaviour
"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.