Packages
nous
0.15.2
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.17
0.12.16
0.12.15
0.12.14
0.12.13
0.12.12
0.12.11
0.12.9
0.12.7
0.12.6
0.12.5
0.12.3
0.12.2
0.12.0
0.11.3
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.5.0
AI agent framework for Elixir with multi-provider LLM support
Current section
Files
Jump to
Current section
Files
lib/nous/providers/llamacpp.ex
if Code.ensure_loaded?(LlamaCppEx) do
defmodule Nous.Providers.LlamaCpp do
@moduledoc """
LlamaCpp NIF-based provider for local LLM inference.
Runs GGUF models directly in-process via `llama_cpp_ex` NIF bindings.
No HTTP server needed.
Requires optional dep: `{:llama_cpp_ex, "~> 0.6.5"}`
## Usage
# Load model once at app start
:ok = LlamaCppEx.init()
{:ok, llm} = LlamaCppEx.load_model("model.gguf", n_gpu_layers: -1)
# Use with Nous
agent = Nous.new("llamacpp:local",
llamacpp_model: llm,
instructions: "You are helpful."
)
{:ok, result} = Nous.run(agent, "What is Elixir?")
## Configuration
The `llamacpp_model` (the loaded model reference) must be passed via options
when creating the model or agent. It is stored in `default_settings`.
No API key or base URL is needed since inference runs locally via NIFs.
## Settings Mapping
Nous settings are mapped to LlamaCppEx options:
| Nous Setting | LlamaCppEx Option | Description |
|---|---|---|
| `:temperature` | `:temp` | Sampling temperature |
| `:max_tokens` | `:max_tokens` | Maximum tokens to generate |
| `:top_p` | `:top_p` | Nucleus sampling |
| `:json_schema` | `:json_schema` | Constrained JSON output |
| `:enable_thinking` | `:enable_thinking` | Enable/disable thinking tokens |
## Thinking Models
Models like Qwen3 emit `<think>...</think>` tags by default. To disable:
agent = Nous.new("llamacpp:local",
llamacpp_model: llm,
model_settings: %{enable_thinking: false}
)
Or via `generate_text`:
{:ok, text} = Nous.generate_text("llamacpp:local", "Hello",
llamacpp_model: llm,
enable_thinking: false
)
"""
use Nous.Provider,
id: :llamacpp,
default_base_url: "local",
default_env_key: "LLAMACPP_MODEL_PATH"
require Logger
@impl Nous.Provider
def chat(_params, _opts \\ []) do
{:error, :use_request_api}
end
@impl Nous.Provider
def chat_stream(_params, _opts \\ []) do
{:error, :use_request_api}
end
@impl Nous.Provider
def request(model, messages, settings) do
start_time = System.monotonic_time()
merged_settings = Map.merge(model.default_settings, settings)
llamacpp_model = merged_settings[:llamacpp_model]
unless llamacpp_model do
raise ArgumentError,
"llamacpp provider requires :llamacpp_model option. " <>
"Pass it when creating the agent: Nous.new(\"llamacpp:local\", llamacpp_model: llm)"
end
:telemetry.execute(
[:nous, :provider, :request, :start],
%{system_time: System.system_time(), monotonic_time: start_time},
%{provider: :llamacpp, model_name: model.model, message_count: length(messages)}
)
provider_messages =
messages
|> Nous.Messages.to_provider_format(:llamacpp)
|> Enum.map(&to_atom_keys/1)
opts = build_llamacpp_opts(merged_settings)
result =
case LlamaCppEx.chat_completion(llamacpp_model, provider_messages, opts) do
{:ok, completion} ->
response_map = completion_to_map(completion)
parsed = Nous.Messages.from_provider_response(response_map, :llamacpp)
{:ok, parsed}
{:error, error} ->
wrapped =
Nous.Errors.ProviderError.exception(
provider: :llamacpp,
message: "Request failed: #{inspect(error)}",
details: error
)
{:error, wrapped}
end
duration = System.monotonic_time() - start_time
case result do
{:ok, parsed_response} ->
usage =
case parsed_response.metadata do
%{usage: %Nous.Usage{} = u} -> u
%{usage: u} when is_map(u) -> u
_ -> %{}
end
:telemetry.execute(
[:nous, :provider, :request, :stop],
%{
duration: duration,
input_tokens: Map.get(usage, :input_tokens) || 0,
output_tokens: Map.get(usage, :output_tokens) || 0,
total_tokens: Map.get(usage, :total_tokens) || 0
},
%{
provider: :llamacpp,
model_name: model.model,
has_tool_calls: length(parsed_response.tool_calls) > 0
}
)
{:error, error} ->
:telemetry.execute(
[:nous, :provider, :request, :exception],
%{duration: duration},
%{provider: :llamacpp, model_name: model.model, kind: :error, reason: error}
)
end
result
end
@impl Nous.Provider
def request_stream(model, messages, settings) do
start_time = System.monotonic_time()
merged_settings = Map.merge(model.default_settings, settings)
llamacpp_model = merged_settings[:llamacpp_model]
unless llamacpp_model do
raise ArgumentError,
"llamacpp provider requires :llamacpp_model option. " <>
"Pass it when creating the agent: Nous.new(\"llamacpp:local\", llamacpp_model: llm)"
end
:telemetry.execute(
[:nous, :provider, :stream, :start],
%{system_time: System.system_time(), monotonic_time: start_time},
%{provider: :llamacpp, model_name: model.model, message_count: length(messages)}
)
provider_messages =
messages
|> Nous.Messages.to_provider_format(:llamacpp)
|> Enum.map(&to_atom_keys/1)
opts = build_llamacpp_opts(merged_settings)
stream_result = LlamaCppEx.stream_chat_completion(llamacpp_model, provider_messages, opts)
# LlamaCppEx may return {:ok, stream} or the stream directly
stream_result =
case stream_result do
{:ok, _} -> stream_result
{:error, _} -> stream_result
stream -> {:ok, stream}
end
case stream_result do
{:ok, stream} ->
duration = System.monotonic_time() - start_time
:telemetry.execute(
[:nous, :provider, :stream, :connected],
%{duration: duration},
%{provider: :llamacpp, model_name: model.model}
)
normalizer = model.stream_normalizer || Nous.StreamNormalizer.LlamaCpp
transformed_stream = Nous.StreamNormalizer.normalize(stream, normalizer)
{:ok, transformed_stream}
{:error, error} ->
duration = System.monotonic_time() - start_time
:telemetry.execute(
[:nous, :provider, :stream, :exception],
%{duration: duration},
%{provider: :llamacpp, model_name: model.model, kind: :error, reason: error}
)
wrapped =
Nous.Errors.ProviderError.exception(
provider: :llamacpp,
message: "Streaming request failed: #{inspect(error)}",
details: error
)
{:error, wrapped}
end
end
# Override macro-injected private functions that would otherwise be dead code
# (since we override request/3 and request_stream/3 which are their only callers)
defp default_stream_normalizer, do: Nous.StreamNormalizer.LlamaCpp
defp build_request_params(_model, _messages, _settings), do: %{}
# Convert string-keyed maps from to_openai_format to atom-keyed maps for LlamaCppEx.
# NEVER call String.to_atom/1 on data flowing from to_openai_format - if the keyset
# ever expands to include user-controlled fields, that becomes an atom-table DoS.
# Whitelist the known message-shape keys; unknown keys stay as binaries.
@llamacpp_message_keys %{
"role" => :role,
"content" => :content,
"name" => :name,
"tool_calls" => :tool_calls,
"tool_call_id" => :tool_call_id,
"id" => :id,
"type" => :type,
"function" => :function,
"arguments" => :arguments
}
defp to_atom_keys(map) when is_map(map) do
Map.new(map, fn
{k, v} when is_binary(k) -> {Map.get(@llamacpp_message_keys, k, k), v}
{k, v} -> {k, v}
end)
end
# Build LlamaCppEx options from Nous settings
defp build_llamacpp_opts(settings) do
opts = []
opts = if settings[:temperature], do: [{:temp, settings[:temperature]} | opts], else: opts
opts =
if settings[:max_tokens], do: [{:max_tokens, settings[:max_tokens]} | opts], else: opts
opts = if settings[:top_p], do: [{:top_p, settings[:top_p]} | opts], else: opts
opts =
if settings[:json_schema], do: [{:json_schema, settings[:json_schema]} | opts], else: opts
opts =
if Map.has_key?(settings, :enable_thinking),
do: [{:enable_thinking, settings[:enable_thinking]} | opts],
else: opts
opts
end
# Convert a %ChatCompletion{} struct to a string-keyed map compatible with from_openai_response/1
defp completion_to_map(completion) do
choices =
Enum.map(completion.choices, fn choice ->
msg = choice.message || %{}
message = %{
"role" => to_string(Map.get(msg, :role, "assistant")),
"content" => Map.get(msg, :content)
}
tool_calls = Map.get(msg, :tool_calls)
message =
if tool_calls && tool_calls != [] do
converted =
Enum.map(tool_calls, fn tc ->
%{
"id" => tc.id,
"type" => "function",
"function" => %{
"name" => tc.function.name,
"arguments" => tc.function.arguments
}
}
end)
Map.put(message, "tool_calls", converted)
else
message
end
%{
"index" => Map.get(choice, :index, 0),
"message" => message,
"finish_reason" => Map.get(choice, :finish_reason)
}
end)
usage_data = Map.get(completion, :usage)
usage =
if usage_data do
%{
"prompt_tokens" => Map.get(usage_data, :prompt_tokens),
"completion_tokens" => Map.get(usage_data, :completion_tokens),
"total_tokens" => Map.get(usage_data, :total_tokens)
}
end
map = %{
"id" => Map.get(completion, :id),
"object" => "chat.completion",
"choices" => choices
}
if usage, do: Map.put(map, "usage", usage), else: map
end
end
else
defmodule Nous.Providers.LlamaCpp do
@moduledoc """
LlamaCpp NIF-based provider for local LLM inference.
**Not available** - add `{:llama_cpp_ex, "~> 0.6.5"}` to your mix.exs deps.
"""
@behaviour Nous.Provider
@not_available "LlamaCppEx is not available. Add {:llama_cpp_ex, \"~> 0.6.5\"} to your mix.exs deps."
@impl true
def provider_id, do: :llamacpp
@impl true
def default_base_url, do: "local"
@impl true
def default_env_key, do: "LLAMACPP_MODEL_PATH"
@impl true
def chat(_params, _opts \\ []), do: {:error, @not_available}
@impl true
def chat_stream(_params, _opts \\ []), do: {:error, @not_available}
@impl true
def request(_model, _messages, _settings) do
{:error,
Nous.Errors.ProviderError.exception(
provider: :llamacpp,
message: @not_available,
details: :not_available
)}
end
@impl true
def request_stream(_model, _messages, _settings) do
{:error,
Nous.Errors.ProviderError.exception(
provider: :llamacpp,
message: @not_available,
details: :not_available
)}
end
@impl true
def count_tokens(messages),
do: messages |> Enum.map(&(inspect(&1) |> String.length() |> div(4))) |> Enum.sum()
end
end