Tool calling
Let models call your functions — the full loop in both OpenAI and Anthropic shapes
Let a model call functions you've defined, run them, and feed the results back. The two API shapes differ on field names; the control flow is identical.
The loop
- Define tools and include them in the request.
- Model either replies directly or returns a tool call.
- You execute the tool with the model's arguments.
- Send the result back in the next turn.
- Model uses the result to produce the final answer (or calls another tool — repeat).
OpenAI shape
from openai import OpenAI
import json
client = OpenAI(
api_key="sk-mel-<YOUR_API_KEY>",
base_url="https://api.melious.ai/v1",
)
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
},
"required": ["city"],
},
},
},
]
messages = [{"role": "user", "content": "What's the weather in Hamburg?"}]
# Turn 1: let the model decide whether to call a tool
response = client.chat.completions.create(
model="glm-4.7",
messages=messages,
tools=tools,
)
choice = response.choices[0]
messages.append(choice.message.model_dump(exclude_unset=True))
# Turn 2: if there's a tool call, execute it
if choice.finish_reason == "tool_calls":
for call in choice.message.tool_calls:
args = json.loads(call.function.arguments)
# ... run your function ...
result = {"city": args["city"], "temp_c": 14, "conditions": "cloudy"}
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result),
})
# Turn 3: model produces the final answer
final = client.chat.completions.create(
model="glm-4.7",
messages=messages,
tools=tools,
)
print(final.choices[0].message.content)import OpenAI from "openai";
const client = new OpenAI({
apiKey: "sk-mel-<YOUR_API_KEY>",
baseURL: "https://api.melious.ai/v1",
});
const tools = [
{
type: "function",
function: {
name: "get_weather",
description: "Current weather for a city",
parameters: {
type: "object",
properties: { city: { type: "string", description: "City name" } },
required: ["city"],
},
},
},
];
const messages = [{ role: "user", content: "What's the weather in Hamburg?" }];
let response = await client.chat.completions.create({
model: "glm-4.7",
messages,
tools,
});
const choice = response.choices[0];
messages.push(choice.message);
if (choice.finish_reason === "tool_calls") {
for (const call of choice.message.tool_calls) {
const args = JSON.parse(call.function.arguments);
const result = { city: args.city, temp_c: 14, conditions: "cloudy" };
messages.push({
role: "tool",
tool_call_id: call.id,
content: JSON.stringify(result),
});
}
const final = await client.chat.completions.create({
model: "glm-4.7",
messages,
tools,
});
console.log(final.choices[0].message.content);
}tool_choice
"auto"— model decides. Default."none"— force a text-only reply."required"— must call a tool.{"type": "function", "function": {"name": "get_weather"}}— must call this specific tool.
Anthropic shape
from anthropic import Anthropic
client = Anthropic(
api_key="sk-mel-<YOUR_API_KEY>",
base_url="https://api.melious.ai",
)
tools = [
{
"name": "get_weather",
"description": "Current weather for a city",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
]
messages = [{"role": "user", "content": "What's the weather in Hamburg?"}]
# Turn 1
response = client.messages.create(
model="claude-sonnet-4",
max_tokens=512,
tools=tools,
messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
# Turn 2: run the tool_use blocks
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = {"city": block.input["city"], "temp_c": 14}
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result),
})
messages.append({"role": "user", "content": tool_results})
# Turn 3
final = client.messages.create(
model="claude-sonnet-4",
max_tokens=512,
tools=tools,
messages=messages,
)
print(final.content[0].text)Anthropic's shape uses tool_use and tool_result blocks inside content arrays rather than dedicated tool_calls and role: "tool" messages. Otherwise the loop is the same.
Streaming tool calls
Tool-call arguments stream as partial JSON. You need to accumulate them across chunks before parsing:
from openai import OpenAI
stream = client.chat.completions.create(
model="glm-4.7",
messages=[...],
tools=[...],
stream=True,
)
tool_calls = {} # keyed by index
for chunk in stream:
for tc in (chunk.choices[0].delta.tool_calls or []):
i = tc.index
slot = tool_calls.setdefault(i, {"id": None, "name": "", "arguments": ""})
if tc.id:
slot["id"] = tc.id
if tc.function.name:
slot["name"] = tc.function.name
if tc.function.arguments:
slot["arguments"] += tc.function.arguments
# After the stream, tool_calls[0]["arguments"] is the complete JSON stringFor Anthropic streams, input_json_delta events do the equivalent — accumulate the partial_json fields.
Which models support tools
Check _meta.capabilities.function_calling on GET /v1/models/{id}?include_meta=true. Most general-purpose chat models do; some reasoning-focused models don't.
Passing tools to a non-tool model returns INFERENCE_3202.
Gotchas
- Tool schemas count as input tokens. Long descriptions and large JSON Schemas are expensive. Keep them tight.
- Some models prefer XML-style tool syntax under the hood. Melious handles the translation — you send JSON shapes, we format for the provider. If a model is misbehaving on tool calls, try a different one before assuming your schema is wrong.
- Tool loops can run away. Add a max-steps counter to your loop; don't trust the model to stop on its own.
Related
Streaming for the general streaming pattern. Chat completions and Messages for the full endpoint shape.