Radondocs

Tool calling

Define one tool schema and it works on every provider. The request-run-respond loop, toolChoice, parsed toolCalls, parallel calls, and InvalidToolArgumentsError.

Tool calling (a.k.a. function calling) lets a model ask your code to run a function — look up the weather, query a database, call an API — and then continue with the result. It is the reason Radon AI exists: the three provider families structure function-calling completely differently, and Radon bridges all of it so you define a tool once and it works everywhere.

Tool calling is a core, free capability on every provider — never a Pro upsell.

Define a tool

A tool is a plain object: a name, an optional description, and a JSON-Schema parameters object describing its arguments. You write this once, in one shape.

Define a tool
const tools = [
  {
    name: "get_weather",
    description: "Get the current weather for a city.",
    parameters: {
      type: "object",
      properties: {
        city: { type: "string", description: "City name" },
      },
      required: ["city"],
    },
  },
];

Radon translates this into OpenAI's { type: "function", function: {...} }, Anthropic's { name, input_schema }, and Gemini's functionDeclarations — and normalizes the calls that come back.

Read the calls

Pass tools to chat(). If the model decides to call one, the calls come back on res.toolCalls, normalized so arguments is always a parsed object — never a JSON string you have to decode.

const res = await ai.chat({
  messages: [{ role: "user", content: "What's the weather in Lagos?" }],
  tools,
});

for (const call of res.toolCalls) {
  console.log(call.id);        // "call_abc123"
  console.log(call.name);      // "get_weather"
  console.log(call.arguments); // { city: "Lagos" }  — a parsed object
}

When the model chose to call a tool, res.finishReason is "tool_calls" and res.content is usually empty.

idstring

The provider's call id. Synthesized (call_0, call_1, …) for providers that don't supply one — see the Gemini note below.

namestring

Which tool the model wants to call.

argumentsRecord<string, unknown>

The parsed arguments object, normalized across all providers.

The full loop

Real tool use is a round trip: the model asks, you run the tool, you send the result back, the model answers. The message shape for feeding results back is the same on every provider.

Request → run → respond
const messages = [{ role: "user", content: "What's the weather in Lagos?" }];

// 1) Model asks to call a tool.
const res = await ai.chat({ messages, tools });

// 2) Run each requested tool and record the assistant turn + tool results.
messages.push({ role: "assistant", content: res.content, toolCalls: res.toolCalls });

for (const call of res.toolCalls) {
  const result = await runYourTool(call.name, call.arguments);
  messages.push({
    role: "tool",
    toolCallId: call.id,
    name: call.name,
    content: JSON.stringify(result),
  });
}

// 3) Ask again with the results in context — now the model can answer.
const final = await ai.chat({ messages, tools });
console.log(final.content); // "It's 29°C and sunny in Lagos."

Include both the assistant turn and the tool results

The assistant message carrying toolCalls and the role: "tool" result messages must both be in the array on the follow-up call. Radon maps them to each provider's convention — OpenAI role: "tool" messages, Anthropic tool_result blocks inside a user turn, Gemini functionResponse parts — but it needs both halves to reconstruct the exchange.

Controlling tool use

toolChoice decides how the model may use tools this turn. Radon maps it onto each provider's own scheme.

autoToolChoicedefault: "auto"

The model decides whether to call a tool. The default.

noneToolChoice

Never call a tool this turn — answer directly.

requiredToolChoice

Must call at least one tool. (Maps to Anthropic's any, Gemini's ANY.)

{ name }ToolChoice

Force a specific tool by name, e.g. { name: "get_weather" }.

await ai.chat({ messages, tools, toolChoice: "required" });
await ai.chat({ messages, tools, toolChoice: { name: "get_weather" } });

Parallel tool calls

A model may request several tools in one turn. Radon preserves them in order on every provider, so res.toolCalls can have more than one entry — run them all and push a role: "tool" message for each.

for (const call of res.toolCalls) {
  const result = await runYourTool(call.name, call.arguments);
  messages.push({ role: "tool", toolCallId: call.id, name: call.name, content: JSON.stringify(result) });
}

Streaming tool calls

While streaming, tool calls arrive as tool_call fragments, but Radon reassembles them: the terminal finish chunk's message.toolCalls (and stream.final()) contains complete, parsed calls — the same shape as chat().

const stream = ai.stream({ messages, tools });
const result = await stream.final();
result.toolCalls; // fully assembled + parsed

When the model produces bad JSON

If a model emits tool-call arguments that aren't valid JSON, Radon throws a typed InvalidToolArgumentsError rather than handing you a broken string. The raw string the model produced is on error.rawArguments, so you can log it or attempt a repair.

import { InvalidToolArgumentsError } from "@radonsdk/ai";

try {
  const res = await ai.chat({ messages, tools });
} catch (err) {
  if (err instanceof InvalidToolArgumentsError) {
    console.error(`Bad args for ${err.toolName}:`, err.rawArguments);
  }
}

Provider notes

Gemini synthesizes call ids and matches on name

Gemini's functionCall parts carry no id, so Radon assigns call_0, call_1, … in order. When you send the result back, Radon keys it by the tool name (which is what Gemini's functionResponse matches on), so the synthesized id is for your own bookkeeping — always set the name on your role: "tool" messages.

Gemini's schema is stricter

Gemini accepts only a subset of JSON Schema. Radon strips the keywords it rejects ($schema, $id, $ref, $defs, definitions, additionalProperties) so a schema authored for OpenAI or Anthropic still works. Keep tool schemas to the common core (types, properties, required, enum, description) for maximum portability.

Next steps

On this page