Skip to content

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.

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 json
from 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)

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.

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: false on 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: content next to tool_calls on Chat Completions, a message item next to the function_call on 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.

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_disabled
from 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_tool
def 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.

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/responses with Genius. Send {"type": "computer"} and you get computer_call items with pixel coordinates, as on OpenAI. Send truncation: "auto" so old screenshots are dropped when the history grows.
  • Your own strict function tools, such as click and type_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 with pixel_x = x * screenshot_width / 1000 and pixel_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.

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.

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.