Skip to main content

Tool Calling

Let a model call functions in your application through the chat completions API, then answer using the results.

Tool calling lets a model ask your application to run a function, such as fetching live data or querying one of your own services, and then answer using what the function returns. You describe the functions in a chat completions request, the model replies with the function it wants to call and the arguments to pass, and your code runs it and sends the result back. AI Studio follows the OpenAI tool-calling format, so you can use the OpenAI SDK by setting its base URL to AI Studio.

Find a Model That Supports Tool Calling​

Tool calling works with models whose tool_call_supported value is true. To list the ones you can run inference with, call the List base models API with both filters set, replacing API_KEY with your Hyperstack API key from the API Keys page:

terminal
curl "https://console.hyperstack.cloud/ai/api/v1/base_models?tool_call_supported=true&inference=true" \
-H "api_key: API_KEY"

Use the hf_repo value of a model in the response as model in your request. Sending tools to a model that does not support tool calling returns a 400 error with the code tool_calls.not_supported.

Define Your Tools​

Describe each function so the model knows when to call it and what arguments to pass. A tool has three parts: a name, a description the model reads to decide when to call it, and parameters that describe the arguments as a JSON Schema object. Pass your tools in the tools field of the request:

terminal
curl -X POST "https://console.hyperstack.cloud/ai/api/v1/chat/completions" \
-H "Content-Type: application/json" \
-H "api_key: API_KEY" \
-d '{
"model": "Qwen/Qwen3-4B-Instruct-2507",
"messages": [
{"role": "user", "content": "What is the weather in Paris right now?"}
],
"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, such as Paris."}
},
"required": ["city"]
}
}
}
]
}'

When the model decides to call a tool, the response has finish_reason set to tool_calls, and the message carries a tool_calls array instead of text:

json
{
"content": null,
"role": "assistant",
"tool_calls": [
{
"function": {
"arguments": "{\"city\": \"Paris\"}",
"name": "get_weather"
},
"id": "chatcmpl-tool-a862370b1ab8fe4e",
"type": "function"
}
]
}

The arguments value is a JSON-encoded string. Parse it before you pass the arguments to your function.

Tool definitions are sent to the model with every request, so they count toward your input tokens.

Send the Result Back​

The model does not run your function. Your code runs it, then calls the API again with the conversation extended by the assistant message that contains the tool calls, followed by one tool message per call with its result. Set the tool_call_id of each tool message to the id of the call it answers. The model then writes its final answer from the results.

Treat the function name and arguments the model returns as untrusted input. Run only functions you have defined, and validate the arguments before you act on them, as you would any user input.

The example below runs the full exchange with the OpenAI Python SDK, pointed at the AI Studio API through base_url. You don't need an OpenAI account. Install the SDK with pip install openai, set the HYPERSTACK_API_KEY environment variable to your Hyperstack API key, and replace get_weather with your own function:

python
import json
import os

from openai import OpenAI

client = OpenAI(
base_url="https://console.hyperstack.cloud/ai/api/v1",
api_key=os.environ["HYPERSTACK_API_KEY"], # Your Hyperstack 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, such as Paris."}
},
"required": ["city"],
},
},
}
]


def get_weather(city):
# Replace with a call to your own weather service.
return {"city": city, "temperature_c": 18, "conditions": "partly cloudy"}


# Map each tool name to the function that runs it.
available_functions = {"get_weather": get_weather}

messages = [{"role": "user", "content": "What is the weather in Paris right now?"}]

response = client.chat.completions.create(
model="Qwen/Qwen3-4B-Instruct-2507",
messages=messages,
tools=tools,
)
message = response.choices[0].message

if message.tool_calls:
messages.append(message)
for tool_call in message.tool_calls:
function = available_functions[tool_call.function.name]
args = json.loads(tool_call.function.arguments)
result = function(**args)
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result),
}
)

response = client.chat.completions.create(
model="Qwen/Qwen3-4B-Instruct-2507",
messages=messages,
tools=tools,
)

print(response.choices[0].message.content)

The second response is a normal text answer, such as The current weather in Paris is partly cloudy with a temperature of 18°C.

Control When the Model Calls a Tool​

By default, the model decides for itself whether to call a tool or answer directly. Set tool_choice to change that:

  • auto: the model decides. This matches leaving tool_choice out.
  • none: the model answers in text and does not call a tool, even when tools are defined.

Some models also accept required, which makes the model call a tool, or an object that names one function for the model to call. Models that don't support these values return a 400 error.

Stream Tool Calls​

Tool calling also works with "stream": true. Each tool call arrives in pieces in the delta.tool_calls field of the streamed chunks, identified by its index. The first piece carries the call's id and function name, and the arguments string can be split across several later pieces. Join the pieces for each index, and parse the arguments once the stream ends.

This continues the example above, reusing its client and tools with a new conversation:

python
stream = client.chat.completions.create(
model="Qwen/Qwen3-4B-Instruct-2507",
messages=[{"role": "user", "content": "What is the weather in Paris right now?"}],
tools=tools,
stream=True,
)

calls = {}
for chunk in stream:
if not chunk.choices:
continue
for piece in chunk.choices[0].delta.tool_calls or []:
call = calls.setdefault(piece.index, {"id": None, "name": None, "arguments": ""})
if piece.id:
call["id"] = piece.id
if piece.function and piece.function.name:
call["name"] = piece.function.name
if piece.function and piece.function.arguments:
call["arguments"] += piece.function.arguments

for call in calls.values():
print(call["id"], call["name"], json.loads(call["arguments"]))

The last chunk of a stream can arrive with an empty choices list and the request's token usage, which is why the loop skips chunks without choices.