Local AI Agents · Module 6 · Lesson 1 of 4

Tool schemas and the call/result round-trip

A tool definition is a JSON Schema contract, not a description — and the call/result round trip that carries a tool request out and its answer back is wired differently on Ollama, OpenAI and Anthropic's APIs, extending the model underneath module's context map to the two pieces it set aside.

Lesson · 20–30 minutes · Text-first

By the end, you can

  • Write a tool definition as a JSON Schema contract with a name, description, typed parameters and a required list (LA-8).
  • Compare how Ollama, OpenAI and Anthropic each represent a model's tool call request and a tool's result, using real, current API shapes (LA-8).
  • Trace one tool call through a full request/response round trip on a chosen runtime's API (LA-8).

Before you start

This is Lesson 1 of 4 in Tool use and the agent loop, the module that follows Safe local setup and turns the course from "operate a model safely" into "give it capability." It assumes the model underneath module's vocabulary — specifically its System, user, tool and retrieved context lesson, which already showed that a tool's result shares the context window with everything else and arrives as one of four content categories on every runtime. This lesson does not re-teach that map. It looks at the two pieces that lesson deliberately set aside: how a tool is actually described to a model in the first place, and the exact wire shape of the request that carries a call out and the reply that carries a result back. No running model is required, though every JSON shape shown here is a real, current API payload, not a simplification.

A tool definition is a JSON Schema contract, not a description

When a builder "gives a model a tool," what actually happens is narrower and more mechanical than the phrase suggests: a JSON object is added to the request, and that object is itself a JSON Schema contract describing one callable function. JSON Schema's own reference is specific about what such a contract can require. Required fields are named explicitly — "the required keyword takes an array of zero or more strings" — and the fields themselves are declared under properties, "an object, where each key is the name of a property and each value is a schema used to validate that property," each with its own type. A contract can also close itself off from anything unnamed: setting additionalProperties to false "means no additional properties will be allowed" beyond the ones the schema lists.

This is not optional detail to skim past. A model never sees a tool's implementation, its docstring or its test suite — it sees exactly this JSON object, and it decides whether and how to call the tool almost entirely from the name, description and parameters fields inside it. A vague description or a loosely typed parameter is not a minor style choice; it is the entire interface the model has to reason from, and — as the next module will build on directly — the entire contract a dispatcher can check a call against before running anything.

Three vendors, one round trip, three different wire shapes

Every runtime that supports tool calling implements the same two-message idea — the model asks for a tool, your code runs it and answers back — but the exact field names differ enough to catch a builder moving between them.

Ollama's API reference documents the request field plainly: tools, "list of tools in JSON for the model to use if supported." A tool definition inside that list follows the same {"type": "function", "function": {name, description, parameters}} shape as the JSON Schema section above. When a supported model decides to call one, the reply's message object carries a tool_calls field — "a list of tools in JSON that the model wants to use" — where each entry's function.arguments is a JSON object, e.g. {"city": "Tokyo"}, not a string. To answer back, your code appends a new message with "role": "tool", the result in content, and an optional tool_name field the docs describe as adding "the name of the tool that was executed to inform the model of the result."

OpenAI's function-calling guide uses the same tools array and the same nested function shape for a definition, with one addition: setting strict to true, the guide states, "will ensure function calls reliably adhere to the function schema, instead of being best effort" — a stronger schema-adherence assurance than Ollama's docs describe for the equivalent field. The reply arrives the same general way, in a tool_calls array, but each call's function.arguments is documented as "JSON-encoded arguments" — a string your code must parse, not an object handed to you directly. Answering back uses the same role: "tool" idea as Ollama, with one more required field: the result message carries a tool_call_id matching the specific call it answers, since one reply can request several tool calls in parallel and each result has to be paired back correctly.

Anthropic's Messages API renames two of these pieces outright rather than just reshaping them. A tool's parameter contract is not called parameters but input_schema; Anthropic's own tool-use walkthrough shows the identical JSON Schema shape underneath — type: object, properties, required — under that different key. When Claude decides to call a tool, there is no tool_calls field on the message at all: the model instead replies with stop_reason: "tool_use" and one or more tool_use content blocks, each carrying its own id, name and input (already a parsed object, matching Ollama's shape rather than OpenAI's string). Your code answers back not with a new role: "tool" message, but with a tool_result content block — matched to the request by tool_use_id, the same call-to-result pairing OpenAI's tool_call_id provides — nested inside an ordinary role: "user" message.

None of these three shapes is more "correct" than the others; each is a different serialization of the same request/dispatch/answer idea. What changes in practice is exactly where you look for the tool's name, whether its arguments arrive parsed or as a string you parse yourself, and what field name and message role carry a result back — three things worth checking against a runtime's own current docs the moment you port a working tool loop from one to another, rather than assuming the last runtime's shape.

A worked example: one tool, three payloads

A check_order_status tool takes one required argument, order_id (string), and returns a status string. Its JSON Schema parameter contract — {"type": "object", "properties": {"order_id": {"type": "string", "description": "The order to check."}}, "required": ["order_id"]} — is identical on all three runtimes; the differences are entirely in the surrounding wire shape.

