Behaviour that abstracts the local/remote differences between LLM backends. A backend is responsible for:
chat/3— synchronous single-shot chat completion.chat_stream/3— server-sent-event streaming chat completion.embed/3— text → vector embeddings, optionally batched.models/0— list ofCandil.Model.t/0the backend supports.
Backends are stateless wrappers around whatever HTTP / port machinery
Candil already exposes (Candil.Engine, Candil.HTTP,
Candil.Embeddings). They are looked up at runtime via
Candil.Backend.for/2 and the result is cached per (provider,
model) pair.
Built-in backends
Candil.Backend.LlamaCpp— wraps the existing local llama-server /Candil.Engineflow.Candil.Backend.OpenAICompat— base for OpenAI-compatible HTTP endpoints (used by OpenAI, Azure, Ollama, Groq, vLLM, etc.).
Custom backends
Define a module that implements this behaviour and register it via
Candil.Backend.register/2:
defmodule MyApp.Backends.Azure do
@behaviour Candil.Backend
@impl true
def chat(model, messages, opts), do: ...
@impl true
def chat_stream(model, messages, opts), do: ...
@impl true
def embed(model, texts, opts), do: ...
@impl true
def models, do: [...]
end
Candil.Backend.register(:azure, MyApp.Backends.Azure)
Summary
Functions
Build a typed :backend_unavailable error.
Look up the backend module registered for (provider, model).
Returns {:ok, module} or {:error, :backend_unavailable}.
Infer the provider atom from a model name and the configured default provider. Public so callers can pre-compute the provider for telemetry/UI.
Register a backend module for the given provider atom (:local,
:openai, :anthropic, :ollama, etc.).
Types
@type chat_response() :: %{ content: String.t(), finish_reason: String.t() | nil, usage: %{input_tokens: non_neg_integer(), output_tokens: non_neg_integer()} }
@type embed_vector() :: [float()]
Callbacks
@callback chat(String.t() | Candil.Model.t(), [map()], keyword()) :: {:ok, chat_response()} | {:error, term()}
@callback chat_stream(String.t() | Candil.Model.t(), [map()], keyword()) :: {:ok, Enumerable.t()} | {:error, term()}
@callback embed(String.t() | Candil.Model.t(), [String.t()], keyword()) :: {:ok, [embed_vector()]} | {:error, term()}
@callback models() :: [Candil.Model.t()]
Functions
@spec for(atom(), String.t() | Candil.Model.t()) :: {:ok, module()} | {:error, term()}
Look up the backend module registered for (provider, model).
Returns {:ok, module} or {:error, :backend_unavailable}.
Falls back to inferring the provider from the model name when no exact match is registered:
- starts with
gpt-/o1-/o3-→:openai - starts with
claude-→:anthropic - contains
:→:ollama(model tag convention) - otherwise →
:local(llama.cpp / llama-server)
@spec infer_provider(atom(), String.t() | Candil.Model.t()) :: atom()
Infer the provider atom from a model name and the configured default provider. Public so callers can pre-compute the provider for telemetry/UI.
Examples
iex> Candil.Backend.infer_provider(:openai, "gpt-4o")
:openai
iex> Candil.Backend.infer_provider(:local, "llama3")
:local
iex> Candil.Backend.infer_provider(:local, "llama3:8b")
:ollama
Register a backend module for the given provider atom (:local,
:openai, :anthropic, :ollama, etc.).