Skip to content

Commit 39c86f8

Browse files
committed
feat(iso-route): bundled preset system — extends: standard + iso-route init
Adds an opinionated tiered preset so newcomers don't have to know that OpenCode takes opencode/* prefixes, Codex accepts Anthropic natively, or which Haiku variant to pin — the decision is centralized in the package and evolves with releases. Two surfaces: 1. `extends: <preset>` in models.yaml — pulls in the bundled preset as the base layer, then deep-merges the user's fields on top. User wins at every key. targets.<harness> sub-objects merge atomically per harness (user's override replaces the preset's for that harness as a unit). Explicit null on a target removes the preset's override. 2. `iso-route init [--preset <name>] [--out <path>] [--force]` — writes a starter models.yaml that extends the preset, with a commented example override block to guide first-time edits. Preset content verified against 2026-04 provider catalogs (Anthropic, OpenAI, OpenCode Zen/Go): default: anthropic/claude-sonnet-4-6 (opencode/glm-5.1 on OpenCode) fast: claude-haiku-4-5 (opencode/big-pickle, openai/gpt-5.4-mini) quality: claude-opus-4-7 high reasoning (opencode-go/kimi-k2.5, openai/gpt-5.4 high reasoning) minimal: claude-haiku-4-5 (opencode/minimax-m2.5-free, openai/gpt-5.4-nano) Cursor has no programmatic model binding, so its per-target override is absent on every role — iso-route build still emits the advisory README with the resolved picks for users to select manually in the Cursor UI. Ships presets/ in the published tarball (added to package.json files). 10 new tests cover preset loading, scalar overrides, atomic target replacement, null-to-remove semantics, unknown preset rejection, and adding-new-roles-alongside-preset cases. Deferred follow-up (flagged, not done): optional cron-scraper that auto-bumps the preset when new models ship upstream. Until then, preset maintenance is a normal release-cycle concern.
1 parent 01bc479 commit 39c86f8

6 files changed

Lines changed: 470 additions & 11 deletions

File tree

.changeset/iso-route-presets.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
"@razroo/iso-route": minor
3+
---
4+
5+
Bundled preset system — `extends: standard` in models.yaml + `iso-route init`.
6+
7+
Ships an opinionated cost-tiered preset so newcomers don't have to know
8+
that OpenCode takes `opencode/*` prefixes, Codex accepts Anthropic
9+
natively, or which Haiku variant to pin — the decision is made once
10+
inside iso-route and evolves with new releases.
11+
12+
**`extends: standard`** in any `models.yaml` pulls in the bundled
13+
preset as the base layer, then deep-merges the user's fields on top.
14+
User wins at every key. `targets.<harness>` sub-objects merge atomically
15+
per harness (a user's override replaces the preset's for that harness
16+
as a unit, not field-by-field). Explicit `null` on a target removes
17+
the preset's override.
18+
19+
```yaml
20+
extends: standard
21+
# Everything below is optional — override only what you want:
22+
roles:
23+
quality:
24+
targets:
25+
codex:
26+
provider: openai
27+
model: gpt-5.4
28+
```
29+
30+
**`iso-route init`** scaffolds a starter `models.yaml` in the current
31+
directory that extends the standard preset and points the user at the
32+
overridable hooks:
33+
34+
```
35+
iso-route init # writes ./models.yaml
36+
iso-route init --preset standard # explicit
37+
iso-route init --out custom/path.yaml # different location
38+
iso-route init --force # overwrite existing
39+
```
40+
41+
**Preset content — `standard`** (verified against 2026-04 provider
42+
catalogs: Anthropic, OpenAI, OpenCode Zen/Go):
43+
44+
- `default`: anthropic/claude-sonnet-4-6; opencode/glm-5.1 on OpenCode
45+
- `fast` role: claude-haiku-4-5; opencode/big-pickle on OpenCode;
46+
openai/gpt-5.4-mini on Codex
47+
- `quality` role: claude-opus-4-7 with high reasoning; opencode-go/
48+
kimi-k2.5 on OpenCode; openai/gpt-5.4 with high reasoning on Codex
49+
- `minimal` role: claude-haiku-4-5; opencode/minimax-m2.5-free on
50+
OpenCode; openai/gpt-5.4-nano on Codex
51+
52+
Cursor has no programmatic model binding, so its per-target override
53+
is absent on every role — `iso-route build` still emits the advisory
54+
README with the resolved picks for users to select manually from the
55+
Cursor UI.
56+
57+
**Additive — fully backwards-compatible.** A `models.yaml` without
58+
`extends:` behaves exactly as before. The `presets/` directory now
59+
ships in the published tarball.
60+
61+
**Deferred follow-up:** an optional cron-scraper that auto-bumps preset
62+
models when new versions ship upstream. Not in this release — the
63+
catalog was hand-verified via web search, and preset maintainers will
64+
bump via a normal release cycle until the scraper lands.
65+
66+
Adds 10 tests across the parser covering preset loading, scalar
67+
overrides, atomic target replacement, null-to-remove semantics, unknown
68+
preset rejection, and adding-new-roles-alongside-preset.

