Lesson 13 / 27
A Complete Tool Loop With the OpenAI SDK
Define a function tool, read tool_calls, return role=tool messages.
tool_calls and role "tool" messages
With OpenAI Chat Completions, tools are {"type": "function", "function": {name, description, parameters}}. When the model wants a tool, message.tool_calls is filled and finish_reason == "tool_calls"; each call has an id and function.name, and function.arguments is a JSON string you must json.loads (and validate: it can be malformed or contain unexpected values). You append the assistant message, then for each call append a message with role: "tool", the matching tool_call_id and the result as a string. Models may return several tool calls in one reply; run them all (concurrently if independent) and return all results. OpenAI's newer Responses API uses different item types for the same idea; the loop logic is the same.
OpenAI tool loop, run
I ran this in a Python virtual environment with the anthropic 1.11.0 and openai 3.22.1 SDKs against a small local stand-in server (shown in the testing topic, saved as mock.py). The server returns canned replies, so no key, network or real model is involved: it proves how the SDK builds requests and handles replies, not what a real model would say. The arguments arrive as a JSON string and are parsed with json.loads before the local function runs. The tool result goes back as a role: "tool" message and the second call returns the final text.
import json
import mock, anthropic, openai
srv = mock.start(); base = f"http://127.0.0.1:{srv.server_address[1]}" # local stand-in server, not a real API
client = openai.OpenAI(api_key="k", base_url=base + "/v1")
tools = [{"type": "function", "function": {"name": "get_order_status", "description": "Look up an order by 6-digit id.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]}}}]
def get_order_status(order_id): return {"order_id": order_id, "status": "shipped"}
messages = [{"role": "user", "content": "Where is order 481516?"}]
for step in range(5):
resp = client.chat.completions.create(model="demo-model", tools=tools, messages=messages)
msg = resp.choices[0].message
if not msg.tool_calls:
print("final:", msg.content); break
messages.append(msg.model_dump(exclude_none=True))
for call in msg.tool_calls:
args = json.loads(call.function.arguments) # arguments arrive as a JSON string
print("model asked for:", call.function.name, args)
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(get_order_status(**args))})
print("calls to the API:", len(mock.STATE["log"]))
Output:
model asked for: get_order_status {'order_id': '481516'}
final: Your order 481516 has shipped.
calls to the API: 2Parse arguments defensively
Wrap json.loads in try/except and validate fields; a malformed argument string should become a tool error, not a crash.
Quick check: In OpenAI tool calls, what type is function.arguments?
- An already validated Python dict
- A JSON string you must parse and validate
- A number
- A file
Answer
A JSON string you must parse and validate — Arguments are model-generated text; never trust them unchecked.