Tool Calling
Define your own tools in OpenAI wire form and get real tool calls back, streaming or not.
The provider accepts a standard tools array. When you define tools, the model can call them: the response carries finish_reason: "tool_calls" and the call appears in tool_calls with a name and JSON arguments. This works on both the streaming and non-streaming paths.
json
{
"model": "zyraxon-codex",
"messages": [{ "role": "user", "content": "Save a hello world file." }],
"tools": [
{
"type": "function",
"function": {
"name": "write_file",
"description": "Write text to a file on disk.",
"parameters": {
"type": "object",
"properties": {
"path": { "type": "string" },
"content": { "type": "string" }
},
"required": ["path", "content"]
}
}
}
]
}The reply ends the turn with the call rather than a prose answer:
json
{
"choices": [
{
"finish_reason": "tool_calls",
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "write_file",
"arguments": "{\"path\":\"hello.txt\",\"content\":\"Hello, world\"}"
}
}
]
}
}
]
}The loop
- 01Send your
toolsarray with the user message. - 02The model replies with
finish_reason: "tool_calls"and one or more calls. - 03Run each call yourself, and append a message with
role: "tool", the matchingtool_call_id, and the result. - 04Call the endpoint again with the extended message list. Repeat until
finish_reasonisstop.
⚠
Send the assistant's tool call back, not only the result
The message list you send on the second call must include the assistant message that asked for the tools — with its
tool_calls array and their ids — followed by the role: "tool" result that names the same tool_call_id. Sending only the tool result leaves the result unmatched, and a model that cannot match a result to its call will ask for the same tool again instead of answering.⚠
You execute the tools
The provider does not run your tools. It returns the call; your code performs the action and sends the result back. That is what keeps the model from touching anything you did not authorise.
ℹ
Arguments are a JSON string
As in the OpenAI schema,
function.arguments is a string containing JSON, not an object. Parse it before use. Streamed calls arrive as fragments — accumulate them by index, then parse.The second request, in full
This is the request that turns a tool call into an answer. The assistant message that requested the tools is echoed back verbatim, then the result is appended as a role: "tool" message carrying the same tool_call_id.
json
{
"model": "zyraxon-codex",
"messages": [
{ "role": "user", "content": "Save a hello world file." },
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "write_file",
"arguments": "{\"path\":\"hello.txt\",\"content\":\"Hello, world\"}"
}
}
]
},
{ "role": "tool", "tool_call_id": "call_abc123", "content": "wrote hello.txt" }
],
"tools": [ /* the same tools array as before */ ]
}✓
How you know it worked
The reply now has
finish_reason: "stop" and a normal content answer. If you get finish_reason: "tool_calls" asking for the same tool again, the assistant's tool_calls block is missing from what you sent — the model never saw its own call, so it cannot see the result either.