Vocabulary for this course

The terms the chapters assume, in plain words. You can open this from any chapter.

backoff
Waiting longer between each retry, so a service that is struggling gets room to recover.
context window
The most text, counted in tokens, that a model can consider in a single request.
cost
The money a request is billed for, usually based on how many input and output tokens it uses.
idempotency
The property that repeating the same request has the same effect as sending it once.
jitter
A small random amount added to a wait, so many clients do not retry at the same moment.
latency
The time from sending a request to receiving the full answer, including time spent waiting in a queue.
prompt caching
Reusing the stored copy a service keeps of unchanged leading text so later requests cost less and start faster.
rate limit
A cap on how many requests you may send in a period, after which the service refuses more.
schema
A written description of the fields a response must contain and the type of each value.
streaming
Receiving an answer in pieces as it is produced instead of waiting for the complete response.
structured output
A response constrained to a schema, so every field and type can be checked and used by code.
timeout
A limit on how long a request may wait before your code gives up and treats it as failed.
token
A small unit of text, such as part of a word, a whole word or punctuation, that a model reads and bills for.
tool call
A request from a model for your code to run a named function with arguments, with the result sent back.

LLM APIs from the ground up

Structured output and tool calls

Asking for JSON in the prompt is a request, not a guarantee. Schemas, validation and a repair step turn a hopeful parse into a contract; tool calls are the same contract with an action attached.

Part 6 of 10

Intermediate16 min readBack to Courses

Prompted JSON fails at the edges

A model asked for JSON usually returns JSON, until it returns a sentence before the JSON, a trailing comma, a markdown fence, or a field with the wrong type. The failure rate is low per call and certain over a million calls, which is why “usually” is not a schema. The failures also cluster: a prompt change that helps one model can make another wrap the object in prose.

The fix is to make the shape part of the request. JSON mode constrains the output to valid JSON; a JSON schema constrains the fields and types as well. Providers differ in how strictly they enforce it, so validation stays on your side either way. Treat the schema as the contract and the prompt as the hint that makes the contract likely to be honoured.

The same applies to numbers and booleans. A model may return “3” instead of 3, or “true” instead of true, because the JSON is generated token by token and the quotes are cheap. A validator that checks types catches this; a parser that only checks syntax does not, and the bug surfaces later as a comparison that is always false.

Validate, then repair once

Parsing is not validation. A parsed object can still be missing a required field or carry a string where a number belongs. Validate against the same schema you sent, and on failure make one repair attempt with the error message in the prompt. The error message is the useful part: “priority should be int” tells the model what to change, while “invalid” tells it nothing.

One attempt, not a loop. A model that failed the schema once will often fix it when shown the validation error, and a model that fails twice is usually failing for a reason a third call will not change. Return a typed error and let the caller decide. A repair loop that runs until success hides a prompt problem and turns a bad request into an unbounded bill.

A schema the SDK can enforce

from pydantic import BaseModel
from openai import OpenAI

class Ticket(BaseModel):
    title: str
    priority: int
    tags: list[str]

client = OpenAI()
ticket = client.beta.chat.completions.parse(
    model="gpt-4.1-mini",
    messages=messages,
    response_format=Ticket,
).choices[0].message.parsed

Parse, validate, repair once

Run the first pass and read the validation error. Then make the repair fail as well and confirm the caller gets a clear result instead of a crash.

Output
Click "Run" to execute your code

A tool call is a protocol

A tool call is not the model doing something. It is a message with an id, a function name and a JSON string of arguments. Your code parses the arguments, decides whether to run the function, and appends a tool message with the same id and the result. The model then continues. The id is what ties the result to the request, so it must be preserved exactly.

The loop is yours, which is the point: validation, permissions and the actual execution all happen in code the model cannot edit. The model proposes; the program disposes. That separation is also what makes the tool layer testable, because a fake model can emit a scripted call and the dispatcher can be tested without a provider.

Tool arguments are untrusted input. Parse them, validate them against the function’s schema, and never pass them straight into a shell, a SQL string or a file path. A model that was prompt-injected can propose any arguments it likes.

Parallel calls and strict schemas

A model can request several tool calls in one turn. Run the independent ones together, return all the results in one user turn, and let the model reconcile them. Serialising them costs latency for no gain. Calls that depend on each other are the exception: those belong in separate turns, because the second argument may depend on the first result.

Strict schemas make the arguments predictable: no extra fields, no missing required ones, types enforced. They also make the tool definition longer, which costs tokens on every request. Define the schema once, keep it small, and reuse it for validation on the server side. The schema is both the instruction to the model and the check on the model, which is why it should have exactly one home.

Tool definitions are also a prompt, and a long one. Every tool costs tokens on every request, whether or not it is called, so a registry of thirty tools is a tax on every conversation. Group related actions behind one tool with an action field, or load the definitions for the task at hand, but measure the effect on selection quality.

Run the tool loop yourself

Run the loop and read the message roles. Then add a second tool and make the fake model request both in the same turn; the loop should dispatch both and append two tool messages.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Does asking for JSON in the prompt guarantee valid JSON?