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.
Files changed (3) hide show
  1. package/package.json +5 -1
  2. package/src/index.ts +318 -23
  3. 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.6",
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
- const cfg = JSON.parse(readFileSync(configPath(), "utf-8")) as RouterConfig;
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 active preset
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 outside package cache so it survives npm updates
105
- const presetState: RouterState = { activePreset: resolved };
106
- const p = statePath();
107
- mkdirSync(dirname(p), { recursive: true });
108
- writeFileSync(p, JSON.stringify(presetState, null, 2) + "\n", "utf-8");
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
- // Keep local tiers.json in sync as best effort
111
- writeFileSync(configPath(), JSON.stringify(cfg, null, 2) + "\n", "utf-8");
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
- "1. Split multi-part requests into atomic tasks.",
166
- "2. Respect explicit tier instructions and [tier:fast|medium|heavy] tags.",
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
- "Delegate with Task(subagent_type=\"fast|medium|heavy\", prompt=\"...\").",
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 — re-reads config each time for live updates
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(); // Re-read for live preset switches
624
+ cfg = loadConfig(); // Returns cache unless invalidated
340
625
  } catch {
341
- // Use cached config if file read fails
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 /preset commands
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
  }