Skip to content

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:

  1. Declare your functions in the tools array
  2. Detect when the assistant wants to call a function (requires_action status or function_call_arguments.done event)
  3. Execute the function in your application
  4. 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_id set to the ID of the requires_action response
  • input containing a function_call_output item
{
  "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.").