Original source
---
id: tool-calling
title: Tool Calling and Structured Outputs
code: TC
subtitle: Declare tools, read calls, return results, run calls in parallel, stream arguments, and force JSON that validates.
hue: 30
sources: phases/11-llm-engineering/03-structured-outputs, phases/11-llm-engineering/09-function-calling, phases/13-tools-and-protocols/01-the-tool-interface, phases/13-tools-and-protocols/02-function-calling-deep-dive, phases/13-tools-and-protocols/03-parallel-and-streaming-tool-calls, phases/13-tools-and-protocols/04-structured-output, phases/13-tools-and-protocols/05-tool-schema-design, phases/19-capstone-projects/21-tool-registry-schema-validation, phases/19-capstone-projects/23-function-call-dispatcher
source_hash: ba9bf49229d13a30
---
## Function call tasks
type: task-table
from: phases/11-llm-engineering/09-function-calling/docs/en.md
| Task | Code |
|---|---|
| Declare a tool, OpenAI | `{"type": "function", "function": {...}}` |
| Declare a tool, Anthropic | `{"name": ..., "input_schema": {...}}` |
| Let the model decide | `tool_choice="auto"` |
| Require a tool call | `tool_choice="required"` |
| Force one tool | `tool_choice={"type":"function", ...}` |
| Read OpenAI calls | `message.tool_calls` |
| Find an Anthropic call | `b.type == "tool_use"` |
| Return an Anthropic result | `{"type": "tool_result", "tool_use_id": ...}` |
## Structured output tasks
type: task-table
from: phases/11-llm-engineering/03-structured-outputs/docs/en.md, phases/13-tools-and-protocols/04-structured-output/docs/en.md
| Task | Code |
|---|---|
| Valid JSON, no schema | `{ type: "json_object" }` |
| JSON that fits a schema | `{type: "json_schema", strict: true}` |
| Parse into a Pydantic model | `response_format=Product` |
| Read the typed result | `message.parsed` |
| Retry on validation errors | `instructor.from_openai(OpenAI())` |
## The tool loop
type: architecture
from: phases/13-tools-and-protocols/01-the-tool-interface/docs/en.md, phases/11-llm-engineering/09-function-calling/docs/en.md
```arch
node | user | 1,1 | User | message
node | host | 2,1 | Host | registry, loop bound
node | model | 3,1 | Model | text, call or refuse
node | result | 1,2 | Tool result | tool message, same id
node | exec | 2,2 | Executor | your code runs it
node | valid | 3,2 | Validator | JSON Schema 2020-12
edge | user | host | message
edge | host | model | tools
edge | model | valid | tool call
edge | valid | exec | valid args
edge | exec | result | result
edge | result | host | observe
```
## One tool call
type: flow
from: phases/13-tools-and-protocols/01-the-tool-interface/docs/en.md, phases/13-tools-and-protocols/01-the-tool-interface/code/main.py, phases/11-llm-engineering/09-function-calling/docs/en.md
1. **Host**: declares name, description and input schema.
2. **Model**: answers, calls tools, or refuses.
3. **Model**: each call has `id`, `name`, `arguments`.
4. **Host**: validates the arguments against the schema.
5. **Executor**: runs the code, returns a serializable result.
6. **Host**: appends a `tool` message with the same id.
7. **Host**: repeats until no calls or `MAX_TURNS`.
## OpenAI round trip
type: annotated-code
lang: python
from: phases/11-llm-engineering/09-function-calling/docs/en.md
```python
response = client.chat.completions.create(
model="gpt-4o", messages=messages,
tools=tools, tool_choice="auto", # model picks tool or text
)
tool_call = response.choices[0].message.tool_calls[0]
args = json.loads(tool_call.function.arguments) # arguments are a string
result = get_weather(**args) # your code executes
messages += [
response.choices[0].message, # keep the assistant turn
{"role": "tool", "tool_call_id": tool_call.id, # same id as the call
"content": json.dumps(result)},
]
final = client.chat.completions.create(model="gpt-4o", messages=messages)
```
## Validate, then repair or refuse
type: annotated-code
lang: python
from: phases/13-tools-and-protocols/04-structured-output/code/main.py, phases/13-tools-and-protocols/04-structured-output/docs/en.md
```python
def process_model_output(raw: str, schema: dict) -> ParsedResult:
if raw.startswith("__REFUSAL__"): # model declined: no retry
return ParsedResult("refusal", raw.removeprefix("__REFUSAL__").strip(), [])
try:
parsed = json.loads(raw)
except json.JSONDecodeError as e: # failure 1: not JSON
return ParsedResult("parse_error", None, [ValidationError("$", str(e))])
errs = validate(schema, parsed) # list of path and message
if errs:
return ParsedResult("violation", parsed, errs) # inject errs, retry
return ParsedResult("ok", parsed, [])
```
## Shapes by provider
type: concepts
from: phases/13-tools-and-protocols/02-function-calling-deep-dive/docs/en.md
- **Schema field**: `parameters`, `input_schema`, Gemini `parameters`.
- **Call**: `tool_calls[]`, a `tool_use` block, a `functionCall` part.
- **Arguments**: OpenAI sends a string. The others send an object.
- **Result link**: `tool_call_id`, `tool_use_id`, `functionResponse` id.
- **Forbid tools**: `tool_choice: "none"`, `{type: "none"}`, `mode: "NONE"`.
- **Strict**: `strict: true`, always on, `responseSchema`.
## Parallel and streaming calls
type: concepts
from: phases/13-tools-and-protocols/03-parallel-and-streaming-tool-calls/docs/en.md, phases/13-tools-and-protocols/03-parallel-and-streaming-tool-calls/code/main.py
- **Enable**: `parallel_tool_calls: true` is the OpenAI default.
- **Serial**: Turn it off when calls depend on each other.
- **Correlate**: Each result carries its call id. Order is free.
- **OpenAI stream**: Join chunks per `index` until `finish_reason = "tool_calls"`.
- **Anthropic stream**: `input_json_delta` chunks until `content_block_stop`.
- **Parse late**: Partial JSON raises. Parse at the end signal.
- **Latency**: 400, 600, 800 ms: 1800 ms serial, 800 ms parallel.
## Schema design rules
type: concepts
from: phases/13-tools-and-protocols/05-tool-schema-design/docs/en.md, phases/13-tools-and-protocols/04-structured-output/docs/en.md
- **Name**: `snake_case`, verb then noun: `get_weather`. No arguments in the name.
- **Description**: "Use when {condition}. Do not use for {close-but-wrong-cases}."
- **Enums**: Put every closed set in an `enum`.
- **Atomic tools**: Split a tool when `action` has more than three values.
- **Strict mode**: Every property in `required`, `additionalProperties: false`, no `$ref`.
## Limits and defaults
type: defaults
from: phases/13-tools-and-protocols/02-function-calling-deep-dive/docs/en.md, phases/13-tools-and-protocols/05-tool-schema-design/docs/en.md, phases/13-tools-and-protocols/01-the-tool-interface/code/main.py, phases/13-tools-and-protocols/04-structured-output/docs/en.md, phases/19-capstone-projects/21-tool-registry-schema-validation/code/main.py
| Setting | Default | Note |
|---|---|---|
| OpenAI tools per request | 128 | Hard limit |
| Anthropic tools per request | 64 | Hard limit |
| OpenAI schema depth | 5 | Strict mode |
| OpenAI argument string | 8192 bytes | Maximum size |
| Description length | 1024 characters | Longer text is truncated |
| `MAX_TURNS` | 5 | Lesson loop bound |
| Repair retries | 3x | More means a bad schema |
| `timeout_ms` | 30_000 | Per tool record |
## Troubleshooting
type: troubleshooting
from: phases/13-tools-and-protocols/03-parallel-and-streaming-tool-calls/docs/en.md, phases/13-tools-and-protocols/04-structured-output/docs/en.md, phases/13-tools-and-protocols/05-tool-schema-design/docs/en.md, phases/13-tools-and-protocols/02-function-calling-deep-dive/docs/en.md, phases/11-llm-engineering/03-structured-outputs/docs/en.md, phases/19-capstone-projects/23-function-call-dispatcher/docs/en.md
| Symptom | Cause | Fix |
|---|---|---|
| `json.loads` raises mid-stream | Arguments are partial JSON | Parse after the end-of-call signal |
| 400 at request time, strict mode | Strict schema rule broken | All in `required`, no `$ref` |
| Wrong tool picked | Two descriptions overlap | Add a "Do not use for" line |
| Value outside the `enum` | Prompt-based output | Use constrained decoding |
| `refusal` in the response | Input does not fit the schema | Handle it as a typed outcome |
| Extra fields in `input` | Anthropic schema is advisory | Validate on the server |
| Schema errors retried | Errors are deterministic | Fix the arguments, no retry |
| Results on the wrong call | Matched by name, not id | Return each result with its id |
## Questions and answers
type: qa
from: phases/11-llm-engineering/09-function-calling/docs/en.md, phases/13-tools-and-protocols/01-the-tool-interface/docs/en.md, phases/13-tools-and-protocols/04-structured-output/docs/en.md, phases/11-llm-engineering/03-structured-outputs/docs/en.md
- **Structured output or call?** Output is final data. A call asks to act.
- **Why not prompt for JSON?** It fails 5 to 15 percent of the time.
- **Why a call id?** Parallel results return out of order.
- **Can a schema catch a wrong value?** No. Type right, value invented.
## Tips
type: tips
from: phases/11-llm-engineering/09-function-calling/docs/en.md, phases/13-tools-and-protocols/05-tool-schema-design/docs/en.md, phases/11-llm-engineering/03-structured-outputs/docs/en.md, phases/19-capstone-projects/21-tool-registry-schema-validation/docs/en.md
- Return errors as results: `{"error": true, "code": "CITY_NOT_FOUND"}`.
- Write errors for the model: "'city' is required."
- Report errors as JSON pointer paths: `/user/email`.
- Make key fields required. The model writes `null`.
- Expose only the functions the user needs.