Credentials
Real models need API keys and endpoints. You don’t configure them up front: the first run that needs a credential asks for it, and offers to save it. Keys stay off your command lines, out of your params, and out of the condition hash.
The first run asks
A model id’s prefix (anthropic/… → anthropic) names a credential set. The first interactive run that needs a set you don’t have asks at the gate, before anything launches:
adb: this run needs credential set 'anthropic' — setting it up now
ANTHROPIC_API_KEY [unset]: ****
ANTHROPIC_BASE_URL [default: https://api.anthropic.com]:
save 'anthropic' for future runs? [Y/n]:
name this profile [default]:
adb: [258b80e5323e r1] run 01KYS…H3 started
adb: ▸ watch http://127.0.0.1:8340/#/runs/01KYS…H3
adb: ▸ store ~/.local/share/adb/runs/258b80e5323e…/01KYS…H3
The gate is the last thing before the run moves, so the link to watch it live is the last thing printed — click it (if the viewer isn’t up yet, the line above it says so, and how to start it).
- Secret prompts are hidden — never echoed, and never on the command line. (There is deliberately no
KEY=VALUEargv form: argv shows up inpsand shell history.) Enteraccepts a shown[default: …].save? [Y/n]—Ystores the set for every future run;nuses it for this run only and forgets it.- The profile name is for keeping several credentials for the same provider —
Enteris the right answer until you need that (see Profiles). - Headless runs never hang on a prompt: with piped stdin or
--json, a missing set refuses the run and prints thecredentials setcommand to run instead.
A name is a condition, a key is environment
model NAME → the condition (a model param, e.g. openai/qwen3.5-9b)
endpoint → environment (not a condition)
API key → environment (not a condition)
Two researchers each running their own local qwen3.5-9b server should land in the same condition bucket — the science is “what does qwen3.5-9b do”, not “what does it do at my URL with my key”. So endpoints and keys are never experiment params, and changing them never changes a condition. See Experiments, conditions, runs.
Setting and managing them yourself
The same prompts, standalone — for setting up in advance or rotating a key:
nix run .#adb-runner -- \
credentials set anthropic
nix run .#adb-runner -- \
credentials list
Also credentials remove <name> and credentials path. credentials set re-prompts with your current values as defaults, so changing one field is Enter-past-the-rest; to script it, pipe one line per prompt on stdin — values still never touch argv. What the dialogue asks, per built-in name:
ANTHROPIC_API_KEY [unset]: **** ANTHROPIC_BASE_URL [default: https://api.anthropic.com]: save 'anthropic' for future runs? [Y/n]: name this profile [default]:Model ids:
anthropic/…, e.g.anthropic/claude-sonnet-4-5-20250929.
OPENAI_API_KEY [unset]: **** OPENAI_BASE_URL [default: https://api.openai.com/v1]: save 'openai' for future runs? [Y/n]: name this profile [default]:Model ids:
openai/…, e.g.openai/gpt-4o-2024-11-20.
GOOGLE_API_KEY [unset]: **** GOOGLE_BASE_URL [default: https://generativelanguage.googleapis.com/v1beta/openai]: save 'google' for future runs? [Y/n]: name this profile [default]:Model ids:
google/…, e.g.google/gemini-2.5-pro.
GROQ_API_KEY [unset]: **** GROQ_BASE_URL [default: https://api.groq.com/openai/v1]: save 'groq' for future runs? [Y/n]: name this profile [default]:Model ids:
groq/…, e.g.groq/llama-3.3-70b-versatile.
MOONSHOTAI_API_KEY [unset]: **** MOONSHOTAI_BASE_URL [default: https://api.moonshot.ai/v1]: save 'moonshotai' for future runs? [Y/n]: name this profile [default]:Model ids:
moonshotai/…, e.g.moonshotai/kimi-k3— Moonshot’s own API. The same models served through OpenRouter areopenrouter/moonshotai/…ids: a different provider, so a different condition.
OPENROUTER_API_KEY [unset]: **** OPENROUTER_BASE_URL [default: https://openrouter.ai/api/v1]: save 'openrouter' for future runs? [Y/n]: name this profile [default]:Model ids:
openrouter/…, e.g.openrouter/deepseek/deepseek-r1.
AZUREAI_API_KEY [unset]: **** AZUREAI_BASE_URL [unset]: https://my-endpoint.eastus.models.ai.azure.com save 'azureai' for future runs? [Y/n]: name this profile [default]:The base URL is your Azure endpoint (no universal default). Model ids:
azureai/…— the model part is your deployment name.
A llama.cpp / ollama / vLLM server speaks the OpenAI API — at the base-URL prompt, type your server’s full URL (scheme, port, and its
/v1prefix). The key can be anything if your server ignores it:OPENAI_API_KEY [unset]: **** OPENAI_BASE_URL [default: https://api.openai.com/v1]: http://localhost:11434/v1 save 'openai' for future runs? [Y/n]: name this profile [default]:Model ids:
openai/<served-model-name>— where the model is served is your environment, never part of the condition, so your runs bucket with everyone else’s runs of that model.
Custom set names — a vendor key and a self-hosted server at the same time
The built-in names are just prompt templates — the ADB knows which field is secret and what a sensible default base URL is, nothing more.
credentials setwith any name creates a named set with the conventional fields (<NAME>_API_KEY,<NAME>_BASE_URL), and model ids of the formopenai-api/<name>/<model>(inspect’s OpenAI-compatible services) route to the set of that name. That’s how a real OpenAI key and a self-hosted server coexist.One caveat: the service name is part of the model id, so it enters the condition. For poolable canonical conditions, prefer the plain
openai/<model>form.
Profiles
A credential set can hold several profiles — a work key and a personal key for the same provider, a proxy endpoint next to the direct one. The default profile is what every run uses silently; the moment a set has named profiles, interactive runs ask:
which 'openai' credentials? [default] work personal new:
always use 'work' for 'concordia'? [y/N]:
Entertakes the bracketed default; typing a name takes that profile;newcreates one on the spot (same prompts as setup, then a name).always use …? [y/N]—yremembers the choice per experiment, so this experiment never asks again. Remembered choices live in~/.config/adb/preferences.toml— profile names only, never values, so it isn’t secret; edit or delete lines freely to forget.- Create and edit profiles directly with
credentials set openai.work; delete one withcredentials remove openai.work. - Headless runs never see the picker: a remembered choice wins, else the
defaultprofile, else the run is refused with the fix. - Profiles are atomic — a profile missing a field never borrows it from another profile.
- The profile choice is environment, never identity: runs of the same model under different profiles land in the same condition bucket.
The store
~/.config/adb/credentials.toml # mode 0600, one [<set>.<profile>] section each
~/.config/adb/preferences.toml # remembered per-experiment choices (names only)
The path honors $XDG_CONFIG_HOME; $ADB_CREDENTIALS_FILE overrides it entirely. That variable is also the CI story: your pipeline materializes this file from its own secret manager and points the variable at it — chmod 600 it as you do, because a store readable by group or others is refused outright (with the chmod to run), the same way ssh treats a leaky private key. Base URLs are validated as you type them (an http(s):// scheme is required), so a typo is one retype instead of a cryptic client error mid-run.
The file is yours to edit by hand — credentials set is a convenience, not a gatekeeper. A set is a free-form field map: any env var you add to a section is injected into every run that routes to it, including vars the dialogue never asks about (an org id, an API version, extra vendor knobs). The dialogue only knows the common shape; the store carries whatever you put in it.
How credentials reach the experiment
A run’s environment is constructed, not inherited — there is no passthrough of your shell into an experiment. A run receives exactly: a minimal set of system basics (PATH, HOME, locale), the credential sets this run routes to, and the ADB_* run vars. A stray key exported in your shell cannot leak into a run, because nothing ambient ever enters one.
Because the environment is constructed, it is also recorded: each run’s record lists the env var names it received and which set each came from — secret values ablated, non-secret values (like base URLs) kept as covariates. What a run ran with is never a mystery; what the secrets were is never written down.
The trust caveat
Running third-party code with your keys is a real trust decision: an experiment process receives the credentials routed to it and could misuse them. Today’s mitigations: experiments in the monorepo are reviewed (nixpkgs-style), a run receives only the sets it routes to — never your whole keyring — and nothing ambient is exposed. VM-isolated execution with a recording proxy, where the raw key never enters the experiment process at all, is the planned next step (see docs/plan/credentials.md in the repo for the design).