Melious
Guides

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

  1. Define tools and include them in the request.
  2. Model either replies directly or returns a tool call.
  3. You execute the tool with the model's arguments.
  4. Send the result back in the next turn.
  5. 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 string

For 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.

Streaming for the general streaming pattern. Chat completions and Messages for the full endpoint shape.

On this page