opencode-model-router 1.0.4 → 1.0.6

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/README.md CHANGED
@@ -95,7 +95,7 @@ All configuration lives in `tiers.json` at the plugin root. Edit it to match you
95
95
 
96
96
  ### Presets
97
97
 
98
- The plugin ships with two presets:
98
+ The plugin ships with four presets:
99
99
 
100
100
  **anthropic** (default):
101
101
  | Tier | Model | Notes |
@@ -111,6 +111,20 @@ The plugin ships with two presets:
111
111
  | medium | `openai/gpt-5.3-codex` | Default settings (no variant/reasoning override) |
112
112
  | heavy | `openai/gpt-5.3-codex` | Variant: `xhigh` |
113
113
 
114
+ **github-copilot**:
115
+ | Tier | Model | Notes |
116
+ |------|-------|-------|
117
+ | fast | `github-copilot/claude-haiku-4-5` | Cheapest, fastest |
118
+ | medium | `github-copilot/claude-sonnet-4-5` | Balanced coding model |
119
+ | heavy | `github-copilot/claude-opus-4-6` | Variant: `thinking` |
120
+
121
+ **google**:
122
+ | Tier | Model | Notes |
123
+ |------|-------|-------|
124
+ | fast | `google/gemini-2.5-flash` | Cheapest, fastest |
125
+ | medium | `google/gemini-2.5-pro` | Balanced coding model |
126
+ | heavy | `google/gemini-3-pro-preview` | Strongest reasoning in default set |
127
+
114
128
  Switch presets with the `/preset` command:
115
129
 
116
130
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-model-router",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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",
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Plugin, PluginInput } from "@opencode-ai/plugin";
2
- import { readFileSync, writeFileSync } from "fs";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
3
4
  import { dirname, join } from "path";
4
5
  import { fileURLToPath } from "url";
5
6
 
@@ -37,6 +38,10 @@ interface RouterConfig {
37
38
  defaultTier: string;
38
39
  }
39
40
 
41
+ interface RouterState {
42
+ activePreset?: string;
43
+ }
44
+
40
45
  // ---------------------------------------------------------------------------
41
46
  // Config loader
42
47
  // ---------------------------------------------------------------------------
@@ -50,13 +55,59 @@ function configPath(): string {
50
55
  return join(getPluginRoot(), "tiers.json");
51
56
  }
52
57
 
58
+ function statePath(): string {
59
+ return join(homedir(), ".config", "opencode", "opencode-model-router.state.json");
60
+ }
61
+
62
+ function resolvePresetName(cfg: RouterConfig, requestedPreset: string): string | undefined {
63
+ if (cfg.presets[requestedPreset]) {
64
+ return requestedPreset;
65
+ }
66
+
67
+ const normalized = requestedPreset.trim().toLowerCase();
68
+ if (!normalized) {
69
+ return undefined;
70
+ }
71
+
72
+ return Object.keys(cfg.presets).find((name) => name.toLowerCase() === normalized);
73
+ }
74
+
53
75
  function loadConfig(): RouterConfig {
54
- return JSON.parse(readFileSync(configPath(), "utf-8")) as RouterConfig;
76
+ const cfg = JSON.parse(readFileSync(configPath(), "utf-8")) as RouterConfig;
77
+
78
+ try {
79
+ if (existsSync(statePath())) {
80
+ const state = JSON.parse(readFileSync(statePath(), "utf-8")) as RouterState;
81
+ if (state.activePreset) {
82
+ const resolved = resolvePresetName(cfg, state.activePreset);
83
+ if (resolved) {
84
+ cfg.activePreset = resolved;
85
+ }
86
+ }
87
+ }
88
+ } catch {
89
+ // Ignore state read errors and keep tiers.json active preset
90
+ }
91
+
92
+ return cfg;
55
93
  }
56
94
 
57
95
  function saveActivePreset(presetName: string): void {
58
96
  const cfg = loadConfig();
59
- cfg.activePreset = presetName;
97
+ const resolved = resolvePresetName(cfg, presetName);
98
+ if (!resolved) {
99
+ return;
100
+ }
101
+
102
+ cfg.activePreset = resolved;
103
+
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");
109
+
110
+ // Keep local tiers.json in sync as best effort
60
111
  writeFileSync(configPath(), JSON.stringify(cfg, null, 2) + "\n", "utf-8");
61
112
  }
62
113
 
@@ -180,19 +231,22 @@ function buildPresetOutput(cfg: RouterConfig, args: string): string {
180
231
  }
181
232
 
182
233
  // Switch preset
