Install
Add Jibu to your dependencies in mix.exs:
{:jibu, "~> 0.2"}Create a client
Jibu.new/1 returns a Req.Request. Build it once and reuse it.
client = Jibu.new(api_key: System.fetch_env!("ANTHROPIC_API_KEY"))Without :api_key, Jibu reads the ANTHROPIC_API_KEY environment variable.
Beta features need their names in :betas:
client = Jibu.new(betas: ["some-beta-2026-01-01"])Set the model and max_tokens once on the client:
client = Jibu.new(model: "claude-opus-5-5", max_tokens: 1024)Jibu.messages/2 and Jibu.count_tokens/2 apply these defaults. A field set in the body wins.
Batch requests do not get them.
:defaults takes further fields, for example defaults: %{temperature: 0}.
Every other option goes to Req.merge/2, for example :receive_timeout.
Ask a question
Jibu.ask/3 sends a prompt and returns the text of the answer.
{:ok, text} = Jibu.ask(client, "Name three rivers in Germany.")The prompt is a string or a list of content blocks, see Images, documents and files. These shortcuts have a fixed meaning:
system:sets the system prompt.cache: truemarks it for prompt caching.effort:setsoutput_config.effort.messages:passes an earlier conversation. The prompt is appended to it.on_text:streams the answer and passes each piece of text to a function. See below.
Every other keyword becomes a body field unchanged.
{:ok, text} =
Jibu.ask(client, "And the longest of them?",
system: "Answer in one sentence.",
effort: :low,
temperature: 0,
messages: [
%{role: "user", content: "Name three rivers in Germany."},
%{role: "assistant", content: "Rhine, Elbe and Danube."}
]
)A refusal and output cut off at max_tokens return {:error, %Jibu.Error{}}.
So does a turn that waits for a tool. Use Tools for those.
on_text: shows the answer while it arrives, for example in a LiveView:
pid = self()
{:ok, text} = Jibu.ask(client, "Write a haiku.", on_text: &send(pid, {:text, &1}))The function sees the text before ask/3 knows whether the answer succeeded.
After a refusal, max_tokens, an error event or a lost connection, it may already have received text.
ask/3 then returns the error.
For every stream event, use Jibu.stream/3.
Send a full request
Jibu.messages/2 takes the whole body as a plain map and returns the whole response.
Jibu sends the body unchanged, apart from the client defaults.
{:ok, response} =
Jibu.messages(client, %{
model: "claude-opus-5-5",
max_tokens: 1024,
messages: [%{role: "user", content: "Name three rivers in Germany."}]
})
Jibu.Response.text(response)Jibu.Response lifts id, model, content, stop_reason and usage from the reply.
response.body holds the complete decoded JSON with string keys.
Check the stop reason
stop_reason tells you why the model stopped.
"end_turn": the answer is complete."max_tokens": the answer was cut off. Raisemax_tokensor shorten the task."refusal": the model declined. The content is not an answer."tool_use": the model asks for a tool. See Tools.
Set the effort
output_config.effort trades depth of reasoning against tokens and latency.
With ask/3 and extract/4, the effort: shortcut sets it.
Jibu.messages(client, %{
model: "claude-opus-5-5",
max_tokens: 1024,
output_config: %{effort: "low"},
messages: messages
})Use "low" for classification and short extraction. Measure before raising it.
Count tokens
Jibu.count_tokens/2 takes the same body as Jibu.messages/2. The API does not bill it.
Of the client defaults, only the model applies here.
{:ok, %{"input_tokens" => count}} = Jibu.count_tokens(client, body)List models
{:ok, model} = Jibu.Models.get(client, "claude-opus-5-5")
client |> Jibu.Models.stream() |> Enum.map(& &1["id"])Test without the network
Pass a Req.Test plug as a client option. No request leaves the test process.
client = Jibu.new(api_key: "test", plug: {Req.Test, MyApp.Claude}, retry: false)
Req.Test.stub(MyApp.Claude, fn conn ->
Req.Test.json(conn, %{
"content" => [%{"type" => "text", "text" => "Hello"}],
"stop_reason" => "end_turn"
})
end)Req.Test needs the plug package in your test dependencies.