Configuration
Opair is configured with environment variables
and a config.json file.
This document is the reference for both.
- Quick start
- How configuration works
- config.json
- A worked example
- Environment variables
- Validation and startup behaviour
- Failover and degradation
- Running against llama.cpp
Quick start
The minimal configuration is two environment variables:
export OPAIR_PROVIDER=anthropic
export ANTHROPIC_API_KEY=sk-...
This is the fallback path.
If a config.json exists, it is used instead
and OPAIR_PROVIDER is ignored.
See How configuration works.
Valid OPAIR_PROVIDER values and their key variables:
| Provider | API key variable |
|---|---|
anthropic |
ANTHROPIC_API_KEY |
deepseek |
DEEPSEEK_API_KEY |
moonshot |
MOONSHOT_API_KEY |
openai |
OPENAI_API_KEY |
zai |
ZAI_API_KEY |
llamacpp |
LLAMACPP_API_KEY (only if your server requires auth) |
enablers |
none (self-authenticating; internal to Algolia) |
If the key variable is unset, Opair refuses to start and says which variable it wanted.
How configuration works
Opair keeps its configuration in a per-user directory:
- MacOS:
~/Library/Application Support/opair/ - Linux:
$XDG_CONFIG_HOME/opair/
The directory holds config.json
alongside the tools/, loops/, prompts/ and mcp/ directories.
Those directories are documented elsewhere
(tools,
MCP);
this document covers config.json and environment variables.
Set OPAIR_CONFIG_FILE to load the configuration
from a different path.
~ and environment variables in the value are expanded.
If the configuration file exists
(at the default path or OPAIR_CONFIG_FILE),
it is used and OPAIR_PROVIDER is ignored.
If it does not exist,
Opair falls back to the environment-variable setup
described in Quick start.
Unlike everything else in the config directory,
config.json is read once, at startup.
Edits require a restart.
The other directories hot-reload;
it is arguably inconsistent that this one doesn't,
but there it is.
config.json
The file is JSON with four top-level properties:
providers (required),
defaults, roles and loops (all optional).
Unknown fields are rejected.
A file that fails validation stops Opair starting;
a typo shouldn't silently change
which model reviews your code.
providers
Each entry enables one provider. To enable a provider with all defaults, set it to an empty object:
{
"providers": {
"zai": {}
}
}
| Field | Required | Meaning |
|---|---|---|
apiKeyEnv |
no | Environment variable holding the API key. Defaults to <PROVIDER>_API_KEY. |
baseUri |
no | Overrides the default base URI for the provider API. Must include any version prefix. |
Default base URIs:
| Provider | Default base URI |
|---|---|
anthropic |
https://api.anthropic.com |
deepseek |
https://api.deepseek.com/v1 |
enablers |
https://inference.api.enablers.algolia.net/v1 |
llamacpp |
http://127.0.0.1:8080/v1 |
moonshot |
https://api.moonshot.ai/v1 |
openai |
https://api.openai.com/v1 |
zai |
https://api.z.ai/api/paas/v4 |
For llamacpp, baseUri is the only way
to point Opair at a non-default host or port;
the environment-variable setup path has no equivalent override.
The API key itself is still read from the environment,
whatever apiKeyEnv names it.
enablers and llamacpp don't need one.
defaults
defaults supplies the models
used when a role doesn't specify one:
| Field | Required | Meaning |
|---|---|---|
large |
no | Default model for large prompts. |
small |
no | Default model for small prompts. |
Both values are model specs (see roles).
With more than one provider configured,
at least one of large or small is required.
Model specs that reference a default
which isn't set are rejected at load.
roles
roles maps a role to a model,
so different parts of the harness
can run on different providers or models:
| Role | Purpose |
|---|---|
driver |
The main coding loop. |
navigator |
Monitor and thinking-partner loop. |
plan |
The /plan sub-agent. |
review |
The /review-* sub-agents. |
document |
The /document sub-agent. |
compactUncached |
Conversation compaction, in the variant that doesn't rely on the prompt cache. |
title |
Session naming. |
A model spec is either
provider/model-id,
e.g. zai/glm-5.3-flash,
or the string large or small,
which resolves via defaults.
The provider must be configured;
references to unknown providers are rejected at load.
Roles you don't set fall back to defaults.large,
except title, which falls back to defaults.small.
If that default isn't set either,
any configured provider's large model is used.
/model switches the model for the current mode at runtime,
overriding the configured role for the rest of the session.
loops
loops overrides the built-in behaviour
of individual loops, per loop:
| Field | Meaning |
|---|---|
approveReads |
Read categories the loop runs without approval. |
approveWrites |
Write categories the loop runs without approval. |
approve |
MCP servers whose tools the loop runs without approval. |
reasoningEffort |
Reasoning effort for the loop: "off", "low" or "high". |
Read categories:
"project", "project-dot", "project-ignore",
"git", "github" and "any".
Write categories are those plus "artifacts" and "agent".
What the categories cover is documented in the
tools reference.
"agent" is a loop-only class:
a loop granted "agent" may edit and delete files
that an agent create tool created earlier in the session
without prompting.
When approveReads or approveWrites is set,
it replaces the loop's built-in list entirely.
It is not merged with it.
Write whatever the loop should have.
approve entries are server names
in the form mcp:github, mcp:gitlab and so on.
A granted server's tools run without prompting,
unless the server config overrides per tool.
See the MCP reference.
Loop names are the built-in ones:
driver, navigator, plan, review and document.
An unknown loop name is silently ignored,
so spell them right.
reasoningEffort is mapped to provider-specific
equivalents on a best-effort basis,
so the setting works across providers
even when they use different nomenclature.
If a provider cannot apply the requested level,
it falls back to the model's default behaviour.
One thing isn't configurable here: tools marked untrusted always prompt for approval, whatever the loop grants. See the tools reference.
A worked example
Three providers, specific models for certain roles, a loop-specific write approval and reasoning effort overrides:
{
"providers": {
"zai": {},
"deepseek": {},
"moonshot": {}
},
"defaults": {
"large": "deepseek/deepseek-v4-pro",
"small": "deepseek/deepseek-v4-flash"
},
"roles": {
"driver": "zai/glm-5.3-flash",
"navigator": "zai/glm-5.3-flash",
"document": "deepseek/deepseek-v4-pro",
"plan": "moonshot/kimi-k3",
"review": "moonshot/kimi-k3"
},
"loops": {
"driver": {
"approveWrites": ["artifacts", "project"],
"reasoningEffort": "low"
},
"navigator": {
"reasoningEffort": "low"
},
"document": {
"reasoningEffort": "high"
},
"plan": {
"reasoningEffort": "low"
},
"review": {
"reasoningEffort": "low"
}
}
}
Example files for each individual provider
(config.anthropic.json, config.openai.json and friends)
ship in the config directory.
Copy one to config.json to start from it.
Environment variables
| Variable | Meaning |
|---|---|
OPAIR_PROVIDER |
Provider for the environment-variable setup. Ignored when a config file exists. |
OPAIR_CONFIG_FILE |
Path to the configuration file, instead of the default. |
OPAIR_TOOLS_DIR |
Tools directory, instead of the default. |
OPAIR_LOOPS_DIR |
Loops directory, instead of the default. |
OPAIR_PROMPTS_DIR |
Prompts directory, instead of the default. |
OPAIR_MCP_DIR |
MCP directory, instead of the default. |
OPAIR_PROJECT_DIR |
Project root, instead of the working directory. |
Directory overrides expand ~ and environment variables,
so paths like ~/opair-tools work.
See providers for the API key variables
and their defaults.
Validation and startup behaviour
-
The configuration is loaded once, at startup. Changes take effect on restart.
-
A missing file is not an error as long as
OPAIR_PROVIDERis set. A missing file with noOPAIR_PROVIDERis. -
Unknown JSON fields stop Opair starting.
-
Invalid values stop Opair starting: no
providers, an unknown provider, missingdefaultswith multiple providers, a model spec referencing an unknown provider or an unset default, an invalidreasoningEffort. -
An unknown entry in
rolesproduces a warning and is ignored. Everything else either works or doesn't start. -
Warnings are printed to stderr and logged to the session.
Failover and degradation
With multiple providers configured,
auth errors (401, 402, 403)
degrade an entire provider.
Model-not-found errors (404)
degrade only that specific model.
In both cases the router transparently fails over
to the next available model or provider.
Transient errors (429, 5xx, network)
are not failed over.
They surface in the status area
and you can retry.
Explicitly selecting a degraded model or provider via /model
clears the degradation and retries.
Useful after topping up API credits.
Failover events are logged to the session.
Running against llama.cpp
Start llama serve with:
-
--jinja: Required for tool calling to work. -
--reasoning-format deepseek: Separatesreasoning_contentfromcontent. -
--no-context-shift: Recommended. Treats context overflow as an error instead of silently dropping earlier messages.
Point baseUri at the server
if it isn't on the default host and port.
No API key is needed unless your server requires authentication.
Reasoning effort is coarser than on hosted providers.
"off" was verified against a live server
to genuinely disable thinking.
"low" and "high" both enable it,
but whether llama-server distinguishes between the two
was not verified.