Melious

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="<MODEL_ID>",
    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="<MODEL_ID>",
        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: "<MODEL_ID>",
  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: "<MODEL_ID>",
    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,
    )
    text = "".join(block.text for block in final.content if block.type == "text")
    print(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.

Parallel tool calls

parallel_tool_calls: false on Chat Completions or Responses asks the model for one tool call per turn; true allows several. We forward a boolean to the provider as sent — Responses echoes it back only when it was forwarded — and ignore absent, null, or non-boolean values rather than rejecting them. On Messages, the same switch is tool_choice: {"type": "auto", "disable_parallel_tool_use": true}, forwarded as the inverse.

Forwarding is the guarantee, not enforcement: not every engine honors the flag, so keep your loop ready for several calls in one turn. Responses function tools also keep strict as you sent it.

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="<MODEL_ID>",
    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. If the provider fails midway, the stream ends with an error event and no tool_use block, so you never get a block with truncated JSON to parse.

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 a 400 with type: invalid_request_error, code: model_not_capable.

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