Skip to content

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

  1. Send tools. The response has finish_reason: "tool_calls" and message.tool_calls.
  2. Run the function. Parse function.arguments (a JSON string) and validate the values.
  3. Send the assistant message and a tool message with the same tool_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.

Response to step 1 (abridged)
{
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "tool_calls": [
          {
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"city\": \"Oslo\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}

Tool choice

tool_choiceResult
"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.typeOutput
json_objectA JSON object; list the required keys in the prompt
json_schemaJSON 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. Set max_tokens above the expected output length.
  • The schema applies to content, not to reasoning.

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.

ModelTools and JSON output
deepseek-v4.1-flashPassed to the model unchanged
qwen3.8-27bYes
qwen3.8-27b-fastYes
qwen3.8-27b-uncensoredYes
qwen3.8-flash-next-abliteratedPassed to the model unchanged

tools, tool_choice, response_format and the tool call response fields: Chat completions reference.