packages/iso-route/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"dist/**/*.js",
1515
"dist/**/*.d.ts",
1616
"examples",
17+
"presets",
1718
"README.md",
1819
"LICENSE"
1920
],
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# @razroo/iso-route — standard preset
2+
#
3+
# Opinionated cost-tiered routing across Claude Code, Cursor, Codex, and
4+
# OpenCode. Verified against each provider's 2026-04 model catalogs:
5+
# Anthropic docs, OpenAI docs, xAI docs, Google Gemini docs, OpenCode
6+
# Zen/Go proxy listings.
7+
#
8+
# Three roles, each with per-harness overrides:
9+
# - fast — cheap + quick, for procedural work
10+
# - quality — top-tier, for judgment / writing / reasoning
11+
# - minimal — smallest credible model, for narrow one-shot transforms
12+
#
13+
# Users extend this preset in their own models.yaml:
14+
#
15+
# extends: standard
16+
# # ...override only what you want:
17+
# roles:
18+
# quality:
19+
# targets:
20+
# codex: { provider: openai, model: gpt-5.4 }
21+
22+
default:
23+
provider: anthropic
24+
model: claude-sonnet-4-6
25+
targets:
26+
opencode:
27+
provider: opencode
28+
model: opencode/glm-5.1
29+
30+
roles:
31+
# fast — procedural worker. Cheap per-call cost; assumes the task is
32+
# scripted / deterministic and quality-sensitive prose is NOT required.
33+
fast:
34+
provider: anthropic
35+
model: claude-haiku-4-5
36+
targets:
37+
opencode:
38+
provider: opencode
39+
model: opencode/big-pickle
40+
codex:
41+
provider: openai
42+
model: gpt-5.4-mini
43+
44+
# quality — top-tier worker. Use for writing, evaluation narratives,
45+
# nuanced judgment calls, or high-stakes single applications where the
46+
# marginal cost is worth the quality delta.
47+
quality:
48+
provider: anthropic
49+
model: claude-opus-4-7
50+
reasoning: high
51+
targets:
52+
opencode:
53+
provider: opencode
54+
model: opencode-go/kimi-k2.5
55+
codex:
56+
provider: openai
57+
model: gpt-5.4
58+
reasoning: high
59+
60+
# minimal — narrow one-shot extractor / classifier. Smallest credible
61+
# model on each harness; input/output is expected to be ≤5K tokens and
62+
# structured. Not for multi-step workflows.
63+
minimal:
64+
provider: anthropic
65+
model: claude-haiku-4-5
66+
targets:
67+
opencode:
68+
provider: opencode
69+
model: opencode/minimax-m2.5-free
70+
codex:
71+
provider: openai
72+
model: gpt-5.4-nano

packages/iso-route/src/cli.ts

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
#!/usr/bin/env node
2-
import { readFileSync } from "node:fs";
2+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
33
import { dirname, resolve } from "node:path";
44
import { fileURLToPath } from "node:url";
55
import { ALL_TARGETS, build } from "./build.js";
6-
import { loadPolicy } from "./parser.js";
6+
import { listPresets, loadPolicy } from "./parser.js";
77
import type { HarnessTarget } from "./types.js";
88

