aaronrussell

aaronrussell

Omni - a universal Elixir client for LLM APIs

This thread is the home for updates and discussion across the Omni family of packages. What started as a single library for calling LLM APIs has grown into three packages that cover the full stack of building with LLMs in Elixir:

  • omni - Universal Elixir client for LLM APIs. Streaming text generation, tool use, an structured output.
  • omni_agent - Stateful LLM agents for Elixir - persistent, branching conversations, tool approval, and multi-session management.
  • omni_tools - Ready-to-use tools for Omni-powered agents - filesystem, shell, REPL, web fetch, and web search.

Original post follows.


Hey everyone - I’ve been building with Elixir on and off for over 8 years, but somehow have never posted on the actual Elixir forum. Time to fix that…

Also, I’d love to share with you Omni - a library for working with LLM APIs across multiple providers through a unified interface. Anthropic, OpenAI, Google Gemini, Ollama, OpenRouter, and OpenCode Zen are supported out of the box.

# Resolve model
{:ok, model} = Omni.get_model(:anthropic, "claude-sonnet-4-6")

# Simple text generation
{:ok, response} = Omni.generate_text(model, "Hello!")

# Stream with composable callbacks
{:ok, stream} = Omni.stream_text(model, "Tell me a story")

{:ok, response} =
  stream
  |> Omni.StreamingResponse.on(:text_delta, &IO.write(&1.delta))
  |> Omni.StreamingResponse.complete()

Tool use and structured outputs are supported. Pass tools in the context and Omni handles the execution loop automatically - calling the model, executing tool handlers, feeding results back, and repeating until the model is done. Structured output uses JSON Schema constraints with validation:

# Tool use - Omni manages the tool execution loop
{:ok, response} = Omni.generate_text(
  model,
  Omni.context(
    messages: [Omni.message(role: :user, content: "What's the weather in London?")],
    tools: [weather_tool]
  )
)

# Structured output
alias Omni.Schema
{:ok, response} = Omni.generate_text(
  model,
  "Extract the contact details: Reach me at jane@example.com or call 01234 567890",
  output: Schema.object(%{
    email: Schema.string(description: "Email address"),
    phone: Schema.string(description: "Phone number")
  }, required: [:email, :phone])
)

Omni also offers a lightweight take on agents. Omni.Agent is a GenServer that manages its own conversation context and tool execution, and communicates with callers via standard process messages. You control behaviour through lifecycle callbacks. It’s a building block, not a framework - what you build on top (planning, memory, multi-agent orchestration) is your concern.

I know req_llm covers similar ground, which - slightly annoyingly - I didn’t realise existed until I was 90% of the way done with Omni :man_facepalming:t2:. On the surface they have quite similar APIs, and both use Req, but how they handle implementing providers is a little different. Omni separates providers (the endpoint, configuration and auth) and dialects (wire format translation). The dialect does the heavy lifting, and as most providers share a dialect, adding a new provider is typically a small, mostly-declarative module. Everything is streaming-first - generate_text is built on top of stream_text, so there’s one code path through each dialect.

Anyway, please check it out. Let me know if you have any questions.

https://github.com/aaronrussell/omni

Most Liked

aaronrussell

aaronrussell

A new package in the family: Omni UI - a LiveView interface for interacting with Omni-powered agents in the browser.

Omni UI is an example of how omni, omni_agent, and omni_tools can be used together to build agents in Elixir, and also a UI kit for building chat interfaces in your own app.

Either mount Omni.UI.AgentLive in your router for a batteries included chat interface with sessions, files, a REPL and web tools all wired up. Or use Omni.UI in your own LiveView and compose from the component library - you own the layout and agent tooling, the macro handles all the wiring.

Highlights

  • Built on Omni — multi-provider LLM support, streaming, tool use, structured output, persistent sessions, branching conversations, and pluggable storage
  • Drop-in agent chat — AgentLive mounts a complete interface with a files panel, Elixir REPL, and web tools wired up out of the box
  • Build your ownuse Omni.UI adds session plumbing to any LiveView; compose with ChatUI and CoreUI components for the rendering layer
  • Themeable — semantic colour tokens with light and dark mode support

Links

  • GitHub - The README has a quick start that gets you from zero to a running agent chat in about 10 lines of config.
  • Hexdocs

And as it’s been a while since my last update, there’s been a few updates to other Omni packages:

