Opair

The coding harness that eschews autonomy

Configuration

Opair is configured with environment variables and a config.json file. This document is the reference for both.

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:

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

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:

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.