Skip to content
ncz-osPublic

About

Free-first, cost-aware, codex-compatible coding & review CLI. Routes work to free/open-weight models first over any OpenAI-compatible fleet.

Resources

Stars

1 star

Watchers

1 watching

Forks

Latest commit

 

History

953 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

zoder

zoder — pair code · dispatch · remember

The full-stack developer's AI pair-coding and headless coding-dispatch system — free-first, cost-governed, MNEMOS-first.

TL;DR — install in one line

curl -fsSL https://raw.githubusercontent.com/ncz-os/zoder/master/install.sh | sh

Detects your OS/arch, verifies the SHA-256 checksum, and installs the selected nightly channel build to ~/.local/bin (zoder). The nightly release bundles zerocode (TUI) and zeroclaw (engine) alongside zoder — the installer installs all three when the channel artifact exists (the installer treats them as optional only for backward compatibility with old channels that predate the trio). Targets: linux-x86_64, linux-aarch64, macOS-arm64 (Windows → WSL). Pin/automate: set ZODER_CHANNEL=YYYY-MM-DD or pass --channel YYYY-MM-DD. Full options, manual, and source builds are below.

Quick usage:

zoder "refactor this for readability" < src/foo.rs   # run a task on the best FREE model
zerocode                                             # interactive pair-coding TUI
zoder review        # code review → structured verdict (add --panel M1,M2 for a multi-model panel)
zoder loop          # author → validate → review → fix, until it passes
zoder report        # spend + what the free path saved you, in dollars

Single-host quick start (standalone profile)

zoder's default deployment is one host. install.sh writes a starter config only when no zoder config exists; zoder init does the same for an existing install. Existing configs are never overwritten.

# 1. One hosted provider key is enough. If only MINIMAX_API_KEY is set,
#    `zoder init` pins the author route to MiniMax.
export DEEPSEEK_API_KEY=...
# or: export MINIMAX_API_KEY=...

# 2. Create the starter config (skipped when a config already exists).
zoder init --profile standalone

# 3. Fill in your provider endpoint(s) in $ZODER_HOME/config.standalone.toml.
#    The template ships every provider with the host-free "unset" sentinel;
#    replace each base_url with the OpenAI-compatible API base your provider
#    publishes.

# 4. First run. Hosted models are metered, so paid calls need --allow-paid.
zoder exec --dry-run "summarize this repo"      # route preview; no model call
zoder exec --allow-paid "summarize this repo"
zoder review --allow-paid
zoder loop --allow-paid --check "cargo test"

The template includes an optional local OpenAI-compatible endpoint, commented out, plus a conservative [budget] backstop. With one provider key the reviewer first tries the other provider and then falls back to the provider you have, printing a warning; with two keys it uses a genuine cross-family reviewer. See docs/PROFILES.md for standalone vs fleet and the settings that differ.


zoder is to ZeroClaw what Ubuntu is to Debian: a curated, opinionated distribution built on the same engine, aimed at a specific audience. ZeroClaw is the general-purpose agent framework; zoder is the developer-facing build of it — tuned for two jobs:

  1. Interactive pair-coding at the terminal (the zerocode TUI), and
  2. Headless, automated coding dispatch — running zoder as a worker in a hive of agents that pick up coding tasks, run them on the cheapest capable model, review, fix, and report cost — with no human in the loop.

It routes work to free / open-weight models first, refuses to silently fall back to a paid backend, tracks every call in a local spend ledger, and produces FinOps-style chargeback reports that show — in dollars — what the free path saved.

zoder is vendor-neutral: it works against any OpenAI-compatible / LiteLLM endpoint. Free, open-weight providers are first-class; enterprise gateways are added via config overlays, never hardcoded.

Relationship to ZeroClaw (a friendly fork)

zoder consumes ZeroClaw's master and carries its own enhancements as a clean, rebasing patch stack on the ncz-os/zeroclaw fork (branch zoder-integration; see docs/VENDORING.md). The fork exists because the objectives differ, not out of antagonism: we keep ingesting upstream, and ZeroClaw is free to adopt any of our patches. We do not open PRs upstream — our roadmap lives here. scripts/package.sh builds the engine + zerocode UI from the fork and ships them next to the zoder binary.

Part of the ncz-os family — MNEMOS-first

zoder belongs to the ncz-os family and is MNEMOS-first for memory. MNEMOS is ncz-os's open-source agent memory system: a versioned, queryable store (REST + an MCP server) for long-lived memories and session history, with semantic + full-text search and pluggable vector backends. It's the system of record that lets a fleet of agents share context across runs and hosts instead of each keeping scattered local files.

When a MNEMOS datastore is configured, zoder logs memories and recall there. For durable session history zoder also ships database-backed session persistence (PostgreSQL / MySQL / Oracle / Db2, feature-gated) — so a hive of headless workers shares one system of record. See Enterprise memory & persistence for the two wiring paths (MCP-per-agent recall vs. direct DB logging).


Commands

# Run a coding task — zoder picks the best FREE model automatically.
zoder "refactor this function for readability" < src/foo.rs

# Non-interactive (CI/automation), codex-compatible. `-` reads stdin.
zoder exec -

# Multi-model code review with a consensus verdict.
zoder review

# Review -> agent applies fixes in place -> re-review, until it passes.
zoder loop

# See which model the router would choose (and the fallback chain).
zoder route "write a unit test"

# See your spend and how much the free models saved you.
zoder report
Command What it does
zoder "<prompt>" / zoder exec Run a task through the free-first router (codex-compatible; - reads stdin).
zoder review Run a code review (single model by default). Pass --panel M1,M2 to fan to additional reviewer models for a multi-model panel. Emits a structured JSON verdict.
zoder loop Author → validate (build/test) → adversarial review → fix, looping until the check passes and the reviewer raises no blocking findings. Pass --check "<cmd>" to gate each iteration on a local command (e.g. cargo test -p foo), or --reviewer <model> for an adversarial second opinion. Multi-model review (the --panel flag) is opt-in and only applies to the review command; loop always uses a single reviewer (configurable via --reviewer or the routed model).
zoder rescue Drive one agentic, write-capable turn at a stubborn bug or failing diagnostic, keeping the partial work a timeout leaves behind. Shares the loop's workspace cleanup (below).
zoder route Show the model the router would pick + the cross-family fallback chain.
zoder report Usage + chargeback report: daily/weekly/by-model, with the savings headline. Pass --vendor <name> to scope to a TOML-defined vendor (e.g. enterprise, ibm, microsoft).
zoder spend Raw spend rollups from the local ledger.
zoder models List the classified model corpus (free routing pool by default).
zoder health Per-model circuit-breaker state + measured latency.
zoder refresh Reconcile the corpus against the live served-model list.
zoder sessions List saved multi-turn sessions.
zoder config Show / validate configuration + corpus.
zoder completions Shell completions (bash/zsh/fish/powershell/elvish).

Everything is local-first: routing, the ledger, health, and reports work on your workstation with no service to stand up.

What a loop/rescue leaves running (and what it cleans up)

An agent started by loop/rescue can background work of its own — load generators, a dev server, a watcher — inside the workspace you handed it (-C). Those processes belong to the engine daemon's tool shells, not to zoder's process group, so neither zoder nor a job runner's killpg can reach them: on 2026-10-21 a loop's leftovers ran for two hours past both the end of the loop and the job runner's own timeout.

