Ollama
Provides an Elixir client for Ollama's API, supporting chat, completion, streaming, structured outputs, and tool use.
What this file does
Provides an Elixir client for Ollama's API, supporting chat, completion, streaming, structured outputs, and tool use.
When to use it
- Building an Elixir app that needs to run LLMs locally via Ollama
- Adding streaming chat responses to a Phoenix LiveView
- Implementing function calling with local models in Elixir
- Generating structured JSON output from LLM prompts
Assumes this stack
Ollama

Ollama is a powerful tool for running large language models locally or on your own infrastructure. This library provides an interface for working with Ollama in Elixir.
- π¦ Full implementation of the Ollama API
- π§ Extended thinking
- π§° Tool use (function calling)
- π§± Structured outputs
- π Streaming requests
- Stream to an Enumerable
- Or stream messages to any Elixir process
Installation
The package can be installed by adding ollama to your list of dependencies in mix.exs.
def deps do
[
{:ollama, "~> 0.9"}
]
end
Quickstart
For more examples, refer to the Ollama documentation.
1. Generate a completion
client = Ollama.init()
Ollama.completion(client, [
model: "llama2",
prompt: "Why is the sky blue?",
])
# {:ok, %{"response" => "The sky is blue because it is the color of the sky.", ...}}
2. Generate the next message in a chat
Ollama.chat(client, [
model: "llama2",
messages: [
%{role: "system", content: "You are a helpful assistant."},
%{role: "user", content: "Why is the sky blue?"},
%{role: "assistant", content: "Due to rayleigh scattering."},
%{role: "user", content: "How is that different than mie scattering?"},
]
])
# {:ok, %{"message" => %{
# "role" => "assistant",
# "content" => "Mie scattering affects all wavelengths similarly, while Rayleigh favors shorter ones."
# }, ...}}
3. Generate structured data
The :format option can be used with both completion/2 and chat/2.
Ollama.completion(client, [
model: "llama3.1",
prompt: "Tell me about Canada",
format: %{
type: "object",
properties: %{
name: %{type: "string"},
capital: %{type: "string"},
languages: %{type: "array", items: %{type: "string"}},
},
required: ["name", "capital", "languages"]
}
])
# {:ok, %{"response" => "{ \"name\": \"Canada\" ,\"capital\": \"Ottawa\" ,\"languages\": [\"English\", \"French\"] }", ...}}
Streaming
Streaming is supported on certain endpoints by setting the :stream option to true or a t:pid/0.
When :stream is set to true, a lazy t:Enumerable.t/0 is returned, which can be used with any Stream functions.
{:ok, stream} = Ollama.completion(client, [
model: "llama2",
prompt: "Why is the sky blue?",
stream: true,
])
stream
|> Stream.each(& Process.send(pid, &1, [])
|> Stream.run()
# :ok
This approach above builds the t:Enumerable.t/0 by calling receive, which may cause issues in GenServer callbacks. As an alternative, you can set the :stream option to a t:pid/0. This returns a t:Task.t/0 that sends messages to the specified process.
The following example demonstrates a streaming request in a LiveView event, sending each streaming message back to the same LiveView process:
defmodule MyApp.ChatLive do
use Phoenix.LiveView
# When the client invokes the "prompt" event, create a streaming request and
# asynchronously send messages back to self.
def handle_event("prompt", %{"message" => prompt}, socket) do
{:ok, task} = Ollama.completion(Ollama.init(), [
model: "llama2",
prompt: prompt,
stream: self(),
])
{:noreply, assign(socket, current_request: task)}
end
# The streaming request sends messages back to the LiveView process.
def handle_info({_request_pid, {:data, _data}} = message, socket) do
pid = socket.assigns.current_request.pid
case message do
{^pid, {:data, %{"done" => false} = data}} ->
# handle each streaming chunk
{^pid, {:data, %{"done" => true} = data}} ->
# handle the final streaming chunk
{_pid, _data} ->
# this message was not expected!
end
end
# Tidy up when the request is finished
def handle_info({ref, {:ok, %Req.Response{status: 200}}}, socket) do
Process.demonitor(ref, [:flush])
{:noreply, assign(socket, current_request: nil)}
end
end
Regardless of the streaming approach used, each streaming message is a plain t:map/0. For the message schema, refer to the Ollama API docs.
Function calling
Ollama 0.3 and later versions support tool use and function calling on compatible models. Note that Ollama currently doesn't support tool use with streaming requests, so avoid setting :stream to true.
Using tools typically involves at least two round-trip requests to the model. Begin by defining one or more tools using a schema similar to ChatGPT's. Provide clear and concise descriptions for the tool and each argument.
stock_price_tool = %{
type: "function",
function: %{
name: "get_stock_price",
description: "Fetches the live stock price for the given ticker.",
parameters: %{
type: "object",
properties: %{
ticker: %{
type: "string",
description: "The ticker symbol of a specific stock."
}
},
required: ["ticker"]
}
}
}
The first round-trip involves sending a prompt in a chat with the tool definitions. The model should respond with a message containing a list of tool calls.
Ollama.chat(client, [
model: "mistral-nemo",
messages: [
%{role: "user", content: "What is the current stock price for Apple?"}
],
tools: [stock_price_tool],
])
# {:ok, %{"message" => %{
# "role" => "assistant",
# "content" => "",
# "tool_calls" => [
# %{"function" => %{
# "name" => "get_stock_price",
# "arguments" => %{"ticker" => "AAPL"}
# }}
# ]
# }, ...}}
Your implementation must intercept these tool calls and execute a corresponding function in your codebase with the specified arguments. The next round-trip involves passing the function's result back to the model as a message with a :role of "tool".
Ollama.chat(client, [
model: "mistral-nemo",
messages: [
%{role: "user", content: "What is the current stock price for Apple?"},
%{role: "assistant", content: "", tool_calls: [%{"function" => %{"name" => "get_stock_price", "arguments" => %{"ticker" => "AAPL"}}}]},
%{role: "tool", content: "$217.96"},
],
tools: [stock_price_tool],
])
# {:ok, %{"message" => %{
# "role" => "assistant",
# "content" => "The current stock price for Apple (AAPL) is approximately $217.96.",
# }, ...}}
After receiving the function tool's value, the model will respond to the user's original prompt, incorporating the function result into its response.
License
This package is open source and released under the Apache-2 License.
Β© Copyright 2024 Push Code Ltd.
What's inside
7 sections: installation, quickstart (3 examples), streaming (2 approaches), function calling (2 round-trips), license
Change this for your project
- Replace
{:ollama, "~> 0.9"}with the latest version from Hex - Replace
model: "llama2"with your chosen Ollama model - Replace
model: "mistral-nemo"with your chosen Ollama model - Replace
MyApp.ChatLivewith your own module name
Where it goes
Keep it in your repository where the agent or team that needs it will read it.
Worth borrowing
- Streaming to self() in LiveView for real-time chat updates
- Two-round-trip pattern for tool calling: send prompt with tools, then send tool result back
- Using
:formatoption to enforce structured JSON output from the model
Related Documents
Tool Use Guide
Explains how to register, configure, and use external tools (Python, search, MCP) in ROLL agent training pipelines.
Tool Use Reference
Explains the tool-use pattern where Claude requests function calls and you execute them, covering schemas, loops, parallel execution, and built-in tools.
Paper List for Machine Learning Systems
Curates 400+ research papers on machine learning systems, organized by topic from data processing to distributed training.