# A Complete Tool Loop With the OpenAI SDK — Claude API / OpenAI API Basics

Source: https://www.geekswithgeeks.com/en/llm-apis/t-openai

> 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.

```python
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: 2
```

## Parse arguments defensively

Wrap json.loads in try/except and validate fields; a malformed argument string should become a tool error, not a crash.

**Quiz:** In OpenAI tool calls, what type is function.arguments?

- [ ] An already validated Python dict
- [x] 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.
