Packages

Graph-based orchestration engine for AI agent pipelines in Elixir. Three-phase node lifecycle (prep → exec → post), composable middleware, checkpointing with resume/rewind, batch flows, OTP supervision, and adapters for Phoenix LiveView and Datastar SSE.

Current section

Files

Jump to
phlox lib phlox llm google.ex
Raw

lib/phlox/llm/google.ex

if Code.ensure_loaded?(Req) do
defmodule Phlox.LLM.Google do
@moduledoc """
LLM provider adapter for Google's Gemini API via AI Studio.
## Setup
1. Get an API key at aistudio.google.com (free, no credit card)
2. Set `GOOGLE_AI_KEY` in your environment
3. Add `{:req, "~> 0.5"}` to your deps
## Usage
Phlox.LLM.Google.chat(
[%{role: "user", content: "Hello"}],
model: "gemini-2.5-flash"
)
## Free tier
- 1,500 requests/day
- 1M token context window
- No credit card required
## Options
- `:model` — default `"gemini-2.5-flash"`
- `:max_tokens` — default `4096` (maps to `maxOutputTokens`)
- `:temperature` — default `nil` (API default)
- `:api_key` — default from `GOOGLE_AI_KEY` env var
"""
@behaviour Phlox.LLM
@default_model "gemini-2.5-flash"
@default_max_tokens 4096
@api_base "https://generativelanguage.googleapis.com/v1beta/models"
@impl Phlox.LLM
def chat(messages, opts \\ []) do
api_key = Keyword.get(opts, :api_key) || System.get_env("GOOGLE_AI_KEY")
unless api_key do
raise ArgumentError,
"Phlox.LLM.Google requires an API key. Set GOOGLE_AI_KEY or pass api_key: in opts."
end
model = Keyword.get(opts, :model, @default_model)
max_tokens = Keyword.get(opts, :max_tokens, @default_max_tokens)
temperature = Keyword.get(opts, :temperature)
# Extract system message
{system, messages} = extract_system(messages)
# Build Gemini request format
contents = Enum.map(messages, &to_gemini_content/1)
body = %{contents: contents}
body =
if system do
Map.put(body, :systemInstruction, %{parts: [%{text: system}]})
else
body
end
gen_config = %{maxOutputTokens: max_tokens}
gen_config = if temperature, do: Map.put(gen_config, :temperature, temperature), else: gen_config
body = Map.put(body, :generationConfig, gen_config)
url = "#{@api_base}/#{model}:generateContent"
case Req.post(url, json: body, params: [key: api_key], receive_timeout: 120_000) do
{:ok, %{status: 200, body: resp}} ->
text =
resp
|> get_in(["candidates", Access.at(0), "content", "parts", Access.at(0), "text"])
if text, do: {:ok, text}, else: {:error, {:no_text_content, resp}}
{:ok, %{status: status, body: resp}} ->
{:error, {:http_error, status, resp}}
{:error, reason} ->
{:error, {:request_failed, reason}}
end
end
defp extract_system(messages) do
case Enum.split_with(messages, fn m -> to_string(m[:role] || m["role"]) == "system" end) do
{[], rest} -> {nil, rest}
{sys, rest} -> {Enum.map_join(sys, "\n\n", &get_content/1), rest}
end
end
defp to_gemini_content(m) do
role =
case to_string(m[:role] || m["role"]) do
"assistant" -> "model"
other -> other
end
%{role: role, parts: [%{text: get_content(m)}]}
end
defp get_content(%{content: c}), do: c
defp get_content(%{"content" => c}), do: c
end
end