Options.apiKeyEnv
Java · package io.github.muthuishere:toolnexus · SPEC §8
public String apiKey; // the key itself; used as-is when setpublic String apiKeyEnv; // the NAME of an env var to read it from (never the value)public Options apiKey(String v)public Options apiKeyEnv(String v)Which key the client sends, and to which host. The order is fixed:
apiKeyset: only it is used.- Else
apiKeyEnvset: the env var with that name. - Else ONLY the env var that belongs to the
baseUrlhost, and only over https:https://api.openai.com→OPENAI_API_KEY,https://api.anthropic.com→ANTHROPIC_API_KEY,https://openrouter.ai(and subdomains) →OPENROUTER_API_KEY.
Any other host (a local server, a proxy, a gateway), or plain http, with neither apiKey nor
apiKeyEnv fails the request with No API key: set apiKey or apiKeyEnv …. There is no
cross-provider fallback, and the value is never logged. The same rules apply to
LlmClient.listModels.
When to use it
Section titled “When to use it”Set apiKeyEnv when the key lives in an env var whose name the library cannot infer from the
host: a self-hosted gateway, an Azure or LiteLLM proxy, a local llama.cpp/Ollama server that checks
a token. Set apiKey when the host already holds the key (a secret manager, a per-tenant key). Set
neither when baseUrl is one of the three public providers and its own env var is set.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — a local server with an explicit key
Section titled “1. The smallest useful call — a local server with an explicit key”import com.sun.net.httpserver.HttpServer;import io.github.muthuishere.toolnexus.*;import java.io.OutputStream;import java.net.InetSocketAddress;import java.nio.charset.StandardCharsets;import java.util.concurrent.atomic.AtomicReference;
public class Example { public static void main(String[] args) throws Exception { AtomicReference<String> auth = new AtomicReference<>(); HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); server.createContext("/", ex -> { auth.set(ex.getRequestHeaders().getFirst("Authorization")); byte[] b = "{\"choices\":[{\"message\":{\"content\":\"ok\"}}]}".getBytes(StandardCharsets.UTF_8); ex.getResponseHeaders().add("Content-Type", "application/json"); ex.sendResponseHeaders(200, b.length); try (OutputStream os = ex.getResponseBody()) { os.write(b); } }); server.start(); try (Toolkit tk = Toolkit.create(new Toolkit.Options().builtins(false))) { LlmClient client = LlmClient.create(new LlmClient.Options() .baseUrl("http://127.0.0.1:" + server.getAddress().getPort()) .model("local-model") .apiKey("YOUR_KEY_HERE")); // a local host needs apiKey or apiKeyEnv LlmClient.RunResult res = client.run("hi", tk); if (!"ok".equals(res.text)) throw new AssertionError(res.text); if (!"Bearer YOUR_KEY_HERE".equals(auth.get())) throw new AssertionError(auth.get()); System.out.println("ok: sent the explicit key"); } finally { server.stop(0); } }}2. The realistic case — no key for a custom host is an error, not a leak
Section titled “2. The realistic case — no key for a custom host is an error, not a leak”Even with OPENAI_API_KEY set in the environment, a client pointed at a non-provider host does not
send it.
import io.github.muthuishere.toolnexus.*;
public class Example { public static void main(String[] args) throws Exception { try (Toolkit tk = Toolkit.create(new Toolkit.Options().builtins(false))) { LlmClient client = LlmClient.create(new LlmClient.Options() .baseUrl("http://127.0.0.1:9") // nothing listens; we never get that far .model("m") .retries(0)); String msg = ""; try { client.run("hi", tk); } catch (RuntimeException e) { msg = e.getMessage(); } if (!msg.contains("No API key")) throw new AssertionError(msg); System.out.println("ok: " + msg.substring(0, msg.indexOf(':'))); } }}3. The full surface — apiKeyEnv names the variable
Section titled “3. The full surface — apiKeyEnv names the variable”apiKeyEnv wins over the host table. When the named variable is unset, the error names it, so the
fix is obvious.
import io.github.muthuishere.toolnexus.*;
public class Example { public static void main(String[] args) throws Exception { try (Toolkit tk = Toolkit.create(new Toolkit.Options().builtins(false))) { LlmClient client = LlmClient.create(new LlmClient.Options() .baseUrl("https://llm-gateway.internal.example/v1") .model("m") .apiKeyEnv("TOOLNEXUS_DOCS_GATEWAY_KEY") // read at request time, never logged .retries(0)); String msg = ""; try { client.run("hi", tk); } catch (RuntimeException e) { msg = e.getMessage(); } if (!msg.contains("TOOLNEXUS_DOCS_GATEWAY_KEY")) throw new AssertionError(msg); System.out.println("ok: error names the env var"); } }}See also
Section titled “See also”LlmClient.create— The unified client: system prompt, skills injection, parallel and chained tool calls, retries, memory.LlmClient.run— Send a prompt, let the loop call tools until the model stops, get a RunResult.LlmClient.stream— The streaming loop: text deltas, tool-call events, and suspension events as they happen.LlmClient.Hooks— Intercept before/after model calls and tool calls: audit, redact, veto, or rewrite.