99
const USAGE = `iso-route — one model policy, every harness
@@ -14,8 +14,11 @@ usage:
1414
iso-route build <models.yaml> [--out <dir>] [--targets claude,codex,opencode,cursor]
1515
[--dry-run]
1616
iso-route plan <models.yaml>
17+
iso-route init [--preset <name>] [--out <path>] [--force]
1718
1819
targets default to all four. --dry-run emits nothing but prints every file it *would* write.
20+
"init" scaffolds a starter models.yaml from a built-in preset. Run "iso-route init --help"
21+
to see available presets.
1922
`;
2023

2124
function readVersion(): string {
@@ -111,6 +114,77 @@ function cmdPlan(args: string[]): number {
111114
return 0;
112115
}
113116

117+
function cmdInit(args: string[]): number {
118+
let preset = "standard";
119+
let outPath = "models.yaml";
120+
let force = false;
121+
let showHelp = false;
122+
for (let i = 0; i < args.length; i++) {
123+
const a = args[i];
124+
if (a === "--preset") {
125+
preset = args[++i] ?? "";
126+
if (!preset) {
127+
console.error("iso-route init: --preset requires a name");
128+
return 2;
129+
}
130+
} else if (a === "--out") {
131+
outPath = args[++i] ?? "";
132+
if (!outPath) {
133+
console.error("iso-route init: --out requires a path");
134+
return 2;
135+
}
136+
} else if (a === "--force") {
137+
force = true;
138+
} else if (a === "--help" || a === "-h") {
139+
showHelp = true;
140+
} else {
141+
console.error(`iso-route init: unknown flag "${a}"`);
142+
return 2;
143+
}
144+
}
145+
if (showHelp) {
146+
console.log(
147+
`iso-route init — scaffold a models.yaml from a built-in preset\n\n` +
148+
`available presets: ${listPresets().join(", ")}\n\n` +
149+
`flags:\n` +
150+
` --preset <name> preset to use (default: standard)\n` +
151+
` --out <path> where to write (default: ./models.yaml)\n` +
152+
` --force overwrite an existing file\n`,
153+
);
154+
return 0;
155+
}
156+
157+
const presets = listPresets();
158+
if (!presets.includes(preset)) {
159+
console.error(
160+
`iso-route init: unknown preset "${preset}". Available: ${presets.join(", ")}`,
161+
);
162+
return 2;
163+
}
164+
165+
const absOut = resolve(process.cwd(), outPath);
166+
if (existsSync(absOut) && !force) {
167+
console.error(
168+
`iso-route init: ${absOut} already exists. Pass --force to overwrite, or --out <other>.`,
169+
);
170+
return 2;
171+
}
172+
173+
// Scaffold a lean consumer models.yaml that extends the preset. Users can
174+
// see the preset's content by reading node_modules/@razroo/iso-route/presets/
175+
// or by running `iso-route plan` on this file.
176+
const header = `# Model policy for this project. Extends @razroo/iso-route's built-in "${preset}"\n`;
177+
const explain =
178+
`# preset — override only what you want to differ. Run \`iso-route plan\` to see\n` +
179+
`# the resolved policy (preset + your overrides applied).\n`;
180+
const body = `\nextends: ${preset}\n\n# Example override — uncomment and edit:\n#\n# roles:\n# quality:\n# targets:\n# codex:\n# provider: openai\n# model: gpt-5.4\n`;
181+
writeFileSync(absOut, header + explain + body);
182+
console.log(`iso-route: wrote ${absOut} (extends preset "${preset}")`);
183+
console.log(` next: \`iso-route plan ${outPath}\` to see resolved roles`);
184+
console.log(` \`iso-route build ${outPath} --out .\` to emit harness configs`);
185+
return 0;
186+
}
187+
114188
function formatBytes(n: number): string {
115189
if (n < 1024) return `${n} B`;
116190
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
@@ -131,6 +205,7 @@ function main(argv: string[]): number {
131205
const rest = args.slice(1);
132206
if (cmd === "build") return cmdBuild(rest);
133207
if (cmd === "plan") return cmdPlan(rest);
208+
if (cmd === "init") return cmdInit(rest);
134209
console.error(`iso-route: unknown command "${cmd}"\n`);
135210
console.error(USAGE);
136211
return 2;

0 commit comments

Comments
 (0)