Function Calling¶
Function calling allows the assistant to invoke custom tools that run in your application. You define what functions are available, the assistant decides when to call them, and you execute them and return the results.
Overview¶
The flow has four steps:
- Declare your functions in the
toolsarray - Detect when the assistant wants to call a function (
requires_actionstatus orfunction_call_arguments.doneevent) - Execute the function in your application
- Return the result in a follow-up request
Your App Nodexa Platform
│ │
│─── POST /v1/responses ────────────>│
│ (model, input, tools) │
│ │ (processes, decides to call tool)
│<── response (requires_action) ─────│
│ function_call: { │
│ name: "get_weather", │
│ call_id: "call_abc", │
│ arguments: '{"city":"Paris"}' │
│ } │
│ │
│─── [execute get_weather locally] ──│
│ │
│─── POST /v1/responses ────────────>│
│ (previous_response_id, │
│ function_call_output) │
│ │
│<── response (completed) ───────────│
│ "The weather in Paris is 18°C" │
Step 1 — Declare Your Functions¶
Include function definitions in the tools array:
{
"model": "YOUR_ASSISTANT_ID",
"input": "What's the weather in Paris right now?",
"tools": [
{
"type": "function",
"name": "get_weather",
"description": "Get the current weather conditions for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The name of the city, e.g. 'Paris' or 'New York'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit. Defaults to celsius."
}
},
"required": ["city"]
}
}
]
}
Function name constraints:
- Alphanumeric characters, underscores (
_), and hyphens (-) only - Maximum 64 characters
- Must be unique within the request (max 128 tools total)
Step 2 — Detect the Tool Call¶
Non-streaming¶
When the assistant wants to call a function, the response has status: "requires_action" and the output array contains a function_call item:
{
"id": "resp_01234567-89ab-cdef-0123-456789abcdef",
"status": "requires_action",
"output": [
{
"type": "function_call",
"name": "get_weather",
"call_id": "call_abc123",
"arguments": "{\"city\": \"Paris\", \"unit\": \"celsius\"}"
}
]
}
Streaming¶
When streaming, listen for response.function_call_arguments.done:
for await (const event of stream) {
if (event.type === 'response.function_call_arguments.done') {
console.log('Function to call:', event.name);
console.log('Call ID:', event.call_id);
console.log('Arguments:', event.arguments); // JSON string
}
}
You may also see response.function_call_arguments.delta events as the arguments are being assembled — you can use these to show a "thinking" indicator.
Step 3 — Execute the Function¶
Parse the arguments string and call your function:
const toolCall = response.output.find(item => item.type === 'function_call');
const args = JSON.parse(toolCall.arguments);
let result;
switch (toolCall.name) {
case 'get_weather':
result = await getWeather(args.city, args.unit ?? 'celsius');
break;
default:
result = { error: `Unknown function: ${toolCall.name}` };
}
Always handle unknown functions
The assistant should only call functions you declared, but defensively handling unknown names prevents crashes and allows you to return a meaningful error to the assistant.
Step 4 — Return the Result¶
Send a follow-up request with:
previous_response_idset to the ID of therequires_actionresponseinputcontaining afunction_call_outputitem
{
"model": "YOUR_ASSISTANT_ID",
"previous_response_id": "resp_01234567-89ab-cdef-0123-456789abcdef",
"input": [
{
"type": "function_call_output",
"call_id": "call_abc123",
"output": "{\"temperature\": 18, \"unit\": \"celsius\", \"conditions\": \"partly cloudy\"}"
}
]
}
| Field | Type | Description |
|---|---|---|
type |
"function_call_output" |
Identifies this as a tool result |
call_id |
string |
Must match the call_id from the function call item |
output |
string |
The function's return value — must be a JSON-encoded string |
Output must be a JSON string
The output field is a string, not an object. Serialize your result with JSON.stringify() before setting it. The assistant will parse the JSON to understand the result.
Complete Example¶
import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'https://your-admin.example.com/v1',
apiKey: process.env.NODEXA_API_KEY,
});
// Simulated weather function
async function getWeather(city, unit = 'celsius') {
// In a real app, call a weather API here
return {
city,
temperature: 18,
unit,
conditions: 'partly cloudy',
humidity: 65,
};
}
async function chatWithTools(userMessage) {
// Step 1: Initial request with tool declarations
let response = await client.responses.create({
model: process.env.NODEXA_ASSISTANT_ID,
input: userMessage,
tools: [
{
type: 'function',
name: 'get_weather',
description: 'Get current weather for a city',
parameters: {
type: 'object',
properties: {
city: { type: 'string', description: 'City name' },
unit: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
description: 'Temperature unit',
},
},
required: ['city'],
},
},
],
});
// Step 2: Handle tool calls in a loop (model may call tools multiple times)
while (response.status === 'requires_action') {
const toolResults = [];
for (const item of response.output) {
if (item.type !== 'function_call') continue;
const args = JSON.parse(item.arguments);
console.log(`Calling ${item.name}(${JSON.stringify(args)})`);
let result;
if (item.name === 'get_weather') {
result = await getWeather(args.city, args.unit);
} else {
result = { error: `Unknown function: ${item.name}` };
}
toolResults.push({
type: 'function_call_output',
call_id: item.call_id,
output: JSON.stringify(result),
});
}
// Step 3: Send results back
response = await client.responses.create({
model: process.env.NODEXA_ASSISTANT_ID,
previous_response_id: response.id,
input: toolResults,
});
}
return response.output_text;
}
const answer = await chatWithTools('What is the weather in Paris right now?');
console.log(answer);
// => "The current weather in Paris is 18°C, partly cloudy with 65% humidity."
import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'https://your-admin.example.com/v1',
apiKey: process.env.NODEXA_API_KEY,
});
async function getWeather(city, unit = 'celsius') {
return { city, temperature: 18, unit, conditions: 'partly cloudy' };
}
async function chatWithToolsStreaming(userMessage) {
const toolDefs = [
{
type: 'function',
name: 'get_weather',
description: 'Get current weather for a city',
parameters: {
type: 'object',
properties: {
city: { type: 'string' },
unit: { type: 'string', enum: ['celsius', 'fahrenheit'] },
},
required: ['city'],
},
},
];
let stream = await client.responses.create({
model: process.env.NODEXA_ASSISTANT_ID,
input: userMessage,
tools: toolDefs,
stream: true,
});
let responseId = null;
let responseStatus = null;
const pendingToolCalls = [];
for await (const event of stream) {
switch (event.type) {
case 'response.output_text.delta':
process.stdout.write(event.delta);
break;
case 'response.function_call_arguments.done':
pendingToolCalls.push({
name: event.name,
call_id: event.call_id,
arguments: event.arguments,
});
break;
case 'response.completed':
responseId = event.response.id;
responseStatus = event.response.status;
break;
}
}
// Handle requires_action
if (responseStatus === 'requires_action' && pendingToolCalls.length > 0) {
const toolResults = await Promise.all(
pendingToolCalls.map(async (call) => {
const args = JSON.parse(call.arguments);
let result;
if (call.name === 'get_weather') {
result = await getWeather(args.city, args.unit);
} else {
result = { error: `Unknown function: ${call.name}` };
}
return {
type: 'function_call_output',
call_id: call.call_id,
output: JSON.stringify(result),
};
})
);
// Continue in the same thread with results
const finalStream = await client.responses.create({
model: process.env.NODEXA_ASSISTANT_ID,
previous_response_id: responseId,
input: toolResults,
stream: true,
});
for await (const event of finalStream) {
if (event.type === 'response.output_text.delta') {
process.stdout.write(event.delta);
}
}
console.log();
}
}
await chatWithToolsStreaming('What is the weather in Paris?');
import os
import json
from openai import OpenAI
client = OpenAI(
base_url="https://your-admin.example.com/v1",
api_key=os.environ['NODEXA_API_KEY'],
)
def get_weather(city: str, unit: str = "celsius") -> dict:
# In production, call a real weather API
return {
"city": city,
"temperature": 18,
"unit": unit,
"conditions": "partly cloudy",
}
tool_defs = [
{
"type": "function",
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
},
},
"required": ["city"],
},
}
]
def dispatch_tool(name: str, arguments: str) -> str:
args = json.loads(arguments)
if name == "get_weather":
result = get_weather(args["city"], args.get("unit", "celsius"))
else:
result = {"error": f"Unknown function: {name}"}
return json.dumps(result)
response = client.responses.create(
model=os.environ['NODEXA_ASSISTANT_ID'],
input="What is the weather in Paris?",
tools=tool_defs,
)
while response.status == "requires_action":
tool_results = []
for item in response.output:
if item.type == "function_call":
output = dispatch_tool(item.name, item.arguments)
tool_results.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": output,
})
response = client.responses.create(
model=os.environ['NODEXA_ASSISTANT_ID'],
previous_response_id=response.id,
input=tool_results,
)
print(response.output_text)
# Step 1: Initial request
RESPONSE=$(curl -s "https://your-admin.example.com/v1/responses" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "YOUR_ASSISTANT_ID",
"input": "What is the weather in Paris?",
"tools": [
{
"type": "function",
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string" },
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
},
"required": ["city"]
}
}
]
}')
echo "Status: $(echo $RESPONSE | jq -r '.status')"
# If requires_action, extract the call details
RESP_ID=$(echo $RESPONSE | jq -r '.id')
CALL_ID=$(echo $RESPONSE | jq -r '.output[0].call_id')
ARGUMENTS=$(echo $RESPONSE | jq -r '.output[0].arguments')
CITY=$(echo $ARGUMENTS | jq -r '.city')
echo "Calling get_weather for: $CITY"
# Step 2: Execute function (simulated) and return result
WEATHER_RESULT='{"temperature":18,"unit":"celsius","conditions":"partly cloudy"}'
curl "https://your-admin.example.com/v1/responses" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"YOUR_ASSISTANT_ID\",
\"previous_response_id\": \"$RESP_ID\",
\"input\": [
{
\"type\": \"function_call_output\",
\"call_id\": \"$CALL_ID\",
\"output\": \"$WEATHER_RESULT\"
}
]
}"
Multiple Tool Calls in One Response¶
The assistant may call multiple functions in a single response. Process all of them before sending the follow-up:
const toolResults = [];
for (const item of response.output) {
if (item.type !== 'function_call') continue;
const args = JSON.parse(item.arguments);
const result = await dispatchTool(item.name, args);
toolResults.push({
type: 'function_call_output',
call_id: item.call_id,
output: JSON.stringify(result),
});
}
// Send ALL results in one request
const nextResponse = await client.responses.create({
model: assistantId,
previous_response_id: response.id,
input: toolResults,
});
Send all results in one request
Do not send individual function results in separate requests. Send all function_call_output items for a given response in a single follow-up request.
Error Handling¶
If your function fails, return an error object as the output — don't leave the call unanswered:
try {
result = await getWeather(args.city);
} catch (err) {
result = {
error: true,
message: err.message,
};
}
toolResults.push({
type: 'function_call_output',
call_id: item.call_id,
output: JSON.stringify(result), // always respond
});
The assistant will see the error and can respond accordingly (e.g., "I wasn't able to get the weather information due to a service error. Please try again later.").
Related Pages¶
- Tools — all tool types and provider compatibility
- Streaming — function call events in the SSE stream
- Conversation Threading — multi-turn conversations