@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/CONTRIBUTING.md +96 -0
  3. package/LICENSE +21 -0
  4. package/README.md +341 -2
  5. package/SECURITY.md +174 -0
  6. package/config.example.json +108 -0
  7. package/dist/allowance.js +487 -0
  8. package/dist/gui-experimental.cjs +1 -0
  9. package/dist/index.js +11111 -0
  10. package/dist/npm/index.js +40 -0
  11. package/dist/tui.js +587 -0
  12. package/docs/ARCHITECTURE.md +104 -0
  13. package/docs/DEBUGGING-GUI.md +70 -0
  14. package/docs/INSTALL.md +120 -0
  15. package/docs/LIMITATIONS.md +213 -0
  16. package/docs/MODELS.md +124 -0
  17. package/docs/OPENDESIGN-INTEGRATION.md +635 -0
  18. package/docs/QA.md +142 -0
  19. package/docs/SHARED-CORE.md +72 -0
  20. package/install +75 -0
  21. package/package.json +92 -4
  22. package/scripts/install.sh +324 -0
  23. package/scripts/uninstall.sh +91 -0
  24. package/src/allowance.ts +135 -0
  25. package/src/core/classify/classifier.ts +305 -0
  26. package/src/core/config-writer.ts +167 -0
  27. package/src/core/config.ts +1030 -0
  28. package/src/core/context/strategy.ts +188 -0
  29. package/src/core/context-health.ts +39 -0
  30. package/src/core/failures.ts +398 -0
  31. package/src/core/history/opencode.ts +173 -0
  32. package/src/core/models/protocol.ts +22 -0
  33. package/src/core/models/registry.ts +203 -0
  34. package/src/core/models/seed.ts +771 -0
  35. package/src/core/offline/mode.ts +321 -0
  36. package/src/core/plan-page.ts +390 -0
  37. package/src/core/plans/catalog.ts +481 -0
  38. package/src/core/policy/calibration.ts +259 -0
  39. package/src/core/policy/eligibility.ts +578 -0
  40. package/src/core/providers/enablement.ts +143 -0
  41. package/src/core/providers/opendesign-credits.ts +206 -0
  42. package/src/core/providers/opendesign-models.ts +628 -0
  43. package/src/core/providers/opendesign.ts +474 -0
  44. package/src/core/quota/calibration.ts +166 -0
  45. package/src/core/quota/commandcode.ts +471 -0
  46. package/src/core/quota/credentials.ts +490 -0
  47. package/src/core/quota/ledger.ts +1273 -0
  48. package/src/core/quota/opencode-go.ts +148 -0
  49. package/src/core/quota/opendesign.ts +1043 -0
  50. package/src/core/quota/reconcile.ts +155 -0
  51. package/src/core/quota/report.ts +261 -0
  52. package/src/core/quota/service.ts +273 -0
  53. package/src/core/reprompt.ts +48 -0
  54. package/src/core/routing/engine.ts +919 -0
  55. package/src/core/types.ts +825 -0
  56. package/src/core/ui.ts +236 -0
  57. package/src/core/util.ts +114 -0
  58. package/src/gui/index.tsx +1670 -0
  59. package/src/gui/viewmodel.ts +456 -0
  60. package/src/index.ts +48 -0
  61. package/src/rpc.ts +204 -0
  62. package/src/server/controller.ts +3281 -0
  63. package/src/server/route-tracker.ts +60 -0
  64. package/src/server/shared-core.ts +671 -0
  65. 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.
@@ -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.