Omni v1.5.4 hex | code

  • New provider module: Near AI
  • Updated model catalogue (Opus 4.8, Fable, Minimax M3, GLM 5.2 have all landed in the last month+)

Omni Agent v0.5.0 hex | code

  • Session Manager now starts it’s own TitleService which will add session titles based on hueristic or a configured model
  • New Manager.rename/3 function for manually setting a session’s title
  • The :tool_timeout option on Omni.Agent now accepts a 1-arity function receiving the tool name, for per-tool timeouts

Omni Tools v0.4.1 hex | code

  • New bang variants of the Files.FS functions that raise descriptive errros
  • WebSearch tool aligns API key resolution with Omni.Provider.resolve_auth/1
  • Fixed some “clause never used” warnings surfaced by Elixir 1.20’s type checker
egeersoz

egeersoz

How does this compare to (or where does it sit in relation to) Langchain (Elixir)?

aaronrussell

aaronrussell

They’re both text generation focused - so fundamentally do the same thing. Just a different style and take.

Langchain has a few things Omni does not: multimodal, RAG text splitting, EEx prompt templates - and probably some other stuff. Omni is light-weight, only has 2 dependencies.

The main difference for users is the surface API. The mental model for Omni: is build a request, get a stream, consume it. For Langchain it’s build a chain, add some messages, run the chain. Omni’s style is functional, data oriented; Langchain’s is stateful structs, callbacks, framework-y.

Internally the big difference is how Omni splits Providers and Dialects into two things, which should make it relatively painless to add more providers over time. Langchain has one big fat module per provider which I think looks hard to maintain. In theory Omni could sit underneath Langchain and be that provider translation layer.

Last Post!

aaronrussell

aaronrussell

Omni just received a pretty big update that reworks how it loads it’s model catalogue, and brings couple of breaking changes to watch out for too.

Omni v1.6.0 hex | code

Model data now comes through pluggable model sources, so you can choose where catalog data comes from - or plug in your own.

:new_button: What’s new?

  • Omni.Source behaviour module - pluggable model sources, configurable globally or per provider.
  • Omni.Sources.ModelsDev (the default) - a bundled models.dev snapshot, with an optional live mode that fetches fresh catalog data when starting your app:
    config :omni, :models, source: {Omni.Sources.ModelsDev, live: true}
    
  • Omni.Sources.LLMDB - an alternative source backed by @mikehostetler’s llm_db package (the model database used by ReqLLM):
    # with {:llm_db, "~> 2026.7"} in your deps
    config :omni, :models, source: Omni.Sources.LLMDB
    
  • The Ollama provider has now been split into two: Omni.Providers.Ollama for using local models, and Omni.Providers.OllamaCloud for Ollama’s hosted cloud service.

A nice side effect: catalog-backed custom providers got much simpler - a provider now only needs an :id and a config/0, and its model list loads from the configured source automatically.

:police_car_light: Breaking changes :police_car_light:

A few config pattern changes that might affect users. If affected, you should get noisy boot-time errors with migration instructions:

  • Custom providers must now declare a canonical id: use Omni.Provider, id: :mistral
  • Moonshot AI’s provider ID has changed from :moonshot to :moonshotai, and Ollama is now split into :ollama (local) and :ollama_cloud. These changes align Omni better with models.dev and llm_db’s naming conventions.
  • Provider registration moved from config :omni, :providers to the providers: key of config :omni, :models. Full module names are required rather than provider ID atoms:
    config :omni, :models, providers: [:builtins, MyApp.Providers.Acme]
    

Full details in the changelog.

Where Next?

Popular in Announcing Top

OvermindDL1
I created a new library (rather I pulled out a couple files from my big project), it manages an operating system PID file for the BEAM. ...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
kevinlang
Hey all, We have made an Ecto3 Adapter for SQLite3, ecto_sqlite3! We have successfully on-boarded the full suite of integration tests (...
New
fuelen
Hey folks! Want to present a toolkit for writing command-line user interfaces. It provides a convenient interface for colorizing text...
New
trisolaran
Hi! :waving_hand: I would like to present LiveSelect, a little library that I wrote to easily add a dynamic selection input to your LV f...
198 11220 107
New
Flo0807
Hello everyone! I am excited to share our heart project Backpex with you. After building several Phoenix applications, we realized that...
New
markmark206
simple_feature_flags is a tiny package that lets you turn features on or off based on which environment (e.g. localhost, staging, product...
New

Other popular topics Top

baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

We're in Beta

About us Mission Statement