Skip to content

api_key_env

Python · package toolnexus · SPEC §8 · python/src/toolnexus/client.py

create_client(
*, base_url: str, style: ClientStyle, model: str,
api_key: str | None = None, # the key itself — used as-is, always wins
api_key_env: str | None = None, # the NAME of an env var to read the key from
...,
) -> Client
# list_models(base_url=..., api_key=..., api_key_env=...) follows the same rule.

Which key the client sends, and to whom, in this order:

  1. api_key set ⇒ exactly that key.
  2. else api_key_env set ⇒ the value of the env var with that name.
  3. else ONLY the env var that belongs to the base_url host, and only over https: api.openai.com → OPENAI_API_KEY, api.anthropic.com → ANTHROPIC_API_KEY, openrouter.ai (and subdomains) → OPENROUTER_API_KEY.
  4. anything else — a local server, a proxy, plain http:// — raises RuntimeError("No API key: set api_key or api_key_env …") on the first request.

There is no cross-provider fallback: an OpenRouter key is never sent to api.openai.com, and no provider key is ever sent to a host you happen to point base_url at. The key’s value never appears in an error or a log line.

  • You point the client at a local model server (llama.cpp, Ollama, vLLM) or a corporate proxy: pass api_key (any placeholder the server accepts) or api_key_env="MY_PROXY_KEY".
  • Your key lives in an env var whose name is not the provider default (for example per-tenant keys): api_key_env names it, so the value never passes through your code.

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 asyncio
from toolnexus import create_client, create_toolkit
class Recorder:
def __init__(self):
self.headers = []
def post(self, url, headers, payload, timeout):
self.headers.append(headers)
return {"choices": [{"message": {"role": "assistant", "content": "ok"}}]}
async def main():
tk = await create_toolkit(builtins=False)
rec = Recorder()
client = create_client(
base_url="http://localhost:8080/v1", style="openai", model="local-model",
api_key="YOUR_KEY_HERE", http_transport=rec,
)
res = await client.run("hi", tk)
assert res.text == "ok"
assert rec.headers[0]["Authorization"] == "Bearer YOUR_KEY_HERE"
print("ok: explicit key sent to the local server")
asyncio.run(main())

2. The realistic case — a proxy key read from a named env var

Section titled “2. The realistic case — a proxy key read from a named env var”
import asyncio
import os
from toolnexus import create_client, create_toolkit
class Recorder:
def __init__(self):
self.headers = []
def post(self, url, headers, payload, timeout):
self.headers.append(headers)
return {"choices": [{"message": {"role": "assistant", "content": "ok"}}]}
async def main():
os.environ["MY_PROXY_KEY"] = "YOUR_KEY_HERE" # set by your deployment, not your code
tk = await create_toolkit(builtins=False)
rec = Recorder()
client = create_client(
base_url="https://llm-proxy.internal.example/v1", style="openai", model="gpt-4o-mini",
api_key_env="MY_PROXY_KEY", http_transport=rec,
)
await client.run("hi", tk)
assert rec.headers[0]["Authorization"] == "Bearer YOUR_KEY_HERE"
print("ok: api_key_env read at request time")
asyncio.run(main())

3. The full surface — a provider key is never sent to a host it does not belong to

Section titled “3. The full surface — a provider key is never sent to a host it does not belong to”
import asyncio
import os
from toolnexus import create_client, create_toolkit
class Recorder:
def __init__(self):
self.calls = 0
def post(self, url, headers, payload, timeout):
self.calls += 1
return {"choices": [{"message": {"role": "assistant", "content": "ok"}}]}
async def main():
os.environ["OPENAI_API_KEY"] = "YOUR_KEY_HERE"
tk = await create_toolkit(builtins=False)
rec = Recorder()
client = create_client(
base_url="http://localhost:8080/v1", style="openai", model="local-model",
http_transport=rec, retries=0,
)
try:
await client.run("hi", tk)
raise AssertionError("expected No API key")
except RuntimeError as e:
assert "No API key" in str(e) and "YOUR_KEY_HERE" not in str(e)
assert rec.calls == 0 # nothing was sent
print("ok: OPENAI_API_KEY was not sent to localhost")
asyncio.run(main())
  • create_client — The unified client: system prompt, skills injection, parallel and chained tool calls, retries, memory.
  • list_models — List a provider’s models under the same key rules.
  • Client.run — Send a prompt, let the loop call tools until the model stops, get a RunResult.