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 }): stringkeyEnvForBaseUrl(baseUrl: string): string // "" when no provider env var may be sent thereWhich key the client sends, and to whom. The rule (SPEC §8, API key by host):
apiKeyset ⇒ only it.- Else
apiKeyEnvset ⇒ the value of that env var. - Else ONLY the env var that belongs to the
baseUrlhost, over https:api.openai.com→OPENAI_API_KEY,api.anthropic.com→ANTHROPIC_API_KEY,openrouter.ai(and subdomains) →OPENROUTER_API_KEY. - Any other host, or plain
http, with neither set ⇒ the request throwsNo API key: set apiKey or apiKeyEnv …. There is no cross-provider fallback.
When to use it
Section titled “When to use it”- 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) orapiKeyEnvnaming the var that holds the real key. - Several keys in one environment —
apiKeyEnvpicks exactly one by name, so the secret value never appears in code or config.
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 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 processlet 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: neverassert.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")See also
Section titled “See also”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.