4.6 KiB
Conol.ai Tool Use Protocol
Gateway v6.0+ supports OpenAI-compatible tool calling through conol.ai's SSE API.
Sending Tools to Conol.ai
Tools are passed in the session creation body as an Anthropic-format array:
session_body = {
"source": {"type": "home"},
"messages": conol_msgs,
"timezone": "Europe/Kyiv",
"agentModel": model,
"agentEffort": "low",
"tools": [
{
"name": "get_weather",
"description": "Get current weather for a city",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}
],
}
Both Anthropic-format (name, input_schema) and OpenAI-format (type: "function", function: {name, parameters}) are accepted by conol.ai. The gateway converts OpenAI format to Anthropic before sending.
Receiving Tool Calls from SSE
Conol.ai emits tool calls as SSE history_delta events with tool_call log entries:
{
"type": "history_delta",
"stages": [{
"id": "...",
"logs": [{
"role": "assistant",
"type": "tool_call",
"content": [{
"name": "WebSearch",
"type": "tool_call",
"input": {"query": "Paris weather"},
"result": [{"text": "83°F, Overcast..."}]
}]
}]
}]
}
Key fields:
name— tool name (string)input— JSON arguments (dict)result— optional: array of{"text": "..."}objects with tool output
Converting to OpenAI Format
def conol_to_openai_tool_calls(events):
tool_calls = []
for ev in events:
if ev.tool_name:
tool_calls.append({
"id": f"call_{uuid.uuid4().hex[:8]}",
"type": "function",
"function": {
"name": ev.tool_name,
"arguments": json.dumps(ev.tool_input, ensure_ascii=False),
},
})
return tool_calls
Built-in Tools (Auto-called by conol.ai)
Conol.ai models have built-in tools that fire automatically even when no user tools are provided:
| Tool | Purpose |
|---|---|
WebSearch |
Web search via Exa.ai |
Fetch |
Fetch URL content with prompt |
SetIntention |
Internal meta-tool for task routing |
WriteNote |
Create Obsidian-style notes |
ReadNote |
Read persisted notes |
These appear in SSE as tool_call logs alongside any user-provided tools.
Streaming Tool Calls
In streaming mode, tool calls are emitted as SSE chunks with delta.tool_calls[]:
{
"object": "chat.completion.chunk",
"choices": [{
"delta": {"tool_calls": [{"id": "call_abc", "type": "function", "function": {"name": "WebSearch", "arguments": "{\"query\":\"...\"}"}}]},
"finish_reason": null
}]
}
Then final chunk with finish_reason: "tool_calls" or "stop".
Tool Result Loop (Tool → Model → Response)
To continue a conversation after tool calls, send tool results back as messages with role: "tool":
{
"model": "gpt-5.6-luna",
"messages": [
{"role": "user", "content": "Weather in Paris?"},
{"role": "assistant", "content": null, "tool_calls": [{"id": "call_1", "function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}}]},
{"role": "tool", "tool_call_id": "call_1", "content": "83°F, Overcast"}
]
}
The gateway converts role: "tool" messages to text: [Tool result #call_1]: 83°F, Overcast.
Pitfalls
Duplicate Tool Calls
Conol.ai sometimes emits duplicate tool_call events in the SSE stream (same name, same arguments, different stage IDs). The gateway currently returns all of them. For production use, deduplicate by (name, json.dumps(input)) key.
Tool + Text in Same Response
Conol.ai models may return BOTH text content AND tool calls in the same response. OpenAI convention is content: null when tool_calls are present. The gateway currently returns both — downstream clients expecting strict OpenAI protocol may need content: null when tool_calls is non-empty.
Tool Choice
tool_choice: "auto"/"none"/"required" is passed but conol.ai does not enforce strict tool choice — it always has its built-in tools available. For tool_choice: "none", filter tool calls out in the gateway.
Streaming + Tool Calls
When streaming with tools, conol.ai sends thinking text first, then tool calls, then final text. The gateway collects all events and emits tool calls first, then text. This causes a delay before the first chunk — the full response must arrive before streaming begins. Future improvement: buffer and emit as events arrive.