Skip to content

apiKeyEnv

JavaScript · package toolnexus · SPEC §8 · js/src/client.ts

createClient({
baseUrl: string,
apiKey?: string, // used as-is
apiKeyEnv?: string, // the NAME of an env var to read at call time
…
})
resolveKey(opts: { apiKey?, apiKeyEnv?, baseUrl }): string
keyEnvForBaseUrl(baseUrl: string): string // "" when no provider env var may be sent there

Which key the client sends, and to whom. The rule (SPEC §8, API key by host):

  1. apiKey set ⇒ only it.
  2. Else apiKeyEnv set ⇒ the value of that env var.
  3. Else ONLY the env var that belongs to the baseUrl host, over https: api.openai.com → OPENAI_API_KEY, api.anthropic.com → ANTHROPIC_API_KEY, openrouter.ai (and subdomains) → OPENROUTER_API_KEY.
  4. Any other host, or plain http, with neither set ⇒ the request throws No API key: set apiKey or apiKeyEnv …. There is no cross-provider fallback.
  • A hosted provider at its own URL — set nothing; the matching env var is read.
  • A local server (Ollama, llama.cpp, vLLM) or a proxy — set apiKey (any non-empty string if the server ignores it) or apiKeyEnv naming the var that holds the real key.
  • Several keys in one environment — apiKeyEnv picks exactly one by name, so the secret value never appears in code or config.

1. The smallest useful call — a local server needs an explicit key

Section titled “1. The smallest useful call — a local server needs an explicit key”
import assert from "node:assert"
import http from "node:http"
import { createClient, createToolkit, defineTool } from "toolnexus"
// A local OpenAI-style stub: replies[i] is the i-th assistant message. Records every body.
function stub(replies: any[], usage = (n: number) => ({ prompt_tokens: 10, completion_tokens: 2, total_tokens: 12 })) {
const bodies: any[] = []
const server = http.createServer((req, res) => {
let raw = ""
req.on("data", (c) => (raw += c))
req.on("end", () => {
bodies.push(JSON.parse(raw))
const n = bodies.length
const msg = replies[Math.min(n, replies.length) - 1]
res.writeHead(200, { "content-type": "application/json" })
res.end(JSON.stringify({ choices: [{ message: { content: null, ...msg }, finish_reason: msg.tool_calls ? "tool_calls" : "stop" }], usage: usage(n) }))
})
})
return new Promise<{ url: string; bodies: any[]; close: () => void }>((r) =>
server.listen(0, "127.0.0.1", () => r({ url: `http://127.0.0.1:${(server.address() as any).port}`, bodies, close: () => server.close() })))
}
const llm = await stub([{ content: "hi" }])
const tk = await createToolkit({ builtins: false })
// No apiKey and a local http host: refused, never a provider key from the environment.
const bare = createClient({ baseUrl: llm.url, style: "openai", model: "stub" })
await assert.rejects(bare.run("hi", { toolkit: tk }), /set apiKey or apiKeyEnv/)
const ok = createClient({ baseUrl: llm.url, style: "openai", model: "stub", apiKey: "YOUR_KEY_HERE" })
assert.equal((await ok.run("hi", { toolkit: tk })).text, "hi")
llm.close()
console.log("ok: local host needs apiKey")

2. The realistic case — name the env var, never the value

Section titled “2. The realistic case — name the env var, never the value”
import assert from "node:assert"
import http from "node:http"
import { createClient, createToolkit, defineTool } from "toolnexus"
// A local OpenAI-style stub: replies[i] is the i-th assistant message. Records every body.
function stub(replies: any[], usage = (n: number) => ({ prompt_tokens: 10, completion_tokens: 2, total_tokens: 12 })) {
const bodies: any[] = []
const server = http.createServer((req, res) => {
let raw = ""
req.on("data", (c) => (raw += c))
req.on("end", () => {
bodies.push(JSON.parse(raw))
const n = bodies.length
const msg = replies[Math.min(n, replies.length) - 1]
res.writeHead(200, { "content-type": "application/json" })
res.end(JSON.stringify({ choices: [{ message: { content: null, ...msg }, finish_reason: msg.tool_calls ? "tool_calls" : "stop" }], usage: usage(n) }))
})
})
return new Promise<{ url: string; bodies: any[]; close: () => void }>((r) =>
server.listen(0, "127.0.0.1", () => r({ url: `http://127.0.0.1:${(server.address() as any).port}`, bodies, close: () => server.close() })))
}
process.env.MY_PROXY_KEY = "YOUR_KEY_HERE" // in real life set outside the process
let auth = ""
const llm = await stub([{ content: "proxied" }])
const client = createClient({
baseUrl: llm.url, style: "openai", model: "stub",
apiKeyEnv: "MY_PROXY_KEY",
fetch: (url, init) => { auth = new Headers(init?.headers).get("authorization") ?? ""; return fetch(url, init) },
})
const res = await client.run("hi", { toolkit: await createToolkit({ builtins: false }) })
assert.equal(res.text, "proxied")
assert.equal(auth, "Bearer YOUR_KEY_HERE")
llm.close()
console.log("ok: apiKeyEnv read at call time")

3. The full surface — which env var a host may receive

Section titled “3. The full surface — which env var a host may receive”
import assert from "node:assert"
import { keyEnvForBaseUrl, resolveKey } from "toolnexus"
assert.equal(keyEnvForBaseUrl("https://api.openai.com/v1"), "OPENAI_API_KEY")
assert.equal(keyEnvForBaseUrl("https://api.anthropic.com"), "ANTHROPIC_API_KEY")
assert.equal(keyEnvForBaseUrl("https://openrouter.ai/api/v1"), "OPENROUTER_API_KEY")
assert.equal(keyEnvForBaseUrl("http://api.openai.com/v1"), "") // plain http: never
assert.equal(keyEnvForBaseUrl("http://localhost:11434/v1"), "") // local: never
process.env.OPENAI_API_KEY = "YOUR_KEY_HERE"
assert.equal(resolveKey({ baseUrl: "https://api.openai.com/v1" }), "YOUR_KEY_HERE")
assert.throws(() => resolveKey({ baseUrl: "https://my-proxy.example/v1" }), /apiKeyEnv/)
assert.equal(resolveKey({ baseUrl: "https://my-proxy.example/v1", apiKey: "explicit" }), "explicit")
console.log("ok: key-by-host table")
  • createClient — The unified client: system prompt, skills injection, parallel and chained tool calls, retries, memory.
  • Client.run — Send a prompt, let the loop call tools until the model stops, get a RunResult.
  • Client.stream — The streaming loop: text deltas, tool-call events, and suspension events as they happen.
  • Hooks — Intercept before/after model calls and tool calls: audit, redact, veto, or rewrite.