The Mojentic.LLM.Broker is the central interface for interacting with Large Language Models in Mojentic. It provides a consistent API across different LLM providers (gateways) and handles tool execution, error handling, and message management.
Overview
The Broker acts as an intermediary between your application and LLM providers:
Your App → Broker → Gateway → LLM Provider
↓
Tool ExecutionCreating a Broker
alias Mojentic.LLM.Broker
alias Mojentic.LLM.Gateways.Ollama
# Basic broker
broker = Broker.new("qwen3:14b", Ollama)
# With correlation ID for request tracking
broker = Broker.new("qwen3:14b", Ollama, "request-abc-123")Broker Structure
%Broker{
model: "qwen3:14b", # Model identifier
gateway: Ollama, # Gateway module
correlation_id: "request-123" # Optional tracking ID
}Text Generation
The primary use case is generating text responses:
messages = [Message.user("Explain Elixir in one sentence")]
case Broker.generate(broker, messages) do
{:ok, response} ->
IO.puts(response)
# "Elixir is a functional, concurrent programming language..."
{:error, reason} ->
Logger.error("Generation failed: #{inspect(reason)}")
endWith Configuration
config = CompletionConfig.new(
temperature: 0.3, # Lower = more focused
max_tokens: 500
)
{:ok, response} = Broker.generate(broker, messages, [], config)With Tools
tools = [DateResolver, CurrentDateTime]
{:ok, response} = Broker.generate(broker, messages, tools, config)Structured Output
Generate responses conforming to a JSON schema:
schema = %{
type: "object",
properties: %{
title: %{type: "string"},
summary: %{type: "string"},
keywords: %{
type: "array",
items: %{type: "string"}
}
},
required: ["title", "summary"]
}
messages = [Message.user("Analyze: Elixir is a functional language")]
case Broker.generate_object(broker, messages, schema) do
{:ok, analysis} ->
IO.inspect(analysis)
# %{
# "title" => "Elixir Language Analysis",
# "summary" => "Functional programming language...",
# "keywords" => ["functional", "concurrent", "BEAM"]
# }
{:error, :invalid_response} ->
Logger.error("LLM didn't return valid JSON")
endTool Execution Flow
When tools are provided, the Broker handles a recursive loop:
- Send messages to LLM
- If LLM requests tools:
- Execute each tool
- Add tool results to conversation
- Return to step 1
- Return final text response
# Example: Date resolution tool usage
messages = [Message.user("What's the date next Monday?")]
tools = [DateResolver]
# The broker will:
# 1. Call LLM with the question
# 2. LLM responds with tool call request
# 3. Broker executes DateResolver.run(args)
# 4. Broker adds result to conversation
# 5. Calls LLM again with tool result
# 6. LLM responds with final answer
{:ok, response} = Broker.generate(broker, messages, tools)Tool Call Handling
The broker automatically:
- Matches tool calls to available tools
- Executes tools with provided arguments
- Handles tool errors gracefully
- Logs tool execution (info, warnings, errors)
# Tool not found
23:10:42.490 [warning] Tool not found: unknown_tool
23:10:42.490 [error] Tool execution failed: Tool error: Tool not found: unknown_tool
# Successful tool execution
23:10:42.491 [info] Processing 1 tool call(s)
23:10:42.491 [info] Executing tool: resolve_dateMessage Management
The Broker accepts a list of messages representing the conversation:
messages = [
Message.system("You are a helpful coding assistant"),
Message.user("How do I read a file in Elixir?"),
Message.assistant("You can use File.read/1..."),
Message.user("What about streaming?")
]
{:ok, response} = Broker.generate(broker, messages)Message Types
system/1- Set LLM behavior and contextuser/1- User inputassistant/1- LLM responses (for history)tool_call/2- LLM requesting tool executiontool_result/3- Tool execution results
Error Handling
The Broker returns standardized error tuples:
case Broker.generate(broker, messages) do
{:ok, response} ->
# Success
{:error, :timeout} ->
# Request timed out
{:error, :invalid_response} ->
# LLM returned invalid format
{:error, {:http_error, 429}} ->
# Rate limited
{:error, {:http_error, 500}} ->
# Server error
{:error, {:gateway_error, message}} ->
# Gateway-specific error
{:error, {:tool_error, message}} ->
# Tool execution failed
endError Recovery
def generate_with_retry(broker, messages, max_retries \\ 3) do
case Broker.generate(broker, messages) do
{:ok, response} ->
{:ok, response}
{:error, :timeout} when max_retries > 0 ->
Logger.warn("Timeout, retrying...")
Process.sleep(1000)
generate_with_retry(broker, messages, max_retries - 1)
error ->
error
end
endCorrelation IDs
Track requests across your system:
# Generate unique ID
correlation_id = UUID.uuid4()
broker = Broker.new("gpt-oss:20b", Ollama, correlation_id)
# ID flows through all operations
{:ok, response} = Broker.generate(broker, messages)
# Later, find logs/events by correlation_id
# [info] [request-abc-123] Processing tool call
# [info] [request-abc-123] Executing tool: resolve_dateConfiguration Options
Fine-tune LLM behavior:
alias Mojentic.LLM.CompletionConfig
# Creative writing
config = CompletionConfig.new(temperature: 1.5, max_tokens: 2000)
# Factual responses
config = CompletionConfig.new(temperature: 0.1, max_tokens: 500)
# Long context
config = CompletionConfig.new(num_ctx: 32768)
# Constrained generation
config = CompletionConfig.new(
temperature: 0.7,
num_predict: 256 # Limit response length
)Reasoning Effort
For reasoning models (like OpenAI's o1/o3 series) or when using Ollama with extended thinking, you can control the reasoning effort level:
# Enable extended reasoning (Ollama)
# or control reasoning effort for OpenAI reasoning models
config = CompletionConfig.new(
reasoning_effort: :high # :low, :medium, or :high
)
messages = [Message.user("Think deeply about the implications of quantum computing")]
{:ok, response} = Broker.generate(broker, messages, [], config)Ollama Gateway: When reasoning_effort is set, Ollama will use the think: true parameter to enable extended thinking. The model's reasoning trace is available in the response's thinking field:
{:ok, %GatewayResponse{
content: "After careful analysis...",
thinking: "Let me break this down step by step..."
}} = Broker.complete(broker, messages, config)OpenAI Gateway: For reasoning models (o1, o3 series), the reasoning_effort parameter (:low, :medium, :high) is passed directly to the API. For non-reasoning models, the parameter is ignored with a warning logged.
Best Practices
1. Use System Messages
Set clear instructions:
messages = [
Message.system("""
You are a helpful assistant that provides concise answers.
Always format code examples with proper syntax highlighting.
"""),
Message.user("How do I create a GenServer?")
]2. Handle Tool Errors
Tools can fail:
defmodule MyTool do
@behaviour Mojentic.LLM.Tools.Tool
def run(args) do
with {:ok, value} <- validate_args(args),
{:ok, result} <- process(value) do
{:ok, result}
else
{:error, reason} -> {:error, {:tool_error, reason}}
end
end
end3. Manage Context Windows
Long conversations need truncation:
def keep_recent_messages(messages, max_count \\ 10) do
messages
|> Enum.take(-max_count)
end
messages = keep_recent_messages(conversation_history)
{:ok, response} = Broker.generate(broker, messages)4. Use Appropriate Timeouts
Configure gateway timeouts for large models:
# In config.exs or environment
System.put_env("OLLAMA_TIMEOUT", "600000") # 10 minutesAdvanced Usage
Multiple Models
# Use different models for different tasks
fast_broker = Broker.new("phi4:14b", Ollama)
smart_broker = Broker.new("gpt-oss:20b", Ollama)
# Quick classification
{:ok, category} = Broker.generate(fast_broker, classify_messages)
# Deep analysis
{:ok, analysis} = Broker.generate(smart_broker, analysis_messages)Tool Composition
# Combine multiple tools
tools = [
DateResolver,
CurrentDateTime,
WeatherTool,
CalculatorTool
]
# LLM can use any tool as needed
{:ok, response} = Broker.generate(broker, messages, tools)Schema Validation
# Strict schemas ensure valid output
schema = %{
type: "object",
properties: %{
confidence: %{type: "number", minimum: 0, maximum: 1},
category: %{type: "string", enum: ["tech", "business", "other"]}
},
required: ["confidence", "category"],
additionalProperties: false
}
case Broker.generate_object(broker, messages, schema) do
{:ok, result} ->
# Guaranteed to have required fields
if result["confidence"] > 0.8 do
process_high_confidence(result)
end
end