Tool calling lets the model ask your code to do something — look up a record, hit an API, run a query — and then use the answer. You describe your functions; the model decides when to call them; you run them and hand back the results.
The model never runs anything
It only ever returns a request: this function, these arguments. Your code decides whether to execute it. That boundary is where you put your permission checks.Describe your tools#
tools = [{"type": "function","function": {"name": "get_weather","description": "Current weather for a city. Call this whenever the user asks about weather.","parameters": {"type": "object","properties": {"city": {"type": "string", "description": "City name, e.g. 'Jaipur'"},"units": {"type": "string", "enum": ["celsius", "fahrenheit"]},},"required": ["city"],},},}]
| Field | Type | Required | Notes |
|---|---|---|---|
| type | string | Yes | Must be "function". Anything else returns 400 unsupported_tool_type. |
| function.name | string | Yes | What you will switch on when the call comes back. |
| function.description | string | — | The most important field. This is how the model decides whether to call it — say when to use it, not just what it is. |
| function.parameters | object | — | JSON Schema. Must be an object schema. Mark the arguments you actually need as required. |
What a tool call looks like#
{"choices": [{"index": 0,"message": {"role": "assistant","content": null,"tool_calls": [{"id": "call_a1b2c3","type": "function","function": {"name": "get_weather","arguments": "{\"city\":\"Jaipur\",\"units\":\"celsius\"}"}}]},"finish_reason": "tool_calls"}]}
arguments is a JSON string, not an object
You mustJSON.parse / json.loads it. The model generates it, so wrap that parse in a try — a malformed argument string is a thing that happens, and it should become a tool error you feed back, not a crash.The loop#
Tool calling is a loop, not a call. Keep going until the model returns a message with no tool calls.
import jsonmessages = [{"role": "user", "content": "What's the weather in Jaipur?"}]while True:response = client.chat.completions.create(model="upfyn-yuva",messages=messages,tools=tools,)message = response.choices[0].messageif not message.tool_calls:print(message.content)break# Append the assistant turn BEFORE the results, or the ids will not line up.messages.append(message)for call in message.tool_calls:args = json.loads(call.function.arguments)result = run_tool(call.function.name, args) # your codemessages.append({"role": "tool","tool_call_id": call.id, # required"content": json.dumps(result),})
Always bound the loop
A model that keeps calling tools will keep costing you money. Cap the iterations — eight is usually generous — and return an error to the user if you hit the cap.Message order matters
- Append the assistant message with its
tool_calls, then onerole: "tool"message per call. - Every tool message needs a
tool_call_idmatching the call it answers. - Answer all the calls before the next request. A missing result is a broken conversation.
- Tool call
argumentsin your history must be valid JSON strings, or you get400 invalid_tool_arguments.
Controlling when tools are used#
| tool_choice | Effect |
|---|---|
"auto" | The model decides. The default when tools are present. |
"none" | Never call a tool. Useful for a final summarising turn. |
"required" | Must call something. |
{"type":"function","function":{"name":"x"}} | Must call that one. |
Parallel calls
Yuva and Rishi can return several tool calls in one turn — check features.parallel_tools on the model. Bala returns one at a time. Set parallel_tool_calls: false if your tools have side effects that must not interleave.
Tools while streaming#
Tool calls arrive in fragments across chunks, exactly as in the OpenAI API: the function name comes first, then arguments accumulates piece by piece.
Accumulate arguments, not the name
Concatenate everyarguments fragment for a given index; take the name once. Appending the name on every chunk produces get_weatherget_weatherget_weather and a tool that does not exist — this is the single most common streaming-tools bug.Tools we host#
Some tools run on our side, so you do not have to implement them. GET /v1/tools lists what is available to your account, and POST /v1/tools/{name} invokes one directly. Web search is the main one; all three models report web_search: true.
