Handle tool calls

Read the tool calls the model returns, run each function, send every result back by tool_call_id, and repeat until finish_reason is stop.

When the model decides to call one of the tools you defined, the turn ends with finish_reason: "tool_calls" and the assistant message carries tool_calls. You run each function, then send a new request with the results; the model reads them and continues. Define tools covers the request that offers the tools.

Read the tool calls

On POST /api/v1/chat with stream: false, the assistant reply is the top-level message. A turn that calls a tool looks like this, trimmed to the fields that matter here:

{
  "message": {
    "role": "assistant",
    "content": "",
    "tool_calls": [
      {
        "id": "call_abc123",
        "type": "function",
        "function": { "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" }
      }
    ]
  },
  "finish_reason": "tool_calls"
}

Each entry in tool_calls has three fields:

FieldWhat it is
idThe call's id. Your result for this call carries it back as tool_call_id.
type"function".
functionThe name of the function to run, and its arguments.

arguments is a JSON-encoded string, not an object. Parse it before you dispatch:

const args = JSON.parse(call.function.arguments) // { city: "Paris" }

Send the results back

  1. Append the assistant message whole

    Push the message the gateway returned onto your conversation, tool_calls included. The model needs its own call in the history next to the result that answers it.

  2. Append one tool message per call

    For every entry in tool_calls, run the function and append a message with role: "tool", the call's id as tool_call_id, and the result as a string in content.

  3. Call chat again with the same tools

    Send the whole conversation back with the same tools. The model reads the results and either answers or calls another tool.

This request carries a finished tool exchange back to the model:

curl https://api.cruxy.ai/api/v1/chat \
  -H "Authorization: Bearer $CRUXY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "kavi",
    "stream": false,
    "messages": [
      { "role": "user", "content": "What is the weather in Paris?" },
      {
        "role": "assistant",
        "content": "",
        "tool_calls": [
          {
            "id": "call_abc123",
            "type": "function",
            "function": { "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" }
          }
        ]
      },
      {
        "role": "tool",
        "tool_call_id": "call_abc123",
        "content": "{\"city\":\"Paris\",\"tempC\":18,\"sky\":\"clear\"}"
      }
    ],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_weather",
          "description": "Get the current weather for a city",
          "parameters": {
            "type": "object",
            "properties": { "city": { "type": "string" } },
            "required": ["city"]
          }
        }
      }
    ]
  }'

Loop on finish_reason

finish_reason tells you what to do with each turn:

finish_reasonWhat it meansWhat you do
tool_callsThe model wants the calls in message.tool_calls.Run them, append the results, and call again.
stopThe model finished its reply.Read the answer in message.content.
lengthThe output hit max_tokens or the tier's cap.Raise max_tokens or lower reasoning_effort; see Reasoning effort.
content_filterThe upstream model stopped for safety.Stop the loop and handle the turn as unanswered.

Keep calling while finish_reason is tool_calls, and stop when it returns stop. Put a ceiling on the number of rounds too, so a model that keeps calling tools cannot run your loop forever.

Several calls in one turn

tool_calls is an array, so one turn can ask for several calls. Run all of them and append one tool message for each before you call chat again. The tool_call_id on each result is what pairs it with its call.

If a function fails, still answer its call: put a short description of the failure in that tool message's content, so every call in the turn has a result.

Send the tools on every request

Keep sending the same tools for as long as the conversation holds tool exchanges. When the history replays a tool call to a function that the request's tools does not declare, the gateway sends that exchange to the model as prose instead of as a tool call, and reports it in cruxy.undeclared_tool_replay:

FieldMeaning
exchangesHow many assistant messages were sent as prose.
tool_callsHow many tool calls those messages carried.

The field is absent when nothing was rewritten, and nothing you store or send is changed: only what the model reads that turn.

Tool calls on a stream

With stream: true, the calls arrive in pieces. Deltas carry tool_calls fragments instead of content; each fragment has an index and a piece of function.arguments, and may carry the call's id, type, and function.name. Assemble the fragments per index, concatenating the arguments pieces. The finish frame with finish_reason: "tool_calls" means the calls are complete; answer them as above. Tool-call deltas shows the frames. On the native wire, POST /api/v2/chat, the fragments ride delta events and the terminal finish event carries finish_reason.

Next steps

Was this page helpful?