Skip to content

Call your UraiJS tools

Urai runs tools for you. You name the tools the model can use. Urai supplies the schema, executes each call the model makes, feeds the result back to the model, and repeats until the model writes an answer.

This is the main difference from the OpenAI endpoint. You never receive a tool_calls array to run yourself. One request gives you the finished answer.

Send the standard OpenAI shape and set only function.name.

{
"model": "anthropic/claude-haiku-4-5",
"messages": [
{ "role": "user", "content": "Text +1 555 010 1234 that the build passed." }
],
"tools": [
{ "type": "function", "function": { "name": "twilio_sms" } }
]
}

The name is the name of a tool in your organization, as it appears in Developer Studio. Urai reads the schema from your organization’s UraiJS catalog.

Urai accepts description, parameters, and strict in the same object and ignores them. A tool specification that a typed OpenAI SDK builds therefore works without a change.

A UraiJS tool is one script. Each @tool method in the script is one function with its own name, description, and parameters.

class MessagingTools {
@tool
static async sendSms({ to, body }: { to: string; body: string }) {
// ...
}
@tool
static async listMessages({ since }: { since: string }) {
// ...
}
}

When you name the tool twilio_sms, the model receives every function that the tool declares. You cannot name a single function in tools[].

Two tools can declare a function with the same name. In that case the last tool wins and the earlier function becomes unreachable. Rename one of the functions.

tool_choice takes four forms.

Value Behaviour
"auto" The model decides. This is the default when you send tools.
"none" The model does not call a tool.
"required" The model must call a tool.
{"type":"function","function":{"name":"..."}} The model must call this one function.

The object form names a function, not a tool. Use a name from the tool’s declarations, such as sendSms, and not the tool name twilio_sms.

You cannot combine tool_choice: "required" with response_format. See Ask for JSON.

The top-level metadata object reaches each tool as meta.vars.

{
"model": "anthropic/claude-haiku-4-5",
"messages": [{ "role": "user", "content": "Send the invoice to the account owner." }],
"tools": [{ "type": "function", "function": { "name": "billing" } }],
"metadata": {
"account_id": "acct_8812",
"locale": "en-IN"
}
}

Inside the tool:

const accountId = meta.vars.account_id;

Urai always adds three more values: thread_id, organization_id, and user_id. These three overwrite any key of the same name in your metadata.

Treat metadata as untrusted input in the tool. It arrives from the caller, and Urai does not check it against any record.

A UraiJS tool declares the secrets it needs. Urai resolves them from your organization’s integrations each time the tool runs. The tool reads each one by name:

import { secrets } from "urai:secrets";
const key = await secrets.get("ACME_API_KEY");

If a secret is not connected, the tool returns an error to the model. The model tells the user that it could not do the work. The request itself still returns 200. Connect the secret on the Integrations page and try again.

Terminal window
curl https://chat.app.urai.dev/api/openai/v1/chat/completions \
-H "Authorization: Bearer $URAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "anthropic/claude-haiku-4-5",
"messages": [
{ "role": "user", "content": "How many open orders does this account have?" }
],
"tools": [
{ "type": "function", "function": { "name": "orders" } }
],
"metadata": { "account_id": "acct_8812" }
}'

Urai runs the turn like this:

  1. The model reads the question and the functions that the orders tool declares.
  2. The model calls listOrders with the arguments it chooses.
  3. Urai runs the function in the UraiJS sandbox with your metadata as meta.vars.
  4. Urai gives the result to the model.
  5. The model writes the answer, or calls another function and the loop repeats.

The response holds the answer only:

{
"id": "chatcmpl-...",
"object": "chat.completion",
"created": 1710000000,
"model": "anthropic/claude-haiku-4-5",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Account acct_8812 has 3 open orders." },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 812, "completion_tokens": 24, "total_tokens": 836 }
}

usage covers every round, not the last one.

Urai supplies four tools itself. Name them the same way as your own tools.

"tools": [{ "type": "function", "function": { "name": "web_search" } }]
Name What it does
web_search Finds pages on the web.
web_fetch Reads the full text of the pages.
search_documents Searches the organization’s knowledge collections.
read_document Reads one document from those collections in full.

Urai does not read these names from your UraiJS catalog. The schemas are built in.

For the two web tools, see Search and read the web. For the two document tools, see Search knowledge collections.

Result Cause
400 unknown_tool The name is not a built-in and not a tool in your organization. The message lists the names that failed.
400 tools_unavailable UraiJS is not available for this organization.
400 tool_not_configured The tool needs something the organization does not have, such as the Brave Search credential or the Jina credential.
An error in the answer text The tool ran and returned an error. The turn continues and the model reports the problem.

Two limits apply to each request:

  • A turn stops after 10 tool rounds. The response returns with finish_reason: "stop".
  • Streaming stays silent during tool rounds. Text resumes when the model writes the answer.

Urai ignores tool messages in the messages array, and it does not read tool_calls on an assistant message. A client-side tool loop therefore does not work against this endpoint.

Send the conversation as system, user, and assistant messages. Let Urai run the tools.