@razroo/iso-route
v0.6.0
Published
Author one model policy; fan out to every harness that supports it. Translates role-based model selection into settings.json, config.toml, opencode.json, .pi/settings.json, and resolved notes/maps.
Maintainers
Readme
@razroo/iso-route
One model policy, every harness.
agentmd, isolint, and iso-harness get your prompts to every coding
agent. @razroo/iso-route does the same thing for your model choices:
you declare a default model plus named roles once, and iso-route compiles
that policy into the config file each harness actually reads —
.claude/settings.json, .codex/config.toml, opencode.json, and
.pi/settings.json — plus notes for harnesses with advisory role binding.
Use it to swap Opus for Sonnet everywhere with a single edit, pin a
cheaper model to a fast-edit role, or send a reviewer role to a
different provider entirely.
v0.1 scope: emits config files for Claude Code, Codex, OpenCode, and Pi, and a resolved role map (
iso-route.resolved.json) thatiso-harnessconsumes when it stamps per-subagent frontmatter. Fallback chains are recorded in the resolved map but not encoded into any harness config — runtime routing lives in proxy layers (OpenRouter, LiteLLM), not iso-route.
Install
npm install -D @razroo/iso-routePolicy shape
# models.yaml
default:
provider: anthropic
model: claude-sonnet-4-6
roles:
planner:
provider: anthropic
model: claude-opus-4-7
reasoning: high
fast-edit:
provider: anthropic
model: claude-haiku-4-5
reviewer:
provider: openai
model: gpt-5
fallback:
- { provider: anthropic, model: claude-sonnet-4-6 }Valid providers: anthropic, openai, google, xai, deepseek,
mistral, groq, ollama, openrouter, opencode, local.
Valid reasoning levels: low, medium, high.
Per-harness overrides
Any role (and the top-level default) can declare a targets.<harness>
block to pick a different provider + model when iso-route emits for
that harness. Use this to run, for example, Claude on Claude Code,
gpt-5.4 on Codex, and an OpenCode proxy token on OpenCode — all from
one role:
roles:
planner:
provider: anthropic
model: claude-opus-4-7
targets:
codex:
provider: openai
model: gpt-5.4
opencode:
provider: opencode
model: opencode-go/deepseek-v4-proWhen emitting for harness X, iso-route uses targets.X if present;
otherwise falls through to the generic provider + model. Emitters
always see a flattened policy so they don't need to know targets:
exists.
Bundled presets (extends:)
Three curated presets ship with the package so you don't have to pin per-harness model picks by hand. Extend one with a single line; override only what you want to differ.
| preset | thesis |
| ------------------ | --------------------------------------------------------------------- |
| standard | Quality-first. Sonnet/Opus on Claude Code and Pi, gpt-5.4 on Codex, OpenCode Zen/Go picks per tier. |
| budget | Cost-optimized. Haiku/Sonnet on Claude Code and Pi, gpt-5.4-mini/nano on Codex, free-tier and pay-once OpenCode picks. |
| openrouter-free | Standard-like Claude/Codex/Pi picks, but OpenCode routes through explicit free OpenRouter model IDs. |
Scaffold a starter with the right boilerplate:
iso-route init # writes ./models.yaml extending "standard"
iso-route init --preset budget # use the budget preset instead
iso-route init --preset openrouter-free
iso-route init --out custom/path.yaml # different location
iso-route init --force # overwrite existingOr write the extension by hand:
extends: standard # or: budget / openrouter-free
# ...override only what you want:
roles:
quality:
targets:
codex:
provider: openai
model: gpt-5.4User fields win at every key. roles merge by name, targets merge
per harness (each harness override is atomic — a user's targets.codex
replaces the preset's targets.codex as a unit, not field-by-field).
Setting an override to null removes the preset's value for that
harness.
Fan-out mapping
| Field | Claude Code | Codex | OpenCode | Cursor | Pi |
| ------------------------- | ------------------------------------ | ---------------------------------------------------- | -------------------------------------- | -------------------------------- | --------------------------------- |
| default.model | .claude/settings.json model | .codex/config.toml model | opencode.json top-level model | README note only | .pi/settings.json defaultModel |
| roles.<name>.model | resolved map (iso-harness stamps) | [profiles.<name>] in config.toml | agent.<name>.model in opencode.json| advisory row in README note | advisory row + enabledModels |
| reasoning | closest model tier | model_reasoning_effort | provider-specific | advisory | defaultThinkingLevel for default |
| fallback[] | resolved map only (runtime unsupported) | resolved map only | resolved map only | resolved map only | notes only |
| provider auth | env var convention | [model_providers.<name>] block (not for reserved Codex built-ins openai / ollama) | provider block with npm package | — | Pi provider/auth settings |
Cursor has no programmatic way to bind a model to a rule or chat, so
iso-route emits a README note at .cursor/iso-route.md and warns at build
time. Pi gets .pi/settings.json for the default provider/model and model
cycling, plus .pi/iso-route.md because Pi does not have native role-specific
subagent binding.
CLI
iso-route init # scaffold ./models.yaml (extends "standard")
iso-route init --preset budget # or start from the budget preset
iso-route init --preset openrouter-free # OpenCode => free OpenRouter shortlist
iso-route build models.yaml --out .
iso-route build models.yaml --verify-models # verify model IDs first, then emit
iso-route build models.yaml --targets claude,codex --dry-run
iso-route plan models.yaml
iso-route verify models.yaml # verify model IDs without emitting files
iso-route catalog openrouter # live advisory shortlist for OpenCode
iso-route catalog openrouter --limit 20 --jsoninit scaffolds a starter models.yaml that extends a built-in
preset. build writes per-harness files under --out (defaults to
.). Add --dry-run to preview without touching disk. plan prints
the resolved role table so you can eyeball what each harness will see.
Opt-in model verification
iso-route verify and iso-route build --verify-models check declared
OpenRouter model IDs against the live Models API before you ship the
config. This keeps typo checking opt-in instead of turning the default
build path into a networked step.
Non-OpenRouter providers are still reported as unverifiable rather than guessed. That means a mixed policy can still pass:
iso-route verify models.yaml
iso-route verify models.yaml --fail-on-unverifiable
iso-route build models.yaml --out . --verify-models--endpoint <url> is also available on both commands for custom
gateways or tests that want to point at a fixture server instead of the
default OpenRouter API endpoint.
Advisory OpenRouter catalog
iso-route catalog openrouter fetches the live OpenRouter models API
and ranks an advisory shortlist for OpenCode. By default it filters
to models that are both:
- free (
prompt == 0andcompletion == 0) - tool-capable (
supported_parametersincludestools)
This command is intentionally separate from the default build path: it
helps you pick candidate model IDs, while verify / build
--verify-models provide the opt-in validation step.
iso-route catalog openrouter
iso-route catalog openrouter --limit 8
iso-route catalog openrouter --allow-paid
iso-route catalog openrouter --allow-no-tools --jsonLibrary API
import { build, loadPolicy, verifyModelFile } from "@razroo/iso-route";
const result = build({ source: "./models.yaml", out: "./.out", dryRun: true });
for (const w of result.warnings) console.warn(w);
const verify = await verifyModelFile("./models.yaml");
console.log(verify.passed ? "ok" : "fix model ids");Individual emitters are exported too (emitClaude, emitCodex,
emitOpenCode, emitCursor, emitPi) if you only need one target. Verification
helpers are exported too (verifyModelFile, verifyPolicyModels,
formatVerifyResult) for callers that want to keep the network check in
their own pipeline code.
How this fits the rest of the pipeline
agent.md → agentmd lint → agentmd render → isolint lint → iso-harness build
+
models.yaml → iso-route build ┘
│
▼
project with CLAUDE.md, settings.json,
config.toml, opencode.json,
.pi/settings.json, …iso-harness owns what the agent reads. iso-route owns which model
reads it. They share one output directory and are designed to be run
back-to-back — the @razroo/iso wrapper will compose them for you.
What iso-route is NOT
- Not a request-level router. Picking a cheaper model per-request based on prompt complexity belongs in a proxy (OpenRouter, LiteLLM, Portkey, Not Diamond). iso-route is a build-time transpiler, not an inference-path component.
- Not a build-time model validator.
iso-route catalog openrouteris advisory only, and the defaultbuildpath still validates provider names, not model IDs.verify/build --verify-modelsare opt-in checks. If you skip them and type a model name your provider doesn't recognize, you'll still find out at runtime.
License
MIT — see LICENSE.
