|> GenAI

One Elixir interface for every LLM

A unified, pipe-based API for inference across providers. Switch models with a single line change. Built on OTP for concurrent agent workloads.

GenAI.chat()
|> GenAI.with_model(:"claude-sonnet-4-20250514")
|> GenAI.with_message(:user, "Explain quantum computing simply.")
|> GenAI.run()

Every provider. One protocol.

Anthropic OpenAI Google Gemini Mistral Groq xAI DeepSeek Cerebras LiteLLM Ollama ex_llama + Your own

Why GenAI

|> Automatic format mapping

Switch models mid-conversation. The encoder protocol automatically re-encodes all messages, tools, and settings for the target provider's wire format. Write once, run on any LLM.

|> Tool calling everywhere

Native function calling where providers support it. Automatic emulation via system-prompt injection for those that don't. Define tools once, use them with any model.

|> Media generation

Text-to-image, speech synthesis, transcription, and audio chat through a unified generate_media/1 API. Providers auto-selected by capability via the media router.

|> Provider-agnostic threads

Conversation threads are provider-independent graph structures. Swap with_model/2 and re-run — the full message history re-encodes for the new provider automatically.

|> Local inference

Run GGUF models on-device with ex_llama (Rustler NIF for llama.cpp) or connect to a local Ollama server. Same thread API, no network required.

|> OTP-native concurrency

Built on BEAM for parallel agent workloads. Fan out to multiple models with Task.async_stream, run inference pools under supervision trees.

In practice

Provider-agnostic threads — same conversation, any model
# Build the thread once — it's provider-independent
thread = GenAI.chat()
|> GenAI.with_message(:user, "Explain quantum computing simply.")

# Run with Claude — messages encode to Anthropic's format
{:ok, _} = thread
|> GenAI.with_model(:"claude-sonnet-4-20250514")
|> GenAI.run()

# Swap to GPT-4o — same thread, messages re-encode for OpenAI
{:ok, _} = thread
|> GenAI.with_model(:"gpt-4o")
|> GenAI.run()

# Or Gemini — tools, settings, safety all re-map automatically
{:ok, _} = thread
|> GenAI.with_model(:"gemini-2.5-flash")
|> GenAI.run()
Define tools once — they work with any provider
weather = %GenAI.Tool{
  name: "get_weather",
  description: "Get current weather for a location",
  parameters: %{
    "type" => "object",
    "properties" => %{
      "location" => %{"type" => "string"}
    }
  }
}

# Use it — encoder protocol translates to each provider's tool format
{:ok, response} = GenAI.chat()
|> GenAI.with_model(:"claude-sonnet-4-20250514")
|> GenAI.with_tool(weather)
|> GenAI.with_message(:user, "What's the weather in Tokyo?")
|> GenAI.run()
Unified media generation — speech, images, transcription
# Text-to-speech — provider auto-selected by capability
{:ok, audio} = %GenAI.Media.Request{
  output: :speech,
  prompt: "Welcome to GenAI!",
  settings: %{voice: "alloy", format: "mp3"}
}
|> GenAI.generate_media()
# => {:ok, %{data: <<audio_bytes>>, mime: "audio/mp3"}}

# Image generation
{:ok, image} = %GenAI.Media.Request{
  output: :image,
  prompt: "A fractal forest at sunset, digital art"
}
|> GenAI.generate_media()
Fan out across models concurrently via OTP
models = [
  :"claude-sonnet-4-20250514",
  :"gpt-4o",
  :"gemini-2.5-flash"
]

# Compare responses in parallel — same prompt, every model
results = models
|> Task.async_stream(fn model ->
  GenAI.chat()
  |> GenAI.with_model(model)
  |> GenAI.with_message(:user, "Explain monads in one sentence.")
  |> GenAI.run()
end)
|> Enum.to_list()
On the roadmap

The directed graph execution engine is architected for parameter sweeps and grid search across model, prompt, and hyperparameter combinations. Optimize cost, quality, and latency by running structured comparisons across your entire provider fleet.

Get started

mix.exs
defp deps do
  [
    {:genai, "~> 0.3.4"}
  ]
end
config/config.exs
config :genai, :anthropic,
  api_key: System.get_env("ANTHROPIC_API_KEY")

config :genai, :openai,
  api_key: System.get_env("OPENAI_API_KEY")