So every loop/rescue exit path — resolved, unresolved, author/check/review timeout, SIGTERM, SIGINT, panic unwind — first cancels the agent session (the id is recorded the moment the engine mints it, so a turn that is still running when the signal arrives is the one that gets cancelled), then reaps what the agent left behind: SIGTERM, a short grace, SIGKILL, by exact PID, logging every pid + comm it kills. Teardown alternates cancel and reap until a round does neither, because the reap itself takes seconds and the agent can fork during it.

Selection is deliberately narrow, because this runs on shared hosts where another session's processes look identical from the outside. A candidate must be: same uid; cwd inside the -C directory or inside this run's own scratch directory under the temp root (never the temp root itself — on a shared host everybody's cwd is under /tmp, and -C /tmp is refused outright rather than honoured); started after the run began (starttime, and absent from the pid snapshot taken at that moment, so a process that merely shares a USER_HZ tick with the run is not swept up); not zoder itself or an ancestor; and, where the kernel supports pidfd, pinned before it is signalled, so a pid recycled mid-teardown cannot put a stranger in front of the signal. There is no pkill-style pattern anywhere in the path.

Nothing in /proc says "the agent spawned this", so that list is the whole ownership test — the alternative is the bug this fixes. Its residual exposure is a same-uid process somebody else starts in this run's own workspace while the run is going. --no-reap is the escape hatch when even that is too much. On pre-5.3 kernels, which have no pidfd, the check-then-kill window is narrowed but cannot be closed. A process that survives SIGKILL is reported as a warning, not as a reaped pid. Linux only; a no-op elsewhere.

One process is exempt from all of it, by identity and never by command line: the shared zeroclaw daemon. It is not the agent's — it is one daemon per host that every zoder drives through <data dir>/daemon.sock, and it deliberately outlives the run that happened to start it. It is spared when this zoder spawned it (spawn()'s pid and its start time, recorded the instant the child exists), when it is the process recorded for that socket (<pid> <start_ticks> in <data dir>/daemon.pid, so another zoder's reap scope can spare it too), or when /proc/<pid>/exe is the exact daemon binary zoder itself spawns (compared by resolved file, so a symlinked install matches and a copy an agent dropped in the workspace does not). Because no arm reads argv, the executable one spares any process running that binary, including one an agent started under a different subcommand; that is the stated cost of an identity-only rule, and its failure direction is "spare something" rather than "kill the shared daemon". Every arm re-checks the live process's start time before it spares anything — a pid on its own is not an identity, so a record the kernel has outgrown spares nobody — and the record is published with a temp file plus rename, so a reaper scanning concurrently never reads a half-written one, and the compare-and-remove that clears a failed daemon's record runs under an exclusive lock so it cannot delete a replacement another process published in between.

The daemon is also started with a neutral cwd of its own — its config directory, falling back to $HOME then / — so it never sits inside a job workspace in the first place; on 2026-10-21 a loop spawned it with no current_dir, it inherited that job's directory, and it matched every rule in the list above while serving everybody else on the host. The daemon's tool shells are not exempt (they are what the agent starts, which is the whole reason the reap exists), and another session's leftovers remain protected by the -C rule.

Pass --no-reap to turn it off when something else owns the workspace and you expect its processes to outlive the run.


Why zoder exists

Teams spend real money on frontier LLM APIs while excellent open-weight models are available for free or near-free. zoder closes that gap by making the free path the default path:

  • Conserve spend. Free and open-weight models cost $0. zoder routes to them first and only touches a paid frontier model when you explicitly opt in — then it shows you, in dollars, exactly what you saved.
  • Exercise open-weight models. Every task that runs on an open-weight model is real-world use of that stack. zoder turns daily coding into continuous, measured evaluation of the free fleet.
  • Make cost safety structural, not advisory. The policy gate is default-deny-paid. A post-call guard inspects each call's telemetry so a model treated as "free" can't silently bill you through a provider-side free→paid fallback. A free workflow stays free, provably.

How it works

A single zoder run is a short, auditable pipeline:

  1. Classify. zoder maintains a corpus of the models your endpoint serves, each tagged free or paid (from live pricing) and scored for capability (LMArena Elo + SWE-bench) and measured latency/throughput.
  2. Route. For your task it picks the best free model for the tier, skipping any model whose health circuit-breaker is open, and prepares a cross-family fallback chain so a single provider outage can't strand you.
  3. Run. The task executes on the chosen model through the zerocore engine (streaming, retries, timeouts, sessions).
  4. Verify free. A post-call guard checks the call's real cost/host telemetry. If a "free" model was actually billed, that's a policy violation — it's recorded and the run exits non-zero.
  5. Record. Every call (free included) is appended to a local spend ledger: timestamp, provider, model, tokens in/out, cost.
  6. Report. zoder report rolls the ledger up, prices it from a refreshable catalog, and reconciles it against your provider's billing.

Paid models are never reached by accident: they are off by default and require an explicit opt-in.

To route a subscription model first and fall back to a different provider's free models (e.g. a subscription model first, then another provider's free open-weight endpoints), see docs/PROVIDER-ROUTING.md: per-model serves routing, a pinned primary_model, and free open-weight ingestion via zoder refresh.


Engines: ZeroClaw + Goose (dual-engine)

zoder is an engine-agnostic governance layer — the corpus, free-first router, policy gate, ledger, and review/fix loop govern whichever agent actually runs the turn. Engine selection is one flag:

zoder exec --engine zeroclaw "…"   # default — the ncz-os fleet engine (daemon)
zoder exec --engine goose    "…"   # Block / Linux-Foundation Goose (goose acp)

Both are Rust agentic coding engines (memory-safe, fast, single-binary) and both speak the Agent Client Protocol (ACP) — so zoder drives them through one transport abstraction (Unix-socket for the resident daemon, spawned stdio for goose acp). But they embody different design principles, which is exactly why they complement rather than duplicate each other:

ZeroClaw (default) Goose
Process model long-running daemon (resident sessions, Unix socket) per-turn subprocess (goose acp, stdio)
Built for the ncz-os fleet: cost-ledger-native, configurable agents/aliases, hive/worker orchestration, networked session backends (PG/MySQL/Oracle/Db2/MNEMOS) broad single-machine use: huge MCP-extension ecosystem, recipes, and ACP-server interop (Zed / JetBrains / VS Code)
Strength governance, multi-agent fleet, durable networked sessions provider/extension breadth, maturity, community momentum
Backed by ncz-os (a friendly ZeroClaw fork) Block → Agentic AI Foundation (Linux Foundation)

