Skip to content

Options.apiKeyEnv

Java · package io.github.muthuishere:toolnexus · SPEC §8

LlmClient.Options
public String apiKey; // the key itself; used as-is when set
public 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:

  1. apiKey set: only it is used.
  2. Else apiKeyEnv set: the env var with that name.
  3. Else ONLY the env var that belongs to the baseUrl host, 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.

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.

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");
}
}
}
  • 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.