@qadanm/opencode-smart-router 0.0.0-stage → 0.4.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/CHANGELOG.md +118 -0
- package/CONTRIBUTING.md +96 -0
- package/LICENSE +21 -0
- package/README.md +341 -2
- package/SECURITY.md +174 -0
- package/config.example.json +108 -0
- package/dist/allowance.js +487 -0
- package/dist/gui-experimental.cjs +1 -0
- package/dist/index.js +11111 -0
- package/dist/npm/index.js +40 -0
- package/dist/tui.js +587 -0
- package/docs/ARCHITECTURE.md +104 -0
- package/docs/DEBUGGING-GUI.md +70 -0
- package/docs/INSTALL.md +120 -0
- package/docs/LIMITATIONS.md +213 -0
- package/docs/MODELS.md +124 -0
- package/docs/OPENDESIGN-INTEGRATION.md +635 -0
- package/docs/QA.md +142 -0
- package/docs/SHARED-CORE.md +72 -0
- package/install +75 -0
- package/package.json +92 -4
- package/scripts/install.sh +324 -0
- package/scripts/uninstall.sh +91 -0
- package/src/allowance.ts +135 -0
- package/src/core/classify/classifier.ts +305 -0
- package/src/core/config-writer.ts +167 -0
- package/src/core/config.ts +1030 -0
- package/src/core/context/strategy.ts +188 -0
- package/src/core/context-health.ts +39 -0
- package/src/core/failures.ts +398 -0
- package/src/core/history/opencode.ts +173 -0
- package/src/core/models/protocol.ts +22 -0
- package/src/core/models/registry.ts +203 -0
- package/src/core/models/seed.ts +771 -0
- package/src/core/offline/mode.ts +321 -0
- package/src/core/plan-page.ts +390 -0
- package/src/core/plans/catalog.ts +481 -0
- package/src/core/policy/calibration.ts +259 -0
- package/src/core/policy/eligibility.ts +578 -0
- package/src/core/providers/enablement.ts +143 -0
- package/src/core/providers/opendesign-credits.ts +206 -0
- package/src/core/providers/opendesign-models.ts +628 -0
- package/src/core/providers/opendesign.ts +474 -0
- package/src/core/quota/calibration.ts +166 -0
- package/src/core/quota/commandcode.ts +471 -0
- package/src/core/quota/credentials.ts +490 -0
- package/src/core/quota/ledger.ts +1273 -0
- package/src/core/quota/opencode-go.ts +148 -0
- package/src/core/quota/opendesign.ts +1043 -0
- package/src/core/quota/reconcile.ts +155 -0
- package/src/core/quota/report.ts +261 -0
- package/src/core/quota/service.ts +273 -0
- package/src/core/reprompt.ts +48 -0
- package/src/core/routing/engine.ts +919 -0
- package/src/core/types.ts +825 -0
- package/src/core/ui.ts +236 -0
- package/src/core/util.ts +114 -0
- package/src/gui/index.tsx +1670 -0
- package/src/gui/viewmodel.ts +456 -0
- package/src/index.ts +48 -0
- package/src/rpc.ts +204 -0
- package/src/server/controller.ts +3281 -0
- package/src/server/route-tracker.ts +60 -0
- package/src/server/shared-core.ts +671 -0
- package/src/tui/index.tsx +168 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Debugging the desktop panel
|
|
2
|
+
|
|
3
|
+
Written after a session where the panel vanished and four plausible causes were
|
|
4
|
+
investigated before the real one. Read this before touching GUI code.
|
|
5
|
+
|
|
6
|
+
## Check the server version first
|
|
7
|
+
|
|
8
|
+
The desktop panel needs **OpenCode Desktop 2.2.x or newer**. A 2.1.x server has no
|
|
9
|
+
GUI plugin API: it will not report `gui` in `/api/plugin`, and it cannot load
|
|
10
|
+
`gui-experimental.cjs` at all.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
opencode api get /api/info # must say 2.2.x
|
|
14
|
+
opencode api get /api/plugin # opcode-smart-router must have features.gui: true
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**The common trap:** `opencode` on `PATH` may be an older Homebrew install than
|
|
18
|
+
the app. Running `opencode service restart` then restarts the *old* server, and it
|
|
19
|
+
takes the port the app was using. The app reconnects to it, and the panel stops
|
|
20
|
+
loading with no useful error.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
which opencode && opencode --version # CLI on PATH
|
|
24
|
+
ls ~/Library/Application\ Support/ai.opencode.desktop/cli/ # app's bundled CLI
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
They must both be 2.2.x. If they are not, either `brew upgrade opencode-v2` or
|
|
28
|
+
restart the app itself (`⌘Q`, then relaunch) instead of using the service command.
|
|
29
|
+
The app spawns its own bundled server, which is the one that can load a GUI.
|
|
30
|
+
|
|
31
|
+
## Then check the id rules
|
|
32
|
+
|
|
33
|
+
Documented in the GUI guide, and easy to misread:
|
|
34
|
+
|
|
35
|
+
> A plugin whose id "nests under one with a dot, has one nested under it, or is in
|
|
36
|
+
> the `opencode` namespace" does not load its GUI.
|
|
37
|
+
|
|
38
|
+
Concretely, this is wrong:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
server part id: "opencode-smart-router"
|
|
42
|
+
tui part id: "opencode-smart-router.tui" ◄ nests under the server id
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The TUI id must be a sibling, not a child. `test/first-run.test.ts` asserts that no
|
|
46
|
+
two plugin ids nest, because the failure costs the entire panel and produces only
|
|
47
|
+
a generic request failure in the app.
|
|
48
|
+
|
|
49
|
+
## What the symptoms actually mean
|
|
50
|
+
|
|
51
|
+
| Symptom | Usual cause |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `Request failed / UnsupportedContentType` | Client and server versions disagree, or the GUI never registered |
|
|
54
|
+
| Tab strip resets to the new-tab page | No GUI entry was loaded for that plugin |
|
|
55
|
+
| `features` has no `gui` | Server too old, or the plugin id nests |
|
|
56
|
+
| Panel renders but RPC fails | Server part failed to load — a plugin with no server part has no GUI |
|
|
57
|
+
| Panel blank, no error | Something in `setup()` threw. A GUI whose setup throws loses the plugin, not just the panel |
|
|
58
|
+
|
|
59
|
+
## Rules that keep this debuggable
|
|
60
|
+
|
|
61
|
+
1. **`features.gui: true` is the single fact to check first.** It answers "did the
|
|
62
|
+
app even find my GUI" before any speculation about bundles, exports or code.
|
|
63
|
+
2. **Never gate a required surface on an optional check.** The panel once rendered
|
|
64
|
+
as an empty box because it waited for `onboarding()` to settle, and that call
|
|
65
|
+
did not. Show the normal state; let optional data arrive later.
|
|
66
|
+
3. **A convenience must never be able to break the product.** The auto-open path
|
|
67
|
+
touches four host APIs inside `setup()`; it is wrapped so a throw cannot remove
|
|
68
|
+
the plugin.
|
|
69
|
+
4. **Test the version you think you are testing.** `/api/info` reports the server
|
|
70
|
+
actually running, which is not necessarily the one you started.
|
package/docs/INSTALL.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Install, verify, and remove
|
|
2
|
+
|
|
3
|
+
# Install, verify, and remove
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
One command, from a checkout. Requires Node 20.12+, npm and git. No `jq`.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
./install # interactive
|
|
11
|
+
./install --yes # non-interactive, for CI or an LLM
|
|
12
|
+
./install --dry-run # print every step, change nothing
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
It validates the source (typecheck and the full suite), backs up your config,
|
|
16
|
+
builds the bundle, installs it, adds exactly one entry to `plugins`, and confirms
|
|
17
|
+
the bundle loads. Any failure exits non-zero and leaves the machine untouched,
|
|
18
|
+
because every check runs before anything is written.
|
|
19
|
+
|
|
20
|
+
The install is **versioned and additive**. Each run writes to its own directory:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
~/.config/opencode/plugins/opencode-smart-router/<timestamp>/index.js
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
and only the config pointer changes. A running OpenCode server keeps the module it
|
|
27
|
+
already loaded; the new version takes effect on the next start. This matters more
|
|
28
|
+
than it sounds: replacing files inside the watched plugin directory invalidates the
|
|
29
|
+
plugin module mid-session and makes the provider's model list go empty, which shows
|
|
30
|
+
up as `Model unavailable: <provider>/<model>` for a model that is perfectly fine.
|
|
31
|
+
|
|
32
|
+
## Verify
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
./install --dry-run # reports the current state without changing it
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then restart OpenCode and run `/router-status` in a session. It prints the active
|
|
39
|
+
plan, the allowance windows, and the model the router would pick.
|
|
40
|
+
|
|
41
|
+
## Disable without uninstalling
|
|
42
|
+
|
|
43
|
+
Set `"enabled": false` in `~/.config/opencode/smart-router.json` and restart. The
|
|
44
|
+
plugin stays installed and inert; your config, credentials and providers are
|
|
45
|
+
untouched either way.
|
|
46
|
+
|
|
47
|
+
## Uninstall
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
./scripts/uninstall.sh
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Removes the plugin directory and every `plugins` entry under it, then offers the
|
|
54
|
+
exact pre-removal config as a restore point. Restart OpenCode afterwards.
|
|
55
|
+
|
|
56
|
+
## Update
|
|
57
|
+
|
|
58
|
+
Run `./install` again. It installs a new versioned directory and repoints the
|
|
59
|
+
config, so old versions can be deleted safely once the new one is confirmed:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
ls ~/.config/opencode/plugins/opencode-smart-router/
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Rollback
|
|
66
|
+
|
|
67
|
+
Every install prints the backup it made. Two options:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
./scripts/uninstall.sh # clean removal
|
|
71
|
+
./scripts/uninstall.sh --restore-backup <file> # restore an exact prior config
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Verify quota and routing
|
|
75
|
+
|
|
76
|
+
The router exposes an RPC status method:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
opencode api POST /api/rpc/smart-router/status -d '{"input":{}}'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Use `opencode api` so authentication is handled by OpenCode. The output labels each provider quota source as `provider`, `estimate`, or `unknown`.
|
|
83
|
+
|
|
84
|
+
For durable usage history, per-model totals and rolling-window forecasts:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
opencode api POST /api/rpc/smart-router/usage -d '{"input":{}}'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This reports each provider's five-hour, seven-day and monthly consumption with input, output, cache-read and cache-write tokens, the cache share, per-model totals, and a forecast per window. A plan with no five-hour window reports that row as not applicable rather than as 0% used. It makes no inference request.
|
|
91
|
+
|
|
92
|
+
For a safe dry-run of a routing decision without changing the model:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
opencode api POST /api/rpc/smart-router/preview \
|
|
96
|
+
-d '{"input":{"sessionID":"ses_…","text":"Implement a small feature in src/app.ts"}}'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
A safe smoke test:
|
|
100
|
+
|
|
101
|
+
1. Create a new session with a cheap or free model.
|
|
102
|
+
2. Send a narrow CSS change with an explicit file path.
|
|
103
|
+
3. The session should switch to an economy model before the request, and the file should change.
|
|
104
|
+
4. Send an ordinary feature request; it should use a standard model at or above the intelligence floor.
|
|
105
|
+
5. Send a redesign with a screenshot; it should require a vision model and record a premium approval suggestion.
|
|
106
|
+
6. Run `/router-approve` and repeat the redesign; a premium model may be selected once.
|
|
107
|
+
7. Select a model manually in the desktop picker; the next prompt must not be overridden, and `/router-status` should show `manual`.
|
|
108
|
+
8. Run `/router-auto` to resume.
|
|
109
|
+
|
|
110
|
+
## Configuration
|
|
111
|
+
|
|
112
|
+
No config file is required. To customize:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
cp ~/Documents/opencode-smart-router/config.example.json \
|
|
116
|
+
~/.config/opencode/smart-router.json
|
|
117
|
+
chmod 600 ~/.config/opencode/smart-router.json
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A project may add `.opencode/smart-router.json`. It can tighten privacy and spending policy but cannot loosen global restrictions.
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Limitations and precise blockers
|
|
2
|
+
|
|
3
|
+
This file records what is proven, what is estimated, and what the installed OpenCode version cannot support.
|
|
4
|
+
|
|
5
|
+
## 1. Desktop GUI
|
|
6
|
+
|
|
7
|
+
Installed OpenCode Desktop is **2.1.4**. Its `app.asar` does not contain the experimental GUI plugin loader or the `./gui-experimental` export. The GUI plugin SDK is present in `@opencode/plugin` 2.2.0 and later.
|
|
8
|
+
|
|
9
|
+
Therefore the deployed interface is:
|
|
10
|
+
|
|
11
|
+
- native slash commands registered by the server plugin (`/router-status`, `/router-explain`, `/router-report`, `/router-mode`, `/router-approve`, `/router-auto`, `/router-pin`, `/router-exclude`, `/router-pause`, `/router-resume`, `/router-dry-run`, `/router-premium-budget`, `/router-reserve`, `/router-reset-metrics`);
|
|
12
|
+
- a typed RPC surface at `rpc.smart-router.*`, suitable for a future official GUI part.
|
|
13
|
+
|
|
14
|
+
The router engine does not depend on any panel being open. A side panel was not claimed or faked. On OpenCode 2.2.0 or later, the same RPC surface can back a native settings page or side panel without changing routing core.
|
|
15
|
+
|
|
16
|
+
## 2. Command Code billing endpoints
|
|
17
|
+
|
|
18
|
+
Command Code exposes no stable public usage API in its documented Provider API. The plugin reads the same read-only alpha routes the official CLI uses:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
/alpha/whoami?limits=1
|
|
22
|
+
/alpha/billing/credits
|
|
23
|
+
/alpha/billing/subscriptions
|
|
24
|
+
/alpha/usage/summary
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
These are **undocumented and unstable**. They can change without notice. The adapter:
|
|
28
|
+
|
|
29
|
+
- uses only `GET`;
|
|
30
|
+
- never discovers or calls mutation endpoints;
|
|
31
|
+
- validates numeric and timestamp shapes;
|
|
32
|
+
- preserves `orgId` when the account belongs to an organization;
|
|
33
|
+
- limits polling to a conservative interval with exponential backoff;
|
|
34
|
+
- keeps the last good snapshot marked `stale` on failure;
|
|
35
|
+
- falls back to a clearly labeled local ledger estimate;
|
|
36
|
+
- never reports missing data as unlimited usage.
|
|
37
|
+
|
|
38
|
+
The provider API key that OpenCode uses for inference is **not authorized** for these billing routes. The adapter discovers the CLI-compatible key instead. In this installation the working source is:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
~/.config/opencode/commandcode.key
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The key is read only from owner-only files, is never logged or persisted by the plugin, and is sent only to `https://api.commandcode.ai`.
|
|
45
|
+
|
|
46
|
+
Provider-reported values are account-level: remaining included credits, purchased/free extras, five-hour and weekly used/cap/reset, plan, and billing-period spend. There is no per-model provider breakdown. Per-model usage and remaining model allowance are local estimates from observed step costs.
|
|
47
|
+
|
|
48
|
+
The billing-period usage summary and the remaining-credit balance are **separate accounting pools** and do not reconcile exactly. The adapter treats the remaining-credit balance as authoritative for monthly usage and allowance protection; the summary is kept only as a diagnostic cross-check. The exact reason for the difference is not documented by the provider.
|
|
49
|
+
|
|
50
|
+
## 3. OpenCode Go usage endpoint
|
|
51
|
+
|
|
52
|
+
The Go meter endpoint is a read-only, best-effort integration:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
GET https://opencode.ai/zen/go/v1/usage
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
It is queried with OpenCode's existing OAuth access token from the `opencode` connection. It returns percentages and reset times, not dollar balances. It is not part of the public inference schema and may change. On failure, the router uses the local ledger and labels the source `estimate` or `unknown`.
|
|
59
|
+
|
|
60
|
+
## 4. Prompt rejection is untyped
|
|
61
|
+
|
|
62
|
+
The V2 prompt admission hook can mutate a prompt but exposes no typed rejection operation. The router blocks admission by throwing `RouterBlockedError` from the prompt hook. On the installed 2.1.4 server this was verified to reject the prompt with HTTP 500 and no provider request; the session title changes to `BLOCKED` and the reason is recorded in the decision.
|
|
63
|
+
|
|
64
|
+
This is safe but not polished: the HTTP error is generic, so the title and notification are the user-facing explanation. `routing.failClosed` defaults to true. Manual selections and paused routing are outside the strict block.
|
|
65
|
+
|
|
66
|
+
A routing exception must also block. The earlier catch-and-continue path was a fail-open defect and is being removed.
|
|
67
|
+
|
|
68
|
+
## 5. Manual CLI selection at session creation
|
|
69
|
+
|
|
70
|
+
Manual model changes made while the plugin is active are detected through `session.model.selected` and respected. A model passed only at CLI session creation, before the plugin has seen a prompt, is indistinguishable from the session default. The first prompt may therefore be routed normally. Selecting a model in the desktop picker after the session exists is detected.
|
|
71
|
+
|
|
72
|
+
## 6. Subagents
|
|
73
|
+
|
|
74
|
+
Child sessions are routed independently. Premium is never propagated from a parent task into a child session; premium mode is capped at balanced for child sessions and one-shot approvals are not inherited. This prevents a premium task from multiplying premium usage through subagent calls. A child can still use a standard model appropriate to its own prompt.
|
|
75
|
+
|
|
76
|
+
## 7. Billing caps
|
|
77
|
+
|
|
78
|
+
Client-side quota estimates are not a mathematically guaranteed billing cap. Provider-side enforcement remains authoritative. The router:
|
|
79
|
+
|
|
80
|
+
- never purchases credits;
|
|
81
|
+
- never enables top-ups;
|
|
82
|
+
- never changes billing settings;
|
|
83
|
+
- blocks automatic routing when provider-reported windows are at the configured reserve;
|
|
84
|
+
- treats a reported zero remaining balance as exhausted even if the provider has not set its `exceeded` flag, so it blocks before knowingly entering purchased credits;
|
|
85
|
+
- blocks when purchased credits would be consumed unless `allowExtraUsage` is explicitly enabled;
|
|
86
|
+
- cannot guarantee that an undocumented provider endpoint or a provider-side fallback will never charge an extra balance.
|
|
87
|
+
|
|
88
|
+
If Command Code's billing routes fail and the local ledger is incomplete, the router labels the data estimated and fails conservatively rather than assuming capacity.
|
|
89
|
+
|
|
90
|
+
The purchased-credit guard is unit-tested, including the explicitly-allowed case. It is a client-side policy guarantee, not a provider-side billing lock.
|
|
91
|
+
|
|
92
|
+
## 8. Cache and context accounting
|
|
93
|
+
|
|
94
|
+
Prompt caches are provider- and model-specific. A model switch is priced as a cache loss in the router's estimate. The router keeps the current model when the estimated savings are below the hysteresis threshold, and uses a wider threshold above 300K context tokens. It never truncates, summarizes, or deletes conversation history to make a model eligible.
|
|
95
|
+
|
|
96
|
+
Command Code DeepSeek V4.1 Flash has documented time-of-day pricing: off-peak rates for 17 hours per day and 2x rates from 01:00 to 04:00 and 06:00 to 10:00 UTC, Monday through Friday. The router applies that multiplier to input, output, cache-read, and cache-loss estimates. Models without documented peak metadata use flat catalog rates; their time-of-day pricing is not assumed.
|
|
97
|
+
|
|
98
|
+
Context-health recommendations are non-destructive: they advise a new session and never compact anything on their own.
|
|
99
|
+
|
|
100
|
+
Router-driven compaction is governed by `contextManagement.mode`:
|
|
101
|
+
|
|
102
|
+
- `native` (default) and `conservative` never compact except when the transcript would genuinely exceed the model's verified accepted window. The provider's native handling is preferred; the router steps in only for that case, and announces it.
|
|
103
|
+
- `budget-optimized` preserves the legacy behavior: it compacts early to keep a session on the cheapest qualified route. It exists for small plans and small-window models.
|
|
104
|
+
|
|
105
|
+
In every mode the window used is the one the provider actually accepts, never a theoretical maximum, and the current model's window is used rather than another provider's variant of the same model. Provider-side prompt caching is never disabled by any mode.
|
|
106
|
+
|
|
107
|
+
## 8a. OpenDesign plan accounting
|
|
108
|
+
|
|
109
|
+
OpenDesign Pro exposes two independent credit pools: Design Plan credits ($360/month, $90 per rolling seven days, no five-hour window) and plan model credits ($120/month, no usage windows). They are reported side by side and **never summed**. The advertised $480 total is not a single spendable balance, and Design Plan credit multipliers apply only inside the Design Plan pool.
|
|
110
|
+
|
|
111
|
+
These figures are **estimated**, not provider-confirmed. An OpenDesign inference key cannot read quota: `GET /v1/usage` and `/v1/me` return 404 and the account-control host rejects an inference key. A control key would enable verified reads; none is configured, and the router never acquires one automatically. Because usage from the OpenDesign app is invisible to the router, the estimate is a lower bound.
|
|
112
|
+
|
|
113
|
+
`minimax-m3.1-flash`'s Design Plan multiplier is unpublished, so a neutral 1x is applied and the uncertainty is reported rather than hidden. The same applies to the GLM cache-read price.
|
|
114
|
+
|
|
115
|
+
OpenDesign quota cannot be verified without an account control key. Re-probed on
|
|
116
|
+
2026-10-30 against the live gateway: `/v1/models` returns 200 with the inference
|
|
117
|
+
key, while `/v1/usage`, `/v1/me`, `/v1/credits`, `/v1/billing`, `/v1/account`,
|
|
118
|
+
`/v1/subscription`, `/v1/limits` and `/v1/quota` all return 404. The key is valid
|
|
119
|
+
and authorized for inference; no usage surface exists for it. Quota therefore
|
|
120
|
+
reports `estimated`, derived from this router's own consumption against the
|
|
121
|
+
published plan caps with a configurable safety margin.
|
|
122
|
+
|
|
123
|
+
That estimate is a lower bound, because usage from the OpenDesign desktop app and
|
|
124
|
+
any other client on the same subscription is invisible to the router. The state
|
|
125
|
+
label is the honest part: `estimated` never becomes `verified` without a control
|
|
126
|
+
key, and `test/opendesign-quota-honesty.test.ts` pins that so it cannot drift.
|
|
127
|
+
|
|
128
|
+
## 8b. Measured speed
|
|
129
|
+
|
|
130
|
+
Generation speed is recorded as output tokens per second, which separates a slow provider from a model that reasons for longer. It is compared against the fastest eligible route rather than an absolute figure, is neutral below a configurable sample floor, and raises effective cost without ever making a route ineligible. With few samples it does not change decisions. It is machinery that needs production traffic before it can discriminate.
|
|
131
|
+
|
|
132
|
+
## 9. Precision of scores
|
|
133
|
+
|
|
134
|
+
The intelligence floor and design thresholds are planning inputs, not guarantees. Intelligence values come from published model cards where available; design scores are curated and explicitly separate. Models without credible scores are not approved for automatic substantive work. The router does not invent benchmark results.
|
|
135
|
+
|
|
136
|
+
## 10. What was not done
|
|
137
|
+
|
|
138
|
+
- No separate dashboard or daemon was installed.
|
|
139
|
+
- No third-party classification service receives prompts, code, or screenshots.
|
|
140
|
+
- No provider billing settings, credentials, or model connections were changed.
|
|
141
|
+
- No project configuration outside the router's optional project override was modified.
|
|
142
|
+
|
|
143
|
+
## 11. Why a session may stop using your default model
|
|
144
|
+
|
|
145
|
+
Two independent reasons can move a session off DeepSeek V4.1 Flash, and both are
|
|
146
|
+
correct behavior rather than faults.
|
|
147
|
+
|
|
148
|
+
**The transcript outgrew the window.** DeepSeek V4.1 Flash has a 328k window, and
|
|
149
|
+
presence in a request requires about 20k of headroom inside 95% of it, so it
|
|
150
|
+
stops being eligible at roughly 291,600 tokens of transcript. Past that point the
|
|
151
|
+
router names the model it dropped, both token counts, and the fact that a new
|
|
152
|
+
session restores it.
|
|
153
|
+
|
|
154
|
+
**Another model is genuinely cheaper right now.** A session can sit inside a
|
|
155
|
+
model's window and still lose on cost, because credit multipliers and peak
|
|
156
|
+
pricing differ per model. On OpenDesign, Minimax M3.1 Flash carries a neutral
|
|
157
|
+
multiplier while DeepSeek's is 0.5, and DeepSeek doubles during 01:00 to 04:00
|
|
158
|
+
and 06:00 to 10:00 UTC on weekdays. At a 309k transcript on a weekday, MiniMax
|
|
159
|
+
can therefore be about 1.3x cheaper, and the router picks it on merit. In that
|
|
160
|
+
case the context warning is deliberately silent, because nothing was displaced:
|
|
161
|
+
the model never would have won.
|
|
162
|
+
|
|
163
|
+
Observed live on 2026-10-30, and superseded later the same day. A 309,321-token
|
|
164
|
+
session selected `opendesign/minimax-m3.1-flash` because three conditions held at
|
|
165
|
+
once:
|
|
166
|
+
|
|
167
|
+
- OpenCode Go was at 90% of a 10% monthly reserve, so it was blocked.
|
|
168
|
+
- Command Code was at 86% of a 15% weekly reserve, so it was blocked.
|
|
169
|
+
- On OpenDesign, DeepSeek was judged against a 328,000 window and needed 329,321.
|
|
170
|
+
|
|
171
|
+
The first two were correct. The third was not: the 328,000 window was an advisory
|
|
172
|
+
gateway budget that the gateway does not enforce, and DeepSeek would now be
|
|
173
|
+
chosen for that session. The quotas remain real; the context rejection was a
|
|
174
|
+
consequence of the wrong window, which is documented in the section below.
|
|
175
|
+
|
|
176
|
+
### OpenDesign's context budget is advisory, and was measured
|
|
177
|
+
|
|
178
|
+
DeepSeek V4.1 Flash shows a 1,048,000-token window in the router. It used to show
|
|
179
|
+
328,000, because the gateway publishes `context_budget: 328000` alongside
|
|
180
|
+
`context_limit: 1048000` and the router treated the budget as the ceiling.
|
|
181
|
+
|
|
182
|
+
That was wrong. The gateway does not enforce `context_budget`. Measured on
|
|
183
|
+
2026-10-30 with six single requests on `deepseek-v4.1-flash`, `max_tokens: 1`,
|
|
184
|
+
synthetic unique filler, so nothing could be prefix-cached:
|
|
185
|
+
|
|
186
|
+
| Prompt tokens | Result |
|
|
187
|
+
|---|---|
|
|
188
|
+
| 355,588 | 200 |
|
|
189
|
+
| 753,476 | 200 |
|
|
190
|
+
| 999,593 | 200 |
|
|
191
|
+
| 1,043,337 | 200 |
|
|
192
|
+
| about 1,060,000 | 400 `context_length_exceeded` |
|
|
193
|
+
| about 2.4M | 400 `context_length_exceeded` |
|
|
194
|
+
|
|
195
|
+
The enforced ceiling is the vendor `context_limit` of 1,048,000, confirmed on both
|
|
196
|
+
sides of the boundary. Total cost of the probes was $0.006.
|
|
197
|
+
|
|
198
|
+
One control mattered: the first probe reported 355,328 of 355,588 tokens as
|
|
199
|
+
cached, which is implausible for a payload that had never been sent. Repeating it
|
|
200
|
+
with random text produced `cached_tokens: 0`, proving the gateway really did
|
|
201
|
+
tokenize and accept the full input. Without that, a 200 on mostly-cached input
|
|
202
|
+
would have been weak evidence.
|
|
203
|
+
|
|
204
|
+
The router now uses the vendor limit for a model whose ceiling has been measured,
|
|
205
|
+
and keeps the conservative budget for every other model. That is a per-model
|
|
206
|
+
allowlist rather than a blanket rule, because guessing a larger window than a
|
|
207
|
+
provider accepts fails mid-session with an opaque rejection, which is the failure
|
|
208
|
+
mode this project already hit once.
|
|
209
|
+
|
|
210
|
+
The router still keeps a 20,000-token margin and a 95% safety fraction, so the
|
|
211
|
+
usable transcript is about 975,600 tokens rather than exactly 1,048,000. That
|
|
212
|
+
margin is not part of the gateway's limit; it is the router refusing to send a
|
|
213
|
+
request that has no room left for the reply.
|
package/docs/MODELS.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Approved model registry
|
|
2
|
+
|
|
3
|
+
The router starts from a curated seed and merges live metadata from OpenCode. A model is eligible only when it is approved here, present in the connected provider catalog, and meets every task requirement.
|
|
4
|
+
|
|
5
|
+
## Scoring
|
|
6
|
+
|
|
7
|
+
- **Intelligence** is a curated coding-intelligence score. Where available it comes from Command Code's published model cards. The standard floor is **39.5**, the DeepSeek V4.1 Flash score. It is not a general "best model" score.
|
|
8
|
+
- **Design** is a separate curated score for front-end and UX quality. It is not benchmarked by the same method as intelligence.
|
|
9
|
+
- **Vision** means the model accepts native image input in the live catalog.
|
|
10
|
+
- **Premium** is a policy label for high-cost models. A premium model is never selected for economy or ordinary standard work.
|
|
11
|
+
- **Monthly allowance** is public-rate usage included by the subscription. OpenCode Go values come from the plan catalog's Go table (Go Plus is a separate, larger table); selecting the wrong tier overstates capacity by roughly 2x. Command Code values are the GOAT per-model allowances. Provider-reported totals override these numbers for the plan-level balance.
|
|
12
|
+
- **Time-of-day pricing** is applied only where documented. Command Code DeepSeek V4.1 Flash is listed at off-peak rates and charges 2x from 01:00 to 04:00 and 06:00 to 10:00 UTC, Monday through Friday; the router applies that multiplier to input, output, cache-read, and cache-loss estimates. Models without documented peak metadata use flat catalog rates.
|
|
13
|
+
|
|
14
|
+
These scores are planning inputs, not marketing claims. Models marked **unverified** or **unscored** are not approved for automatic substantive work.
|
|
15
|
+
|
|
16
|
+
## Economy (Tier 1)
|
|
17
|
+
|
|
18
|
+
Used only for narrow, mechanically verifiable work with an explicit scope and deterministic validation.
|
|
19
|
+
|
|
20
|
+
| Provider / model | Intelligence | Design | Vision | Monthly allowance |
|
|
21
|
+
|---|---:|---:|---|---:|
|
|
22
|
+
| `opencode-go/gpt-6-luna` | 38.1 | 55 | text, image, PDF | $60 |
|
|
23
|
+
| `opencode-go/mimo-v2.6-flash` | 37.9 | 50 | text, image, audio, video | $120 |
|
|
24
|
+
| `opencode-go/hy3` | 25.3 | 45 | text | $240 |
|
|
25
|
+
| `opencode-go/longcat-2.0` | 19.1 | 40 | text | $240 |
|
|
26
|
+
| `commandcode/gpt-6-luna` | 38.1 | 55 | text, image, PDF | $20 |
|
|
27
|
+
|
|
28
|
+
These models are below the standard intelligence floor. They cannot be selected for feature work, debugging, refactoring, architecture, security, or tests.
|
|
29
|
+
|
|
30
|
+
## Standard (Tier 2)
|
|
31
|
+
|
|
32
|
+
The default for substantive software engineering. Every model here meets or exceeds the DeepSeek V4.1 Flash floor.
|
|
33
|
+
|
|
34
|
+
| Provider / model | Intelligence | Design | Vision | Monthly allowance |
|
|
35
|
+
|---|---:|---:|---|---:|
|
|
36
|
+
| `opencode-go/deepseek-v4.1-flash` | 39.5 | 65 | text, image | $160 |
|
|
37
|
+
| `opencode-go/glm-5.3-flash` | 41.8 | 65 | text, image, video | $180 |
|
|
38
|
+
| `opencode-go/minimax-m3.1-flash` | 42.7 | 60 | text, image, video | $80 |
|
|
39
|
+
| `opencode-go/claude-haiku-5-5` | 43.4 | 70 | text, image, PDF | $60 |
|
|
40
|
+
| `opencode-go/mimo-v2.6-pro` | 46.3 | 62 | text, image, audio, video | $60 |
|
|
41
|
+
| `commandcode/deepseek-v4.1-flash` | 39.5 | 65 | text, image | $80 |
|
|
42
|
+
| `commandcode/z-ai/glm-5.3-flash` | 41.8 | 65 | text, image, video, PDF | $80 |
|
|
43
|
+
| `commandcode/claude-haiku-5-5` | 43.4 | 70 | text, image, PDF | $20 |
|
|
44
|
+
|
|
45
|
+
Design-intensive work requires at least a 60 design score. Sophisticated design work requires at least 75. A standard model below 75 still requires premium approval for sophisticated design.
|
|
46
|
+
|
|
47
|
+
Claude Haiku 5.5 (**43.4** intelligence, **70** design) is a concrete example of why intelligence and design are separate gates. It clears the 39.5 intelligence floor, so it is valid for ordinary feature work and standard design. It does **not** clear the 75 sophisticated-design floor. When no sophisticated-qualified model is available, the router may use it only as an explicitly labeled standard-design fallback:
|
|
48
|
+
|
|
49
|
+
> `Standard design route: design 70 is below the sophisticated 75 threshold`
|
|
50
|
+
|
|
51
|
+
It is never reported as meeting every hard requirement for sophisticated design, and a premium design route still requires approval.
|
|
52
|
+
|
|
53
|
+
## Premium (Tier 3)
|
|
54
|
+
|
|
55
|
+
Premium models are available only when:
|
|
56
|
+
|
|
57
|
+
- the task is premium-tier or the user explicitly asks for a premium model,
|
|
58
|
+
- `premium.mode` is not `off`,
|
|
59
|
+
- and either a one-shot approval is active for the session or Premium Allowed mode has a remaining configured budget.
|
|
60
|
+
|
|
61
|
+
They are never used for trivial or ordinary standard work. They are never selected silently.
|
|
62
|
+
|
|
63
|
+
| Provider / model | Intelligence | Design | Vision | Monthly allowance |
|
|
64
|
+
|---|---:|---:|---|---:|
|
|
65
|
+
| `opencode-go/glm-5.3` | 44.8 | 80 | text | $120 |
|
|
66
|
+
| `opencode-go/kimi-k3` | 43.6 | 72 | text, image, video | $60 |
|
|
67
|
+
| `opencode-go/grok-4.7` | 46.4 | 75 | text, image, PDF | $60 |
|
|
68
|
+
| `opencode-go/qwen3.8-max` | 40.2 | 70 | text, image, video | $60 |
|
|
69
|
+
| `opencode-go/step-5-preview` | 43.7 | 70 | text, image, video | $60 |
|
|
70
|
+
| `commandcode/claude-sonnet-5-5` | 56.0 | 90 | text, image, PDF | $10 |
|
|
71
|
+
| `commandcode/meta/muse-spark-1.3` | 48.1 | 82 | text, image, video, PDF, audio | $20 |
|
|
72
|
+
| `commandcode/xai/grok-4.7` | 46.4 | 75 | text, image, PDF | $20 |
|
|
73
|
+
| `commandcode/Qwen/Qwen3.8-Max` | 40.2 | 70 | text, image, video, PDF | $20 |
|
|
74
|
+
| `commandcode/stepfun/Step-5-Preview` | 43.7 | 70 | text, image, video | $20 |
|
|
75
|
+
|
|
76
|
+
`opencode-go/glm-5.3` is text-only and can never receive a screenshot, even if its intelligence would otherwise qualify. This is enforced as a hard vision rule.
|
|
77
|
+
|
|
78
|
+
## Known but unapproved
|
|
79
|
+
|
|
80
|
+
These models are discovered but deliberately excluded from automatic routing:
|
|
81
|
+
|
|
82
|
+
- `commandcode/google/gemini-4-argon`, `opencode-go/gemini-4-argon`: unscored in the current model cards.
|
|
83
|
+
- `commandcode/claude-opus-5-5`, `commandcode/gpt-6.1-sol`, `commandcode/gpt-6-astra`, `commandcode/claude-fable-5-1`: frontier-priced and unscored here. They require explicit user qualification.
|
|
84
|
+
- `opencode-go/kimi-k3.1-pro`: unscored.
|
|
85
|
+
- `opencode-go/deepseek-v4-pro`: below the current floor and text only.
|
|
86
|
+
- `commandcode/deepseek/deepseek-v4.1-flash`: duplicate route for the same upstream model; the GOAT ID is approved instead.
|
|
87
|
+
- `meta/muse-spark-1.3-contributor`, `opencode-go/muse-spark-1.3-contributor`, `opencode-go/iceman-contributor-free`: training-enabled. Blocked unless `privacy.allowTrainingModels` is explicitly enabled.
|
|
88
|
+
- `opencode-go/beam-free`: free promotional model, not qualified for automatic work.
|
|
89
|
+
|
|
90
|
+
## Changing the registry
|
|
91
|
+
|
|
92
|
+
Use `/router-exclude provider/model` to remove a model from automatic routing. To approve a discovered model, add its exact key to `models.approved` in `~/.config/opencode/smart-router.json` and assign it a tier in `models.tierOverrides`. A discovered model without a tier remains unapproved.
|
|
93
|
+
|
|
94
|
+
Do not approve a model solely because it is cheaper or newer. Qualify it on real tasks first.
|
|
95
|
+
|
|
96
|
+
## User-defined model definitions
|
|
97
|
+
|
|
98
|
+
If a provider exposes a model that is not in the seed, qualify it explicitly in `models.definitions`. A definition is metadata, not a new provider or a proxy. The model still comes from the connected provider catalog.
|
|
99
|
+
|
|
100
|
+
```jsonc
|
|
101
|
+
{
|
|
102
|
+
"models": {
|
|
103
|
+
"definitions": [
|
|
104
|
+
{
|
|
105
|
+
"key": "acme/coder-v2",
|
|
106
|
+
"providerID": "acme",
|
|
107
|
+
"modelID": "coder-v2",
|
|
108
|
+
"name": "Acme Coder V2",
|
|
109
|
+
"context": 200000,
|
|
110
|
+
"maxOutput": 32768,
|
|
111
|
+
"input": ["text", "image"],
|
|
112
|
+
"cost": [{ "input": 0.2, "output": 0.8, "cache": { "read": 0.02, "write": 0 } }],
|
|
113
|
+
"intelligence": 41.0,
|
|
114
|
+
"design": 60,
|
|
115
|
+
"tier": 2,
|
|
116
|
+
"approved": true,
|
|
117
|
+
"monthlyAllowanceUsd": 100
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Definitions are merged with the seed and live catalog. `tier` is required; `approved` defaults to true only because the definition itself is an explicit qualification. Do not add a definition for a model you have not tested.
|