Constructdocs
section · guides

Plug in your own LLM

Swap models per tier (reasoning / standard / fast): Anthropic, OpenAI, OpenRouter, Ollama, or any OpenAI-compatible endpoint.

6 min read·guides / cookbook / plug-in-your-own-llm

Construct doesn't hardcode a single LLM. It assigns three tiers (reasoning, standard, fast) and each specialist declares which tier it uses. Swapping providers is a config change, not a code change.

The tier model

reasoning tierlong context · architecture opus · o1 · gemini-prostandard tiereveryday work sonnet · gpt-4o · mistralfast tierquick lookups haiku · gpt-4o-mini · qwen

Three tiers. Every specialist uses one of them.

  • reasoning: slow, expensive, best at long-context analysis and architectural decisions. Defaults to Claude Opus.
  • standard: everyday work. Defaults to Claude Sonnet.
  • fast: quick lookups, low-stakes responses. Defaults to Claude Haiku.

Specialists declare their tier in specialists/org. Changing the tier→model mapping changes the model for every specialist on that tier.

Inspect current assignments

construct models

Shows the active tier→model mapping plus the fallback chain per tier. Output looks like:

reasoning  →  anthropic/claude-opus-4-7
standard   →  anthropic/claude-sonnet-4-6
fast       →  anthropic/claude-haiku-4-5

Fallbacks (used on rate-limit or model-unavailable):
  reasoning: [openrouter/anthropic/claude-opus, openrouter/meta-llama/llama-3.3]
  standard:  [openrouter/anthropic/claude-sonnet, openrouter/openai/gpt-4o-mini]
  fast:      [openrouter/anthropic/claude-haiku, openrouter/openai/gpt-4o-mini]

Swap one tier

construct models set --tier=reasoning --model=openrouter/anthropic/claude-opus-4-7

This writes to specialists/org under the models block. The change takes effect on the next session-start. No construct sync needed.

For a temporary swap (single session), set the env var instead:

export CX_MODEL_REASONING=openrouter/anthropic/claude-opus-4-7

Swap to a non-Anthropic provider

Any OpenAI-compatible endpoint works. The model identifier is whatever the provider expects.

OpenRouter:

construct models set --tier=standard --model=openrouter/openai/gpt-4o

OpenRouter exposes most major models through one API. Set OPENROUTER_API_KEY in your environment or ~/.config/construct/config.env (see construct creds list).

OpenAI direct:

construct models set --tier=standard --model=openai/gpt-4o

Set OPENAI_API_KEY.

Ollama (local):

construct models set --tier=fast --model=ollama/llama3.3:70b

Ollama needs to be running locally (ollama serve). No API key. Construct talks to http://localhost:11434 by default; override with OLLAMA_BASE_URL. OLLAMA_HOST is accepted as a legacy alias where supported.

Any OpenAI-compatible endpoint:

construct models set --tier=standard --model=custom/my-model \
  --endpoint=https://my-host.example.com/v1

The custom/<id> prefix routes through the generic OpenAI-compatible client.

Prefer free / local models

First poll the free catalog, then set a tier with --prefer-free to auto-populate its fallback chain from the cheapest available models:

construct models free
construct models set --tier=<reasoning|standard|fast> --model=<provider/model-id> --prefer-free

--prefer-free biases the tier's inferred fallback chain toward the cheapest available models; --prefer-free-same-family keeps that chain within the primary model's provider family. Useful for development, exploration, and when you don't want to burn cloud credits on routine work.

For smaller local checkpoints, set CONSTRUCT_MODEL_PROFILE=small in ~/.config/construct/config.env. That keeps runtime prompts tighter, compresses role overlays, and biases prompt assembly toward retrieval-first context instead of broad prompt stuffing.

Model policy presets

When you want every tier moved at once instead of setting them one by one, use the policy presets. A preset is computed from your configured providers' live pricing and persisted to specialists/org/models.json — the single edit surface for model assignment. Env pins (CX_MODEL_<TIER>) still override whatever a preset writes, so nothing new enters the resolution chain.

