Tool Calling and Structured Output on the Server Side
By the end of this lesson you will be able to make a local server return function calls and schema-valid JSON the way a hosted API does, name the two independent things that have to be right for that to work, and design a small test that tells you how reliably your model and server actually do it.
This is the least glamorous lesson in the part and the one Level 5 depends on most. Part 24 builds an agent loop, Part 25 puts a coding agent on a local model, Part 26 builds multi-agent systems. All three are a client sending a tool list and expecting a parseable tool call back. When that fails, it fails as a silent behavioural fault, not as an error.
A tool call is not a model feature
Section titled “A tool call is not a model feature”The OpenAI-shaped conversation is: the client sends messages and a list of tools; the model
decides to call one; the response comes back with a tool_calls array holding a function name and a
JSON argument object; the client runs the function and sends the result back as another message.
Underneath, none of that exists. The model emits tokens. Two pieces of server-side machinery turn tokens into that clean structure, and either can be wrong independently.
What a server does between a tools list and a tool_calls array
- The client sends messages and toolsOrdinary JSON over the chat completions endpoint. The client knows nothing about the model.
- The chat template renders them into one token sequenceEach model family was trained with a specific way of presenting available functions. The template is shipped with the model and rendered by a Jinja engine.
- The model generates tokensIt emits whatever pattern its training associated with calling a function: a JSON blob, a Python-looking call, a special token followed by arguments, or text in a named channel.
- The tool-call parser reads them backA per-family parser recognises that pattern and extracts the name and arguments. Choose the wrong parser and the call arrives as ordinary text.
- The server emits a tool_calls arrayNow it looks like a hosted API, and the client can run the function.
- The result goes back as a tool messageThe template renders it again, in the form the model was trained to read results in, and the loop continues.
Turning it on, per engine
Section titled “Turning it on, per engine”Every engine needs to be told two things: use tool calling, and use this parser.
| Engine | What you pass | Notes |
|---|---|---|
| vLLM | --enable-auto-tool-choice together with --tool-call-parser <name>, optionally --chat-template <file> |
--tool-parser-plugin registers a custom parser |
| SGLang | --tool-call-parser <name> |
The documented launch form is python3 -m sglang.launch_server --model-path <MODEL> --tool-call-parser <PARSER_NAME> |
| llama-server | --jinja |
The function-calling document states that function calling “is supported for all models” when the server is started with it |
| Ollama | Nothing at the server; pass tools in the request |
The API reference documents tools as “list of tools in JSON for the model to use if supported” |
Choosing the parser
Section titled “Choosing the parser”The parser name is a property of the model family, not of the engine, and the two engines spell them differently. A short comparison of the ones this course’s reference models need, from both documentation pages as read on 2026-09-09:
| Model family | vLLM parser | SGLang parser |
|---|---|---|
| Qwen3, general | hermes |
qwen |
| Qwen3-Coder | qwen3_xml |
qwen3_coder |
| gpt-oss | openai |
gpt-oss |
| Llama 3.1 to 3.3 | llama3_json |
llama3 |
| Llama 4 | llama4_pythonic |
llama4 |
| DeepSeek-V3 | deepseek_v3 |
deepseekv3 |
| Mistral | mistral |
mistral |
| GLM | glm45, glm47 |
glm |
| Models emitting Python-style calls | pythonic |
pythonic |
vLLM’s list is longer than this and includes parsers for Granite, Jamba, InternLM, Kimi, Hunyuan, xLAM, Olmo and others; SGLang’s includes Apertus, Step-3 and the DeepSeek 3.1 and 3.2 variants. Read the engine’s own page for the model you are actually serving, because the sets change with each release.
What tool_choice does
Section titled “What tool_choice does”vLLM documents four behaviours: "auto", where the model generates tool calls when it judges them
appropriate; "required", where it “must generate at least one tool call”; "none", where no tool
calls are generated; and named function calling, where the request specifies exactly which function
to call.
"required" is the one that saves agent loops. When your control flow has no sensible branch for a
chatty answer, forcing a call turns a class of runtime surprise into a schema you can rely on.
vLLM also states limitations plainly, and they are worth reading before you design around a model:
Llama 3 does not support parallel tool calls while Llama 4 does; Mistral 7B’s parallel calling is
described as degraded; some models produce malformed parameter formats; and the pythonic parser
“requires models to output tool calls without mixed text generation”, so a model that likes to explain
itself first will break it.
llama-server
Section titled “llama-server”Track M and any track without vLLM does this with llama.cpp, and it works well.
RunnableAll tracks
~/llama.cpp/build/bin/llama-server \ -m ~/models/unsloth/Qwen3-8B-GGUF/Qwen3-8B-Q4_K_M.gguf \ --host 127.0.0.1 --port 8080 \ --alias local-chat \ -c 8192 -ngl 99 \ --jinjaThe server README gives --jinja as enabled by default, and the function-calling document ties tool
calling to it explicitly. That document lists the families with native tool-call formats supported,
naming Llama 3.1, 3.2 and 3.3 including built-in tools, Functionary v3.1 and v3.2, Hermes 2 and 3,
Qwen 2.5, Mistral Nemo and Command R7B. For anything else, “generic tool call is supported when the
template isn’t recognized by native format handlers”, with the honest caveat that “generic support may
consume more tokens and be less efficient than a model’s native format”.
When a GGUF’s packaged template is wrong or missing, --chat-template-file replaces it, and the
document offers replacements for several model families. Part 6’s /apply-template endpoint is how
you check what the server is actually rendering before you blame the model.
Structured output
Section titled “Structured output”Tool calling is a special case of a more general capability: constraining generation so that the output conforms to a grammar. The general case is worth having on its own, because a great deal of practical work is extraction and classification where you want a schema, not a paragraph.
The mechanism is the same everywhere. At each step the model produces a probability distribution over the vocabulary; the constraint engine works out which tokens could legally come next given the schema and what has been emitted so far, and sets the rest to zero probability. The output is then valid by construction rather than by hope.
The engines that do this locally:
- xgrammar — SGLang’s default backend, supporting “JSON schema, regular expression, and EBNF constraints”. Also available in vLLM.
- outlines — supports “JSON schema and regular expression constraints”.
- llguidance / guidance — JSON schema, regex and EBNF.
- llama.cpp’s own GBNF grammars — the server README documents
--grammaras a “BNF-like grammar to constrain generations”,--grammar-fileto read one from a file, and-j, --json-schemato constrain to a JSON schema directly.
vLLM’s structured-outputs page names xgrammar, guidance, outlines, lm-format-enforcer and
auto, selected with --structured-outputs-config.backend, and describes auto as choosing a
backend from the details of each request. It also notes that the older guided_json, guided_regex,
guided_choice and guided_grammar request fields are deprecated from version 0.12.0 in favour of a
structured_outputs object, so "guided_json" -> {"structured_outputs": {"json": ...}}. SGLang takes
json_schema, regex or ebnf, exactly one per request, with --grammar-backend choosing between
XGrammar, Outlines and Llguidance.
The portable way to ask for JSON across all of them is the OpenAI response_format field:
RunnableAll tracks
curl -s http://127.0.0.1:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "local-chat", "messages": [ {"role": "system", "content": "Extract the fields. Reply with JSON only."}, {"role": "user", "content": "The DGX Spark has 128 GB of unified memory and one GB10 chip."} ], "response_format": { "type": "json_schema", "json_schema": { "name": "machine", "schema": { "type": "object", "properties": { "machine": {"type": "string"}, "memory_gb": {"type": "integer"}, "chip": {"type": "string"} }, "required": ["machine", "memory_gb", "chip"], "additionalProperties": false } } } }'Ollama does the same thing through its own format parameter, documented as “the format to return a
response in. Format can be json or a JSON schema”, on both /api/generate and /api/chat. LM Studio
serves an OpenAI-compatible API from its developer surface, covered in Part 7; its documentation
indexes the OpenAI compatibility page under Developer, and its Advanced section covers parallel
requests and speculative decoding for the same server.
Measuring whether it works
Section titled “Measuring whether it works”Do not take any of this on trust for your model, your quantisation and your server settings. The failure modes are quiet and they are quantisation-sensitive: a heavily quantised model often keeps its prose quality while losing its ability to emit a format reliably.
The test is small and worth keeping. Write twenty prompts that should produce a tool call, and five that should not. Send each one ten times at the temperature you actually use. Then count four things.
Call rate. How often a tool call was produced when one was expected.
Parse rate. How often the response contained a well-formed tool_calls array rather than prose.
Argument validity. How often the arguments matched the function’s schema: right names, right types, no invented parameters.
False call rate. How often a call was produced for one of the five prompts where none was wanted. This is the one people forget, and it is the one that makes an agent loop wander.
Record model, quantisation, engine, engine version, parser, temperature and date beside the four numbers. Part 10’s evaluation lab turns this into a harness you keep; Part 16 uses the same test to show what a quantisation step costs; and Part 25 is the part that will thank you, because a coding agent on a model with an argument validity of four in five is not a coding agent, it is a source of plausible nonsense with a retry loop.
Separate transport, syntax, semantics and authority
Section titled “Separate transport, syntax, semantics and authority”A tool response has several independent success conditions. The HTTP request must succeed; the client must receive a tool-call object; the arguments must parse and validate; the selected tool and values must be appropriate; and the application must authorise the action. Passing an earlier condition does not imply the next one.
Build a small probe set containing a valid call, an ambiguous request, a request needing no tool and an invalid argument. Inspect the raw response before connecting a framework that may repair or hide errors. If a model emits textual JSON instead of a tool-call object, inspect template and parser configuration. If it selects the wrong customer identifier in valid JSON, investigate the task and validation logic.
The application owns execution. Validate arguments against the declared schema, resolve identifiers against permitted resources and apply the caller’s permissions before invoking the function. Feed a structured success or error result back into the conversation with the matching call identifier. Constrained decoding can help syntax; it cannot grant authority or establish that the proposed action is correct.
A tool call is produced by two pieces of server machinery working together: a chat template that
renders the tool list into the format the model was trained on, and a parser that recognises the
model’s output and turns it back into a tool_calls array. Either can be wrong on its own, and the
symptom in both cases is a structured call arriving as prose rather than an error. vLLM needs
--enable-auto-tool-choice with a --tool-call-parser, SGLang needs a --tool-call-parser whose
names differ from vLLM’s for the same models, llama-server needs --jinja and has a generic fallback
for unrecognised templates, and Ollama takes tools in the request. tool_choice: "required" removes
a whole class of surprise from an agent loop. Structured output is the same idea generalised, with
xgrammar, outlines, llguidance and llama.cpp’s GBNF grammars all masking illegal tokens at each step,
and it makes output parseable without making it correct. And none of it should be assumed: measure
call rate, parse rate, argument validity and false calls for your model and quantisation, and record
the conditions beside the numbers.
Check your understanding
Sources for this lesson
8 verified · checked 2026-09-09
- 01vLLM — Tool calling§ Automatic function calling; parsers per model family; tool_choicedocs.vllm.ai/en/latest/features/tool_calling.html2026-09-09
- 02vLLM — Structured outputs§ Request fields; backendsdocs.vllm.ai/en/latest/features/structured_outputs.html2026-09-09
- 03SGLang — Tool parser§ Supported parsersdocs.sglang.io/advanced_features/tool_parser.html2026-09-09
- 04SGLang — Structured outputs§ Grammar backends; constraint parametersdocs.sglang.io/advanced_features/structured_outputs.html2026-09-09
- 05llama.cpp — Function calling§ Native formats; generic fallback; template overridesgithub.com/ggml-org/llama.cpp/blob/master/docs/function-calling.md2026-09-09
- 06llama.cpp — llama-server README§ Command-line options; chat templates; grammarsgithub.com/ggml-org/llama.cpp/blob/master/tools/server/README.md2026-09-09
- 07Ollama — API reference§ Chat request with tools; format parametergithub.com/ollama/ollama/blob/main/docs/api.md2026-09-09
- 08LM Studio — Documentation§ Developer; OpenAI compatibility APIlmstudio.ai/docs2026-09-09
Every technical claim on this page was checked against the official documentation of the tool, vendor or model publisher on the date shown, at the version pinned for the course. Where the course disagrees with folklore, the source is how you can tell which one to trust.