On Ollama, the request carries "tools": [{"type": "function", "function": {"name": "check_order_status", "description": "...", "parameters": {...}}}]; a call comes back as "tool_calls": [{"function": {"name": "check_order_status", "arguments": {"order_id": "A-1029"}}}]; the answer goes back as {"role": "tool", "content": "shipped", "tool_name": "check_order_status"}.

On OpenAI, the request's tools entry adds "strict": true if you want the schema enforced rather than best-effort; a call comes back as "tool_calls": [{"id": "call_abc123", "function": {"name": "check_order_status", "arguments": "{\"order_id\": \"A-1029\"}"}}] — note the quoted, string-typed arguments; the answer goes back as {"role": "tool", "tool_call_id": "call_abc123", "content": "shipped"}.

On Anthropic, the request's tools entry uses "input_schema" instead of "parameters"; a call comes back as a content block {"type": "tool_use", "id": "toolu_01A...", "name": "check_order_status", "input": {"order_id": "A-1029"}}; the answer goes back as a tool_result content block, {"type": "tool_result", "tool_use_id": "toolu_01A...", "content": "shipped"}, inside a role: "user" message.

Same tool, same underlying idea, three genuinely different payloads to write and parse — exactly the reason a tool loop's dispatcher (the module's next lesson) is worth writing against the schema and the registry, not against one runtime's field names hard-coded throughout.

Accessibility notes

This lesson is text-first, with no images, audio, video or downloadable artifacts. All JSON and API examples appear as plain inline or block text, readable and copyable by assistive technology and keyboard-only users. The practice exercise's model answer sits behind a native disclosure control that is reachable and operable by keyboard and correctly announced by screen readers. The knowledge check uses native radio-button inputs with a visible question and options, and posts its result to a live status region so assistive technology announces the outcome without a page reload.

Practice

Translate a package-tracking tool across three runtimes

A track_package tool takes one required argument, tracking_number (string), and an optional carrier (string). It returns a short status string such as 'in transit' or 'delivered'. You need to wire this tool into an agent loop, but you don't yet know which of the three runtimes this lesson covered the project will end up using.

  1. Write the tool's JSON Schema parameter contract: the properties, their types, and the required list, using this lesson's additionalProperties note to decide whether to allow any other argument.
  2. Write what the request's tools entry looks like on Ollama and on OpenAI. Which field name differs between them for the parameter contract, and which stays the same?
  3. Write what the request's tools entry looks like on Anthropic. Name the one field that changes name here specifically.
  4. The model decides to call track_package with tracking_number '1Z999AA1' and no carrier. Write the tool_calls or tool_use shape each of the three runtimes would use to represent that call, being careful about whether arguments/input arrives as an object or a string.
  5. Write the result message or content block each runtime expects back, given the tool returns 'in transit'.
Compare with a bounded first version

Parameter contract: {type: object, properties: {tracking_number: {type: string}, carrier: {type: string}}, required: [tracking_number]}; additionalProperties: false is reasonable here since only these two named fields make sense for this tool. Ollama and OpenAI both nest this under tools: [{type: function, function: {name, description, parameters: (the contract above)}}] — the parameter contract's own field name, parameters, is identical on both; what differs between them is only how a later tool_calls response represents arguments (object on Ollama, JSON-encoded string on OpenAI), not the definition itself. Anthropic's tools entry is {name, description, input_schema: (the same contract)} — input_schema is the one renamed field; everything inside it is the identical JSON Schema shape. A call with no carrier supplied: Ollama's tool_calls entry is {function: {name: track_package, arguments: {tracking_number: '1Z999AA1'}}} (arguments is an object, and carrier is simply absent since it wasn't required); OpenAI's is {id: call_..., function: {name: track_package, arguments: a JSON-encoded string containing tracking_number set to '1Z999AA1'}} (arguments is a string, not an object); Anthropic's is a tool_use block {type: tool_use, id: toolu_..., name: track_package, input: {tracking_number: '1Z999AA1'}} (input is an object, like Ollama's). Results back: Ollama expects {role: tool, content: 'in transit', tool_name: track_package}; OpenAI expects {role: tool, tool_call_id: call_..., content: 'in transit'}; Anthropic expects a tool_result content block {type: tool_result, tool_use_id: toolu_..., content: 'in transit'} nested inside a role: user message.

Knowledge check

Try the idea

A dispatcher reads a tool call's arguments and calls json.loads() on the value before using it, assuming it is always a JSON string. On which of this lesson's three runtimes would that step fail?
Low-stakes practice only. A correct response marks this lesson complete; it does not affect your final assessment score.

Sources and limits

This lesson synthesises the sources below into a practical learning model. It is not a security standard, legal advice or a guarantee that any particular agent design is safe.

  1. ObjectJSON Schema. Documents the required, properties and additionalProperties keywords used to write a tool's parameter contract.
  2. ollama/ollama docs/api.mdOllama (GitHub). Documents the tools request field, the tool_calls response field with arguments as a JSON object, and the role: tool result message with an optional tool_name field.
  3. Function callingOpenAI. Documents the tools array's function/parameters/strict shape, tool_calls with JSON-encoded string arguments, and submitting a result as a role: tool message with a matching tool_call_id.
  4. Tool use with ClaudeAnthropic. Documents a tool's input_schema, the tool_use content block Claude returns to request a call, and the tool_result content block that returns a matching answer.