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.
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.
idstringThe provider's call id. Synthesized (call_0, call_1, …) for providers that
don't supply one — see the Gemini note below.
namestringWhich 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.
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.
noneToolChoiceNever call a tool this turn — answer directly.
requiredToolChoiceMust call at least one tool. (Maps to Anthropic's any, Gemini's ANY.)
{ name }ToolChoiceForce 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 + parsedWhen 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
Streaming
Stream a chat completion token-by-token with ai.stream() — textStream(), the normalized chunk union, final(), the single-consumption rule, and the no-fallback caveat.
Embeddings
Turn text into vectors with ai.embed() — a Pro feature. Which providers support it, batching, dimensions, and the normalized EmbedResult.