Tool calling turns an LLM into a controller. You describe tools as JSON schemas; the model returns a structured call; your code executes it and feeds the result back as a tool message.
ts
const tools = [{
type: "function",
function: {
name: "get_gas_price",
description: "Current gas price in gwei for an EVM chain",
parameters: { type: "object", properties: { chainId: { type: "integer" } }, required: ["chainId"], additionalProperties: false },
},
}];
let messages = [{ role: "user", content: "Is Base cheap right now?" }];
const first = await client.chat.completions.create({ model, messages, tools });
for (const call of first.choices[0].message.tool_calls ?? []) {
const args = JSON.parse(call.function.arguments);
const result = await getGasPrice(args.chainId); // you run it, with your own authz
messages.push(first.choices[0].message, { role: "tool", tool_call_id: call.id, content: JSON.stringify(result) });
}
const final = await client.chat.completions.create({ model, messages, tools });Design rules: few tools with sharp descriptions beat many vague ones; validate arguments server-side; make tools idempotent where possible; return compact results (the model pays for every token).
Gotcha: never let the model's arguments decide authorisation. Check permissions in your handler against the real session, not against anything the model said.