← Blog

Function Calling in LLMs: A Guide to Reliable Tool Loops

2026-09-16 · 5 min read · SubToAPI Team

Function calling in LLMs is the mechanism that lets a model request a specific action — call a database, hit an API, run a calculation — instead of just generating text. You define a set of functions with names, descriptions, and JSON schemas for their arguments, pass them to the model alongside a prompt, and the model decides whether to respond directly or to output a structured request to call one of your functions. Your code executes that function and sends the result back, and the model continues the conversation with real data in hand.

This is the piece of the puzzle that turns an LLM from a text generator into something that can act: look up an order status, book a slot on a calendar, query internal metrics, or run a piece of arithmetic it would otherwise get wrong. Almost every production LLM feature — coding assistants, support bots, data agents — is built on this loop. The rest of this article walks through how the mechanics actually work, how to build a loop that doesn't break in production, and the mistakes that cause most function-calling bugs.

The three-step loop

Every function-calling implementation, regardless of provider, follows the same shape:

  1. You send tools + prompt. Each tool has a name, a description, and a JSON schema describing its parameters.
  2. The model responds with a tool call (or plain text). If it decides a function is needed, it returns a structured object with the function name and arguments — not free text you have to parse yourself.
  3. You execute and send the result back. Your code runs the actual function, then appends the result to the conversation. The model reads it and produces a final answer, or calls another tool.

A minimal request looks like this:

curl https://api.subtoapi.app/v1/messages \
  -H "Authorization: Bearer $SUBTOAPI_KEY" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-sonnet-4",
    "max_tokens": 512,
    "tools": [{
      "name": "get_order_status",
      "description": "Look up the status of a customer order by ID",
      "input_schema": {
        "type": "object",
        "properties": {
          "order_id": { "type": "string" }
        },
        "required": ["order_id"]
      }
    }],
    "messages": [
      { "role": "user", "content": "Where is order 48213?" }
    ]
  }'

The model doesn't call anything itself — it returns a tool_use block with the function name and arguments. You run the actual lookup, then send the output back as a tool_result message so the model can turn raw data into a natural-language answer. The full request/response shape for this is covered in /docs/tools and /docs/messages.

Writing schemas the model can actually use

Most function-calling bugs aren't model failures — they're schema failures. Three rules fix the majority of issues:

If you're debugging why a model isn't calling a tool at all, check that the tool's description actually matches the user's likely phrasing — models match based on semantic similarity between the request and the description, not just the function name.

Handling multi-step and parallel calls

Real tasks often need more than one function call. A user asking "compare pricing for these two plans and tell me which is cheaper for a 5-person team" might require two lookups before the model can answer. Two patterns matter here:

Sequential loops — the model calls one function, reads the result, decides whether it needs another, and repeats until it has enough to answer. Your code just needs a loop that keeps feeding tool results back until the model returns a plain-text response instead of a tool call.

Parallel calls — the model can request multiple independent function calls in a single turn (e.g., look up two orders at once). Execute them concurrently and return all results together rather than round-tripping one at a time — it cuts latency significantly on multi-tool tasks.

async function runToolLoop(messages, tools) {
  let response = await callModel(messages, tools);

  while (response.stop_reason === "tool_use") {
    const toolResults = await Promise.all(
      response.content
        .filter(block => block.type === "tool_use")
        .map(async block => ({
          type: "tool_result",
          tool_use_id: block.id,
          content: await executeFunction(block.name, block.input)
        }))
    );

    messages.push({ role: "assistant", content: response.content });
    messages.push({ role: "user", content: toolResults });
    response = await callModel(messages, tools);
  }

  return response;
}

This loop pattern is the same whether you're calling one function or ten — the complexity lives in executeFunction, not in the orchestration.

Streaming and error handling

If your app streams responses, function calling adds a wrinkle: tool-use blocks arrive as structured deltas mid-stream, not as plain text tokens. You need to buffer the arguments until the block closes before you can parse and execute the function — trying to execute on a partial JSON payload will fail. See /docs/streaming for the event shapes involved.

On error handling: if a function call fails (bad input, downstream API down), don't just drop the turn. Return a tool_result with an error message describing what went wrong. Models handle this well — they'll often retry with corrected arguments or explain the failure to the user instead of hanging.

Where SubToAPI fits

If you're already paying for Claude access through a subscription and want to add function calling to an app, SubToAPI turns that subscription into an HTTPS API with application keys (sub_live_...), full tool-use support, streaming, and usage metadata per key — so you're not stuck copy-pasting from a chat UI to test tool schemas. Plans start at €9/month for solo use, with team and scale tiers for shared API keys across a project. You can try it with a free trial at /signup, see plan details at /pricing, or jump straight into the tool-calling reference at /docs/tools.

questions

Does function calling let the model execute code directly? No. The model only outputs a structured request naming a function and its arguments. Your application code is responsible for actually running the function and returning the result — the model never has direct access to your systems.

What happens if the model calls a function with invalid arguments? You should validate arguments before executing, and if they're invalid, return a tool_result describing the error instead of crashing. The model will typically correct itself and retry with fixed input.

Can a single response include multiple function calls? Yes, most modern implementations support parallel tool calls in one turn. Execute them concurrently and return all results together to avoid unnecessary round trips and reduce latency on multi-step tasks.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →