Toolnexus.Adapters.to_openai
Elixir · package toolnexus · SPEC §4 · elixir/lib/toolnexus/adapters.ex
@spec to_openai([Toolnexus.Tool.t()]) :: [map()]def to_openai(tools)Turns a list of tools into the tools array an OpenAI-shaped chat completion expects. This is the
bridge between “toolnexus knows about these tools” and “the model can call them”.
When to use it
Section titled “When to use it”When you are driving the LLM call yourself and need schema to put in the request body. Every OpenAI-compatible endpoint takes this shape — OpenAI, OpenRouter, Groq, Together, a local Ollama, or your own gateway.
Why this and not the client
Section titled “Why this and not the client”Toolnexus.Toolkit.to_openai/1 takes a toolkit and delegates here with its tools — use that when
you have a toolkit, and this when you have a bare list.
Examples
Section titled “Examples”1. One tool to OpenAI schema
Section titled “1. One tool to OpenAI schema”Keys in the emitted maps are strings, not atoms — this is wire data headed for JSON.
alias Toolnexus.{Adapters, Native}
weather = Native.define_tool(%{ name: "get_weather", description: "Current weather for a city", input_schema: %{ "type" => "object", "properties" => %{"city" => %{"type" => "string"}}, "required" => ["city"] }, execute: fn args, _ctx -> "sunny in #{args["city"]}" end })
schema = Adapters.to_openai([weather])
1 = length(schema)entry = hd(schema)true = entry["type"] == "function" or entry[:type] == "function"
fn_block = entry["function"] || entry[:function]true = fn_block["name"] == "get_weather" or fn_block[:name] == "get_weather"
IO.puts("ok: #{fn_block["name"] || fn_block[:name]}")Note the nesting: OpenAI wraps each tool in a "function" block. The input_schema on a tool
becomes function.parameters — the key is renamed.
2. Feeding it straight into a request body
Section titled “2. Feeding it straight into a request body”The output is plain maps and lists, so Jason.encode!/1 handles it with no custom encoder.
alias Toolnexus.{Adapters, Native}
mk = fn name, desc -> Native.define_tool(%{ name: name, description: desc, input_schema: %{"type" => "object", "properties" => %{}}, execute: fn _args, _ctx -> name end })end
tools = [mk.("search", "Search the docs"), mk.("ping", "Health check")]
body = %{ "model" => "gpt-4o-mini", "messages" => [%{"role" => "user", "content" => "search for adapters"}], "tools" => Adapters.to_openai(tools)}
entries = body["tools"]2 = length(entries)
# Order is preserved.names = Enum.map(entries, fn e -> f = e["function"] || e[:function] f["name"] || f[:name] end)
true = names == ["search", "ping"]
IO.puts("ok: #{Enum.join(names, ", ")}")3. Round-tripping a call back to the tool
Section titled “3. Round-tripping a call back to the tool”Schema out, tool call in. The name the model returns is the same name you look up.
alias Toolnexus.{Adapters, Context, Native}
weather = Native.define_tool(%{ name: "get_weather", description: "Current weather for a city", input_schema: %{ "type" => "object", "properties" => %{"city" => %{"type" => "string"}}, "required" => ["city"] }, execute: fn args, _ctx -> "sunny in #{args["city"]}" end })
tools = [weather]
# What a model would send back. OpenAI encodes arguments as a JSON STRING;# decode it before calling the tool.called_name = "get_weather"args = %{"city" => "Chennai"}
called = Enum.find(tools, &(&1.name == called_name))true = called != nil
res = called.execute.(args, %Context{})true = res.output == "sunny in Chennai"false = res.is_error
# An empty tool list is valid — it just means "no tools this turn".[] = Adapters.to_openai([])
IO.puts("ok: #{called_name} -> #{res.output}")| Path | From | Notes |
|---|---|---|
[]["type"] |
— | Always the literal "function". |
[]["function"]["name"] |
Tool.name |
What the model calls back with. |
[]["function"]["description"] |
Tool.description |
|
[]["function"]["parameters"] |
Tool.input_schema |
Renamed — input_schema → parameters. |
See also
Section titled “See also”Adapters.to_anthropic·Adapters.to_geminiToolnexus.create_toolkit—Toolkit.to_openai/1delegates hereToolnexus.Client.create— calls the adapter for you