ExAthena supports two tool-calling protocols and falls back between them automatically when a model misbehaves.
Native tool calls
The OpenAI / Anthropic / Ollama native shape. Providers declaring
native_tool_calls: true in their capabilities use this by default.
OpenAI / Ollama
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\": \"/tmp/foo\"}"
}
}
]
}Anthropic Claude
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_abc",
"name": "read_file",
"input": {"path": "/tmp/foo"}
}
]
}ExAthena.ToolCalls.Native.parse/1 handles both shapes and returns
canonical %ExAthena.Messages.ToolCall{} structs.
Text-tagged tool calls
For models without native tool-call support, ExAthena supports a prompt-engineered protocol:
~~~tool_call
{"name": "read_file", "arguments": {"path": "/tmp/foo"}}
~~~Rules:
- One block per call; multiple blocks in a single response are allowed.
- Both fences must be on their own lines.
idis optional; missing ids are generated server-side.- Malformed JSON in a block returns an error.
ExAthena.ToolCalls.augment_system_prompt/2 appends instructions for
this protocol to the system prompt, along with each tool's schema:
ExAthena.ToolCalls.augment_system_prompt(
"Be helpful.",
[
%{name: "read_file", description: "read a file", schema: %{type: "object", properties: %{path: %{type: "string"}}}}
]
)Auto-fallback
ExAthena.ToolCalls.extract/2 picks the protocol based on provider
capabilities AND the response shape:
| Provider says | Response has | Parser used |
|---|---|---|
native: true | tool_calls array | Native |
native: true | empty tool_calls, text contains ~~~tool_call | TextTagged (fallback) |
native: true | empty, no fences | returns {:ok, []} |
native: false | any | TextTagged |
The agent loop (Phase 2) uses this to recover when a provider declares native support but the model actually emits text-tagged blocks — common with smaller Ollama models.
Arguments
Both parsers accept:
- A decoded map (most common from Claude's
input). - A JSON-encoded string (OpenAI's
arguments). - An empty string (treated as
{}).
Anything else returns an error. Never trust arbitrary tool-call payloads without schema validation at the tool-execution layer.