Function Calling

Intermediate4 min

The model returns a request your code chooses to run. Arguments are model-generated, and providers document the model inventing ones nobody supplied.

#api
#security

What happens when a model "calls" your function

A model cannot run your code. It produces text. Function calling is the convention that turns some of that text into a structured request your application may choose to execute.

Anthropic's documentation puts it plainly for the tools you define: the model "returns a structured call that your application executes." You declare what is available, the model asks for one, and your code decides whether to obey.

Every security property below follows from that sentence.

Providers also ship tools they run themselves, such as web search or code execution, where the result comes back without your involvement. The distinction is where the code runs. The tools you write are the subject below, because those are the ones where the decision is yours.

The round trip, message by message

Four parts, in order.

1. You declare the tools. Each gets a name, a description, and a schema for its parameters:

{
  "name": "issue_refund",
  "description": "Refund a charge to the customer's original payment method.",
  "input_schema": {
    "type": "object",
    "properties": {
      "charge_id": { "type": "string" },
      "amount_cents": { "type": "integer" }
    },
    "required": ["charge_id", "amount_cents"]
  }
}

2. The model asks. Instead of prose, the response comes back with a stop reason of tool_use and a block naming the tool and its arguments.

3. Your code runs it. This is your function, in your process, under your permissions. Nothing has happened yet that you did not choose.

4. You send the result back. The outcome returns as a tool_result keyed to the original request, and the model writes the reply the user sees.

The loop can repeat. A model that gets a refund confirmation may then ask you to send an email, and each request arrives the same way.

Writing a declaration the model can use

The description is the instruction. The model reads it to decide whether this tool fits the situation, so write it for someone who has never seen your codebase: what the tool does, when it applies, and what it costs. "Refund a charge" invites guessing. "Refund a charge to the customer's original payment method. Only for charges under 90 days old" gives the model the boundary.

Constrain parameters in the schema rather than in prose. An enum with four values removes a whole class of argument you would otherwise validate and reject.

Keep the list short. Tool definitions ride in the input on every request, names, descriptions, schemas and all, and enabling tools adds a few hundred tokens of system prompt on top. Thirty tools you rarely use cost you on every call and give the model more chances to pick the wrong one.

Treating arguments as untrusted input

The arguments are model-generated. They are shaped by the conversation, which includes text you did not write.

Anthropic documents what happens when a prompt lacks the information a required parameter needs: the model "might infer a reasonable value." Their example asks for the weather with no location, and the model supplies one:

{ "location": "New York, NY", "unit": "fahrenheit" }

Two invented arguments, one of them a parameter the user never mentioned. The docs add that the behavior "is not guaranteed," which is the part that matters. You cannot predict when it happens, so you cannot rely on arguments being sane.

Now put issue_refund in place of get_weather. A model that infers an amount is a model that hands your code a number to pay out. Schema enforcement, which providers offer as strict: true, guarantees the argument is an integer. It does not guarantee it is the right integer, and it knows nothing about whether this user may issue refunds at all.

So the authorization check lives in your handler:

def issue_refund(charge_id, amount_cents, *, actor):
    charge = charges.get(charge_id)
    require_permission(actor, "refund", charge)      # not the model's call
    if amount_cents > charge.amount_cents:
        raise ValueError("refund exceeds charge")
    if charge.age_days > 90:
        raise PolicyError("outside the refund window")
    return payments.refund(charge, amount_cents)

A sentence in a prompt asking the model to be careful is not a control. It is a suggestion to a component that takes suggestions from anyone who can get text into the context, which is the reason prompt injection matters here more than anywhere else in an application.

Function calling against structured output

They use the same schema machinery for different jobs. Structured output shapes the answer; function calling requests an action.

If you want the model's reply parsed into fields, you want structured output. If you want the model to reach something outside the conversation, you want this, and you want the permission check that comes with it.

Further reading

  • Anthropic, Tool use: the round trip, the inferred-argument example, and strict schemas.
  • OpenAI, Function calling: the same mechanism, different field names.
  • MCP in one sitting: how tools arrive from servers you did not write.

Knowledge check

Question 1 of 4

You declare an `issue_refund` tool and the model returns a call to it. What has happened to the customer's money at that moment?

Sign in to save your progress and pick up where you left off.

Open this article on its own page

Also in Function Calling

1 article