OpenAI SDK tools
The outbound tools as OpenAI function definitions for the Responses and Chat Completions APIs, with one function that runs a call from the model and returns the string to send back.
openaiTools(outbound) from outbound-sdk/tools returns the tools in the shape the OpenAI SDK takes. It is built without importing openai, so it adds no dependency, and it works with any client that accepts the same function-tool shape.
It returns three things:
| Property | What it is |
|---|---|
tools |
Function tools for the Responses API: type, name, description, parameters, strict. Pass as tools to client.responses.create. |
chatTools |
The same tools for Chat Completions, each nested under function. Pass as tools to client.chat.completions.create. |
handle(name, argumentsJson) |
Runs one function call. Takes the call’s name and its arguments string, returns the result as a JSON string. |
openaiTools takes the same only and extra options as createTools.
Build the tools and run a call
import { createOutbound } from "outbound-sdk";
import { demoData, memory } from "outbound-sdk/memory";
import { openaiTools } from "outbound-sdk/tools";
const outbound = createOutbound({
adapter: memory({ data: demoData(), workspaceId: "acme" }),
});
const { chatTools, handle, tools } = openaiTools(outbound);
// `tools` is the Responses API shape, `chatTools` the Chat Completions shape.
const [first] = tools;
console.log(tools.length, "tools");
console.log({
description: `${first?.description.slice(0, 40)}...`,
name: first?.name,
parameters: "(JSON Schema)",
strict: first?.strict,
type: first?.type,
});
console.log(Object.keys(chatTools[0] ?? {}), chatTools[0]?.function.name);
// `handle` takes the call's name and its `arguments` string, and returns the
// string to send back as the call's output.
console.log(await handle("campaign_overview", '{"status":"paused","limit":1}'));
// It never throws. A tool the set does not have and arguments that are not
// JSON are results too.
console.log(await handle("drop_database", "{}"));
console.log(await handle("sender_health", "{not json"));
9 tools
{
description: 'How every campaign is doing right now: s...',
name: 'campaign_overview',
parameters: '(JSON Schema)',
strict: false,
type: 'function'
}
[ 'function', 'type' ] campaign_overview
{"data":{"campaigns":[{"campaign":"memory:acme:cmp_agencies","channel":"email","name":"Agency owners","metrics":{"bounce_rate":0.0667,"bounced":4,"connections_accepted":null,"connections_sent":null,"contacted":60,"opened":18,"positive":0,"positive_reply_rate":0,"replied":1,"reply_rate":0.0167,"reply_rate_basis":"contacted","sent":60},"status":"paused"}]},"meta":{"evidence":"exact","provider":"memory","requests":1,"today":"2026-11-14"},"next":"Use campaign_trend for one campaign over time, or winning_steps to see which step drives replies.","ok":true}
{"error":{"code":"Invalid","message":"Unknown tool drop_database"},"next":"Call one of the listed tools with a JSON object.","ok":false}
{"error":{"code":"Invalid","message":"The arguments are not valid JSON"},"next":"Call one of the listed tools with a JSON object.","ok":false}
strictisfalse. The schemas have optional fields, which OpenAI’s strict mode does not accept. The tool validates the arguments itself and reports anything wrong as a result.- An empty
argumentsstring means no arguments.handlereads it as{}. handlenever throws. An unknown tool name and arguments that are not JSON come back as{ ok: false }with the codeInvalid.
Answer the model’s calls
When a response contains function_call items, run each through handle and send the outputs back in the next request.
import { createOutbound } from "outbound-sdk";
import { demoData, memory } from "outbound-sdk/memory";
import { openaiTools } from "outbound-sdk/tools";
const outbound = createOutbound({
adapter: memory({ data: demoData(), workspaceId: "acme" }),
});
const { handle } = openaiTools(outbound);
/** A `function_call` item from a Responses API response. */
interface FunctionCall {
readonly type: "function_call";
readonly call_id: string;
readonly name: string;
readonly arguments: string;
}
/** Runs each call and builds the `function_call_output` items to send back. */
const answer = (calls: readonly FunctionCall[]) =>
Promise.all(
calls.map(async (call) => ({
call_id: call.call_id,
output: await handle(call.name, call.arguments),
type: "function_call_output" as const,
}))
);
// Written by hand in the shape a model emits. No model was called.
const outputs = await answer([
{
arguments: '{"limit":1}',
call_id: "call_1",
name: "sender_health",
type: "function_call",
},
]);
console.log(JSON.stringify(outputs, null, 2));
[
{
"call_id": "call_1",
"output": "{\"data\":{\"senders\":[{\"daily_limit\":40,\"kind\":\"mailbox\",\"sender\":\"memory:acme:mbx_1\",\"status\":\"active\",\"warmup_enabled\":true,\"warmup_score\":96}],\"unhealthy\":0},\"meta\":{\"evidence\":\"exact\",\"provider\":\"memory\",\"requests\":0,\"today\":\"2026-11-14\"},\"next\":\"A sender address is not included. Look a sender up by its ref if you need more.\",\"ok\":true}",
"type": "function_call_output"
}
]
Wiring it to a model
The loop around these pieces is yours:
- Call
client.responses.createwith your model, your input andtools. - For each
function_callitem in the output, callhandle(item.name, item.arguments). - Append a
function_call_outputitem with the samecall_idand the returned string, and call the API again. - Stop when a response has no function calls.
With Chat Completions, pass chatTools, read message.tool_calls, and answer each with a tool message whose content is the string handle returned.
Reply text
A model given reply_samples can ask for reply text. It comes back truncated and marked as untrusted. See untrusted content.