Text generation
Tool calling
Let models call your functions through Inworld Inference: define tools once in the OpenAI format, handle tool calls, and return results, on any provider.
Tool calling (also called function calling) lets a model call functions that you define. Describe your tools in the OpenAI format. Inworld Inference translates them for the provider serving the request. The same request works on OpenAI, Anthropic, Google and other models, including when the router falls back to another provider.
Building a voice agent on a realtime session instead? See Tool calling for the Realtime API. Tools also work on the Responses API and the Anthropic Messages API.
How it works
- Send a chat completion with a
toolsarray that describes your functions. - If the model decides to call one, the response has
finish_reason: "tool_calls"and atool_callsarray on the assistant message. - Run the function in your own code.
- Send a second request with the assistant message and a
role: "tool"message that carries the result. The model uses it to write the final answer.
The model never runs anything itself. Your application executes every tool call.
Tool calling needs a billing method on your workspace. Without one, a request that includes tools returns 400.
Define tools and get a tool call
curl --request POST \
--url https://api.inworld.ai/v1/chat/completions \
--header "Authorization: Basic $INWORLD_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"model": "openai/gpt-5",
"messages": [
{"role": "user", "content": "What is the weather in Paris?"}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. Paris"}
},
"required": ["city"]
}
}
}
]
}'import json
import os
from openai import OpenAI
api_key = os.environ["INWORLD_API_KEY"]
client = OpenAI(
base_url="https://api.inworld.ai/v1",
api_key=api_key,
default_headers={"Authorization": f"Basic {api_key}"},
)
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. Paris"}
},
"required": ["city"],
},
},
}
]
messages = [{"role": "user", "content": "What is the weather in Paris?"}]
response = client.chat.completions.create(
model="openai/gpt-5",
messages=messages,
tools=tools,
)When the model calls the tool, the assistant message looks like this:
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_weather",
"type": "function",
"index": 0,
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"Paris\"}"
}
}
]
}function.arguments is a JSON string, so parse it before use. content is an empty string rather than null when the model only calls tools.
Tool definition fields
| Field | Type | Description |
|---|---|---|
type | string | "function". |
function.name | string | The name the model uses to call the tool. |
function.description | string | What the tool does and when to use it. The model relies on this to decide whether to call the tool. |
function.parameters | object | A JSON Schema object that describes the arguments. Any other JSON type returns 400. |
Return the tool result
Append the assistant message exactly as you received it, then add one role: "tool" message per tool call with the matching tool_call_id:
assistant_message = response.choices[0].message
messages.append(assistant_message)
for tool_call in assistant_message.tool_calls:
args = json.loads(tool_call.function.arguments)
result = get_weather(args["city"]) # your function
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result),
})
final = client.chat.completions.create(
model="openai/gpt-5",
messages=messages,
tools=tools,
)
print(final.choices[0].message.content)The tool message content can be a string or an array of text parts:
{
"role": "tool",
"tool_call_id": "call_weather",
"content": [{"type": "text", "text": "The temperature in Paris is 18ยฐC."}]
}A tool message without a tool_call_id fails with 400.
A model can return several tool calls in one turn. Run each call and return a tool message for every id before requesting the next completion.
Send the assistant message back unchanged. Gemini models attach a thought_signature to each tool call's function object, and it has to come back on the follow-up request. Fields the router does not use on a message you send back, such as reasoning, are ignored, not rejected.
Control when tools are called
Set tool_choice to steer the model. The router translates each value to the provider's equivalent.
| Value | Behavior |
|---|---|
"auto" | The model decides whether to call a tool. |
"none" | The model does not call tools. |
"required" | The model must call at least one tool. |
{"type": "function", "function": {"name": "get_weather"}} | The model must call the named tool. |
{"type": "allowed_tools", "allowed_tools": {"mode": "auto", "tools": [...]}} | The model may call only the listed tools. Set mode to required to make it call one of them. |
"any" is read as "required". Any other value returns 400.
Forcing a tool call, with "required" or a named tool, works on most models. Claude models are the exception. tool_choice in a model's spec.supportedParameters from List models means the model accepts the parameter, not that it honors a forced call, so it does not tell you which Claude models do:
- Some Claude models never accept a forced tool call. The router sends
"auto"instead, without an error, so the model can answer without calling a tool. - The other Claude models accept a forced tool call only while reasoning is off. With reasoning on, the provider rejects the request. Leave reasoning off for that request, or use
"auto".
When a request can reach a Claude model, name the tool in your prompt and check finish_reason for "tool_calls" before you rely on the call.
Streaming
With stream: true, tool calls arrive as delta.tool_calls chunks. Each entry has an index that identifies the tool call, and the arguments arrive as string fragments:
{"index": 0, "id": "call_weather", "type": "function", "function": {"name": "get_weather", "arguments": ""}}
{"index": 0, "id": "", "type": "function", "function": {"name": "", "arguments": "{\"city\":"}}
{"index": 0, "id": "", "type": "function", "function": {"name": "", "arguments": " \"Paris\"}"}}Group the chunks by index and join the function.arguments fragments. Later chunks still send id and name, as empty strings, so keep the first non-empty value of each. The final chunk has finish_reason: "tool_calls". See Streaming for the stream format.
Differences from OpenAI
The request and response shapes follow OpenAI's chat completions API, with these differences:
parallel_tool_callsis sent to the provider only when the request hastools. Leave it unset to keep the provider's default, which allows several tool calls in one turn. Set it tofalseto ask for at most one tool call per turn. OpenAI and Anthropic models honor it; Google models ignore it and can still return several tool calls.function.strictis enforced on most OpenAI and Anthropic models and some Inworld-hosted models, where the provider checks a strict schema and returns400for one it does not accept. Other models never receivestrict, so their arguments can differ from your schema. Validate arguments before you run the tool.- The legacy
functionsandfunction_callparameters are ignored. Usetoolsandtool_choice. - Each entry in a non-streaming
tool_callsarray also has anindex.
Tools with other features
- Voice responses: with voice responses, a turn that calls a tool produces no audio, and the answer after the tool result is spoken.
- Web search: router-run web search runs its own tool loop on the server, so you do not define or execute a search tool yourself.
- Routing: tool definitions are translated per provider, so fallbacks and conditional routing work without changes to your tools.