Function calling and tools
Function calling lets the model ask your code to run a function, such as a database lookup. ZenithAI
speaks the OpenAI format for this on /v1/chat/completions and /v1/responses, so the official OpenAI
SDKs and the OpenAI Agents SDK work on your own server. Mark your functions strict: true and every call
matches your JSON schema.
Function calling works on tiers whose capabilities.tools is true in /v1/models,
which is Smart and Genius on a standard server. Other tiers answer HTTP 400 unsupported_tools.
The model never runs anything itself. Your code runs the function and sends the result back.
How do I define a tool and run the loop?
Section titled “How do I define a tool and run the loop?”A tool has a name, a description and a JSON schema for its arguments. The loop has four steps: send the question with the tools, run each call the model returns, send the results back, and repeat until the model answers in text.
import jsonfrom openai import OpenAI
client = OpenAI(base_url="https://dummydomain/v1", api_key="YOUR_API_KEY")
tools = [{ "type": "function", "function": { "name": "get_order_status", "description": "Look up an order by its number", "strict": True, "parameters": { "type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"], "additionalProperties": False, }, },}]
def get_order_status(order_id): return {"order_id": order_id, "status": "shipped", "expected": "14-10-2026"}
messages = [{"role": "user", "content": "Where is my order SO-4471?"}]
for _ in range(5): # cap the rounds reply = client.chat.completions.create(model="Smart", messages=messages, tools=tools) message = reply.choices[0].message if not message.tool_calls: break messages.append(message) for call in message.tool_calls: result = get_order_status(**json.loads(call.function.arguments)) messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
print(message.content)import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://dummydomain/v1", apiKey: process.env.API_KEY });
const tools = [{ type: "function", function: { name: "get_order_status", description: "Look up an order by its number", strict: true, parameters: { type: "object", properties: { order_id: { type: "string" } }, required: ["order_id"], additionalProperties: false, }, },}];
const getOrderStatus = ({ order_id }) => ({ order_id, status: "shipped", expected: "14-10-2026" });
const messages = [{ role: "user", content: "Where is my order SO-4471?" }];let message;for (let round = 0; round < 5; round++) { // cap the rounds const reply = await client.chat.completions.create({ model: "Smart", messages, tools }); message = reply.choices[0].message; if (!message.tool_calls?.length) break; messages.push(message); for (const call of message.tool_calls) { const result = getOrderStatus(JSON.parse(call.function.arguments)); messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result) }); }}console.log(message.content);curl --fail-with-body "https://dummydomain/v1/chat/completions" \ -H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \ -d '{ "model": "Smart", "messages": [{"role": "user", "content": "Where is my order SO-4471?"}], "tools": [{"type": "function", "function": { "name": "get_order_status", "description": "Look up an order by its number", "strict": true, "parameters": { "type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"], "additionalProperties": false }}}] }'The reply has finish_reason: "tool_calls" and a tool_calls list. Run the function, then send the
earlier messages, that assistant message and one tool message per call:
{"role": "tool", "tool_call_id": "call_8f2c...", "content": "{\"status\": \"shipped\"}"}On /v1/responses the same tool is flat ({"type": "function", "name": ..., "parameters": ...}) and the
result goes back as a function_call_output item. See the Responses guide.
What does each tool_choice value do?
Section titled “What does each tool_choice value do?”tool_choice |
What the model may do |
|---|---|
auto (default) |
Answer in text, or call one or more of your functions |
required |
Must call a function |
{"type": "function", "function": {"name": "get_order_status"}} |
Must call that one function (on /v1/responses: {"type": "function", "name": "..."}) |
none |
Text only. No call is made |
parallel_tool_calls lets the model ask for several functions in one turn. The arguments of every call are
checked against your schema before you receive them, even when strict is left out. If a call still does
not match after one regeneration, you get a 502 invalid_tool_call error, never a partial call.
How do I make tool calls always match my schema?
Section titled “How do I make tool calls always match my schema?”Set strict: true on every function. With tool_choice set to auto or required, ZenithAI then
constrains generation while the call is written, so the arguments always match the function’s schema.
Required properties, enums, const, minItems and maxItems hold by construction. minLength and
pattern are checked after generation.
A strict schema must follow OpenAI’s rules:
- Describe an object, and set
additionalProperties: falseon every object. - List every property in
required. For an optional value, make its type nullable, such as["string", "null"].
{ "name": "click", "strict": true, "parameters": { "type": "object", "properties": { "x": {"type": "integer", "description": "Horizontal position on a 0 to 1000 grid"}, "y": {"type": "integer", "description": "Vertical position on a 0 to 1000 grid"} }, "required": ["x", "y"], "additionalProperties": false }}Strict mode applies only when every active function is strict: true and tool_choice is auto or
required. In that case:
- With
auto, a turn is one function call or a text answer. - With
required, a turn is one call. - A call can carry a short note, one sentence on what the model is doing:
contentnext totool_callson Chat Completions, amessageitem next to thefunction_callon Responses. OpenAI allows both. The note is not the final answer, so run the call and continue. The run is finished when a turn has text and no call. - A strict text answer that reaches the output limit returns the text so far with
finish_reason: "length"(status: "incomplete"on/v1/responses). - A strict turn returns at most one call. Run it, send the result, and ask again.
Mixed or non-strict tools, a named tool_choice and none work as before: the model writes the call in
its own way and the server checks it against the schema afterwards, with one regeneration.
What happens when tool_choice is none?
Section titled “What happens when tool_choice is none?”The request succeeds and the model answers in text. Send tool_choice: "none", or send no tools, on a
conversation that already contains tool calls and tool results. ZenithAI shows the earlier calls and
results to the model as plain text, so it can use what they found. Your own message history is not
changed.
This is how you end an agent loop at your step limit:
final = client.chat.completions.create( model="Smart", messages=messages, # the whole run so far, tool calls and results included tools=tools, tool_choice="none",)print(final.choices[0].message.content)Before this change, such a request could fail with a 502 tool_choice_violation error.
How does the final answer after tool use work?
Section titled “How does the final answer after tool use work?”When no tool can be called on a turn that follows tool use, ZenithAI asks the model for a real prose answer built from what the steps showed. It is told not to copy tool text, not to describe further tool calls, and never to invent links, prices or other details. If you asked for something the steps did not show, such as a product link, the model should say it was not seen.
This is a safeguard, not a guarantee. A small model can still get a detail wrong, so check any link or figure that matters. If your conversation ends with an assistant message that you wrote for the model to continue, nothing is added after it.
How do I use the OpenAI SDKs and the Agents SDK?
Section titled “How do I use the OpenAI SDKs and the Agents SDK?”The official OpenAI libraries need only the base URL and a tier name, as in the examples above. Their default retry setting is safe here: ZenithAI tells them which errors are not worth resending (see Errors).
The OpenAI Agents SDK works through
OpenAIResponsesModel. It sends its function tools with strict: true by default, so on ZenithAI their
calls are generated to match the schema without any extra setting.
from agents import Agent, OpenAIResponsesModel, Runner, function_tool, set_tracing_disabledfrom openai import AsyncOpenAI
set_tracing_disabled(True) # keep every prompt and answer on your own server
client = AsyncOpenAI(base_url="https://dummydomain/v1", api_key="YOUR_API_KEY")
@function_tooldef get_order_status(order_id: str) -> str: """Look up an order by its number.""" return f"Order {order_id} was shipped and is expected on 14-10-2026."
agent = Agent( name="Support desk", instructions="Answer customers briefly and politely.", model=OpenAIResponsesModel(model="Smart", openai_client=client), tools=[get_order_status],)print(Runner.run_sync(agent, "Where is my order SO-4471?").final_output)Set a turn limit on the runner (max_turns) so a confused run stops. Newer SDK fields such as
reasoning_effort and verbosity are accepted and have no effect; see
accepted and ignored fields.
How do I build a computer-use agent?
Section titled “How do I build a computer-use agent?”A computer-use agent looks at screenshots and asks your code to click, type and scroll. Your client owns the screen: it sends the screenshot, runs the action the model proposes, and sends the next screenshot. There are two ways to build one:
- OpenAI’s built-in computer tool, on
/v1/responseswith Genius. Send{"type": "computer"}and you getcomputer_callitems with pixel coordinates, as on OpenAI. Sendtruncation: "auto"so old screenshots are dropped when the history grows. - Your own strict function tools, such as
clickandtype_text, on either endpoint and on Smart or Genius. The vision models answer screen positions on a 0 to 1000 grid of the screenshot, so map them withpixel_x = x * screenshot_width / 1000andpixel_y = y * screenshot_height / 1000.
The Computer use guide has both ways in full, with runnable Python and Node code, error handling and safety advice.
How long can a tool-using answer be?
Section titled “How long can a tool-using answer be?”If you send no max_tokens, max_completion_tokens or max_output_tokens, the limit on a turn without
active tools is what the context window leaves, between 2,048 and 8,192 tokens. Turns with active tools use
the tier’s tool limit instead. Set the limit yourself for long answers. See the
request fields.
Related
Section titled “Related”- Chat Completions API reference and Responses API reference
- Computer use: build a screen agent, a full loop with screenshots, clicks and typing
- Structured JSON output, for schema-checked answers that are not tool calls
- Use your OpenAI client
- Errors and limits
Common questions
Does ZenithAI support OpenAI-style function calling?
Yes. Send tools and tool_choice to /v1/chat/completions or /v1/responses on a tier whose capabilities.tools is true, such as Smart or Genius. Your code runs the function and sends the result back, as with OpenAI.
How do I make tool calls always match my JSON schema?
Mark every function strict true, with additionalProperties false and every property required, and use tool_choice auto or required. The server then constrains generation so each call matches the schema.
What does tool_choice none do on ZenithAI?
The model may only write text. Earlier tool calls and results in the conversation are shown to it as plain text, so the request succeeds and you get a prose answer instead of an error.
Does the OpenAI Agents SDK work with ZenithAI tools?
Yes, through OpenAIResponsesModel. The SDK marks its function tools strict by default, so on ZenithAI their calls are generated to match the schema. Turn tracing off to keep data on your server.
Which tool_choice settings does strict mode change?
Only auto and required, and only when every active function is strict. A named tool_choice, tool_choice none and requests with any non-strict function behave as before.
How many tool calls does a strict turn return?
One. A strict turn is one call, or with auto a text answer. The call may come with a one-sentence note in content; that note is not the final answer. Run the call, send the result, and ask again. Do not rely on parallel calls in strict mode.
