Line

You are reading docs for v0.8. See latest (v0.9)

Construct

Using an AI model, optionally

When a model is involved, how to choose one, and how output is checked.

Construct never needs an AI model. Two places can use one, and only when you ask.

Construct is a library of small, deterministic, non-LLM building blocks. The only place an LLM is ever involved is where a human explicitly opts in with --llm <provider> (CLI) or a Settings capability (UI) — and even then, in one of exactly two shapes:

  1. Execution-class calls — small, scoped, mechanical: "write this one file's real implementation." One call per file, never one call for a whole batch. These may route to a local model (Ollama/Qwen Coder) as well as a hosted one (Claude).
  2. Plan-analysis-class calls — whole-feature, high-context reasoning: "look at this entire existing feature and propose how to decompose it." Exactly one call, always. Never allowed to route to a local model — enforced in code (ui/server/src/settings.mjs's updateSettings throws if planAnalysis is set to ollama), not just left as a UI convention.

Locating files, deciding which layers/files to scaffold, and deciding a feature's shape are never delegated to an LLM in either case — that stays deterministic code, or a human/Claude-in-conversation decision made before any of this runs.

Choosing a provider#

--llm claude runs the local claude command-line tool (it must be installed and signed in). --llm ollama calls a local Ollama server, by default at http://localhost:11434 with the model qwen2.5-coder:7b.

construct create layer Invoice --feature billing --layers domain,hook,page,controller --llm ollama

Which layers get created is still decided by Construct; the model only fills in the body of files that were going to be created anyway.

Model output is checked before it is written#

Every per-file fill (import, create, generate) goes through packages/core/llm-fill.mjs: the prompt states the reply is captured from stdout and that the model has no file access; a reply that wraps exactly one fenced block in prose is unwrapped; the result must parse as TypeScript/JavaScript (parseToAst) and contain a declaration, import or export. A rejected reply gets one corrected retry; a provider call that throws is not retried. Whatever happens, an unfilled file keeps its scaffolded stub (plus the TODO(import) breadcrumb on the import path), the command reports it per file, other files continue, and the exit code is 3.