# The Responses API tool loop in Python

Function calling on the Responses API is an explicit loop you own. Verified
2026-09-26.

## The loop

1. Call `client.responses.create(model=..., input=messages, tools=[{...}])`.
   Function tools are declared with fields at the top level of the tool object
   (type "function", name, description, parameters), not nested under a
   "function" key.
2. Scan `response.output` for items with `type == "function_call"`. Each has
   `call_id`, `name`, and `arguments` (a JSON string).
3. `json.loads(item.arguments)` inside try/except. Malformed arguments happen;
   on failure, send back a function_call_output whose output describes the parse
   error so the model can retry with valid JSON.
4. Append to the input list:
   - the original function_call items, and
   - `{"type": "function_call_output", "call_id": item.call_id, "output": json.dumps(result)}`.
   The `call_id` linkage is mandatory; a mismatched or missing call_id 400s.
5. Call `responses.create` again with the extended input. Repeat until no
   function_call items remain.

## Guards

- Cap iterations (for example 8). A model stuck calling a failing tool will
  otherwise bill you in a circle. When the cap hits, return the partial result
  with a note instead of looping.
- Set `parallel_tool_calls=False` when your functions have ordering dependencies;
  the default lets the model fire several at once.
- Validate `name` against your registry before dispatching. The model can invent
  tool names; never `getattr` a module on a model-supplied string.

## Check before you ship

- Unit-test with a canned function_call item: assert the output item carries the
  same call_id.
- Log every iteration count; alert if production traffic ever hits the cap.