Documentation menu

Concepts

Providers

The one method a provider implements, the messages and events it handles, and how -p NAME finds it.

A provider connects elelem to a model. It is any Ruby object with a fetch method, registered under a name with config.provider. You pick one with -p NAME.

The interface #

def fetch(messages, tools = [], &block)

That is the whole contract. elelem calls fetch once per round trip to the model, passes the conversation and the tool definitions, and reads events from the block. The return value is ignored.

messages #

An array of hashes with symbol keys. The first is always the system prompt.

[
  { role: "system", content: "You are a terminal coding agent.\n\n..." },
  { role: "user", content: "what is in README.md?" },
  { role: "assistant", content: "", tool_calls: [{ id: "toolu_01", name: "read", arguments: { "path" => "README.md" } }] },
  { role: "tool", tool_call_id: "toolu_01", content: "{\"content\":\"# My project\",\"path\":\"README.md\",\"ok\":true}" }
]
RoleKeys
systemcontent
usercontent
assistantcontent, and tool_calls when the model asked for tools earlier in this turn
tooltool_call_id, content (the tool result as a JSON string)

Each entry in tool_calls is { id:, name:, arguments: }, in the same shape your provider emitted. Your provider translates all of this into whatever its API expects. elelem-anthropic, for example, pulls the system message out into Anthropic’s system field.

tools #

An array of tool definitions in the OpenAI function format:

[
  {
    type: "function",
    function: {
      name: "read",
      description: "Read file",
      parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] }
    }
  }
]

Events #

Call the block with a hash. The type is a string.

EventKeysWhat elelem does
sayingtextAppends to the reply.
thinkingtextPrints it dimmed. Not saved.
doingid, name, argumentsRecords a tool call to run after fetch returns.

Any other type is ignored. Stream saying and thinking as the tokens arrive; elelem prints the reply once fetch returns. Emit doing once per complete tool call, with arguments already parsed into a hash with string keys, as JSON.parse returns it.

When fetch returns, elelem runs the tool calls in the order you emitted them. If there were none, the turn is over.

Errors #

Raise. elelem catches StandardError and ScriptError around fetch, prints the message, and ends the turn with the reply Error: <message>. Do not retry or swallow errors inside the provider unless the API calls for it.

A complete provider #

Save this as .agents/plugins/echo.rb:

class EchoProvider
  def initialize(prefix:)
    @prefix = prefix
  end

  def fetch(messages, tools = [], &block)
    last = messages.reverse.find { |m| m[:role] == "user" }
    block.call(type: "saying", text: "#{@prefix}#{last[:content]}")
  end
end

Elelem.configure do |config|
  config.provider(:echo) do |model: nil, **|
    EchoProvider.new(prefix: model ? "#{model}: " : "")
  end
end
elelem ask -p echo --model parrot "hello there"
parrot: hello there

The factory #

config.provider(:name) { |model: nil, **| ... }

The block is a factory: it returns a new provider. elelem calls it every time it builds a provider, with no caching. The keyword arguments are the provider options. Today there is one, model:, set by --model. Accept ** as well, so new options do not break your factory.

A factory that takes no arguments also works. It ignores --model. elelem-ggml, elelem-candle, and elelem-llama2 work this way and read their model from environment variables only.

Registering a second factory under the same name replaces the first.

How -p NAME resolves #

  1. elelem loads plugin files from ~/.agents/plugins/ and .agents/plugins/, once.

  2. It looks for a provider registered as NAME. Names are compared as strings, so :echo and "echo" are the same.

  3. If there is none, it runs require "elelem/NAME", which loads the gem elelem-NAME, and looks again.

  4. If the require fails, you get:

    elelem: unknown provider: "NAME". Available: stub, echo. Run: gem install elelem-NAME
  5. If the gem loads but registers no provider called NAME, you get elelem-NAME did not register a provider named "NAME".

  6. It calls the factory with the provider options.

Two things follow from step 3:

  • A gem is found automatically only under its own name. elelem-anthropic also registers vertex, but -p vertex requires elelem/vertex, which does not exist. Load the gem first with a one-line plugin file, ~/.agents/plugins/anthropic.rb:

    require "elelem/anthropic"
  • Any LoadError during that require, including one from a missing dependency inside the gem, is reported as unknown provider. If a provider you installed shows up as unknown, try ruby -e 'require "elelem/NAME"' to see the real error.

Known providers #

-p nameGemHonors --model
stubelelem (core)No
anthropicelelem-anthropicYes
vertexelelem-anthropic (load it first, as above)Yes
openaielelem-openaiYes
ollamaelelem-ollamaYes
llamaelelem-llamaYes, a path to a .gguf file
ggmlelelem-ggmlNo
candleelelem-candleNo
llama2elelem-llama2No

Each provider’s settings are on its plugin page and in Environment variables.

Switching provider in a session #

elelem-builtins adds /provider. With no argument it lists registered providers. With a name, it builds that provider and swaps it into the running session. The conversation is kept. --model is not passed on a switch, so the new provider uses its default model.

In your own code, set it on the session:

session.provider = Elelem::Config.build_provider("ollama", model: "gpt-oss:latest")