opencode-model-router 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -1
- package/src/index.ts +318 -23
- package/tiers.json +88 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-model-router",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "OpenCode plugin that routes tasks to tiered subagents (fast/medium/heavy) based on complexity",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -35,5 +35,9 @@
|
|
|
35
35
|
],
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@opencode-ai/plugin": ">=1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.2.3",
|
|
41
|
+
"typescript": "^5.9.3"
|
|
38
42
|
}
|
|
39
43
|
}
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ interface TierConfig {
|
|
|
22
22
|
variant?: string;
|
|
23
23
|
thinking?: ThinkingConfig;
|
|
24
24
|
reasoning?: ReasoningConfig;
|
|
25
|
+
costRatio?: number;
|
|
25
26
|
color?: string;
|
|
26
27
|
description: string;
|
|
27
28
|
steps?: number;
|
|
@@ -31,21 +32,45 @@ interface TierConfig {
|
|
|
31
32
|
|
|
32
33
|
type Preset = Record<string, TierConfig>;
|
|
33
34
|
|
|
35
|
+
interface FallbackConfig {
|
|
36
|
+
global?: Record<string, string[]>;
|
|
37
|
+
presets?: Record<string, Record<string, string[]>>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ModeConfig {
|
|
41
|
+
defaultTier: string;
|
|
42
|
+
description: string;
|
|
43
|
+
overrideRules?: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
interface RouterConfig {
|
|
35
47
|
activePreset: string;
|
|
48
|
+
activeMode?: string;
|
|
36
49
|
presets: Record<string, Preset>;
|
|
37
50
|
rules: string[];
|
|
38
51
|
defaultTier: string;
|
|
52
|
+
fallback?: FallbackConfig;
|
|
53
|
+
taskPatterns?: Record<string, string[]>;
|
|
54
|
+
modes?: Record<string, ModeConfig>;
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
interface RouterState {
|
|
42
58
|
activePreset?: string;
|
|
59
|
+
activeMode?: string;
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
// ---------------------------------------------------------------------------
|
|
46
|
-
// Config loader
|
|
63
|
+
// Config loader with caching
|
|
47
64
|
// ---------------------------------------------------------------------------
|
|
48
65
|
|
|
66
|
+
let _cachedConfig: RouterConfig | null = null;
|
|
67
|
+
let _configDirty = true;
|
|
68
|
+
|
|
69
|
+
/** Mark config cache as stale so it is re-read on next access. */
|
|
70
|
+
function invalidateConfigCache(): void {
|
|
71
|
+
_configDirty = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
function getPluginRoot(): string {
|
|
50
75
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
51
76
|
return join(__dirname, ".."); // src/ -> plugin root
|
|
@@ -72,8 +97,93 @@ function resolvePresetName(cfg: RouterConfig, requestedPreset: string): string |
|
|
|
72
97
|
return Object.keys(cfg.presets).find((name) => name.toLowerCase() === normalized);
|
|
73
98
|
}
|
|
74
99
|
|
|
100
|
+
function validateConfig(raw: unknown): RouterConfig {
|
|
101
|
+
if (typeof raw !== "object" || raw === null) {
|
|
102
|
+
throw new Error("tiers.json: expected a JSON object at root");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const obj = raw as Record<string, unknown>;
|
|
106
|
+
|
|
107
|
+
if (typeof obj.activePreset !== "string" || !obj.activePreset) {
|
|
108
|
+
throw new Error("tiers.json: 'activePreset' must be a non-empty string");
|
|
109
|
+
}
|
|
110
|
+
if (typeof obj.presets !== "object" || obj.presets === null || Array.isArray(obj.presets)) {
|
|
111
|
+
throw new Error("tiers.json: 'presets' must be a non-null object");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const presets = obj.presets as Record<string, unknown>;
|
|
115
|
+
for (const [presetName, preset] of Object.entries(presets)) {
|
|
116
|
+
if (typeof preset !== "object" || preset === null || Array.isArray(preset)) {
|
|
117
|
+
throw new Error(`tiers.json: preset '${presetName}' must be an object`);
|
|
118
|
+
}
|
|
119
|
+
const tiers = preset as Record<string, unknown>;
|
|
120
|
+
for (const [tierName, tier] of Object.entries(tiers)) {
|
|
121
|
+
if (typeof tier !== "object" || tier === null) {
|
|
122
|
+
throw new Error(`tiers.json: tier '${presetName}.${tierName}' must be an object`);
|
|
123
|
+
}
|
|
124
|
+
const t = tier as Record<string, unknown>;
|
|
125
|
+
if (typeof t.model !== "string" || !t.model) {
|
|
126
|
+
throw new Error(`tiers.json: '${presetName}.${tierName}.model' must be a non-empty string`);
|
|
127
|
+
}
|
|
128
|
+
if (typeof t.description !== "string") {
|
|
129
|
+
throw new Error(`tiers.json: '${presetName}.${tierName}.description' must be a string`);
|
|
130
|
+
}
|
|
131
|
+
if (!Array.isArray(t.whenToUse)) {
|
|
132
|
+
throw new Error(`tiers.json: '${presetName}.${tierName}.whenToUse' must be an array`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!Array.isArray(obj.rules)) {
|
|
138
|
+
throw new Error("tiers.json: 'rules' must be an array of strings");
|
|
139
|
+
}
|
|
140
|
+
if (typeof obj.defaultTier !== "string") {
|
|
141
|
+
throw new Error("tiers.json: 'defaultTier' must be a string");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Validate modes if present
|
|
145
|
+
if (obj.modes !== undefined) {
|
|
146
|
+
if (typeof obj.modes !== "object" || obj.modes === null || Array.isArray(obj.modes)) {
|
|
147
|
+
throw new Error("tiers.json: 'modes' must be an object");
|
|
148
|
+
}
|
|
149
|
+
const modes = obj.modes as Record<string, unknown>;
|
|
150
|
+
for (const [modeName, mode] of Object.entries(modes)) {
|
|
151
|
+
if (typeof mode !== "object" || mode === null) {
|
|
152
|
+
throw new Error(`tiers.json: mode '${modeName}' must be an object`);
|
|
153
|
+
}
|
|
154
|
+
const m = mode as Record<string, unknown>;
|
|
155
|
+
if (typeof m.defaultTier !== "string") {
|
|
156
|
+
throw new Error(`tiers.json: mode '${modeName}.defaultTier' must be a string`);
|
|
157
|
+
}
|
|
158
|
+
if (typeof m.description !== "string") {
|
|
159
|
+
throw new Error(`tiers.json: mode '${modeName}.description' must be a string`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Validate taskPatterns if present
|
|
165
|
+
if (obj.taskPatterns !== undefined) {
|
|
166
|
+
if (typeof obj.taskPatterns !== "object" || obj.taskPatterns === null || Array.isArray(obj.taskPatterns)) {
|
|
167
|
+
throw new Error("tiers.json: 'taskPatterns' must be an object");
|
|
168
|
+
}
|
|
169
|
+
const tp = obj.taskPatterns as Record<string, unknown>;
|
|
170
|
+
for (const [tierName, patterns] of Object.entries(tp)) {
|
|
171
|
+
if (!Array.isArray(patterns)) {
|
|
172
|
+
throw new Error(`tiers.json: taskPatterns.'${tierName}' must be an array of strings`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return raw as RouterConfig;
|
|
178
|
+
}
|
|
179
|
+
|
|
75
180
|
function loadConfig(): RouterConfig {
|
|
76
|
-
|
|
181
|
+
if (_cachedConfig && !_configDirty) {
|
|
182
|
+
return _cachedConfig;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const raw = JSON.parse(readFileSync(configPath(), "utf-8"));
|
|
186
|
+
const cfg = validateConfig(raw);
|
|
77
187
|
|
|
78
188
|
try {
|
|
79
189
|
if (existsSync(statePath())) {
|
|
@@ -84,14 +194,43 @@ function loadConfig(): RouterConfig {
|
|
|
84
194
|
cfg.activePreset = resolved;
|
|
85
195
|
}
|
|
86
196
|
}
|
|
197
|
+
if (state.activeMode && cfg.modes?.[state.activeMode]) {
|
|
198
|
+
cfg.activeMode = state.activeMode;
|
|
199
|
+
}
|
|
87
200
|
}
|
|
88
201
|
} catch {
|
|
89
|
-
// Ignore state read errors and keep tiers.json
|
|
202
|
+
// Ignore state read errors and keep tiers.json defaults
|
|
90
203
|
}
|
|
91
204
|
|
|
205
|
+
_cachedConfig = cfg;
|
|
206
|
+
_configDirty = false;
|
|
92
207
|
return cfg;
|
|
93
208
|
}
|
|
94
209
|
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// State persistence helpers
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
/** Read current persisted state (or empty object on failure). */
|
|
215
|
+
function readState(): RouterState {
|
|
216
|
+
try {
|
|
217
|
+
if (existsSync(statePath())) {
|
|
218
|
+
return JSON.parse(readFileSync(statePath(), "utf-8")) as RouterState;
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
// ignore
|
|
222
|
+
}
|
|
223
|
+
return {};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Write state to disk (merges with existing keys). */
|
|
227
|
+
function writeState(patch: Partial<RouterState>): void {
|
|
228
|
+
const state = { ...readState(), ...patch };
|
|
229
|
+
const p = statePath();
|
|
230
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
231
|
+
writeFileSync(p, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
232
|
+
}
|
|
233
|
+
|
|
95
234
|
function saveActivePreset(presetName: string): void {
|
|
96
235
|
const cfg = loadConfig();
|
|
97
236
|
const resolved = resolvePresetName(cfg, presetName);
|
|
@@ -101,14 +240,22 @@ function saveActivePreset(presetName: string): void {
|
|
|
101
240
|
|
|
102
241
|
cfg.activePreset = resolved;
|
|
103
242
|
|
|
104
|
-
// Persist user-selected preset
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
243
|
+
// Persist user-selected preset to state file only — never mutate tiers.json
|
|
244
|
+
writeState({ activePreset: resolved });
|
|
245
|
+
|
|
246
|
+
// Invalidate cache so next read picks up the new active preset
|
|
247
|
+
invalidateConfigCache();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function saveActiveMode(modeName: string): void {
|
|
251
|
+
const cfg = loadConfig();
|
|
252
|
+
if (!cfg.modes?.[modeName]) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
109
255
|
|
|
110
|
-
|
|
111
|
-
|
|
256
|
+
cfg.activeMode = modeName;
|
|
257
|
+
writeState({ activeMode: modeName });
|
|
258
|
+
invalidateConfigCache();
|
|
112
259
|
}
|
|
113
260
|
|
|
114
261
|
function getActiveTiers(cfg: RouterConfig): Preset {
|
|
@@ -142,6 +289,73 @@ function buildAgentOptions(tier: TierConfig): Record<string, unknown> {
|
|
|
142
289
|
return Object.keys(opts).length > 0 ? opts : {};
|
|
143
290
|
}
|
|
144
291
|
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Mode helpers
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
function getActiveMode(cfg: RouterConfig): ModeConfig | undefined {
|
|
297
|
+
if (!cfg.modes || !cfg.activeMode) return undefined;
|
|
298
|
+
return cfg.modes[cfg.activeMode];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Fallback instructions builder
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
function buildFallbackInstructions(cfg: RouterConfig): string {
|
|
306
|
+
const fb = cfg.fallback;
|
|
307
|
+
if (!fb) return "";
|
|
308
|
+
|
|
309
|
+
const presetMap = fb.presets?.[cfg.activePreset];
|
|
310
|
+
const map = presetMap && Object.keys(presetMap).length > 0 ? presetMap : fb.global;
|
|
311
|
+
if (!map) return "";
|
|
312
|
+
|
|
313
|
+
const providerLines = Object.entries(map).flatMap(([provider, presetOrder]) => {
|
|
314
|
+
if (!Array.isArray(presetOrder)) return [];
|
|
315
|
+
const validOrder = presetOrder.filter(
|
|
316
|
+
(preset) => preset !== cfg.activePreset && Boolean(cfg.presets[preset]),
|
|
317
|
+
);
|
|
318
|
+
return validOrder.length > 0 ? [`- ${provider}: ${validOrder.join(" -> ")}`] : [];
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (providerLines.length === 0) return "";
|
|
322
|
+
|
|
323
|
+
return [
|
|
324
|
+
"Fallback on delegated task errors:",
|
|
325
|
+
"1. If Task(...) returns provider/model/rate-limit/timeout/auth errors, retry once with a different tier suited to the same task.",
|
|
326
|
+
"2. If retry also fails, stop delegating that task and complete it directly in the primary agent.",
|
|
327
|
+
"3. Use the failing model prefix and this preset fallback order for next-run recovery (`/preset <name>` + restart):",
|
|
328
|
+
...providerLines,
|
|
329
|
+
].join("\n");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
// Cost & taxonomy builders
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
function buildTaskTaxonomy(cfg: RouterConfig): string {
|
|
337
|
+
if (!cfg.taskPatterns || Object.keys(cfg.taskPatterns).length === 0) return "";
|
|
338
|
+
|
|
339
|
+
const lines = ["Coding task routing guide:"];
|
|
340
|
+
for (const [tier, patterns] of Object.entries(cfg.taskPatterns)) {
|
|
341
|
+
if (Array.isArray(patterns) && patterns.length > 0) {
|
|
342
|
+
lines.push(`- @${tier}: ${patterns.join(", ")}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return lines.join("\n");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function buildCostAwareness(cfg: RouterConfig): string {
|
|
349
|
+
const tiers = getActiveTiers(cfg);
|
|
350
|
+
const costs = Object.entries(tiers)
|
|
351
|
+
.filter(([_, t]) => t.costRatio != null)
|
|
352
|
+
.map(([name, t]) => `@${name}=${t.costRatio}x`)
|
|
353
|
+
.join(", ");
|
|
354
|
+
|
|
355
|
+
if (!costs) return "";
|
|
356
|
+
return `Cost ratios: ${costs}. Always use the cheapest tier that can reliably handle the task.`;
|
|
357
|
+
}
|
|
358
|
+
|
|
145
359
|
// ---------------------------------------------------------------------------
|
|
146
360
|
// System prompt builder
|
|
147
361
|
// ---------------------------------------------------------------------------
|
|
@@ -157,21 +371,44 @@ function buildDelegationProtocol(cfg: RouterConfig): string {
|
|
|
157
371
|
})
|
|
158
372
|
.join(" | ");
|
|
159
373
|
|
|
374
|
+
// Build per-tier whenToUse descriptions so the agent knows when to pick each tier
|
|
375
|
+
const tierDescriptions = Object.entries(tiers)
|
|
376
|
+
.map(([name, t]) => {
|
|
377
|
+
const uses = t.whenToUse.length > 0 ? t.whenToUse.join(", ") : t.description;
|
|
378
|
+
return `- @${name}: ${uses}`;
|
|
379
|
+
})
|
|
380
|
+
.join("\n");
|
|
381
|
+
|
|
382
|
+
// Task taxonomy from config
|
|
383
|
+
const taxonomy = buildTaskTaxonomy(cfg);
|
|
384
|
+
|
|
385
|
+
// Cost awareness
|
|
386
|
+
const costLine = buildCostAwareness(cfg);
|
|
387
|
+
|
|
388
|
+
// Mode-aware rules: if active mode has overrideRules, use those; otherwise use global rules
|
|
389
|
+
const mode = getActiveMode(cfg);
|
|
390
|
+
const effectiveRules = mode?.overrideRules?.length ? mode.overrideRules : cfg.rules;
|
|
391
|
+
const numberedRules = effectiveRules
|
|
392
|
+
.map((rule, i) => `${i + 1}. ${rule}`)
|
|
393
|
+
.join("\n");
|
|
394
|
+
|
|
395
|
+
const fallbackInstructions = buildFallbackInstructions(cfg);
|
|
396
|
+
|
|
160
397
|
return [
|
|
161
398
|
"## Model Delegation Protocol",
|
|
162
399
|
`Preset: ${cfg.activePreset}. Tiers: ${tierSummary}.`,
|
|
163
400
|
"",
|
|
401
|
+
"Tier capabilities:",
|
|
402
|
+
tierDescriptions,
|
|
403
|
+
...(taxonomy ? ["", taxonomy] : []),
|
|
404
|
+
...(costLine ? ["", costLine] : []),
|
|
405
|
+
...(mode ? [`\nActive mode: ${cfg.activeMode} (${mode.description})`] : []),
|
|
406
|
+
"",
|
|
164
407
|
"Apply to every user message (plan and ad-hoc):",
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
"3. Route read-only search/exploration tasks to @fast.",
|
|
168
|
-
"4. Route implementation/edit/refactor/test/bugfix tasks to @medium.",
|
|
169
|
-
"5. Route architecture/security/performance/complex debugging to @heavy.",
|
|
170
|
-
"6. For mixed requests, delegate each subtask to the matching tier, then synthesize one final response.",
|
|
171
|
-
"7. For trivial single read/grep tasks, execute directly.",
|
|
172
|
-
`8. If uncertain, default to @${cfg.defaultTier}.`,
|
|
408
|
+
numberedRules,
|
|
409
|
+
...(fallbackInstructions ? ["", fallbackInstructions] : []),
|
|
173
410
|
"",
|
|
174
|
-
|
|
411
|
+
`Delegate with Task(subagent_type="fast|medium|heavy", prompt="...").`,
|
|
175
412
|
"Keep orchestration and final synthesis in the primary agent.",
|
|
176
413
|
].join("\n");
|
|
177
414
|
}
|
|
@@ -209,6 +446,50 @@ function buildTiersOutput(cfg: RouterConfig): string {
|
|
|
209
446
|
return lines.join("\n");
|
|
210
447
|
}
|
|
211
448
|
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
// /budget command output
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
|
|
453
|
+
function buildBudgetOutput(cfg: RouterConfig, args: string): string {
|
|
454
|
+
const modes = cfg.modes;
|
|
455
|
+
if (!modes || Object.keys(modes).length === 0) {
|
|
456
|
+
return 'No modes configured in tiers.json. Add a "modes" section to enable budget mode.';
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const requested = args.trim().toLowerCase();
|
|
460
|
+
const currentMode = cfg.activeMode || "normal";
|
|
461
|
+
|
|
462
|
+
// No args: show current mode and available modes
|
|
463
|
+
if (!requested) {
|
|
464
|
+
const lines = ["# Routing Modes\n"];
|
|
465
|
+
for (const [name, mode] of Object.entries(modes)) {
|
|
466
|
+
const active = name === currentMode ? " <- active" : "";
|
|
467
|
+
lines.push(`- **${name}**${active}: ${mode.description} (default tier: @${mode.defaultTier})`);
|
|
468
|
+
}
|
|
469
|
+
lines.push(`\nSwitch with: \`/budget <mode>\``);
|
|
470
|
+
return lines.join("\n");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Switch mode
|
|
474
|
+
if (modes[requested]) {
|
|
475
|
+
saveActiveMode(requested);
|
|
476
|
+
const mode = modes[requested];
|
|
477
|
+
return [
|
|
478
|
+
`Routing mode switched to **${requested}**.`,
|
|
479
|
+
"",
|
|
480
|
+
mode.description,
|
|
481
|
+
`Default tier: @${mode.defaultTier}`,
|
|
482
|
+
...(mode.overrideRules?.length
|
|
483
|
+
? ["", "Active rules:", ...mode.overrideRules.map((r) => `- ${r}`)]
|
|
484
|
+
: []),
|
|
485
|
+
"",
|
|
486
|
+
"Mode change takes effect immediately on the next message.",
|
|
487
|
+
].join("\n");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return `Unknown mode: "${requested}". Available: ${Object.keys(modes).join(", ")}`;
|
|
491
|
+
}
|
|
492
|
+
|
|
212
493
|
// ---------------------------------------------------------------------------
|
|
213
494
|
// /preset command output
|
|
214
495
|
// ---------------------------------------------------------------------------
|
|
@@ -302,6 +583,10 @@ const ModelRouterPlugin: Plugin = async (_ctx: PluginInput) => {
|
|
|
302
583
|
template: "$ARGUMENTS",
|
|
303
584
|
description: "Show or switch model presets (e.g., /preset openai)",
|
|
304
585
|
};
|
|
586
|
+
opencodeConfig.command["budget"] = {
|
|
587
|
+
template: "$ARGUMENTS",
|
|
588
|
+
description: "Show or switch routing mode (e.g., /budget, /budget budget, /budget quality)",
|
|
589
|
+
};
|
|
305
590
|
opencodeConfig.command["annotate-plan"] = {
|
|
306
591
|
template: [
|
|
307
592
|
"Annotate the plan with tier directives for model delegation.",
|
|
@@ -332,19 +617,19 @@ const ModelRouterPlugin: Plugin = async (_ctx: PluginInput) => {
|
|
|
332
617
|
},
|
|
333
618
|
|
|
334
619
|
// -----------------------------------------------------------------------
|
|
335
|
-
// Inject delegation protocol —
|
|
620
|
+
// Inject delegation protocol — uses cached config (invalidated on /preset or /budget)
|
|
336
621
|
// -----------------------------------------------------------------------
|
|
337
622
|
"experimental.chat.system.transform": async (_input: any, output: any) => {
|
|
338
623
|
try {
|
|
339
|
-
cfg = loadConfig(); //
|
|
624
|
+
cfg = loadConfig(); // Returns cache unless invalidated
|
|
340
625
|
} catch {
|
|
341
|
-
// Use
|
|
626
|
+
// Use last known config if file read fails
|
|
342
627
|
}
|
|
343
628
|
output.system.push(buildDelegationProtocol(cfg));
|
|
344
629
|
},
|
|
345
630
|
|
|
346
631
|
// -----------------------------------------------------------------------
|
|
347
|
-
// Handle /tiers and /
|
|
632
|
+
// Handle /tiers, /preset, and /budget commands
|
|
348
633
|
// -----------------------------------------------------------------------
|
|
349
634
|
"command.execute.before": async (input: any, output: any) => {
|
|
350
635
|
if (input.command === "tiers") {
|
|
@@ -363,6 +648,16 @@ const ModelRouterPlugin: Plugin = async (_ctx: PluginInput) => {
|
|
|
363
648
|
text: buildPresetOutput(cfg, input.arguments ?? ""),
|
|
364
649
|
});
|
|
365
650
|
}
|
|
651
|
+
|
|
652
|
+
if (input.command === "budget") {
|
|
653
|
+
try {
|
|
654
|
+
cfg = loadConfig();
|
|
655
|
+
} catch {}
|
|
656
|
+
output.parts.push({
|
|
657
|
+
type: "text" as const,
|
|
658
|
+
text: buildBudgetOutput(cfg, input.arguments ?? ""),
|
|
659
|
+
});
|
|
660
|
+
}
|
|
366
661
|
},
|
|
367
662
|
};
|
|
368
663
|
};
|
package/tiers.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"activePreset": "anthropic",
|
|
3
|
+
"activeMode": "normal",
|
|
3
4
|
"presets": {
|
|
4
5
|
"anthropic": {
|
|
5
6
|
"fast": {
|
|
6
7
|
"model": "anthropic/claude-haiku-4-5",
|
|
8
|
+
"costRatio": 1,
|
|
7
9
|
"description": "Haiku 4.5 for exploration, search, and simple reads",
|
|
8
10
|
"steps": 30,
|
|
9
11
|
"prompt": "You are a fast exploration agent. Focus on speed and efficiency. Read files, search code, and return findings concisely. Do NOT make edits unless explicitly asked.",
|
|
@@ -17,6 +19,7 @@
|
|
|
17
19
|
"medium": {
|
|
18
20
|
"model": "anthropic/claude-sonnet-4-5",
|
|
19
21
|
"variant": "max",
|
|
22
|
+
"costRatio": 5,
|
|
20
23
|
"description": "Sonnet 4.5 max for implementation, refactoring, and tests",
|
|
21
24
|
"steps": 50,
|
|
22
25
|
"prompt": "You are an implementation agent. Write clean, production-quality code matching existing project patterns. Run linters/tests after changes when possible.",
|
|
@@ -31,6 +34,7 @@
|
|
|
31
34
|
"heavy": {
|
|
32
35
|
"model": "anthropic/claude-opus-4-6",
|
|
33
36
|
"variant": "max",
|
|
37
|
+
"costRatio": 20,
|
|
34
38
|
"description": "Opus 4.6 max for architecture, complex debugging, and security",
|
|
35
39
|
"steps": 30,
|
|
36
40
|
"prompt": "You are a senior architecture consultant. Analyze deeply, consider tradeoffs, and provide thorough reasoning. Be exhaustive in your analysis.",
|
|
@@ -45,6 +49,7 @@
|
|
|
45
49
|
"openai": {
|
|
46
50
|
"fast": {
|
|
47
51
|
"model": "openai/gpt-5.3-codex-spark",
|
|
52
|
+
"costRatio": 1,
|
|
48
53
|
"description": "GPT-5.3 Codex Spark for fast exploration and simple tasks",
|
|
49
54
|
"steps": 30,
|
|
50
55
|
"prompt": "You are a fast exploration agent. Focus on speed and efficiency. Read files, search code, and return findings concisely. Do NOT make edits unless explicitly asked.",
|
|
@@ -56,6 +61,7 @@
|
|
|
56
61
|
},
|
|
57
62
|
"medium": {
|
|
58
63
|
"model": "openai/gpt-5.3-codex",
|
|
64
|
+
"costRatio": 5,
|
|
59
65
|
"description": "GPT-5.3 Codex default settings for implementation and standard coding",
|
|
60
66
|
"steps": 50,
|
|
61
67
|
"prompt": "You are an implementation agent. Write clean, production-quality code matching existing project patterns. Run linters/tests after changes when possible.",
|
|
@@ -69,6 +75,7 @@
|
|
|
69
75
|
"heavy": {
|
|
70
76
|
"model": "openai/gpt-5.3-codex",
|
|
71
77
|
"variant": "xhigh",
|
|
78
|
+
"costRatio": 20,
|
|
72
79
|
"description": "GPT-5.3 Codex xhigh for architecture and complex tasks",
|
|
73
80
|
"steps": 30,
|
|
74
81
|
"prompt": "You are a senior architecture consultant. Analyze deeply, consider tradeoffs, and provide thorough reasoning.",
|
|
@@ -83,6 +90,7 @@
|
|
|
83
90
|
"github-copilot": {
|
|
84
91
|
"fast": {
|
|
85
92
|
"model": "github-copilot/claude-haiku-4-5",
|
|
93
|
+
"costRatio": 1,
|
|
86
94
|
"description": "Claude Haiku 4.5 via GitHub Copilot for fast exploration and simple tasks",
|
|
87
95
|
"steps": 30,
|
|
88
96
|
"prompt": "You are a fast exploration agent. Focus on speed and efficiency. Read files, search code, and return findings concisely. Do NOT make edits unless explicitly asked.",
|
|
@@ -95,6 +103,7 @@
|
|
|
95
103
|
},
|
|
96
104
|
"medium": {
|
|
97
105
|
"model": "github-copilot/claude-sonnet-4-5",
|
|
106
|
+
"costRatio": 5,
|
|
98
107
|
"description": "Claude Sonnet 4.5 via GitHub Copilot for implementation, refactoring, and tests",
|
|
99
108
|
"steps": 50,
|
|
100
109
|
"prompt": "You are an implementation agent. Write clean, production-quality code matching existing project patterns. Run linters/tests after changes when possible.",
|
|
@@ -109,6 +118,7 @@
|
|
|
109
118
|
"heavy": {
|
|
110
119
|
"model": "github-copilot/claude-opus-4-6",
|
|
111
120
|
"variant": "thinking",
|
|
121
|
+
"costRatio": 20,
|
|
112
122
|
"description": "Claude Opus 4.6 via GitHub Copilot for architecture, complex debugging, and security",
|
|
113
123
|
"steps": 30,
|
|
114
124
|
"prompt": "You are a senior architecture consultant. Analyze deeply, consider tradeoffs, and provide thorough reasoning. Be exhaustive in your analysis.",
|
|
@@ -123,6 +133,7 @@
|
|
|
123
133
|
"google": {
|
|
124
134
|
"fast": {
|
|
125
135
|
"model": "google/gemini-2.5-flash",
|
|
136
|
+
"costRatio": 1,
|
|
126
137
|
"description": "Gemini 2.5 Flash for fast exploration and simple tasks",
|
|
127
138
|
"steps": 30,
|
|
128
139
|
"prompt": "You are a fast exploration agent. Focus on speed and efficiency. Read files, search code, and return findings concisely. Do NOT make edits unless explicitly asked.",
|
|
@@ -135,6 +146,7 @@
|
|
|
135
146
|
},
|
|
136
147
|
"medium": {
|
|
137
148
|
"model": "google/gemini-2.5-pro",
|
|
149
|
+
"costRatio": 5,
|
|
138
150
|
"description": "Gemini 2.5 Pro for implementation, refactoring, and tests",
|
|
139
151
|
"steps": 50,
|
|
140
152
|
"prompt": "You are an implementation agent. Write clean, production-quality code matching existing project patterns. Run linters/tests after changes when possible.",
|
|
@@ -148,6 +160,7 @@
|
|
|
148
160
|
},
|
|
149
161
|
"heavy": {
|
|
150
162
|
"model": "google/gemini-3-pro-preview",
|
|
163
|
+
"costRatio": 20,
|
|
151
164
|
"description": "Gemini 3 Pro Preview for architecture, complex debugging, and security",
|
|
152
165
|
"steps": 30,
|
|
153
166
|
"prompt": "You are a senior architecture consultant. Analyze deeply, consider tradeoffs, and provide thorough reasoning. Be exhaustive in your analysis.",
|
|
@@ -160,6 +173,77 @@
|
|
|
160
173
|
}
|
|
161
174
|
}
|
|
162
175
|
},
|
|
176
|
+
"taskPatterns": {
|
|
177
|
+
"fast": [
|
|
178
|
+
"Find, search, locate, or grep files and code patterns",
|
|
179
|
+
"List or show directory structure and file contents",
|
|
180
|
+
"Read or display specific files or sections",
|
|
181
|
+
"Check git status, log, diff, or blame",
|
|
182
|
+
"Lookup documentation, API signatures, or type definitions",
|
|
183
|
+
"Count occurrences, lines, or matches",
|
|
184
|
+
"Check if a file, function, or class exists",
|
|
185
|
+
"Simple rename or string replacement across files"
|
|
186
|
+
],
|
|
187
|
+
"medium": [
|
|
188
|
+
"Implement a new feature, function, or component",
|
|
189
|
+
"Refactor or restructure existing code",
|
|
190
|
+
"Write or update tests",
|
|
191
|
+
"Fix a bug (first or second attempt)",
|
|
192
|
+
"Modify or update existing code logic",
|
|
193
|
+
"Code review with suggested changes",
|
|
194
|
+
"Run build/lint/test and fix resulting errors",
|
|
195
|
+
"Create a new file from a template or pattern",
|
|
196
|
+
"Database migration or schema changes",
|
|
197
|
+
"API endpoint implementation",
|
|
198
|
+
"Configuration or dependency updates"
|
|
199
|
+
],
|
|
200
|
+
"heavy": [
|
|
201
|
+
"Design system or module architecture from scratch",
|
|
202
|
+
"Debug a problem after 2+ failed attempts",
|
|
203
|
+
"Security audit or vulnerability review",
|
|
204
|
+
"Performance profiling and optimization",
|
|
205
|
+
"Migration strategy (framework, language, infrastructure)",
|
|
206
|
+
"Complex multi-system integration design",
|
|
207
|
+
"Evaluate tradeoffs between competing approaches",
|
|
208
|
+
"Root cause analysis of complex or elusive failures"
|
|
209
|
+
]
|
|
210
|
+
},
|
|
211
|
+
"modes": {
|
|
212
|
+
"normal": {
|
|
213
|
+
"defaultTier": "medium",
|
|
214
|
+
"description": "Balanced quality and cost — delegates based on task complexity"
|
|
215
|
+
},
|
|
216
|
+
"budget": {
|
|
217
|
+
"defaultTier": "fast",
|
|
218
|
+
"description": "Aggressive cost savings — defaults to cheapest tier, escalates only when needed",
|
|
219
|
+
"overrideRules": [
|
|
220
|
+
"Default ALL tasks to @fast unless they clearly require code edits or complex reasoning",
|
|
221
|
+
"Use @medium ONLY for: multi-file edits, complex refactors, test suites, or build-fix cycles",
|
|
222
|
+
"Use @heavy ONLY when explicitly requested by user or after 2+ failed @medium attempts",
|
|
223
|
+
"Prefer executing simple tasks directly (grep, read, glob) over delegating — zero delegation overhead",
|
|
224
|
+
"Batch multiple related searches into a single @fast delegation instead of multiple calls",
|
|
225
|
+
"When uncertain between @fast and @medium, choose @fast — escalate only on failure"
|
|
226
|
+
]
|
|
227
|
+
},
|
|
228
|
+
"quality": {
|
|
229
|
+
"defaultTier": "medium",
|
|
230
|
+
"description": "Quality-first — uses stronger models more liberally for better results",
|
|
231
|
+
"overrideRules": [
|
|
232
|
+
"Default to @medium for all tasks including exploration when deep context understanding matters",
|
|
233
|
+
"Use @heavy for any task involving architecture, debugging, security, or multi-file coordination",
|
|
234
|
+
"Use @fast only for trivial single-tool operations (one grep, one file read)",
|
|
235
|
+
"Prefer thoroughness over speed — better to over-qualify a task than under-qualify it"
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
"fallback": {
|
|
240
|
+
"global": {
|
|
241
|
+
"anthropic": ["openai", "google", "github-copilot"],
|
|
242
|
+
"openai": ["anthropic", "google", "github-copilot"],
|
|
243
|
+
"github-copilot": ["anthropic", "openai", "google"],
|
|
244
|
+
"google": ["openai", "anthropic", "github-copilot"]
|
|
245
|
+
}
|
|
246
|
+
},
|
|
163
247
|
"rules": [
|
|
164
248
|
"When a plan step contains [tier:fast], [tier:medium], or [tier:heavy], delegate to that agent",
|
|
165
249
|
"When a plan says 'use a fast/cheap model' -> delegate to @fast",
|
|
@@ -169,7 +253,10 @@
|
|
|
169
253
|
"Use @fast for any read-only exploration or research task",
|
|
170
254
|
"Keep orchestration (planning, decisions, verification) for yourself - delegate execution",
|
|
171
255
|
"For trivial tasks (single grep, single file read), execute directly without delegation",
|
|
172
|
-
"Never delegate to @heavy if you are already running on an opus-class model - do it yourself"
|
|
256
|
+
"Never delegate to @heavy if you are already running on an opus-class model - do it yourself",
|
|
257
|
+
"If a task takes 1-2 tool calls, execute directly — delegation overhead is not worth the cost",
|
|
258
|
+
"Consult the task routing guide below to match task type to the correct tier",
|
|
259
|
+
"Consider cost ratios when choosing tiers — always use the cheapest tier that can reliably handle the task"
|
|
173
260
|
],
|
|
174
261
|
"defaultTier": "medium"
|
|
175
262
|
}
|