Toolnexus.Client.create
Elixir · package toolnexus · SPEC §8
@spec create(keyword() | map()) :: Toolnexus.Client.t()def create(opts)
Toolnexus.Client.create( base_url: "https://openrouter.ai/api/v1", style: "openai", # "openai" | "anthropic" — the provider wire format model: "openai/gpt-4o-mini", api_key: System.get_env("OPENROUTER_API_KEY"), # env fallback: OPENROUTER_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY headers: %{}, system_prompt: nil, # prepended to the toolkit's skills prompt max_turns: 10, # default retries: 2, # default — see client/resilience retry_base_ms: 500, # default retryable_statuses: nil, # default — ADDS to the built-in retryable set timeout_ms: nil, # default — whole-run deadline, nil = none on_error: nil, # default — see client/resilience hooks: %{}, # before_llm / after_llm / before_tool / after_tool — see client/hooks store: nil, # ConversationStore struct, default: in-memory — see client/store on_metric: nil, # (event_map -> any) — see client/metrics wait_for: nil, # §10 suspension resolver — see suspension/wait-for request_params: nil, # shallow-merged into every LLM body — §8 Gap 1 body_transform: nil, # run LAST after the request_params merge — §8 Gap 1 http_options: [], # extra Req options for the LLM path — §8 Gap 2 transport: nil, # injectable HTTP transport, replaces the wire call only — §8 Gap 2 max_part_bytes: nil, # cap on a content part's decoded bytes — §1B on_unsupported_part: nil, # "error" | "text" — §8A provenance rule override registry: nil # inject a shared MetricsRegistry, nil = a fresh private one)The unified client: system prompt, skills injection, parallel and chained tool calls, retries, memory.
Client.create/1 (elixir/lib/toolnexus/client.ex:345) is the other half of the “zero to
agent” pair — a Toolnexus.Toolkit gives you tools, Client.create/1
gives you the loop that calls them. It takes a keyword list or a map, fills every unset field
with its documented default (retries: 2, max_turns: 10, a fresh in-memory conversation store,
a fresh private metrics registry, …), and returns a plain %Toolnexus.Client{} struct — there is
no process, no supervision tree, nothing to start or stop. Pass the resulting client into
Toolnexus.Client.run/4 or
Toolnexus.Client.stream/4 together with a toolkit (or nil for a
tool-less run) to actually drive a conversation.
When to use it
Section titled “When to use it”- You are wiring up “zero to agent” — one
Toolnexus.create_toolkit/1for tools, oneClient.create/1for the model connection, thenClient.run/4to send a prompt and get aRunResult. This is the entry point every README and example starts from. - You need to point at a different provider or gateway —
base_url+styleis the whole provider surface ("openai"covers OpenAI-compatible gateways like OpenRouter;"anthropic"covers Claude’s native wire shape). Swapping providers is a one-line change to this call, never a different client type. - You want retries, a deadline, hooks, metrics, or a custom conversation store — all of it is an option on this same call. Nothing here requires a second construction step or a different entry point; the struct just carries more fields.
- You need the toolkit’s tools mapped to OpenAI/Anthropic/Gemini schema automatically — pass
the toolkit into
run/4/stream/4, not intocreate/1.Client.create/1only knows about the provider connection; the toolkit is supplied per-call so the same client can drive different toolkits across calls.
Why this and not the alternative
Section titled “Why this and not the alternative”The one thing genuinely absent from this surface is a way to change a client’s options after
construction — there is no Client.update/2. A different timeout_ms or a different model is a
new create/1 call, not a mutation, which keeps the struct’s fields honest: every field you read
off a %Toolnexus.Client{} was set once, at construction, and nothing later in a run loop
reaches back to change it underneath a caller holding a reference.
Examples
Section titled “Examples”1. The smallest useful call — construct and run once
Section titled “1. The smallest useful call — construct and run once”alias Toolnexus.Client
client = Client.create( base_url: "https://openrouter.ai/api/v1", style: "openai", model: "openai/gpt-4o-mini", api_key: "test-key" )
%Client{} = clienttrue = client.retries == 2true = client.max_turns == 10
IO.puts("ok: client created with defaults (retries=#{client.retries}, max_turns=#{client.max_turns})")2. The realistic case — a toolkit, custom retries, and a deadline
Section titled “2. The realistic case — a toolkit, custom retries, and a deadline”{:ok, toolkit} = Toolnexus.create_toolkit(skills_dir: ["../examples/skills"])
client = Toolnexus.Client.create( base_url: "https://openrouter.ai/api/v1", style: "openai", model: "openai/gpt-4o-mini", api_key: System.get_env("OPENROUTER_API_KEY") || "test-key", retries: 3, retry_base_ms: 250, timeout_ms: 30_000 )
# result = Toolnexus.Client.run(client, "What tools do you have?", toolkit)# IO.puts(result.text)
IO.puts("ok: client + toolkit wired (#{length(Toolnexus.Toolkit.tools(toolkit))} tools)")Toolnexus.Toolkit.close(toolkit)3. The full surface — hooks, a custom store, metrics, and a resolver
Section titled “3. The full surface — hooks, a custom store, metrics, and a resolver”alias Toolnexus.Clientalias Toolnexus.Client.InMemoryConversationStore
client = Client.create( base_url: "https://openrouter.ai/api/v1", style: "openai", model: "openai/gpt-4o-mini", api_key: "test-key", system_prompt: "You are a support agent. Be concise.", max_turns: 6, retries: 2, retry_base_ms: 500, retryable_statuses: [520, 521, 522, 523, 524], on_error: fn %{retryable: retryable} -> if retryable, do: :retry, else: :fail end, hooks: %{ before_tool: fn %{name: name} -> IO.puts("-> calling #{name}") && nil end }, store: InMemoryConversationStore.new(), on_metric: fn ev -> if ev.event == "llm", do: IO.puts("metric: llm status=#{ev.status}") end, request_params: %{"temperature" => 0.2}, max_part_bytes: 5_000_000 )
%Client{} = clienttrue = client.max_turns == 6
IO.puts("ok: full-surface client created (max_turns=#{client.max_turns}, retryable_statuses=#{inspect(client.retryable_statuses)})")See also
Section titled “See also”Toolnexus.Client.run— Send a prompt, let the loop call tools until the model stops, get a RunResult.Toolnexus.Client.stream— The streaming loop: text deltas, tool-call events, and suspension events as they happen.Toolnexus.Client.create— hooks — Intercept before/after model calls and tool calls: audit, redact, veto, or rewrite.Toolnexus.Client conversation— Keep a transcript across turns so the model remembers what it already did.Toolnexus.Client.create— resilience — Classify an LLM error into retry / fail, bound a run with a deadline, and cancel cleanly.Toolnexus.Client.InMemoryConversationStore— Swap in-memory history for your own store so a conversation survives a process restart.Toolnexus.Client.MetricsRegistry— Emit one metric event per model call and per tool call: tokens, latency, outcome.Toolnexus.ProviderError— The typed exception a non-2xx provider response raises, carrying status/body/retry_after.Toolnexus.create_toolkit— Collapse MCP servers, skills, built-ins, A2A agents and your own functions into one tool list.