Sign inGet started

Docs

Quick start, editor setup and FAQ for the OpenAI-compatible API.

Tool calling

Let the model call your functions. You describe what a function does, the model decides when to use it and with what arguments, your code runs it, and you send the result back.

LIBRA passes this straight through to the provider. Your code runs the function, not LIBRA. Nothing is executed on our side, so there is no hidden extra request and nothing to pay for that you did not ask for.

A first call

curl -s http://43.173.12.157/v1/chat/completions \
  -H "Authorization: Bearer $LIBRA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "asix-deepseek-v4/deepseek-v4-flash",
    "messages": [{"role": "user", "content": "What is the weather in Jakarta?"}],
    "tools": [{
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Get current weather for a city",
        "parameters": {
          "type": "object",
          "properties": {"city": {"type": "string"}},
          "required": ["city"]
        }
      }
    }]
  }'

What comes back:

{
  "choices": [{
    "finish_reason": "tool_calls",
    "message": {
      "role": "assistant",
      "tool_calls": [{
        "id": "call_abc123",
        "type": "function",
        "function": {"name": "get_weather", "arguments": "{\"city\": \"Jakarta\"}"}
      }]
    }
  }]
}

finish_reason is tool_calls instead of stop. That is your signal to run the function rather than show the text to the user.

Finishing the round trip

Run your function, then send the result back as a tool message so the model can answer in words:

from openai import OpenAI

client = OpenAI(base_url="http://43.173.12.157/v1", api_key="YOUR_KEY")

messages = [{"role": "user", "content": "What is the weather in Jakarta?"}]
tools = [{"type": "function", "function": {
    "name": "get_weather",
    "description": "Get current weather for a city",
    "parameters": {"type": "object",
                   "properties": {"city": {"type": "string"}},
                   "required": ["city"]},
}}]

first = client.chat.completions.create(
    model="asix-deepseek-v4/deepseek-v4-flash", messages=messages, tools=tools)

call = first.choices[0].message.tool_calls[0]
result = my_weather_function(call.function.arguments)   # your code

messages.append(first.choices[0].message)
messages.append({"role": "tool", "tool_call_id": call.id, "content": result})

second = client.chat.completions.create(
    model="asix-deepseek-v4/deepseek-v4-flash", messages=messages, tools=tools)
print(second.choices[0].message.content)

Two requests, so two charges. That is how tool calling works everywhere, not a LIBRA quirk.

Which models can do it

Models with the agent capability. Today that is:

  • asix-deepseek-v4/deepseek-v4-flash
  • anthropic/claude-sonnet-4-5
  • openai/gpt-5
  • gemini/gemini-2-5-pro
  • the libra/smart alias, which resolves to one of them

Send tools to a model without it and you get 400 unsupported_parameter. That is deliberate: the alternative is answering while quietly ignoring your functions, which looks like the model chose not to call them.

Check the model catalog in the console, or GET /v1/models.

Streaming

Works. Set stream: true and tool call deltas arrive in the chunks:

data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"name":"get_weather"}}]}}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"ci"}}]}}]}
data: [DONE]

Arguments arrive in pieces. Concatenate them before parsing the JSON.

`tool_choice`

  • "auto" is the default: the model decides.
  • "none" means never call a tool.
  • "required" means call one.
  • {"type": "function", "function": {"name": "get_weather"}} forces that one.

Naming a function that is not in your tools array returns 400 invalid_request.

What gets rejected

All of these return 400 invalid_request:

  • an empty tools array
  • tools that is not an array
  • a tool missing function.name
  • two tools with the same name
  • parameters that is not a JSON object
  • more than 128 tools, or a tools array over 512 KB

We check these here so you get a LIBRA error with a request id, instead of a provider's error text about a request you did not knowingly send.

Cost

Tool definitions are part of the prompt, so they cost prompt tokens on every call. A long JSON schema on a chatty endpoint adds up. Keep descriptions short and send only the tools that are relevant.

One limitation worth knowing

The tool-support check runs against the model you asked for. If that model is healthy but fails part way through, LIBRA falls back to another provider, and if that fallback cannot do tools it will ignore them. Rare, but possible. Checking every fallback attempt needs a deeper change and is not done yet.

If a reply comes back with finish_reason: stop when you expected a tool call, compare model in the response to the one you asked for. If they differ, a fallback served it.