construct models policy show            # effective policy: winning source per tier + work-category map
construct models policy set budget      # all tiers → cheapest eligible models for your providers
construct models policy set free        # :free OpenRouter slugs only
construct models policy set frontier    # best-available (flagship) models
construct models policy set local       # Ollama / local server first
  • budget ranks every configured provider's tier candidates by live pricing (via the OpenRouter pricing endpoint) and picks the cheapest. Flagship models are hard-excluded, and a candidate whose price can't be fetched is ranked last — never treated as free — so a budget policy on an OpenRouter-only setup provably resolves no frontier model for any tier. If pricing is unreachable it falls back to a static ordering (a :free slug first) and says so.
  • free assigns only :free slugs. A tier with no free model available is left unset and reported, never silently substituted.
  • frontier picks the best-available model per tier from your configured providers.
  • local prefers Ollama or a local OpenAI-compatible server.

A preset refuses (non-zero exit, nothing written) when no provider credential can back it — free needs an OpenRouter key, local needs a running local provider — so it never writes an unbacked default. policy set writes only models.json; it touches no env file or construct.config.json.

Trace how a single specialist resolves its model:

construct models explain --role cx-reviewer

It names the specialist's declared tier, the tier it maps to, the resolved model, and the winning rule — the same result construct models resolve --json --tier <tier> reports.

Resolve keys from 1Password

If you keep provider keys in 1Password and run other LLM CLIs through op run, point Construct at the same env-file:

# ~/.config/construct/config.env
CONSTRUCT_OP_ENV_FILE=~/.config/claude/.env.op

The env-file holds op:// references (OPENROUTER_API_KEY=op://vault/item/credential). When it is set and the op CLI is installed, construct dev re-execs itself once under a single op run --env-file …, so every reference resolves one time at startup (one biometric unlock) and every detached daemon inherits the resolved keys — you do not wrap the CLI by hand. OpenCode and other supported hosts then connect through the synced Construct MCP server. This is strictly opt-in: unset the variable and Construct never invokes op, resolving plain keys from the environment or config.env as usual.

Non-interactive auth (OP_SERVICE_ACCOUNT_TOKEN)

The desktop-app integration prompts for biometric unlock and depends on a warm GUI session — fine for a laptop, but it breaks in headless, daemon, and CI contexts, and a cold session produces repeated prompts. For those, 1Password's own recommended path is a service account: set OP_SERVICE_ACCOUNT_TOKEN and op read/op run authenticate with no desktop app, biometric, or op signin.

export OP_SERVICE_ACCOUNT_TOKEN=<token>   # from the OS keychain in practice, not plaintext

Construct forwards the token from its env into every op invocation, so an op:// reference resolves non-interactively. construct doctor credentials reports the active auth mode (service-account vs desktop-session, and whether the desktop session is signed in). Caveats from 1Password: a service account cannot read your built-in Personal/Private vault (put the refs in a custom vault you grant it), the token is a bearer secret (store it in the OS keychain, never in config.env), and the Individual/Family tier is rate-limited to ~1,000 reads/day.

Fallback chains

Each tier has a fallback chain. If the primary model returns rate-limit, model-unavailable, or a timeout, Construct walks the chain in order — primary first, then each entry in the tier's fallback list.

You don't configure the chain by hand. Construct populates it automatically when you set a tier: tier inference picks sensible alternates for the model you chose. Passing --prefer-free-same-family on construct models set biases that inferred chain toward cheaper models within the primary's provider family:

construct models set --tier=reasoning --model=openrouter/anthropic/claude-opus-4-7 --prefer-free-same-family

Run construct models to see the resulting chain per tier.

Verify

construct models
construct doctor

doctor checks that the active models are reachable (it does a models list call against each provider). If a model is configured but the API key is missing, doctor reports it as a critical failure.

Per-specialist override

Most of the time you swap by tier: but a single specialist can override its tier with a direct model:

{
  "name": "performance-auditor",
  "role": "Performance specialist",
  "model": "openrouter/openai/o1-preview",
  "model_tier": "reasoning"
}

When model is set, it overrides the tier mapping for this specialist only. Use sparingly: it defeats the point of the tier model.

Track cost across providers

construct status --json | jq .sessionUsage shows the local token and estimated-cost ledger across configured providers. The dashboard's Doctor page shows live per-persona burn. If you swap to a new provider, construct optimize can rebalance specialists to the new cost curve.

What about embeddings?

Embedding model is separate from LLM model. See Plug in a retrieval backend for swapping embedders.

Reference