Tool calling
Tool calling lets the model ask your code to do something, such as look up an order or run a query, and then use the result in its answer. Belarel relays the call to you. It never executes your tools: your code runs them, on your side, and sends the result back in the next request.
Belarel can also run one tool itself: the provider’s native web search, which you turn on per request.
Declare your tools
Section titled “Declare your tools”curl https://api.belarel.com/v1/chat/completions \ -H "Authorization: Bearer $BELAREL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-5", "messages": [{"role": "user", "content": "Where is order 7731?"}], "tools": [{ "type": "function", "function": { "name": "get_order_status", "description": "Returns the shipping status of an order.", "parameters": { "type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"] } } }] }'import jsonimport osfrom openai import OpenAI
client = OpenAI(base_url="https://api.belarel.com/v1", api_key=os.environ["BELAREL_API_KEY"])
tools = [{ "type": "function", "function": { "name": "get_order_status", "description": "Returns the shipping status of an order.", "parameters": { "type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"], }, },}]
def get_order_status(order_id: str) -> dict: return {"order_id": order_id, "status": "shipped"} # your code
messages = [{"role": "user", "content": "Where is order 7731?"}]first = client.chat.completions.create(model="openai/gpt-5", messages=messages, tools=tools)reply = first.choices[0].message
if first.choices[0].finish_reason == "tool_calls": messages.append(reply.model_dump(exclude_none=True)) # the assistant turn, with its tool_calls for call in reply.tool_calls: result = get_order_status(**json.loads(call.function.arguments)) messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)}) final = client.chat.completions.create(model="openai/gpt-5", messages=messages, tools=tools) print(final.choices[0].message.content)import OpenAI from 'openai';
const client = new OpenAI({ baseURL: 'https://api.belarel.com/v1', apiKey: process.env.BELAREL_API_KEY });
const tools: OpenAI.ChatCompletionTool[] = [{ type: 'function', function: { name: 'get_order_status', description: 'Returns the shipping status of an order.', parameters: { type: 'object', properties: { order_id: { type: 'string' } }, required: ['order_id'], }, },}];
const getOrderStatus = (orderId: string) => ({ order_id: orderId, status: 'shipped' }); // your code
const messages: OpenAI.ChatCompletionMessageParam[] = [{ role: 'user', content: 'Where is order 7731?' }];const first = await client.chat.completions.create({ model: 'openai/gpt-5', messages, tools });const reply = first.choices[0].message;
if (first.choices[0].finish_reason === 'tool_calls' && reply.tool_calls) { messages.push(reply); for (const call of reply.tool_calls) { if (call.type !== 'function') continue; const { order_id } = JSON.parse(call.function.arguments); messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(getOrderStatus(order_id)) }); } const final = await client.chat.completions.create({ model: 'openai/gpt-5', messages, tools }); console.log(final.choices[0].message.content);}Rules for tool definitions
Section titled “Rules for tool definitions”- Only
type: "function"tools are read. - Up to 24 tools per request. Entries beyond the 24th are dropped without an error — the model never sees them — and skipped entries (below) still count toward the 24. A name is 1 to 64 letters, digits,
_or-. parametersis a JSON Schema object of at most 16,000 characters. If you omit it, the tool takes an empty object.- An invalid entry (wrong type, bad or duplicate name, oversized schema) is skipped, not refused. The model simply does not see it.
parallel_tool_callsis accepted and has no effect.
Tool choice
Section titled “Tool choice”tool_choice |
Effect |
|---|---|
Omitted or "auto" |
The model decides whether to call a tool. |
"none" |
Passed to the model: your tools stay declared, so a conversation that already contains tool calls still reads correctly, and the model answers without calling any. |
"required", or a named function |
400 unsupported_parameter, param: "tool_choice". Belarel cannot force a tool call. If one must happen, check finish_reason and handle the case where the model answered in text. |
The same values apply on the Responses API.
Models that cannot call tools
Section titled “Models that cannot call tools”If you send tools to a model that cannot call them (belarel.capabilities.tools: false in GET /v1/models), the call is refused before the model runs, so it costs nothing:
{ "error": { "message": "Model \"<model-id>\" cannot call tools", "type": "invalid_request_error", "code": "model_lacks_tools", "param": "tools" }}Pick a model whose belarel.capabilities.tools is true and whose supported_parameters include tools, or send the request without tools.
The call, and the result you send back
Section titled “The call, and the result you send back”When the model calls one of your tools, the turn ends:
finish_reasonistool_calls.message.tool_callsholds each call:id,type: "function",function.nameandfunction.arguments(a JSON string).
To continue, send the whole conversation again, adding:
- the assistant message, with its
tool_calls; - one
role: "tool"message per call, with the matchingtool_call_idand the result ascontent. The content is read as text: serialize objects to JSON.
When streaming, each call arrives in one chunk with complete arguments. See Streaming.
On the Responses API
Section titled “On the Responses API”Tools use the flat Responses shape: {"type": "function", "name", "description", "parameters"}. A call comes back as a function_call output item with a call_id. Send it back in input, followed by a function_call_output item with the same call_id and your result as output. Tool types other than function and web_search return 400 unsupported_parameter. See Responses API.
Web search
Section titled “Web search”Web search lets the model ground its answer in current web results. The search runs at the model’s provider, not in your code.
| Dialect | How to turn it on |
|---|---|
| Chat Completions | "web_search": true in the request body (a Belarel extension). OpenAI’s web_search_options is not supported and returns 400 unsupported_parameter. |
| Responses | {"type": "web_search"} (or web_search_preview) in tools. |
curl https://api.belarel.com/v1/responses \ -H "Authorization: Bearer $BELAREL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "openai/gpt-5", "input": "What changed in the latest TLS RFC?", "tools": [{"type": "web_search"}]}'completion = client.chat.completions.create( model="openai/gpt-5", messages=[{"role": "user", "content": "What changed in the latest TLS RFC?"}], extra_body={"web_search": True},)const completion = await client.chat.completions.create({ model: 'openai/gpt-5', messages: [{ role: 'user', content: 'What changed in the latest TLS RFC?' }], web_search: true,} as OpenAI.ChatCompletionCreateParamsNonStreaming & { web_search: boolean });Which models. Web search is available on models whose belarel.capabilities.web_search is true in GET /v1/models. It uses each provider’s native search, for OpenAI, Anthropic, Google, xAI and Perplexity models. Perplexity models search on every turn.
When it is refused. Belarel never returns an ungrounded answer as if it had searched. It refuses instead:
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
web_search_not_allowed |
403 | Web search is not available for this model in your organization. | Pick a model with web_search: true, or ask your organization admin. |
web_search_unavailable |
400 | This model has no provider-native search where it is served. | Pick another model. |
Sources. On Responses, the message’s output_text carries url_citation annotations, and a web_search_call item marks that a search ran. On Chat Completions, sources arrive while streaming, as chunks with a top-level citations array of {url, title}. A non-streamed chat completion does not include them. Stream, or use the Responses API, if you need sources.
Do not name your own tools web_search or google_search when search is on. A tool of yours whose name collides with the search tool is dropped.

