Skip to content

to_openai

Python · package toolnexus · SPEC §4 · python/src/toolnexus/adapters.py

def to_openai(tools: list[Tool]) -> list[dict[str, Any]]

Turns a list[Tool] 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 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.

tk.to_openai() on a Toolkit is the same function applied to that toolkit’s tools — use it when you have a toolkit, and the free function when you have a bare list.

from toolnexus import to_openai, define_tool
def get_weather(city: str) -> str:
"""Current weather for a city."""
return f"sunny in {city}"
weather = define_tool(get_weather, name="get_weather", description="Current weather for a city")
schema = to_openai([weather])
assert len(schema) == 1
assert schema[0]["type"] == "function"
assert schema[0]["function"]["name"] == "get_weather"
assert schema[0]["function"]["description"] == "Current weather for a city"
# The schema was inferred from the type hints.
assert "city" in schema[0]["function"]["parameters"]["properties"]
print("ok:", schema[0]["function"]["name"])

Note the nesting: OpenAI wraps each tool in {"type": "function", "function": {...}}. 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 dicts, designed to be dropped into tools verbatim.

import json
from toolnexus import to_openai, define_tool
def search(q: str) -> str:
"""Search the docs."""
return f"results for {q}"
def ping() -> str:
"""Health check."""
return "pong"
tools = [
define_tool(search, name="search", description="Search the docs"),
define_tool(ping, name="ping", description="Health check"),
]
body = {
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "search for adapters"}],
"tools": to_openai(tools),
}
# Order is preserved, one entry per tool.
assert len(body["tools"]) == 2
assert [t["function"]["name"] for t in body["tools"]] == ["search", "ping"]
# Plain JSON — serializes with no custom encoder.
assert len(json.dumps(body)) > 0
print("ok:", ", ".join(t["function"]["name"] for t in body["tools"]))

Schema out, tool call in. The name the model returns is the same name you look up.

import asyncio
import json
from toolnexus import to_openai, define_tool
def get_weather(city: str) -> str:
"""Current weather for a city."""
return f"sunny in {city}"
tools = [define_tool(get_weather, name="get_weather", description="Current weather for a city")]
schema = to_openai(tools)
# What a model would send back for that schema.
tool_call = {
"id": "call_1",
"type": "function",
"function": {"name": "get_weather", "arguments": '{"city":"Chennai"}'},
}
async def main():
# Look the tool up by the name you advertised, then execute it.
called = next(t for t in tools if t.name == tool_call["function"]["name"])
res = await called.execute(json.loads(tool_call["function"]["arguments"]))
assert res.output == "sunny in Chennai"
assert res.is_error is False
# An empty tool list is valid — it just means "no tools this turn".
assert to_openai([]) == []
print("ok:", schema[0]["function"]["name"], "->", res.output)
asyncio.run(main())
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_schemaparameters.