Tools and structured output
Function calling with tools, and JSON output that matches a schema.
Function calling and JSON output in the OpenAI format.
Tool calls
- Send
tools. The response hasfinish_reason: "tool_calls"andmessage.tool_calls. - Run the function. Parse
function.arguments(a JSON string) and validate the values. - Send the assistant message and a
toolmessage with the sametool_call_id. The response contains the answer.
With thinking off ("chat_template_kwargs": {"enable_thinking": false}), the model writes the tool call without a
reasoning step first. Both examples ask for the weather in Oslo, run a stand-in get_weather, send its result back
and print the answer.
Python
import json, os
from openai import OpenAI
client = OpenAI(
base_url="https://api.albastov.com/v1",
api_key=os.environ["ALBASTAR_API_KEY"],
)
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
messages = [{"role": "user", "content": "What is the weather in Oslo?"}]
no_thinking = {"chat_template_kwargs": {"enable_thinking": False}}
# 1. Request with tools: the response contains a tool call.
first = client.chat.completions.create(
model="qwen3.8-27b", messages=messages, tools=tools,
extra_body=no_thinking,
)
call = first.choices[0].message.tool_calls[0]
# 2. Run the function, then send the assistant message and the result.
args = json.loads(call.function.arguments)
result = {"city": args["city"], "temp_c": 7, "sky": "overcast"}
messages.append(first.choices[0].message.model_dump(exclude_none=True))
messages.append({"role": "tool", "tool_call_id": call.id,
"content": json.dumps(result)})
final = client.chat.completions.create(
model="qwen3.8-27b", messages=messages, tools=tools,
extra_body=no_thinking,
)
print(final.choices[0].message.content)curl
cat > tools.json <<'EOF'
[{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}]
EOF
# 1. Request with tools: the response contains a tool call.
curl -s https://api.albastov.com/v1/chat/completions \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3.8-27b",
"tools": '"$(cat tools.json)"',
"messages": [
{"role": "user", "content": "What is the weather in Oslo?"}
],
"chat_template_kwargs": {"enable_thinking": false}
}' > first.json
# 2. Run the function, then send the assistant message and the result.
python3 - > second.json <<'EOF'
import json
message = json.load(open("first.json"))["choices"][0]["message"]
call = message["tool_calls"][0]
args = json.loads(call["function"]["arguments"])
result = {"city": args["city"], "temp_c": 7, "sky": "overcast"}
print(json.dumps({
"model": "qwen3.8-27b",
"tools": json.load(open("tools.json")),
"messages": [
{"role": "user", "content": "What is the weather in Oslo?"},
{"role": "assistant", "tool_calls": message["tool_calls"]},
{"role": "tool", "tool_call_id": call["id"],
"content": json.dumps(result)},
],
"chat_template_kwargs": {"enable_thinking": False},
}))
EOF
curl -s https://api.albastov.com/v1/chat/completions \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
-H "Content-Type: application/json" \
-d @second.json |
python3 -c 'import json, sys
print(json.load(sys.stdin)["choices"][0]["message"]["content"])'The response to step 1, abridged to the message (the response id, the call's id and usage left out). The
call's id goes into the tool message as tool_call_id.
{
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"tool_calls": [
{
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"Oslo\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
]
}Tool choice
tool_choice | Result |
|---|---|
"auto" | The model decides; the default with tools |
"none" | No tool calls |
"required" | At least one tool call |
{"type": "function", "function": {"name": "get_weather"}} | A call to the named function |
JSON output
response_format constrains the reply to JSON.
response_format.type | Output |
|---|---|
json_object | A JSON object; list the required keys in the prompt |
json_schema | JSON that matches json_schema.schema |
Both examples ask for one programming language and the year it appeared, as JSON that matches a schema.
curl
cat > format.json <<'EOF'
{
"type": "json_schema",
"json_schema": {
"name": "language",
"strict": true,
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["name", "year"],
"properties": {
"name": {"type": "string"},
"year": {"type": "integer"}
}
}
}
}
EOF
curl https://api.albastov.com/v1/chat/completions \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3.8-27b",
"messages": [
{"role": "user",
"content": "Name one programming language and the year it appeared."}
],
"response_format": '"$(cat format.json)"',
"chat_template_kwargs": {"enable_thinking": false}
}'Python
import json, os
from openai import OpenAI
client = OpenAI(
base_url="https://api.albastov.com/v1",
api_key=os.environ["ALBASTAR_API_KEY"],
)
schema = {
"type": "object",
"additionalProperties": False,
"required": ["name", "year"],
"properties": {
"name": {"type": "string"},
"year": {"type": "integer"},
},
}
reply = client.chat.completions.create(
model="qwen3.8-27b",
messages=[{
"role": "user",
"content": "Name one programming language and the year it appeared.",
}],
response_format={
"type": "json_schema",
"json_schema": {"name": "language", "strict": True, "schema": schema},
},
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
)
language = json.loads(reply.choices[0].message.content)
print(language["name"], language["year"])- Output cut by
max_tokens(finish_reason: "length") is invalid JSON. Setmax_tokensabove the expected output length. - The schema applies to
content, not toreasoning.
Streamed tool calls
A streamed tool call arrives as delta.tool_calls fragments. Concatenate function.arguments per index until
finish_reason. The OpenAI SDKs concatenate the fragments.
Model support
The API sends tools, tool_choice and response_format to every model unchanged. On a model marked Yes, the reply
carries tool calls in message.tool_calls and follows response_format.
| Model | Tools and JSON output |
|---|---|
| deepseek-v4.1-flash | Passed to the model unchanged |
| qwen3.8-27b | Yes |
| qwen3.8-27b-fast | Yes |
| qwen3.8-27b-uncensored | Yes |
| qwen3.8-flash-next-abliterated | Passed to the model unchanged |
tools, tool_choice, response_format and the tool call response fields:
Chat completions reference.