Skip to content

api_key_env

Elixir · package toolnexus · SPEC §8

Toolnexus.Client.create(
base_url: String.t(),
api_key: String.t() | nil, # used as-is when set
api_key_env: String.t() | nil, # the NAME of an env var, read at call time
...
)

Which key the client sends, and to whom. In order: :api_key when set; else the env var named by :api_key_env; else only the env var that belongs to the :base_url host, over https — api.openai.com → OPENAI_API_KEY, api.anthropic.com → ANTHROPIC_API_KEY, openrouter.ai (and its subdomains) → OPENROUTER_API_KEY. Any other host, or plain http, with neither option set raises ArgumentError (“No API key: …”) before any request is made. The same rule applies to Toolnexus.Client.list_models/1, which returns {:error, message} instead of raising.

  • A local or self-hosted endpoint — http://localhost:11434/v1 has no “own” env var, so pass api_key: "unused" (many local servers ignore it) or the key it really wants.
  • A proxy or gateway that fronts a provider — name the env var that holds the gateway key with api_key_env: "GATEWAY_KEY"; the value is read at call time and never logged.
  • Several provider keys in one environment — leave both unset on the official hosts; each host gets only its own key.
alias Toolnexus.Client
# A custom host with no key raises before any request is made.
no_key = Client.create(base_url: "http://127.0.0.1:1/v1", model: "m")
raised =
try do
Client.run(no_key, "hi", [])
false
rescue
e in ArgumentError -> e.message =~ "No API key"
end
true = raised
t = fn _req ->
{:ok,
%{status: 200, headers: %{},
body: %{"choices" => [%{"message" => %{"role" => "assistant", "content" => "local ok"}}],
"usage" => %{"prompt_tokens" => 1, "completion_tokens" => 1, "total_tokens" => 2}}}}
end
local = Client.create(base_url: "http://127.0.0.1:11434/v1", model: "m", api_key: "YOUR_KEY_HERE", transport: t)
true = Client.run(local, "hi", []).text == "local ok"
IO.puts("ok: explicit key required for a local host")
alias Toolnexus.Client
System.put_env("MY_GATEWAY_KEY", "YOUR_KEY_HERE")
me = self()
t = fn req ->
send(me, {:auth, req.headers["authorization"]})
{:ok, %{status: 200, headers: %{}, body: %{"data" => []}}}
end
{:ok, []} =
Client.list_models(base_url: "https://gateway.example/v1", api_key_env: "MY_GATEWAY_KEY", transport: t)
receive do
{:auth, "Bearer YOUR_KEY_HERE"} -> IO.puts("ok: the gateway got the key named by api_key_env")
end

3. Each official host gets only its own env var

Section titled “3. Each official host gets only its own env var”
alias Toolnexus.Client
System.put_env("OPENAI_API_KEY", "fake-openai")
System.put_env("OPENROUTER_API_KEY", "fake-openrouter")
me = self()
t = fn req ->
send(me, {:auth, req.headers["authorization"]})
{:ok, %{status: 200, headers: %{}, body: %{"data" => []}}}
end
{:ok, []} = Client.list_models(base_url: "https://api.openai.com/v1", transport: t)
receive do: ({:auth, "Bearer fake-openai"} -> :ok)
{:ok, []} = Client.list_models(base_url: "https://openrouter.ai/api/v1", transport: t)
receive do: ({:auth, "Bearer fake-openrouter"} -> :ok)
# No fallback: another host never sees either key, and plain http never qualifies.
{:error, msg} = Client.list_models(base_url: "https://example.com/v1", transport: t)
true = msg =~ "No API key" and not (msg =~ "fake-")
{:error, _} = Client.list_models(base_url: "http://api.openai.com/v1", transport: t)
IO.puts("ok: keys go only to their own host")
  • Toolnexus.Client.create — The unified client: system prompt, skills injection, parallel and chained tool calls, retries, memory.
  • 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 — Intercept before/after model calls and tool calls: audit, redact, veto, or rewrite.