@s-berdia/opencode-router 1.0.1 → 1.1.1
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 +34 -1
- package/USAGE.md +92 -8
- package/dist/bin.js +159 -11
- package/dist/bin.js.map +1 -1
- package/dist/index.js +86 -3
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# opencode-router
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@s-berdia/opencode-router)
|
|
4
|
+
[](https://www.npmjs.com/package/@s-berdia/opencode-router)
|
|
5
|
+
[](https://github.com/s-berdia/Opencode_router/actions/workflows/ci.yml)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
**Switch models before you hit your [OpenCode](https://opencode.ai) limit.** Automatic
|
|
9
|
+
model routing and quota failover for the **Go**, **Go Plus**, and **Free** plans.
|
|
4
10
|
|
|
5
11
|

|
|
6
12
|
|
|
@@ -41,6 +47,15 @@ the router, read its [known limits](./USAGE.md#11-known-limits).
|
|
|
41
47
|
|
|
42
48
|
## Quick start
|
|
43
49
|
|
|
50
|
+
If you have the OpenCode command-line tool, run this command. Then close OpenCode, start
|
|
51
|
+
it again, and go to step 4.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
opencode plugin @s-berdia/opencode-router --global
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If you do not have the command-line tool, edit the config file:
|
|
58
|
+
|
|
44
59
|
1. Open your global OpenCode config file in a text editor:
|
|
45
60
|
- Windows: `C:\Users\<your-user-name>\.config\opencode\opencode.json`
|
|
46
61
|
- macOS and Linux: `~/.config/opencode/opencode.json`
|
|
@@ -66,6 +81,10 @@ the router, read its [known limits](./USAGE.md#11-known-limits).
|
|
|
66
81
|
|
|
67
82
|
The router now selects models automatically. You do not have to do more.
|
|
68
83
|
|
|
84
|
+
OpenCode does not update plugins automatically. When a new version of the router is
|
|
85
|
+
available, the router shows a note. Then type `/routing-update` and restart OpenCode.
|
|
86
|
+
See [Update the router](./USAGE.md#12-update-the-router).
|
|
87
|
+
|
|
69
88
|
## Commands
|
|
70
89
|
|
|
71
90
|
### In OpenCode
|
|
@@ -76,6 +95,7 @@ The router now selects models automatically. You do not have to do more.
|
|
|
76
95
|
| `/routing-status` | Show the model in use for each agent (after failover) and its 5-hour quota use |
|
|
77
96
|
| `/routing-plan go-plus` | Save a plan profile (`go`, `go-plus`, or `free`) and apply it immediately |
|
|
78
97
|
| `/routing-eval` | Send one test prompt to the `build` models and write a report |
|
|
98
|
+
| `/routing-update` | Look for a newer version and remove the old copy that OpenCode keeps. Then restart OpenCode |
|
|
79
99
|
|
|
80
100
|
### In a terminal
|
|
81
101
|
|
|
@@ -88,6 +108,7 @@ npx @s-berdia/opencode-router status # models in use and quota u
|
|
|
88
108
|
npx @s-berdia/opencode-router plan go-plus # save the plan profile and rebuild
|
|
89
109
|
npx @s-berdia/opencode-router cat # catalog file, date, model count
|
|
90
110
|
npx @s-berdia/opencode-router install --scope global # write slash-command files
|
|
111
|
+
npx @s-berdia/opencode-router@latest update # remove OpenCode's old copy
|
|
91
112
|
npx @s-berdia/opencode-router --help # all commands and options
|
|
92
113
|
```
|
|
93
114
|
|
|
@@ -140,6 +161,7 @@ or a value that is not valid, the router ignores it and shows a note in the scor
|
|
|
140
161
|
| `blend` | `{ "input": 0.75, "output": 0.25, "cacheRead": 0.1 }` | Token mix used to calculate the cost of one million tokens |
|
|
141
162
|
| `enduranceScale` | `{ "minTokensPer5h": 0.25, "maxTokensPer5h": 40 }` | Millions of tokens per 5 hours that give an endurance score of 0 and 100 |
|
|
142
163
|
| `feeds` | `{ "enabled": false }` | Optional benchmark feed: `{ "enabled": true, "url": "https://...", "timeoutMs": 5000 }` |
|
|
164
|
+
| `updateCheck` | `true` | Once each 24 hours, ask npm for the newest router version. `false` stops the check |
|
|
143
165
|
|
|
144
166
|
The [usage guide](./USAGE.md#6-change-the-router-settings) has examples for each key.
|
|
145
167
|
|
|
@@ -172,6 +194,7 @@ score = w_intelligence * intelligence
|
|
|
172
194
|
| `~/.config/opencode/router/routing-table.json` | The last routing table |
|
|
173
195
|
| `~/.config/opencode/router/last-apply.json` | The result of the last live apply |
|
|
174
196
|
| `~/.config/opencode/router/eval-report.md` | The report of the last `/routing-eval` |
|
|
197
|
+
| `~/.config/opencode/router/update-check.json` | The time of the last update check and the newest version on npm |
|
|
175
198
|
| `opencode.json` or `opencode.jsonc` (global, or the project file with `--scope project`) | `model`, `small_model`, and `agent.<role>.model` and `variant`. Comments and all other keys stay. A file with a JSON error is not changed |
|
|
176
199
|
|
|
177
200
|
If `XDG_CONFIG_HOME` is set, the router uses `$XDG_CONFIG_HOME/opencode` in place of
|
|
@@ -183,6 +206,10 @@ Models with `-free` in their name can collect your prompts for training. The sco
|
|
|
183
206
|
marks them `free*`. To keep your main work off these models, set
|
|
184
207
|
`"privacy": { "freeModelsOk": false }`. To never use them, set `"denyModels": ["*-free"]`.
|
|
185
208
|
|
|
209
|
+
Once each 24 hours, the router sends one request to `registry.npmjs.org` to get the
|
|
210
|
+
newest version number of the router. It sends no other data. To stop this, set
|
|
211
|
+
`"updateCheck": false`.
|
|
212
|
+
|
|
186
213
|
## Development
|
|
187
214
|
|
|
188
215
|
```bash
|
|
@@ -198,6 +225,12 @@ To use your local build in OpenCode, put the absolute path of the folder in the
|
|
|
198
225
|
list. Example for Windows: `"plugin": ["file:///C:/code/Opencode_router"]`. Run
|
|
199
226
|
`npm run build` after each change, then restart OpenCode.
|
|
200
227
|
|
|
228
|
+
## Contributing
|
|
229
|
+
|
|
230
|
+
Bug reports, model scores, and code changes are welcome. Read
|
|
231
|
+
[CONTRIBUTING.md](./CONTRIBUTING.md). For questions and ideas, use
|
|
232
|
+
[Discussions](https://github.com/s-berdia/Opencode_router/discussions).
|
|
233
|
+
|
|
201
234
|
## License
|
|
202
235
|
|
|
203
236
|
[MIT](./LICENSE)
|
package/USAGE.md
CHANGED
|
@@ -16,7 +16,8 @@ Do the steps in sequence. Each command has a label that tells you which shell to
|
|
|
16
16
|
9. [Files and folders](#9-files-and-folders)
|
|
17
17
|
10. [Troubleshooting](#10-troubleshooting)
|
|
18
18
|
11. [Known limits](#11-known-limits)
|
|
19
|
-
12. [
|
|
19
|
+
12. [Update the router](#12-update-the-router)
|
|
20
|
+
13. [Remove the router](#13-remove-the-router)
|
|
20
21
|
|
|
21
22
|
## 1. Before you start
|
|
22
23
|
|
|
@@ -41,7 +42,22 @@ Make sure that you have these items:
|
|
|
41
42
|
|
|
42
43
|
## 2. Install the plugin
|
|
43
44
|
|
|
44
|
-
### Option A:
|
|
45
|
+
### Option A: with the OpenCode command-line tool (fastest)
|
|
46
|
+
|
|
47
|
+
1. Run this command. It works in PowerShell, CMD, Git Bash, macOS, and Linux:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
opencode plugin @s-berdia/opencode-router --global
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The command downloads the plugin and adds `"@s-berdia/opencode-router"` to the
|
|
54
|
+
`plugin` list of your global config file.
|
|
55
|
+
2. Close OpenCode. Start OpenCode again.
|
|
56
|
+
|
|
57
|
+
If the `opencode` command is not found, use Option B. Or install the command-line tool
|
|
58
|
+
with `npm install -g opencode-ai`, and then do step 1 again.
|
|
59
|
+
|
|
60
|
+
### Option B: from npm, by hand
|
|
45
61
|
|
|
46
62
|
1. Open the global OpenCode config file.
|
|
47
63
|
|
|
@@ -81,7 +97,7 @@ Make sure that you have these items:
|
|
|
81
97
|
To use the router in one project only, put the `plugin` entry in `opencode.json` in the
|
|
82
98
|
root folder of that project, not in the global file.
|
|
83
99
|
|
|
84
|
-
### Option
|
|
100
|
+
### Option C: from source
|
|
85
101
|
|
|
86
102
|
Use this option to test changes to the code.
|
|
87
103
|
|
|
@@ -119,8 +135,8 @@ Use this option to test changes to the code.
|
|
|
119
135
|
|
|
120
136
|
1. In the OpenCode prompt, type `/` and look at the list of commands. Make sure that
|
|
121
137
|
these commands are in the list: `/refresh-routing`, `/routing-status`,
|
|
122
|
-
`/routing-plan`, and `/routing-
|
|
123
|
-
[Troubleshooting](#10-troubleshooting).
|
|
138
|
+
`/routing-plan`, `/routing-eval`, and `/routing-update`. If they are not in the list,
|
|
139
|
+
go to [Troubleshooting](#10-troubleshooting).
|
|
124
140
|
2. Type this command and push Enter:
|
|
125
141
|
|
|
126
142
|
```text
|
|
@@ -311,6 +327,19 @@ models of that family.
|
|
|
311
327
|
}
|
|
312
328
|
```
|
|
313
329
|
|
|
330
|
+
### Stop the update check
|
|
331
|
+
|
|
332
|
+
Once each 24 hours, the router asks npm for the newest version number of the router. It
|
|
333
|
+
sends one request to `registry.npmjs.org` and no other data. When a newer version is
|
|
334
|
+
available, the router shows a toast in OpenCode and a line under the status and the
|
|
335
|
+
scorecard. To stop the check:
|
|
336
|
+
|
|
337
|
+
```json
|
|
338
|
+
{
|
|
339
|
+
"updateCheck": false
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
314
343
|
### Advanced cost settings
|
|
315
344
|
|
|
316
345
|
| Key | Default | Meaning |
|
|
@@ -356,6 +385,7 @@ These commands work in PowerShell, CMD, Git Bash, macOS, and Linux.
|
|
|
356
385
|
| `opencode-router plan go-plus --scope global` | Save the plan in the global settings, then calculate and write the routing table |
|
|
357
386
|
| `opencode-router cat` | Show the catalog path, its date, and the number of models |
|
|
358
387
|
| `opencode-router install --scope global` | Write the slash commands as files in `~/.config/opencode/command/`. If `router.config.json` does not exist, it also writes an empty one |
|
|
388
|
+
| `opencode-router update` | Ask npm for the newest version. If the copy that OpenCode keeps is older, remove it. See [Update the router](#12-update-the-router) |
|
|
359
389
|
|
|
360
390
|
The terminal tool reads the global `router.config.json` and the project file of the
|
|
361
391
|
current folder, in the same way as the plugin.
|
|
@@ -436,6 +466,7 @@ The router writes these files:
|
|
|
436
466
|
| `~/.config/opencode/router/routing-table.json` | Plugin | The last routing table |
|
|
437
467
|
| `~/.config/opencode/router/last-apply.json` | Plugin | The result of the last live apply |
|
|
438
468
|
| `~/.config/opencode/router/eval-report.md` | `/routing-eval` | The test report |
|
|
469
|
+
| `~/.config/opencode/router/update-check.json` | Plugin, `status`, `refresh` | The time of the last update check and the newest version on npm |
|
|
439
470
|
| `~/.config/opencode/opencode.json` | `/refresh-routing`, `/routing-plan` | `model`, `small_model`, and `agent.<role>.model` and `variant` |
|
|
440
471
|
| `~/.config/opencode/router.config.json` | `/routing-plan`, `install` (only if missing) | Your router settings |
|
|
441
472
|
|
|
@@ -466,7 +497,7 @@ Do not commit the `router/` folder. It contains your usage data.
|
|
|
466
497
|
|
|
467
498
|
## 11. Known limits
|
|
468
499
|
|
|
469
|
-
These are the limits of version 1.
|
|
500
|
+
These are the limits of version 1.1.1.
|
|
470
501
|
|
|
471
502
|
1. The router sets the model of the `plan`, `build`, `general`, and `explore` agents at
|
|
472
503
|
each message. If you select a different model by hand, the router replaces it. To
|
|
@@ -483,7 +514,59 @@ These are the limits of version 1.0.1.
|
|
|
483
514
|
rate-limit error for a full session puts only the model of that session into a
|
|
484
515
|
cooldown.
|
|
485
516
|
|
|
486
|
-
## 12.
|
|
517
|
+
## 12. Update the router
|
|
518
|
+
|
|
519
|
+
OpenCode downloads the plugin one time and then uses its own copy. It does not download
|
|
520
|
+
a newer version automatically. The router looks for a newer version once each 24 hours
|
|
521
|
+
and tells you when one is available.
|
|
522
|
+
|
|
523
|
+
### With the slash command
|
|
524
|
+
|
|
525
|
+
1. In the OpenCode prompt, type this command and push Enter:
|
|
526
|
+
|
|
527
|
+
```text
|
|
528
|
+
/routing-update
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
2. Read the result:
|
|
532
|
+
- `OpenCode has opencode-router <version>, the newest version.`: you do not have to
|
|
533
|
+
do more.
|
|
534
|
+
- `Removed the cached copy: ...`: go to step 3.
|
|
535
|
+
- `opencode.json pins an older version ...`: do the steps in
|
|
536
|
+
[With a fixed version](#with-a-fixed-version).
|
|
537
|
+
3. Close OpenCode. Start OpenCode again. OpenCode downloads the newest version when it
|
|
538
|
+
starts.
|
|
539
|
+
4. Type `/routing-status`. If the line `opencode-router <version> is available` does not
|
|
540
|
+
show, the update is complete.
|
|
541
|
+
|
|
542
|
+
Between steps 2 and 3, the router slash commands do not work, because the files that they
|
|
543
|
+
use are removed. Restart OpenCode immediately.
|
|
544
|
+
|
|
545
|
+
### From a terminal
|
|
546
|
+
|
|
547
|
+
If the slash commands do not work, close OpenCode and run this command. Then start
|
|
548
|
+
OpenCode again.
|
|
549
|
+
|
|
550
|
+
```bash
|
|
551
|
+
npx @s-berdia/opencode-router@latest update
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
This command works in PowerShell, CMD, Git Bash, macOS, and Linux.
|
|
555
|
+
|
|
556
|
+
### With a fixed version
|
|
557
|
+
|
|
558
|
+
To select the version yourself, write it in the `plugin` list:
|
|
559
|
+
|
|
560
|
+
```json
|
|
561
|
+
{
|
|
562
|
+
"plugin": ["@s-berdia/opencode-router@1.1.1"]
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
To update, change the number to the new version and restart OpenCode. The newest version
|
|
567
|
+
is on <https://www.npmjs.com/package/@s-berdia/opencode-router>.
|
|
568
|
+
|
|
569
|
+
## 13. Remove the router
|
|
487
570
|
|
|
488
571
|
1. Open `opencode.json` (see [step 2](#2-install-the-plugin)).
|
|
489
572
|
2. Remove `"@s-berdia/opencode-router"` from the `plugin` list.
|
|
@@ -509,5 +592,6 @@ These are the limits of version 1.0.1.
|
|
|
509
592
|
```
|
|
510
593
|
|
|
511
594
|
6. If you ran `install`, delete these files from `~/.config/opencode/command/`:
|
|
512
|
-
`refresh-routing.md`, `routing-status.md`, `routing-plan.md`,
|
|
595
|
+
`refresh-routing.md`, `routing-status.md`, `routing-plan.md`, `routing-eval.md`, and
|
|
596
|
+
`routing-update.md`.
|
|
513
597
|
7. Restart OpenCode.
|
package/dist/bin.js
CHANGED
|
@@ -822,12 +822,12 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
|
|
|
822
822
|
}
|
|
823
823
|
return result;
|
|
824
824
|
}
|
|
825
|
-
function findNodeAtLocation(root,
|
|
825
|
+
function findNodeAtLocation(root, path6) {
|
|
826
826
|
if (!root) {
|
|
827
827
|
return void 0;
|
|
828
828
|
}
|
|
829
829
|
let node = root;
|
|
830
|
-
for (let segment of
|
|
830
|
+
for (let segment of path6) {
|
|
831
831
|
if (typeof segment === "string") {
|
|
832
832
|
if (node.type !== "object" || !Array.isArray(node.children)) {
|
|
833
833
|
return void 0;
|
|
@@ -1181,14 +1181,14 @@ function getNodeType(value) {
|
|
|
1181
1181
|
|
|
1182
1182
|
// node_modules/jsonc-parser/lib/esm/impl/edit.js
|
|
1183
1183
|
function setProperty(text, originalPath, value, options) {
|
|
1184
|
-
const
|
|
1184
|
+
const path6 = originalPath.slice();
|
|
1185
1185
|
const errors = [];
|
|
1186
1186
|
const root = parseTree(text, errors);
|
|
1187
1187
|
let parent = void 0;
|
|
1188
1188
|
let lastSegment = void 0;
|
|
1189
|
-
while (
|
|
1190
|
-
lastSegment =
|
|
1191
|
-
parent = findNodeAtLocation(root,
|
|
1189
|
+
while (path6.length > 0) {
|
|
1190
|
+
lastSegment = path6.pop();
|
|
1191
|
+
parent = findNodeAtLocation(root, path6);
|
|
1192
1192
|
if (parent === void 0 && value !== void 0) {
|
|
1193
1193
|
if (typeof lastSegment === "string") {
|
|
1194
1194
|
value = { [lastSegment]: value };
|
|
@@ -1413,8 +1413,8 @@ function printParseErrorCode(code) {
|
|
|
1413
1413
|
}
|
|
1414
1414
|
return "<unknown ParseErrorCode>";
|
|
1415
1415
|
}
|
|
1416
|
-
function modify(text,
|
|
1417
|
-
return setProperty(text,
|
|
1416
|
+
function modify(text, path6, value, options) {
|
|
1417
|
+
return setProperty(text, path6, value, options);
|
|
1418
1418
|
}
|
|
1419
1419
|
function applyEdits(text, edits) {
|
|
1420
1420
|
let sortedEdits = edits.slice(0).sort((a, b) => {
|
|
@@ -1443,10 +1443,13 @@ function globalConfigDir() {
|
|
|
1443
1443
|
const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), ".config");
|
|
1444
1444
|
return path.join(base, "opencode");
|
|
1445
1445
|
}
|
|
1446
|
-
function
|
|
1446
|
+
function opencodeCacheDir() {
|
|
1447
1447
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
1448
1448
|
const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), ".cache");
|
|
1449
|
-
return path.join(base, "opencode"
|
|
1449
|
+
return path.join(base, "opencode");
|
|
1450
|
+
}
|
|
1451
|
+
function catalogCacheFile() {
|
|
1452
|
+
return path.join(opencodeCacheDir(), "models.json");
|
|
1450
1453
|
}
|
|
1451
1454
|
function globalRouterConfigFile() {
|
|
1452
1455
|
return path.join(globalConfigDir(), "router.config.json");
|
|
@@ -1814,6 +1817,7 @@ function sanitizeUserConfig(raw, file, notes) {
|
|
|
1814
1817
|
if (threshold !== void 0 && !(typeof threshold === "number" && threshold > 0 && threshold <= 1)) {
|
|
1815
1818
|
drop("softThreshold", "must be a number above 0 and at most 1");
|
|
1816
1819
|
}
|
|
1820
|
+
if (config.updateCheck !== void 0 && typeof config.updateCheck !== "boolean") drop("updateCheck", "must be true or false");
|
|
1817
1821
|
for (const key of ["denyModels", "allowModels"]) {
|
|
1818
1822
|
const list = config[key];
|
|
1819
1823
|
if (list !== void 0 && !(Array.isArray(list) && list.every((item) => typeof item === "string"))) {
|
|
@@ -2542,7 +2546,8 @@ var SLASH_COMMANDS = [
|
|
|
2542
2546
|
{ name: "refresh-routing", description: "Recompute and apply OpenCode model routing", args: "refresh" },
|
|
2543
2547
|
{ name: "routing-status", description: "Show the model in use per role and its 5-hour quota use", args: "status" },
|
|
2544
2548
|
{ name: "routing-plan", description: "Switch the routing plan (go | go-plus | free) and rebuild", args: "plan $1" },
|
|
2545
|
-
{ name: "routing-eval", description: "Run the router scoring stress test", args: "eval" }
|
|
2549
|
+
{ name: "routing-eval", description: "Run the router scoring stress test", args: "eval" },
|
|
2550
|
+
{ name: "routing-update", description: "Update opencode-router to the newest version", args: "update" }
|
|
2546
2551
|
];
|
|
2547
2552
|
var OUTPUT_BEGIN = "---BEGIN ROUTER OUTPUT---";
|
|
2548
2553
|
function cliScriptPath() {
|
|
@@ -2594,6 +2599,9 @@ function ledgerFile() {
|
|
|
2594
2599
|
function evalReportFile() {
|
|
2595
2600
|
return path4.join(stateDir(), "eval-report.md");
|
|
2596
2601
|
}
|
|
2602
|
+
function updateCheckFile() {
|
|
2603
|
+
return path4.join(stateDir(), "update-check.json");
|
|
2604
|
+
}
|
|
2597
2605
|
function applyMarkerFile() {
|
|
2598
2606
|
return path4.join(stateDir(), "last-apply.json");
|
|
2599
2607
|
}
|
|
@@ -2740,6 +2748,97 @@ async function runEval(opts = {}) {
|
|
|
2740
2748
|
return 0;
|
|
2741
2749
|
}
|
|
2742
2750
|
|
|
2751
|
+
// src/update.ts
|
|
2752
|
+
import fs6 from "fs";
|
|
2753
|
+
import path5 from "path";
|
|
2754
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2755
|
+
var PACKAGE_NAME = "@s-berdia/opencode-router";
|
|
2756
|
+
var LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME.replace("/", "%2f")}/latest`;
|
|
2757
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
2758
|
+
var TIMEOUT_MS = 3e3;
|
|
2759
|
+
function parseVersion(version) {
|
|
2760
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
2761
|
+
return match ? match.slice(1).map(Number) : null;
|
|
2762
|
+
}
|
|
2763
|
+
function isNewerVersion(a, b) {
|
|
2764
|
+
const left = parseVersion(a);
|
|
2765
|
+
const right = parseVersion(b);
|
|
2766
|
+
if (!left || !right) return false;
|
|
2767
|
+
for (let i = 0; i < 3; i++) {
|
|
2768
|
+
if (left[i] !== right[i]) return left[i] > right[i];
|
|
2769
|
+
}
|
|
2770
|
+
return false;
|
|
2771
|
+
}
|
|
2772
|
+
function installedVersion() {
|
|
2773
|
+
const file = fileURLToPath2(new URL("../package.json", import.meta.url));
|
|
2774
|
+
return readJson(file)?.version ?? "0.0.0";
|
|
2775
|
+
}
|
|
2776
|
+
async function fetchLatestVersion() {
|
|
2777
|
+
const controller = new AbortController();
|
|
2778
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
2779
|
+
try {
|
|
2780
|
+
const res = await fetch(LATEST_URL, { signal: controller.signal, headers: { accept: "application/json" } });
|
|
2781
|
+
if (!res.ok) return void 0;
|
|
2782
|
+
const body = await res.json();
|
|
2783
|
+
return typeof body.version === "string" ? body.version : void 0;
|
|
2784
|
+
} catch {
|
|
2785
|
+
return void 0;
|
|
2786
|
+
} finally {
|
|
2787
|
+
clearTimeout(timeout);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
async function checkForUpdate(opts = {}) {
|
|
2791
|
+
try {
|
|
2792
|
+
if (loadUserConfig(opts.projectDir).config.updateCheck === false) return null;
|
|
2793
|
+
const current = opts.current ?? installedVersion();
|
|
2794
|
+
const now = opts.now ?? Date.now();
|
|
2795
|
+
const state = readJson(updateCheckFile());
|
|
2796
|
+
let latest = state?.latest;
|
|
2797
|
+
const fresh = state !== void 0 && now >= state.checkedAt && now - state.checkedAt < CHECK_INTERVAL_MS;
|
|
2798
|
+
if (!fresh) {
|
|
2799
|
+
latest = await fetchLatestVersion() ?? latest;
|
|
2800
|
+
writeJson(updateCheckFile(), { checkedAt: now, latest });
|
|
2801
|
+
}
|
|
2802
|
+
return latest && isNewerVersion(latest, current) ? { current, latest } : null;
|
|
2803
|
+
} catch {
|
|
2804
|
+
return null;
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
function updateNotice(update) {
|
|
2808
|
+
return `opencode-router ${update.latest} is available (installed: ${update.current}). To update, type /routing-update in OpenCode.`;
|
|
2809
|
+
}
|
|
2810
|
+
function clearOutdatedCopies(latest) {
|
|
2811
|
+
const [scope, name] = PACKAGE_NAME.split("/");
|
|
2812
|
+
const scopeDir = path5.join(opencodeCacheDir(), "packages", scope);
|
|
2813
|
+
const result = { removed: [], pinned: [], current: [], failed: [] };
|
|
2814
|
+
let entries = [];
|
|
2815
|
+
try {
|
|
2816
|
+
entries = fs6.readdirSync(scopeDir);
|
|
2817
|
+
} catch {
|
|
2818
|
+
return result;
|
|
2819
|
+
}
|
|
2820
|
+
for (const entry of entries) {
|
|
2821
|
+
const dir = path5.join(scopeDir, entry);
|
|
2822
|
+
if (entry === name || entry === `${name}@latest`) {
|
|
2823
|
+
const manifest = path5.join(dir, "node_modules", scope, name, "package.json");
|
|
2824
|
+
const version = readJson(manifest)?.version ?? "0.0.0";
|
|
2825
|
+
if (!isNewerVersion(latest, version)) {
|
|
2826
|
+
result.current.push(dir);
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2829
|
+
try {
|
|
2830
|
+
fs6.rmSync(dir, { recursive: true, force: true, maxRetries: 3 });
|
|
2831
|
+
result.removed.push(dir);
|
|
2832
|
+
} catch (error) {
|
|
2833
|
+
result.failed.push({ dir, error: error instanceof Error ? error.message : String(error) });
|
|
2834
|
+
}
|
|
2835
|
+
} else if (entry.startsWith(`${name}@`) && isNewerVersion(latest, entry.slice(name.length + 1))) {
|
|
2836
|
+
result.pinned.push(entry);
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
return result;
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2743
2842
|
// src/cli.ts
|
|
2744
2843
|
var HELP = `opencode-router - dynamic model routing for OpenCode Go / Go Plus / Free
|
|
2745
2844
|
|
|
@@ -2750,6 +2849,7 @@ Usage:
|
|
|
2750
2849
|
opencode-router install [--scope] Write slash-command files and a starter config
|
|
2751
2850
|
opencode-router cat Show catalog metadata (path, date, model count)
|
|
2752
2851
|
opencode-router eval Run the scoring stress test
|
|
2852
|
+
opencode-router update Look for a newer version and clear OpenCode's cached copy
|
|
2753
2853
|
|
|
2754
2854
|
Options:
|
|
2755
2855
|
--plan <go|go-plus|free> Plan profile for this run
|
|
@@ -2814,6 +2914,12 @@ function parseCommandLine(argv, cwd) {
|
|
|
2814
2914
|
function build(opts) {
|
|
2815
2915
|
return buildRoutingTable({ plan: opts.plan, catalogFile: opts.catalog, projectDir: opts.cwd });
|
|
2816
2916
|
}
|
|
2917
|
+
async function writeUpdateNotice(opts) {
|
|
2918
|
+
const update = await checkForUpdate({ projectDir: opts.cwd });
|
|
2919
|
+
if (update) process.stdout.write(`
|
|
2920
|
+
${updateNotice(update)}
|
|
2921
|
+
`);
|
|
2922
|
+
}
|
|
2817
2923
|
async function cmdRefresh(opts) {
|
|
2818
2924
|
const { table } = await build(opts);
|
|
2819
2925
|
if (opts.json) {
|
|
@@ -2845,6 +2951,7 @@ Applied: ${detail}
|
|
|
2845
2951
|
"A running OpenCode session recomputes routing on its next message (within 15 s). Restart to reload the UI display now.\n"
|
|
2846
2952
|
);
|
|
2847
2953
|
}
|
|
2954
|
+
await writeUpdateNotice(opts);
|
|
2848
2955
|
}
|
|
2849
2956
|
return 0;
|
|
2850
2957
|
}
|
|
@@ -2858,6 +2965,7 @@ async function cmdStatus(opts) {
|
|
|
2858
2965
|
process.stdout.write(JSON.stringify({ table: result.table, rows, usage }, null, 2) + "\n");
|
|
2859
2966
|
} else {
|
|
2860
2967
|
process.stdout.write(formatStatus(result.table, rows) + "\n");
|
|
2968
|
+
await writeUpdateNotice(opts);
|
|
2861
2969
|
}
|
|
2862
2970
|
return 0;
|
|
2863
2971
|
}
|
|
@@ -2901,6 +3009,44 @@ function cmdInstall(opts) {
|
|
|
2901
3009
|
);
|
|
2902
3010
|
return 0;
|
|
2903
3011
|
}
|
|
3012
|
+
async function cmdUpdate() {
|
|
3013
|
+
const latest = await fetchLatestVersion();
|
|
3014
|
+
if (!latest) {
|
|
3015
|
+
process.stderr.write("error: could not reach the npm registry. Try again later.\n");
|
|
3016
|
+
return 1;
|
|
3017
|
+
}
|
|
3018
|
+
const { removed, pinned, current, failed } = clearOutdatedCopies(latest);
|
|
3019
|
+
if (failed.length > 0) {
|
|
3020
|
+
for (const { dir, error } of failed) process.stderr.write(`error: could not remove ${dir} (${error})
|
|
3021
|
+
`);
|
|
3022
|
+
process.stderr.write(`Close OpenCode, then run this command in a terminal: npx ${PACKAGE_NAME}@latest update
|
|
3023
|
+
`);
|
|
3024
|
+
return 1;
|
|
3025
|
+
}
|
|
3026
|
+
for (const dir of removed) process.stdout.write(`Removed the cached copy: ${dir}
|
|
3027
|
+
`);
|
|
3028
|
+
if (removed.length > 0) {
|
|
3029
|
+
process.stdout.write(`Restart OpenCode. It then installs opencode-router ${latest}.
|
|
3030
|
+
`);
|
|
3031
|
+
}
|
|
3032
|
+
if (pinned.length > 0) {
|
|
3033
|
+
process.stdout.write(
|
|
3034
|
+
`opencode.json pins an older version (${pinned.join(", ")}). Change the plugin entry to "${PACKAGE_NAME}@${latest}", then restart OpenCode.
|
|
3035
|
+
`
|
|
3036
|
+
);
|
|
3037
|
+
}
|
|
3038
|
+
if (removed.length > 0 || pinned.length > 0) return 0;
|
|
3039
|
+
if (current.length > 0) {
|
|
3040
|
+
process.stdout.write(`OpenCode has opencode-router ${latest}, the newest version.
|
|
3041
|
+
`);
|
|
3042
|
+
} else {
|
|
3043
|
+
process.stdout.write(
|
|
3044
|
+
`OpenCode has no cached copy of opencode-router. The newest version is ${latest}. If you use a local build, run git pull and npm run build. If your OpenCode has the command "opencode plugin update", run it.
|
|
3045
|
+
`
|
|
3046
|
+
);
|
|
3047
|
+
}
|
|
3048
|
+
return 0;
|
|
3049
|
+
}
|
|
2904
3050
|
function usageError(error) {
|
|
2905
3051
|
process.stderr.write(`error: ${error.message}
|
|
2906
3052
|
|
|
@@ -2928,6 +3074,8 @@ async function main(argv, cwd = process.cwd()) {
|
|
|
2928
3074
|
return cmdInstall(opts);
|
|
2929
3075
|
case "eval":
|
|
2930
3076
|
return await runEval({ plan: opts.plan, catalog: opts.catalog, json: opts.json, projectDir: opts.cwd });
|
|
3077
|
+
case "update":
|
|
3078
|
+
return await cmdUpdate();
|
|
2931
3079
|
default:
|
|
2932
3080
|
throw new UsageError(`unknown command "${command}"`);
|
|
2933
3081
|
}
|