183
- if (cfg.presets[requestedPreset]) {
184
- saveActivePreset(requestedPreset);
185
- const tiers = cfg.presets[requestedPreset]!;
234
+ const resolvedPreset = resolvePresetName(cfg, requestedPreset);
235
+ if (resolvedPreset) {
236
+ saveActivePreset(resolvedPreset);
237
+ cfg.activePreset = resolvedPreset;
238
+ const tiers = cfg.presets[resolvedPreset]!;
186
239
  const models = Object.entries(tiers)
187
240
  .map(([tier, t]) => ` @${tier} -> ${t.model}`)
188
241
  .join("\n");
189
242
  return [
190
- `Preset switched to **${requestedPreset}**.`,
243
+ `Preset switched to **${resolvedPreset}**.`,
191
244
  "",
192
245
  models,
193
246
  "",
194
- "Restart OpenCode for agent registration to take effect.",
195
- "System prompt delegation rules will update immediately.",
247
+ "Selection is now persisted in ~/.config/opencode/opencode-model-router.state.json.",
248
+ "Restart OpenCode for subagent model registration to take effect.",
249
+ "System prompt delegation rules update immediately.",
196
250
  ].join("\n");
197
251
  }
198
252
 
package/tiers.json CHANGED
@@ -79,6 +79,85 @@
79
79
  "Performance optimization"
80
80
  ]
81
81
  }
82
+ },
83
+ "github-copilot": {
84
+ "fast": {
85
+ "model": "github-copilot/claude-haiku-4-5",
86
+ "description": "Claude Haiku 4.5 via GitHub Copilot for fast exploration and simple tasks",
87
+ "steps": 30,
88
+ "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.",
89
+ "whenToUse": [
90
+ "Codebase exploration and search",
91
+ "Simple file reads and listing",
92
+ "Grep/glob operations",
93
+ "Quick lookups and research"
94
+ ]
95
+ },
96
+ "medium": {
97
+ "model": "github-copilot/claude-sonnet-4-5",
98
+ "description": "Claude Sonnet 4.5 via GitHub Copilot for implementation, refactoring, and tests",
99
+ "steps": 50,
100
+ "prompt": "You are an implementation agent. Write clean, production-quality code matching existing project patterns. Run linters/tests after changes when possible.",
101
+ "whenToUse": [
102
+ "Feature implementation",
103
+ "Refactoring",
104
+ "Writing tests",
105
+ "Code review",
106
+ "Bug fixes"
107
+ ]
108
+ },
109
+ "heavy": {
110
+ "model": "github-copilot/claude-opus-4-6",
111
+ "variant": "thinking",
112
+ "description": "Claude Opus 4.6 via GitHub Copilot for architecture, complex debugging, and security",
113
+ "steps": 30,
114
+ "prompt": "You are a senior architecture consultant. Analyze deeply, consider tradeoffs, and provide thorough reasoning. Be exhaustive in your analysis.",
115
+ "whenToUse": [
116
+ "Architecture decisions",
117
+ "Complex debugging (after 2+ failures)",
118
+ "Security review",
119
+ "Performance optimization"
120
+ ]
121
+ }
122
+ },
123
+ "google": {
124
+ "fast": {
125
+ "model": "google/gemini-2.5-flash",
126
+ "description": "Gemini 2.5 Flash for fast exploration and simple tasks",
127
+ "steps": 30,
128
+ "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.",
129
+ "whenToUse": [
130
+ "Codebase exploration and search",
131
+ "Simple file reads and listing",
132
+ "Grep/glob operations",
133
+ "Quick lookups and research"
134
+ ]
135
+ },
136
+ "medium": {
137
+ "model": "google/gemini-2.5-pro",
138
+ "description": "Gemini 2.5 Pro for implementation, refactoring, and tests",
139
+ "steps": 50,
140
+ "prompt": "You are an implementation agent. Write clean, production-quality code matching existing project patterns. Run linters/tests after changes when possible.",
141
+ "whenToUse": [
142
+ "Feature implementation",
143
+ "Refactoring",
144
+ "Writing tests",
145
+ "Code review",
146
+ "Bug fixes"
147
+ ]
148
+ },
149
+ "heavy": {
150
+ "model": "google/gemini-3-pro-preview",
151
+ "description": "Gemini 3 Pro Preview for architecture, complex debugging, and security",
152
+ "steps": 30,
153
+ "prompt": "You are a senior architecture consultant. Analyze deeply, consider tradeoffs, and provide thorough reasoning. Be exhaustive in your analysis.",
154
+ "whenToUse": [
155
+ "Architecture decisions",
156
+ "Complex debugging (after 2+ failures)",
157
+ "Security review",
158
+ "Performance optimization"
159
+ ]
160
+ }
82
161
  }
83
162
  },
84
163
  "rules": [
@@ -88,9 +167,9 @@
88
167
  "When a plan says 'use a heavy/powerful model' -> delegate to @heavy",
89
168
  "Default to @medium for implementation tasks you could delegate",
90
169
  "Use @fast for any read-only exploration or research task",
91
- "Keep orchestration (planning, decisions, verification) for yourself delegate execution",
170
+ "Keep orchestration (planning, decisions, verification) for yourself - delegate execution",
92
171
  "For trivial tasks (single grep, single file read), execute directly without delegation",
93
- "Never delegate to @heavy if you are already running on an opus-class model do it yourself"
172
+ "Never delegate to @heavy if you are already running on an opus-class model - do it yourself"
94
173
  ],
95
174
  "defaultTier": "medium"
96
175
  }