Why dual-engine is a valid strategy (not fragmentation):

  • The governance is the moat; the engine is pluggable. zoder's value — free-first routing, the default-deny-paid gate, the corpus, health-aware selection, the cost ledger, the adversarial review/fix loop — is engine-agnostic. Adding Goose widens the substrate zoder governs; it does not split it.
  • Resilience. The two engines fail independently. When the resident ZeroClaw daemon is mis/un-configured or non-invokable mid-loop, Goose needs no daemon and is an immediate fallback (and vice-versa).
  • Engine diversity as a correctness signal. Two independently-architected engines can author + cross-review the same task — engine diversity catches engine-specific failure modes, the same way cross-family model reviewers catch model-specific ones. For the hardest tasks, run both and reconcile.
  • A standards bet, not a Goose bet. Because the seam is ACP, zoder is really an ACP orchestrator: any future ACP agent (Zed's registry, others) plugs in for free. Goose is simply the first second engine that proves the abstraction.

They are selectable alternatives, not run simultaneously by default — doubling cost buys little on easy/medium work. Simultaneity is opt-in: a resilience fallback when one engine is degraded, or an ensemble for the hardest tasks.

The systems-architecture diagram below shows both engines sitting under zoder's one ACP transport abstraction: --engine zeroclaw (daemon, ACP over a Unix socket) and --engine goose (goose acp, ACP over a stdio subprocess), governed identically and fed by the free-first provider pool.

Goose is bundled core, not the kitchen sink

zoder builds and ships goose in a lean core configuration — cargo build -p goose-cli --bin goose --no-default-features --features rustls-tls — and nothing else. That declines goose's default feature stack: local-inference (a full local-model runtime — candle + llama-cpp), aws-providers (Bedrock/ SageMaker), nostr, goose's own tui, update (sigstore self-update), otel/telemetry, and system-keyring.

Why core:

  • zoder never invokes any of it. It drives goose purely as a remote-API ACP agent over stdio: model calls go to the free-first provider pool (not a local GPU), keys come from zoder's config (not the OS keyring), and zoder governs telemetry/updates itself. Those features are dead weight in this role.
  • Footprint + build honesty. Core takes the arm64 binary from 242 MB → 65 MB (−73%) and the from-source build from ~8m → ~3m. The whole zoder stack (zoder ≈ 8 MB + zeroclaw ≈ 19 MB + zerocode ≈ 33 MB ≈ 60 MB) is smaller than a single default goose.
  • Smaller surface. Fewer dependencies and no bundled ML/crypto self-update path — less to audit, faster reproducible builds.
  • Still a faithful upstream build. Core is feature selection, not a fork: GOOSE_REF stays pinned and we patch nothing, so tracking Block/LF goose releases stays cheap. (goose core's own default feature set is already empty — []; it is goose-cli that stacks the heavy defaults, and those are what we decline.)

Want the full goose (local inference, code-mode, AWS, …)? Copy it in. zoder spawns whatever goose is on PATH (goose acp), so a heavier build is a pure drop-in: build it yourself (--features local-inference, …) or install Block's official goose, put it ahead on PATH, and zoder drives it identically — no zoder change. To widen the bundled build instead, set GOOSE_FEATURES for scripts/package.sh, and re-run the acp-client real-goose integration test after widening (it is the gate that the feature set still drives a live turn).

Verified: goose core's own test suite passes on the lean feature set (1423/1423 library tests, ACP module included); the one feature-conditioned snapshot (code-mode) passes once that feature is re-enabled.


Systems architecture

zoder is layered on the ZeroClaw / zerocore foundation (vendored from the ncz-os/zeroclaw fork). Each layer is independently useful; together they make a complete coding system.

zoder systems architecture

zerocore — the (pluggable) engine. ZeroClaw's agent/turn engine and its OpenAI-compatible provider abstraction is the default engine; zoder rides on the fork's loop and inherits streaming, retries, timeouts, and sessions instead of reimplementing them. This is the part that actually talks to models. Durable session history is pluggable: a JSONL file by default, or a networked database backend (PostgreSQL / MySQL / Oracle / Db2) / MNEMOS when configured. As of the dual-engine work the engine itself is pluggable too — --engine goose drives Block/LF Goose over ACP instead (see Engines: ZeroClaw + Goose); zoder's governance is engine-agnostic.

zerocode — the terminal UX. The interactive pair-coding experience. It surfaces cost at the point of decision: a free-vs-paid model picker, per-turn and session cost, and a live savings readout (one color for $0 work, another for real paid spend).

zerocode → zodercode — where the TUI is going (roadmap). zerocode today is a single-engine (ZeroClaw) TUI with no model selection — and that is by design: choosing the model is the CLI's job (zoder auto-routes free-first). The dual-engine work opens a gap zerocode was never meant to fill — a terminal user who wants to pick the engine and the model directly, and to see why one over another. That is zodercode, zoder's own dual-engine TUI:

  • It consumes zerocode; it does not fork it. zodercode reuses zerocode's ACP chat core (the same client / chat / render layer) and gates the ZeroClaw-specific config panes behind engine == zeroclaw. Same posture as zoder-the-CLI over zeroclaw: wrap and extend, don't re-implement.
  • Direct engine + direct model choice. An engine toggle (zeroclaw ⇄ goose) and an explicit model picker — the deliberate counterpart to the CLI's automatic routing. The CLI stays the "just do the cheap right thing" path; zodercode is the "I know exactly what I want" path.
  • A model-consultant pane — the thing neither zerocode nor any upstream agent TUI has: a live view of the corpus zoder already maintains — per-model health (circuit-breaker state + measured latency) and SWE-bench / LMArena rankings — so a human chooses with the same signals the router uses.

Positioning, stated plainly:

role status
zoder (CLI) automatic, free-first, headless / fleet dispatch ships today
zerocode (TUI) single-engine interactive pair-coding UX ships today
zodercode (TUI) dual-engine · direct model choice · model-consultant pane roadmap (skeleton not yet built)

zodercode supersedes zerocode as zoder's branded TUI by containing it — the same way zoder-the-CLI contains the zeroclaw engine. Until zodercode lands, zerocode is the shipping TUI and nothing here is vaporware-by-omission.

zoder — the governance + dispatch layer. The cost- and quality-governance brain. It provides the classified model corpus, the free-first router with cross-family fallback, the default-deny-paid policy gate with its anti-paid-fallback guard, per-model health (circuit breaker + measured latency), adversarial review (with an opt-in multi-model panel via --panel), and the headless dispatch surface (zoder exec -) that lets a hive of workers run coding tasks unattended. It is the part that turns ZeroClaw into a distribution.

Pricing engine + FinOps reporting. A deterministic, conformance-tested pricing engine (with an optional live LiteLLM/OpenRouter cache) prices every call so the savings headline and chargeback numbers are real, not estimated.

Cost accounting + reporting (ledger-backed). This is what makes the savings real and visible:

  • Every call lands in a local append-only spend ledger (timestamp, provider, model, tokens, cost) — free calls included, so adoption is measurable.
  • A refreshable pricing catalog prices each call; free models resolve to $0.
  • zoder report rolls the ledger up into daily / weekly / by-model views with an avoided-spend headline and a free-token share.

Model rankings — LMArena (Elo) + SWE-bench. Selection is driven by synced rankings, not a hardcoded table: LMArena Elo for general capability and SWE-bench Verified for coding skill. Because zoder is a coding tool, the router ranks SWE-bench-primary, Elo-secondary, filtered free-first and health-aware. Both the model catalog and the rankings refresh periodically and are cached, so nothing goes stale in the binary.


The CI-parity gate — compliance-first

zoder runs a full, fail-closed CI simulation on both authoring and code review. Before a change converges in the zoder loop — and before an adversarial reviewer can approve it — it must pass the same checks the upstream project's CI will run, plus a baseline of universal open-source hygiene. A change that passes zoder's gate shouldn't surprise GitHub / GitLab / Codeberg CI, and should already meet the community's norms.

This is deliberate, and it costs authoring speed. The trade is worth it:

  • It's a differentiator. General-purpose coding agents (Codex, Cursor, …) don't simulate the target repo's full CI before proposing a change. zoder does — so "it passed my gate" is an honest, load-bearing claim.
  • It de-slops the work. Red CI, a failing license/audit gate, unformatted code, a missing sign-off — those are the fingerprints of careless automation that make maintainers distrust AI contributions. Arriving already green + compliant removes the tells, which is the direct antidote to that friction.
  • It respects the community. Good citizenship on GitHub / GitLab / Codeberg means running their declared CI and hygiene, not a tool's own "good enough."

The gate = the repo's own CI (GitHub Actions / .gitlab-ci.yml / Woodpecker, so local == upstream) ∪ a multi-language baseline (Rust, Node/TS, Python, Go, … — format, lint, build, test, supply-chain/security audit, license/SPDX, secret scan, conventional-commits + DCO, SBOM on release).

It degrades honestly — 🟢 Green (all required ran + passed), 🟡 Yellow (passed what could run; skips are reported with the risk they leave unverified), 🔴 Red (a required check failed). Jobs that genuinely can't run locally (cloud secrets, GPU, self-hosted runners) are never silently passed — the claim is "CI parity within local compute/network scope," never false total parity. Default mode is strict (fail-closed; a missing required tool is a hard error, not a silent skip); a fast local-iterate mode logs every skip and is disabled before push.

Design of record + roadmap: docs/CI-PARITY-GATE.md. Slice 1 (the gate-planning core: ecosystem detection, step model, Green/Yellow/Red aggregation, baseline plans) has landed; the CI-file derivation, runner, and loop/review wiring are in progress.

Example: zoder report

An example zoder report — a blended organization view, enumerated by model, across both the paid AI coding tools (Claude Code, Cursor, Codex, Claude.ai) and the free open-weight models served via an enterprise gateway. Of $337,420 spent, routing the open-weight slice free avoided $1,851,423 at the frontier baseline (claude-opus-4.8) — 34× cheaper on the free tier, with Claude Code the top cost driver at 74%:

zoder report — blended paid + free org spend by model: $337,420 paid, $1,851,423 avoided on the free tier

zoder's cost engine is shared with tokenomics, the unified LLM-spend ledger across Hermes, Goose, OpenClaw, and zoder. tokenomics also provides the FinOps observability view (spend allocation, realized $/Mtok, a cheapest-equivalent advisor, and a burn forecast) over the same ledger:

FinOps observability — totals, by-caller/host, realized $/Mtok by model, burn forecast

Subscription & OAuth billing (in progress). Flat-rate tools — e.g. Codex on a ChatGPT subscription, or any provider used via an OAuth login rather than a metered API key — report $0 per token, so they currently surface as free even though a fixed subscription fee sits behind them. We're adding subscription-aware cost modeling — OAuth- and API-key-based subscription support for OpenAI, Anthropic, MiniMax, and others — to amortize those flat fees across real usage so the report reflects true effective cost.

tokenomics: repo https://gitlab.com/ncz-os/tokenomics (mirror https://github.com/ncz-os/tokenomics) · package ncz-tokenomics.

What the report is telling you at a glance:

  • The counterfactual — what this period would have cost on the frontier baseline vs what you actually paid, and the multiple (e.g. "34× cheaper"). This is the headline number for free-model adoption.
  • Free token share — proof the free models are doing real work (token share, not just call count).
  • Top cost driver — the one model eating most of your remaining spend.

Vendor-scoped reports: zoder report --vendor <name>

To see what your spend looks like against one vendor's providers — useful for chargeback to a team, an org, or a finance review — pass --vendor. The flag filters the ledger to entries whose provider id was contributed by the named vendor's TOML, then recomputes totals, the counterfactual, and the avoided-spend headline over that slice, so the headline numbers are the vendor's story, not the whole mixed fleet:

$ zoder report --days 200 --vendor frontier
ZODER usage report  last 200 days  vendor=frontier  2025-12-11 -> 2026-06-29  (200 days)
filtered to providers:  ai-tools

By day
  day           cost($)  tokens(M)  calls
  ───────────────────────────────────────
  2026-03-15   50613.12    77281.0   773K
  2026-04-15  118097.29   180322.4   1.8M
  2026-05-15   84355.21   128801.7   1.3M
  2026-06-15   84355.21   128801.7   1.3M

Paid models  billed cloud usage — real $ (input/output per Mtok)
  model                            cost($)  in $/Mtok  out $/Mtok  tokens(M)  calls  tok%
  ───────────────────────────────────────────────────────────────────────────────────────
  anthropic/claude-opus-4.8      253219.28       5.00       25.00   346682.5   3.5M   80%
  anthropic/claude-sonnet-4.6     49965.80       3.00       15.00    48313.8   483K   11%
  openai/gpt-5.5                  27428.43       5.00       30.00    31550.6   316K    7%
  google/gemini-3.1-pro-preview    6807.32       2.00       12.00     8417.2    84K    2%

Free models  $0 chargeback
  model                         tokens(M)  calls  tok%  share           
  ──────────────────────────────────────────────────────────────────────
  openai/gpt-5-codex              78506.9   785K   98%  ████████████████
  enterprise-gateway/megamodel     1735.8    17K    2%  █░░░░░░░░░░░░░░░

By host  model publisher, summed across all providers
  host                  cost($)  tokens(M)  calls  tok%
  ─────────────────────────────────────────────────────
  anthropic           303185.08   394996.2   3.9M   77%
  openai               27428.43   110057.5   1.1M   21%
  google                6807.32     8417.2    84K    2%
  enterprise-gateway       0.00     1735.8    17K    0%

Summary
  On claude-opus-4.8 this would cost $11334549.56; you paid $337420.83.
  free models saved you $10997128.73  (34x cheaper)
  free token share    : 16%  (80242.8M of 515206.8M tokens ran $0)
  top cost driver     : anthropic/claude-opus-4.8  75% of spend ($253219.28)
  external chargeback : $337420.83
  free tokens     : 80242.8M  ($0 chargeback)
  avoided spend       : $1765340.55  (80242.8M free tok @ claude-opus-4.8 = $22.00/M)

--vendor is invalid unless ~/.zoder/config.<name>.toml is present and contributes at least one [[providers]] entry — zoder exits non-zero with a clear message listing the vendors it does see. Add a new vendor by copying config.ibm.toml (a commented template) to config.<name>.toml, uncomment the [[providers]] blocks, and you're done — no code change.

JSON output includes vendor and vendor_provider_ids keys when --vendor is set, so dashboards can pin to a specific vendor slice without re-parsing the by-model table.


Install / build targets

Install — one line (prebuilt trio: zoder + zerocode + zeroclaw)

curl -fsSL https://raw.githubusercontent.com/ncz-os/zoder/master/install.sh | sh

Detects your OS/arch, downloads the selected nightly channel build for your platform, verifies the SHA-256 checksum, and installs zoder (CLI) plus zerocode (TUI) and zeroclaw (engine) — all three are bundled together in every nightly release. The installer marks zerocode and zeroclaw as optional only for backward compatibility with old channels that may predate the trio; a missing artifact is a warn-and-skip, never a fatal.

Agent / non-interactive (no prompts; pin the nightly channel + dir, exits nonzero on error):

curl -fsSL https://raw.githubusercontent.com/ncz-os/zoder/master/install.sh \
  | ZODER_CHANNEL=2026-07-09 ZODER_BIN_DIR="$HOME/.local/bin" sh

The equivalent flag form:

curl -fsSL https://raw.githubusercontent.com/ncz-os/zoder/master/install.sh \
  | sh -s -- --channel 2026-07-09 --bin-dir "$HOME/.local/bin"

Knobs: ZODER_CHANNEL / --channel <name> (default master; use YYYY-MM-DD to pin a nightly), ZODER_BIN_DIR / --bin-dir <dir> (default ~/.local/bin), ZODER_REPO / --repo <owner/repo>, ZODER_HOST / --host <host>, ZODER_NO_VERIFY=1 / --no-verify, ZODER_NO_CORPUS=1 / --no-corpus.

Manual (tarball): grab your platform's tarball from the GitHub nightly release https://github.com/ncz-os/zoder/releases/tag/nightly — refreshed every night with all three arches; each .tar.gz has a sibling .sha256:

base=https://github.com/ncz-os/zoder/releases/download/nightly
curl -fLO "$base/zoder-<target>.tar.gz"
curl -fLO "$base/zoder-<target>.tar.gz.sha256"
sha256sum -c zoder-<target>.tar.gz.sha256      # macOS: shasum -a 256 -c
tar -xzf zoder-<target>.tar.gz
sudo install zoder-<target>/{zoder,zerocode,zeroclaw} /usr/local/bin/
zoder --help          # the trio is now on your PATH

The raw per-binary artifacts install.sh pulls (rolling + date-pinned) live in the GitLab package registry: https://gitlab.com/ncz-os/zoder/-/packages (zoder-nightly/master, or zoder-nightly/<YYYY-MM-DD> for a pinned day).

Targets: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, aarch64-apple-darwin (Apple Silicon). Windows → use WSL.

Debian / Ubuntu (apt)

Tagged releases publish amd64 and arm64 packages to the NCZ APT registry, so zoder upgrades with the rest of the system:

curl -fsSL https://packages.buildkite.com/ncz-os/ncz/gpgkey \
  | sudo gpg --dearmor -o /etc/apt/keyrings/ncz-os-ncz.gpg
echo "deb [signed-by=/etc/apt/keyrings/ncz-os-ncz.gpg] https://packages.buildkite.com/ncz-os/ncz/any/ any main" \
  | sudo tee /etc/apt/sources.list.d/ncz-os-ncz.list
sudo apt-get update && sudo apt-get install zoder

The package carries the zoder CLI only; zerocode and zeroclaw come from install.sh or the container image. Packaging and publishing details are in packaging/deb/README.md.

Container (Docker)

The full stack (zoder + zerocode + zeroclaw + goose) ships as a multi-arch image (linux/amd64 + linux/arm64) on GitHub Container Registry, rebuilt weekly:

docker pull ghcr.io/ncz-os/zoder:latest        # or :weekly-<ISO-week>, e.g. :weekly-2026-W27
docker run --rm ghcr.io/ncz-os/zoder --help
# persist routing state:  docker run -v zoder-home:/root/.zoder ghcr.io/ncz-os/zoder ...

Build from source

zoder builds natively for:

Platform Target
macOS arm (Apple Silicon) aarch64-apple-darwin
macOS x86_64 (Intel) x86_64-apple-darwin
linux x86 x86_64-unknown-linux-gnu
linux arm aarch64-unknown-linux-gnu
  • Local build: ./scripts/build.sh mac (or mac-x86, linux).
  • Nightly binaries: .github/workflows/native-builds.yml builds all three arches natively on GitHub (macOS + arm64-Linux + x86_64-Linux), publishes raw binaries to the GitLab zoder-nightly channel and tarballs to the GitHub nightly release. Tagged v* releases: .github/workflows/release.yml.
  • The per-push quality gate runs on GitLab (.gitlab-ci.yml): cargo fmt --all -- --check, cargo clippy --workspace --all-targets --all-features -- -D warnings, and cargo test --workspace --locked --all-features.
  • GitLab also runs a scheduled/manual RustSec advisory scan: cargo deny check advisories. It is intentionally outside the per-push gate so a newly-published advisory does not unexpectedly block ordinary merge work.

Windows? Use WSL.

zoder does not ship a native Windows build. Its engine relies on Unix domain sockets and a Unix-oriented runtime, and the developer workflows it targets assume a POSIX environment. Windows users should run zoder inside WSL2 (Ubuntu or any glibc distro) — install the Linux target there and it behaves exactly as it does on native Linux. This is a deliberate ncz-os policy, not a temporary gap: there is no Windows target in the build or CI matrix.


Configuration

zoder reads ~/.zoder/ (override with $ZODER_HOME): config.json, the model corpus, the spend ledger, and the pricing catalog. Without a config it falls back to a single OpenAI-compatible provider entry you can point at any endpoint. A paid frontier provider can be added but is default-deny and only used on explicit opt-in.

Budget caps (pre-call estimate)

Beyond the per-model paid/free gate, zoder can gate on projected dollars. Before a paid call it estimates the cost (prompt tokens × the pricing catalog, plus an assumed output size) and checks it against optional caps. A call that would breach a cap prompts the same confirmation as a paid model; --allow-paid bypasses it, and a $0 (free-model) estimate is never gated.

{
  "budget": {
    "max_cost_per_call_usd": 0.50,
    "monthly_cap_usd": 100.0,
    "est_output_tokens": 1024
  }
}
  • max_cost_per_call_usd — confirm any single call estimated above this.
  • monthly_cap_usd — confirm a call that would push month-to-date ledger spend (current calendar month) over this total.
  • est_output_tokens — assumed completion size for the estimate (default 1024).

Omit the budget block (or any field) for no cap. The estimate is a ballpark (token counts are approximate until the call's real telemetry lands in the ledger); it is a guard rail, not a hard meter.

Client discipline for a shared, rate-limited endpoint

Several zoder processes (or several [[providers]] entries pointing at the same hosted endpoint) can burst a free shared endpoint into 503 "overloaded" bursts and 429s of their own making. Every provider accepts optional keys to make all of them behave like one well-mannered client. All are optional; with none of them set, behaviour is exactly as before.

{
  "providers": [
    {
      "id": "free-api",
      "base_url": "https://<host>/v1",
      "auth": { "type": "bearer", "var": "FREE_API_KEY" },
      "billing": "free",
      "overload_backoff_min_s": 5,
      "first_token_timeout_s": 60,
      "stream": true,
      "rate_limit_group": "shared-free-endpoint",
      "max_concurrent": 4,
      "max_requests_per_minute": 30
    }
  ]
}
  • overload_backoff_min_s (u64, > 0) — floor for the retry delay after a provider overload: an HTTP 503 or 529, or an Anthropic-style overloaded_error stream frame. A free shared endpoint that is overloaded stays overloaded for seconds, so the default 500 ms exponential base would burst it into more 503s. The floor is applied in every retry loop through one helper and never overrides a server-provided Retry-After.
  • first_token_timeout_s (u64, > 0) — abort a streamed call as a retryable timeout when no token (answer or reasoning) arrives within this window, instead of riding the whole request budget. Sizing for reasoning models on a shared endpoint: measured first-token p50 ~4–6 s, p90 up to ~34 s.
  • stream (bool) — when true, zoder sends stream: true on OpenAI-compatible chat requests to this provider even on call paths that build non-streaming requests (reviewer, health probe), so first-token time is measured there. Anthropic wire requests always stream.
  • rate_limit_group (string) — the shared-endpoint group this provider's call budget belongs to. Two provider entries pointing at the same endpoint set the same group to share one budget. Default: the provider's own id.
  • max_concurrent (u32, > 0) — host-wide cap on in-flight requests to this provider's endpoint, enforced across ALL zoder processes on the host via flock-ed slot files under <zoder home>/state/slots/<group>/. A crashed process cannot leak a slot (the kernel releases the lock on exit).
  • max_requests_per_minute (u32, > 0) — host-wide cap on requests started per sliding 60-second window, enforced across all zoder processes via a locked timestamp file under <zoder home>/state/rate/<group>.json.

Both caps are per host — a cap across several hosts is not possible without a shared coordinator. Divide the account's limit by the number of hosts that share the account. A waiting call sleeps for its slot / turn (bounded by ZODER_QUEUE_TIMEOUT_S, default 900 s) and prints one stderr notice after ~10 s, instead of failing. The reviewer-only ZODER_PROVIDER_CONCURRENCY env var still works and takes precedence for reviewer calls.

Retry-After: the numeric-seconds form is honoured (capped at 60 s, so a hostile huge value waits the cap then falls through as before), and the HTTP-date form is now parsed too.

Note: the engine daemon (zeroclaw) makes its own provider calls for loop/exec author turns; zoder's client-side limits cannot cover those.

Request timeout, per model

Every provider call is bounded by an overall request budget (default 120s; --request-timeout, request_timeout_s, ZODER_TIMEOUT_S). One provider can serve models with very different latency — measured 2026-10-21, MiniMax-M3 ran past 120s on a ~650-line review chunk while gemma4-31b and MiniMax-M3.1-Flash-Preview answered the same diff — so request_timeout_s can be pinned per model. A model with no per-model value keeps the host-wide budget, so raising it for one slow model never delays failure detection for the fast ones.

# engine config.toml
[providers.models.custom.minimax]
model = "MiniMax-M3"
request_timeout_s = 600      # this model alone
// config.json — the zoder-side models map, same key
{
  "models": {
    "custom.minimax": { "model": "MiniMax-M3", "request_timeout_s": 600 }
  }
}

Precedence: --request-timeout > per-model models entry > per-model engine profile > global request_timeout_s > ZODER_TIMEOUT_S > default. Health probes and provider catalog fetches keep the global budget. Full details in docs/CONCURRENCY.md.

ACP idle watchdog (two-window watchdog)

An agentic turn is bounded by two different clocks:

  • --agent-timeout (default 900s) is the total turn budget.
  • the idle windows below bound the silence of the ACP session.

The idle watchdog has TWO windows, because the two waits have different causes:

window default (self-hosted route) default (hosted route) measured from
first_event 600s 60s the start of the turn, until the first SUBSTANTIVE output
between_events 180s 60s the last SUBSTANTIVE output

A substantive output is a token (a message chunk or a thought chunk) or a tool call. Session metadata, usage reports and keepalives are not substantive: they neither end the first phase nor restart any clock. The first window is the long one on purpose — the wait for the first token of a turn is a cold start (a local model loading its weights, a hosted provider queueing a cold request), not a wedge. Once the session has spoken it is alive, so the gap between two events can be short.

Phase rules

  1. From the start of the turn until the first substantive output, the first_event window applies.
  2. After the first substantive output, the shorter between_events window applies.
  3. A pending tool call suspends the watchdog (that wait is bounded by the total turn budget). When the tool round completes, the phase resets to the first_event window, because the next model turn is a new cold start.

The windows are measured between substantive events, never against the total runtime, so a long but steady stream is never killed — only a silence is. This is what turns "zoder exec -m qwen38 streamed zero tokens for minutes and only the 900s --agent-timeout ended it" into a distinct, structured idle_timeout outcome in minutes instead of a quarter of an hour:

zoder: ACP idle timeout: the session produced no substantive output for 181.2s
       (window=between_events, window_secs=180.000, last-event age 181.2s, model=qwen38)

The turn ends with outcome = "idle_timeout" (distinct from the total-budget "timeout"), the reason names which window tripped together with the window length, the last-event age and the model, the session is cancelled through the existing teardown path, and a spawned engine child is reaped — so an idle timeout never leaves an orphan behind.

A model that legitimately thinks silently for longer than the window needs a bigger one. 0 disables one window independently (the other stays armed); 0 on both restores the pre-watchdog behaviour (only --agent-timeout can end a turn).

# a slow cold start, but a tight gap between events
zoder exec -m qwen38 --agent-idle-timeout-first-event 900
zoder exec -m qwen38 --agent-idle-timeout-between-events 120

# the legacy single flag is an ALIAS: it sets BOTH windows
zoder exec -m qwen38 --agent-idle-timeout 600

# disable one window only
zoder exec -m qwen38 --agent-idle-timeout-between-events 0
// config.json
{
  "agent_idle_timeout_first_event_s": 900,
  "agent_idle_timeout_between_events_s": 120
}

Environment variables: ZODER_AGENT_IDLE_FIRST_EVENT_S and ZODER_AGENT_IDLE_BETWEEN_EVENTS_S (each 0 disables that window). The legacy ZODER_AGENT_IDLE_S sets both.

Precedence per window (highest first):

  1. --agent-idle-timeout-first-event / --agent-idle-timeout-between-events
  2. --agent-idle-timeout (the legacy alias — sets both windows)
  3. config agent_idle_timeout_first_event_s / agent_idle_timeout_between_events_s
  4. config agent_idle_timeout_s (the legacy key — sets both windows)
  5. ZODER_AGENT_IDLE_FIRST_EVENT_S / ZODER_AGENT_IDLE_BETWEEN_EVENTS_S
  6. ZODER_AGENT_IDLE_S (the legacy variable — sets both windows)
  7. the built-in default: 600s / 180s, for a self-hosted (local) route and a hosted provider alike (a hosted gateway can queue or prefill a request for minutes before its first token). zoder tells the two apart from the endpoint (a loopback or local-network address, such as a LAN GPU box, or a self-hosted engine family such as vllm / sglang / llamacpp is local; everything else is hosted), so the defaults follow the route instead of being guessed.

The same windows apply to every agentic dispatch (exec, loop, rescue, run) and to both engines (Zeroclaw and goose).

Vendor overlays: config.<name>.toml

Config::load() reads config.json (or the default free-tier config) and then layers every config.<vendor>.toml in the same directory on top. Each TOML is a vendor profile (e.g. config.enterprise.toml, config.ibm.toml, config.microsoft.toml) that contributes additional [[providers]] and, optionally, a [profile] table that selects a default_provider. The TOMLs are the source of truth for what counts as a given org's spend in zoder report --vendor <name>.

An overlay may contain [profile], [deployment], [context], [budget], [review], [model_families], [theme], [health], and [[providers]] tables. [model_families] maps a served model id or */? glob to a family name for cross-family reviewer exclusion; it is merged per key over the base map, with a later config.<vendor>.toml winning a key two overlays share. A binary older than this change rejects an overlay that contains [model_families], so deploy the new binary before adding the table.

# config.enterprise.toml — an Enterprise gateway profile.
[[providers]]
id = "enterprise-gateway"
base_url = "https://YOUR_ENTERPRISE_GATEWAY/v1"
kind = "openai-chat"
auth = { type = "env", var = "ENTERPRISE_API_KEY" }
# — or, for gateways that use an api-key request header (e.g. Azure OpenAI):
# auth = { type = "api_key_header", header = "api-key", var = "ENTERPRISE_API_KEY" }
paid = true
billing = "metered"

[profile]
name = "enterprise"
# default = false   # leave config.json's default_provider alone

# Optional cross-family reviewer families (generic ids).
# An overlay entry wins over config.json for the same key.
# [model_families]
# "provider-a/*" = "provider-a"

Rules the loader enforces (fail loud, don't silently merge):

  • Duplicate provider id across two overlays is a hard load error. Rename one in the offending TOML; last-wins is disabled so misconfigurations don't masquerade as working installs.
  • At most one overlay may set [profile].default = true. Multiple defaults = ambiguous routed-default = error.
  • [profile].default_provider must name a provider the same overlay contributes. Otherwise the routed default would point at a non-existent provider.
  • An overlay with no [[providers]] and no [profile].default is an error unless it carries another table such as [deployment], [context], [budget], [review], or [model_families]. A file that does nothing is almost certainly a half-finished template — delete it instead of leaving it on disk.
  • An overlay that cannot be parsed is a load error. A plain zoder config still loads it as a diagnostic and prints the warning, but zoder config --validate and every normal startup fail with the file name and the parse error instead of silently running without its providers. This usually means the file uses a key the binary does not know (unknown keys are rejected, never ignored).

Adding a new vendor is a copy-paste exercise:

  1. cp config.ibm.toml config.<name>.toml (the IBM/Microsoft templates are fully commented out as starters).
  2. Fill in id, base_url, kind, and auth for each provider. Use the matching ENTERPRISE_API_KEY / AZURE_OPENAI_API_KEY / etc. env var name.
  3. Set paid = true / billing = "metered" for token-billed providers; the policy gate uses paid to decide whether a call is a "free fallback" or a "paid escalation".
  4. Drop the file in ~/.zoder/ (or $ZODER_HOME). zoder config --validate will catch the obvious errors and zoder report --vendor <name> will pick it up automatically — no restart, no re-install.

free_api_hosts lives on the base Config (not on overlays), so any new vendor host that should be treated as free for the policy gate still needs to be added to config.json's free_api_hosts array — TOML overlays cannot extend that list. This is intentional: free-tier policy is a security knob, not a vendor knob.

Keep enterprise specifics out of the public tree. The public zoder source refers only to a generic enterprise profile. A real organization's gateway URLs, auth header names, and report color scheme belong in its own private config repo (e.g. an internal zoder-config), kept out of this repository and synced into $ZODER_HOME at deploy time — scripts/package.sh honors a CONFIG_REPO hook for exactly this. That keeps vendor identity and endpoints private while the public code stays vendor-neutral.

Report color scheme

An overlay's [theme] table sets the named color palette zoder report uses for that vendor's reports, so a chargeback view can match an org's brand without any code change.


Corpus & pricing sync

zoder routes off two data files in $ZODER_HOME (~/.zoder):

  • model_corpus.json — the classified routing corpus (which models exist, which are free/paid, capability/latency signals).
  • data/pricing.json — per-token rates, the cost source for zoder report / spend / finops.

An optional third file, model-aliases.json, closes the window between a vendor shipping a new model id and the next corpus refresh. It is provider-neutral and generated outside zoder; when present, Corpus::load folds it in so zoder models lists the current ids (marked overlay in the via column) and an exact alias resolves everywhere an explicit id does (-m codex:sol → gpt-6-sol). An overlay row is metadata only: never a route candidate, never auto-ranked, never priced — the corpus builder still decides what can route. Missing file = no-op; malformed file = one warning on stderr, ignored. Override the location with $ZODER_MODEL_ALIASES.

Both are public, self-serve, and rebuilt daily from public price data — they do not depend on any private/internal build process:

  • Source of truth: corpus/model_corpus.json + pricing/catalog.json in this repo, regenerated daily by scripts/build-public-corpus.py from the public LiteLLM + OpenRouter price lists (plus an optional public coding- benchmark overlay for tier ranking). A model is marked free/routable only on an explicit zero public price — unpriced placeholders and enterprise SKUs are filtered out.
  • Pulled from: https://raw.githubusercontent.com/ncz-os/zoder/master/{corpus/model_corpus.json,pricing/catalog.json}.

Keeping it current

You want… Run
Seed on first install the installer does it automatically (--no-corpus to skip)
Refresh rates from public price lists zoder pricing sync
Re-pull the latest public corpus curl -fsSL https://raw.githubusercontent.com/ncz-os/zoder/master/corpus/model_corpus.json -o ~/.zoder/model_corpus.json
Reconcile the corpus against your own endpoint's served models zoder refresh

The installer seeds both files and leaves any existing copy untouched, so your own zoder refresh / zoder pricing sync edits are never clobbered. The same public artifacts back the tokenomics plugins' pricing, so they stay current on the same cadence without any shared private dependency. (A built-in TTL self-heal — auto re-pull when the file is missing or stale — is on the roadmap; until then use the commands above or re-run the installer.)

Note: the public corpus marks models free only when their public price is zero. Your working providers (a self-hosted endpoint, a free-tier key, etc.) may be free for you though they carry a list price — zoder refresh against your own endpoint, plus local config, is what marks those routable for you.

Using your own price and corpus files

By default (auto) zoder treats the installed price and corpus files as public data it may refresh: when the engine's copy is missing or older than 24 h (by file modification time), zoder report and zoder tui spawn a detached zoder pricing refresh, and zoder pricing refresh / zoder refresh always overwrite their target files.

Set the pricing source to file to make the installed files authoritative: they are used exactly as installed — never re-pulled, never overwritten, and staleness is judged by the data's own date rather than the file's modification time. Any ONE of these three switches selects file (the first match wins):

  • env var: ZODER_PRICING_SOURCE=file (auto or unset keeps the default; any other value prints one warning and is treated as auto);
  • config key: [pricing] source = "file" in your config (validated by zoder config --validate);
  • marker in the data file itself: a top-level "managed": true in ~/.zoder/pricing.json or ~/.zoder/model_corpus.json. This works even when the installed binary is older or newer than the config, so an installer can mark its own files without a config change.

In file mode:

  • the daily auto-refresh never spawns (the age warning below is printed instead);
  • zoder pricing refresh refuses to overwrite the report catalog or the engine copy and exits non-zero, with one stderr line naming the switch that selected file; pass --force to refresh anyway;
  • zoder refresh still does its read-only reconciliation and prints it, but refuses to save the reconciled corpus (and the alias-overlay rows persisted with it) without --force.

The file protection does not depend on your config loading: the env var and the "managed": true marker are honoured even when the config cannot be read, and [pricing] source = "file" is honoured even when the rest of the config fails validation. A broken config is exactly when an overwrite is least likely to be noticed, so it must never switch the protection off.

Both files may also carry optional provenance metadata: a top-level "source" (a string label), "generated" (a YYYY-MM-DD date or an RFC3339 timestamp; the first 10 characters are taken) and "generated_at" (an RFC3339 timestamp with Z, which wins when both parse). Files without these fields load and save without them.

Age warning. When the installed data is more than 7 days old by its own date, one line per installed file is printed on stderr (it never fails the command and never touches stdout) when zoder report, zoder tui, or zoder pricing show runs. Both installed files are covered: the pricing catalog (~/.zoder/pricing.json) and the model corpus (~/.zoder/model_corpus.json) — the corpus is the file the router reads, so a stale corpus matters at least as much as a stale price list. In file mode the hint says the data was installed by its installer and should be updated there — it does not tell you to re-pull the public files. In auto mode it points at zoder pricing refresh. A file whose date cannot be parsed gets age unknown once. The threshold is ZODER_CORPUS_STALE_DAYS (integer days; an invalid value falls back to 7; 0 disables the warning).

Each file is measured by its own date rule: the pricing catalog by its generated/generated_at fields, the corpus by generated_at, then generated, then the arena_date fallback.

Freshness gates

The daily corpus-sync job refreshes the public artifacts from the public sources (LiteLLM + OpenRouter for prices, public coding benchmarks for the routing overlay). Two independent gates decide what may be published, so an outage in one source can never freeze the other:

Check Threshold Result
LiteLLM price list fetched must succeed blocked — prices are not published
OpenRouter price overlay fetched must succeed blocked — prices are not published
Pricing catalog size vs. the previously committed catalog not more than 25% fewer models blocked — prices are not published
Every bench source fetched fresh (or had a local last-known-good file) all sources covered overlay kept stale — prices still published
Candidate overlay size at least 50 models with an agentic_score overlay kept stale — prices still published
Candidate overlay size vs. the committed overlay not more than 25% fewer scored models overlay kept stale — prices still published

A blocked run prints PRICE SOURCE DEGRADED (<source>: <reason>) - not publishing prices. and exits non-zero; a stale overlay prints a warning with the overlay's age and the failing sources, and the run still succeeds. The committed overlay is kept byte-for-byte while it is stale: a transient benchmark outage can never erase routing scores.

--force-publish overrides every gate (both the price gate and the overlay gate) and says so in its output. Use it only for an explicit operator acknowledgement.

corpus/freshness.json (schema 2) records the run:

Field Meaning
schema 2
ts run time (UTC)
sources[] one entry per bench source: name, kind, outputs, ok, reason
prices.ok did both price sources succeed?
prices.generated the pricing catalog's generation date
prices.litellm / prices.openrouter {ok, models|added, reason} per source
prices.catalog_models / prices.previous_catalog_models the new and previously committed catalog sizes
overlay.stale was the committed overlay kept?
overlay.built_at when the published overlay was built (carried forward while stale)
overlay.age_days age of the published overlay in whole days
overlay.models_with_score / overlay.candidate_models_with_score the published and the candidate scored-model counts
overlay.reason why the candidate was not published (empty when fresh)
overlay_stale / overlay_age_days the same two facts at the top level, for old readers

A schema-1 report (no schema key) is still accepted and keeps the historic single-gate behaviour.

Price and name-matching conformance

The exact rules the pipeline applies — how a raw id is split into provider and model, which LiteLLM and OpenRouter rows count as priced (and which missing or zero prices mean), what makes a model free, how names are matched across sources, how peak/off-peak rates are derived, and which corpus fields are hand-curated rather than fetched — are recorded as executable cases in tests/fixtures/pricing-conformance.json. The fixture is self-describing: each rule carries a plain description and the function that implements it, and each case states its input, the expected result, and whether it is enforced today, a spec requirement the public pipeline does not yet implement, or unresolved. It is the single contract an independent pricing implementation tests against, so the two agree by construction rather than by coincidence. Run it with python3 scripts/tests/pricing-conformance-tests.py (offline; it calls the real builder functions). The fixture records behaviour, it does not set policy — a rule worth changing is argued in review, and changing it means changing the fixture and the pipeline together.

Staleness alerts

The daily sync is only useful if someone notices when it stops. From 2026-10-14 to 2026-10-26 it failed every day and nobody noticed for 13 days: the published corpus and pricing catalog stayed at 2026-10-13, and every client kept routing on a two-week-old index. Two independent channels now close that gap.

A tracked issue, opened and closed automatically. A .post job (corpus-sync-alert) runs after every scheduled pipeline, even when the sync failed (when: always), and reads the corpus/freshness.json report the sync job publishes as an artifact. It opens exactly one open issue labelled corpus-sync — title corpus-sync: scheduled data sync is failing or stale — when either:

  • the corpus-sync job failed in this pipeline and in the previous scheduled pipeline on the default branch (two consecutive failures), or
  • the report says the benchmark overlay is more than 7 days old, flagged stale, or prices are not ok.

While the problem persists it adds a note to the same issue (never a second one); on the first scheduled run with fresh prices and a current overlay it adds a final note and closes the issue itself, so no one has to remember.

It authenticates with a masked project variable CORPUS_ALERT_TOKEN (api scope — it must create issues and post notes; GITLAB_PUSH_TOKEN is the fallback). Create it under Settings → CI/CD → Variables, masked, and never commit a token. If no token is set, or the API call fails, the job prints corpus-sync-alert: ALERT NOT DELIVERED: <reason> and exits non-zero — the job is allow_failure, so this shows as a warning rather than silently passing. python3 scripts/ci/corpus-sync-alert.py --dry-run prints the decision and the API calls it would make without performing any of them. The decision logic has offline tests (python3 scripts/tests/corpus-sync-alert-tests.py) that fake the GitLab API, so the alert path is exercised without a token or a network.

GitLab's own failure notifications are a second channel worth enabling. Email/Slack notifications for failed pipelines (Settings → Integrations, or your user notification settings) catch any scheduled job, not just this one, and they keep working if the alert job itself is broken. Point them at the schedule's owner — a red pipeline nobody is subscribed to is not monitoring.

The client warns you too. zoder checks the age of the data it actually loaded: zoder config, zoder models and zoder pricing refresh print a one-line warning: model corpus is N days old (generated YYYY-MM-DD) … on stderr (never stdout, so scripts parsing the output are unaffected) when the corpus or pricing catalog is more than 7 days old, naming the command that fixes it. The threshold is $ZODER_CORPUS_STALE_DAYS; 0 disables the warning, and an unparsable value falls back to 7. This never fails a command — an old corpus is a nudge, not an error. zoder pricing refresh --json carries pricing_age_days / pricing_stale (and the same text in pricing_warning) instead of the text; zoder models --json keeps its bare array of model objects (scripts parse it), so its warning stays on stderr.


Enterprise memory & persistence

zoder has two complementary ways to give agents durable, shared memory backed by your own infrastructure. Use either or both.

1. MCP recall via MNEMOS (per-agent chat memory)

Point an agent at a MNEMOS server as an MCP tool source — the agent gains save/search memory tools and recalls across runs. MNEMOS itself is backed by your enterprise database (PostgreSQL, Oracle, Db2, MySQL/MariaDB), so this is the path to those engines. MCP grants are bundle-scoped and secure-by-default (omission is not a grant):

# 1) Declare the MNEMOS MCP server.
[[mcp.servers]]
name = "mnemos"
transport = "http"                       # or stdio (command/args) for the local bridge
url = "https://mnemos.internal/mcp"
headers = { Authorization = "Bearer ${MNEMOS_TOKEN}" }   # secret; sourced from env

# 2) Put it in a bundle.
[mcp_bundles.memory]
servers = ["mnemos"]

# 3) Grant that bundle to the agent(s) that should recall.
[agents.coder]
mcp_bundles = ["memory"]

2. Direct memory persistence to an enterprise database

Write the engine's memory subsystem straight to a database via the [storage.*] layer, then point [memory] (globally) or a single agent at it. Engine-native backends: postgres, sqlite, qdrant, markdown, lucid.

# Define a Postgres storage instance.
[storage.postgres.work]
url = "postgres://zoder:${PGPASSWORD}@db.internal:5432/zoder_memory"

# Use it for all memory…
[memory]
backend = "postgres.work"

# …or scope it to one agent (per-agent overrides the global default).
[agents.coder.memory]
backend = "postgres.work"

A hive of headless workers pointed at the same instance shares one system of record instead of scattered local files. For enterprise engines beyond Postgres (Oracle / Db2 / MySQL / MariaDB), use path 1 — MNEMOS provides those backends and zoder reaches them over MCP.


License

Apache-2.0. See LICENSE.

Build infrastructure & partners

Continuous integration and package distribution for this project are generously supported by our open-source infrastructure partners:

  • GitLab — canonical source hosting and CI pipelines (format / lint / test gates), via the GitLab for Open Source program.
  • Buildkite — CI/CD orchestration with hosted macOS and Linux agents, and our APT package registry host (packages.buildkite.com/ncz-os/ncz), via the Buildkite Open Source program.

Thank you to both for backing open-source software.

About

Free-first, cost-aware, codex-compatible coding & review CLI. Routes work to free/open-weight models first over any OpenAI-compatible fleet.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages