@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,635 @@
|
|
|
1
|
+
# OpenDesign Go provider integration
|
|
2
|
+
|
|
3
|
+
OpenDesign Go is a third subscription provider alongside OpenCode Go Plus and
|
|
4
|
+
Command Code GOAT. This document records what the integration does, where its
|
|
5
|
+
numbers come from, and what it cannot promise.
|
|
6
|
+
|
|
7
|
+
It extends the existing Option C router. It is not a second router, and it does
|
|
8
|
+
not change the behavior of the two existing providers.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. What was implemented
|
|
13
|
+
|
|
14
|
+
| Area | Change |
|
|
15
|
+
|---|---|
|
|
16
|
+
| Provider adapter | `src/core/providers/opendesign.ts` : base URL handling, protocol selection, read-only model discovery, error and size normalization |
|
|
17
|
+
| Model catalog | `src/core/providers/opendesign-models.ts` : the 39 models the gateway serves, with credit multipliers and curated quality scores |
|
|
18
|
+
| Credit semantics | `src/core/providers/opendesign-credits.ts` : published multipliers, promotion handling, credit conversion |
|
|
19
|
+
| Quota integration | `src/core/quota/opendesign.ts` : verified / estimated / unknown / exhausted states |
|
|
20
|
+
| Credential discovery | `src/core/quota/credentials.ts` : inference key and explicitly supplied control key, kept separate |
|
|
21
|
+
| Routing | `src/core/routing/engine.ts` : credit multiplier in the cost ranking, cooldown exclusion, reliability weighting |
|
|
22
|
+
| Policy | `src/core/policy/eligibility.ts` : subscription-only guard and unknown-quota policy |
|
|
23
|
+
| Quota coordinator | `src/core/quota/service.ts` : OpenDesign probes run through the same single-flight service as the other two providers |
|
|
24
|
+
| Model registry | `src/core/models/seed.ts`, `src/core/models/registry.ts`: OpenDesign seeds, gateway metadata merged over them |
|
|
25
|
+
| Shared core | `src/server/shared-core.ts` : one single-flight discovery per process, merged into the one model catalog |
|
|
26
|
+
| Commands | `/router-opendesign` added; `/router-status`, `/router-report`, `/router-explain` updated |
|
|
27
|
+
|
|
28
|
+
The provider is **optional and disabled by default**. A user with no OpenDesign
|
|
29
|
+
credentials sees no behavior change at all.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 2. Source revisions examined
|
|
34
|
+
|
|
35
|
+
Recorded 2026-10-28.
|
|
36
|
+
|
|
37
|
+
| Source | Revision | Used for |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `nexu-io/open-design` | `c6bb5a8b8190` | Billing preflight, model probing, error taxonomy |
|
|
40
|
+
| `nexu-io/open-design` PR #8339 | merge `eb27acd32250`, merged 2026-09-24 | Advisory preflight vs gateway funding vs wallet display |
|
|
41
|
+
| `nexu-io/open-design` #8674 | open | No API-key-readable quota endpoint |
|
|
42
|
+
| `nexu-io/open-design` #8870 | open | Control-key lifecycle and revocation gaps |
|
|
43
|
+
| `nexu-io/open-design` #8687 | open | Anthropic-messages drops reasoning for DeepSeek V4.1 Flash |
|
|
44
|
+
| `KouzakiUmi/dsh-opendesign-corouter` | `afb390a6bd66` | Gateway shape, protocol endpoints, empty-turn behavior |
|
|
45
|
+
| `KouzakiUmi/dsh-subusage` | `d6d997ddce7a` | Control-plane paths, window classification, credential handling |
|
|
46
|
+
| `open-design.ai/help/cloud-api/`, `/help/billing/`, `/model-credits/` | retrieved 2026-10-28 | Plan shape, credit multipliers, window rules |
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 3. What the gateway actually reports
|
|
51
|
+
|
|
52
|
+
`GET {base}/v1/models` with the inference key returns the account's entitled
|
|
53
|
+
models with authoritative metadata. Captured 2026-10-28.
|
|
54
|
+
|
|
55
|
+
| Model | Usable input | Output | Image input | Credits |
|
|
56
|
+
|---|---|---|---|---|
|
|
57
|
+
| DeepSeek V4.1 Flash | 1,048,000 (measured) | 128,000 | yes | 0.5× |
|
|
58
|
+
| Claude Haiku 5.5 | 228,000 | 128,000 | yes | 2× |
|
|
59
|
+
| GLM-5.3 Flash | 232,000 | 32,000 | yes | 1× |
|
|
60
|
+
| GLM-5.3 Flash-X | 232,000 | 32,000 | yes | 2× |
|
|
61
|
+
| GPT-6 Luna | 400,000 | 128,000 | yes | 4× |
|
|
62
|
+
| MiMo V2.6 Flash | 232,000 | 32,000 | yes | 1× |
|
|
63
|
+
| MiMo V2.6 Pro | 232,000 | 32,000 | yes | 1× |
|
|
64
|
+
| MiniMax M3.1 Flash | unknown | unknown | yes | not published |
|
|
65
|
+
| DeepSeek V4 Flash | 232,000 | 32,000 | no | 1× |
|
|
66
|
+
| DeepSeek V4 Flash Vision Exp | 232,000 | 32,000 | yes | 1× |
|
|
67
|
+
| DeepSeek V4 Pro | 328,000 | 128,000 | no | 1× |
|
|
68
|
+
| Hunyuan H4 Preview | 232,000 | 32,000 | no | 1× |
|
|
69
|
+
| Kimi K2.7 Code | 262,000 | 32,000 | yes | 1× |
|
|
70
|
+
|
|
71
|
+
Three findings that change how the router treats OpenDesign:
|
|
72
|
+
|
|
73
|
+
1. **`context_budget` is advisory, not an enforced ceiling.** This was corrected
|
|
74
|
+
on 2026-10-30 by direct measurement. The gateway advertises a
|
|
75
|
+
`context_budget` of 228k to 400k against a vendor `context_limit` of up to
|
|
76
|
+
1.05M, but it does not enforce the budget. On `deepseek-v4.1-flash` the
|
|
77
|
+
gateway accepted 1,043,337 input tokens with HTTP 200 and rejected only above
|
|
78
|
+
the 1,048,000 vendor limit with `context_length_exceeded`. Six probes, $0.006
|
|
79
|
+
total; see `docs/AUDIT-deepseek-context-2026-10-30.md`.
|
|
80
|
+
|
|
81
|
+
The router therefore uses `context_budget` only for models whose real ceiling
|
|
82
|
+
has not been measured. For a measured model it uses the vendor limit. Assuming
|
|
83
|
+
the smaller number is not free: it discarded roughly 716,000 usable tokens on
|
|
84
|
+
DeepSeek and compacted sessions at 304,000 that the gateway would have served
|
|
85
|
+
past a million.
|
|
86
|
+
2. **Image support is verified from the gateway**, not inferred from a model
|
|
87
|
+
card. `hy4-preview`, `deepseek-v4-flash` and `deepseek-v4-pro` are text only
|
|
88
|
+
on this gateway.
|
|
89
|
+
3. **MiniMax M3.1 Flash reports no context or output budget.** The router uses
|
|
90
|
+
the vendor limit of 1,000,000 and flags the multiplier as unverified. An
|
|
91
|
+
earlier version used a hand-written 232k guess here, which shrank the window
|
|
92
|
+
four-fold and blocked a live session.
|
|
93
|
+
|
|
94
|
+
A fourth finding, observed live on 2026-10-28: the gateway resolves a **billing
|
|
95
|
+
scope** before answering, even a read-only `/v1/models` request. During this
|
|
96
|
+
work it returned `503 workspace_billing_scope_unavailable` with an upstream
|
|
97
|
+
`context deadline exceeded` against `amr-api.open-design.ai/api/internal/billing/scope/…`,
|
|
98
|
+
then succeeded on retry. Two consequences:
|
|
99
|
+
|
|
100
|
+
- Every OpenDesign request, including model discovery, depends on a billing
|
|
101
|
+
service that can time out. Transient 503s are expected and must be retried
|
|
102
|
+
with backoff, which the router's quota backoff and failure classification
|
|
103
|
+
already handle.
|
|
104
|
+
- This is a plausible upstream contributor to dropped streams: the billing
|
|
105
|
+
scope resolution sits in front of the response stream.
|
|
106
|
+
|
|
107
|
+
Models without a curated quality score are seeded unapproved. Approve them
|
|
108
|
+
explicitly in `models.approved` if you want them routed.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 4. How quota is obtained
|
|
113
|
+
|
|
114
|
+
OpenDesign publishes no quota API for an inference key. Verified empirically on
|
|
115
|
+
2026-10-28:
|
|
116
|
+
|
|
117
|
+
| Path | Result |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `GET amr-link.open-design.ai/v1/usage` | 404 |
|
|
120
|
+
| `GET amr-link.open-design.ai/v1/me` | 404 |
|
|
121
|
+
| `GET amr-api.open-design.ai/api/v1/workspaces/current` with the inference key | 401 `unauthenticated` |
|
|
122
|
+
| `GET amr-api.open-design.ai/api/v1/billing/summary` with the inference key | 401 `unauthenticated` |
|
|
123
|
+
| `GET amr-api.open-design.ai/api/v1/wallet/balance` with the inference key | 401 `unauthenticated` |
|
|
124
|
+
|
|
125
|
+
Issues #8674 and #8870, which ask for exactly this, are both still open.
|
|
126
|
+
|
|
127
|
+
The integration therefore has three states:
|
|
128
|
+
|
|
129
|
+
- **`verified`**: a control key was supplied explicitly. The router reads
|
|
130
|
+
`workspaces/current`, `workspaces/{id}/billing/coding-plan-usage` and
|
|
131
|
+
`billing/summary`, and reports provider numbers. This is the only state in
|
|
132
|
+
which the wallet balance is reported.
|
|
133
|
+
- **`estimated`**: only the inference key is available. The router models its
|
|
134
|
+
own credit consumption against the published plan caps, applies a
|
|
135
|
+
configurable safety margin (default 20%), and reports it as a lower bound.
|
|
136
|
+
- **`unavailable`**: nothing could be established. OpenDesign is then not
|
|
137
|
+
routed to.
|
|
138
|
+
|
|
139
|
+
`stale` marks verified or estimated data older than twice the probe interval.
|
|
140
|
+
`exhausted` marks a confirmed or modelled window that has been used up.
|
|
141
|
+
|
|
142
|
+
The router never obtains a control key, never asks for a browser session, and
|
|
143
|
+
never scrapes cookies. A control key is used only when you point
|
|
144
|
+
`providers.opendesign.controlKeyPath` at a file you created yourself.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 5. Billing accuracy and limits
|
|
149
|
+
|
|
150
|
+
The Go plan shape is:
|
|
151
|
+
|
|
152
|
+
- $10 per month
|
|
153
|
+
- Design Plan monthly maximum **$120**
|
|
154
|
+
- 7-day window maximum **$30**
|
|
155
|
+
- 5-hour window maximum **$10**
|
|
156
|
+
- Plan-model credits **$0**
|
|
157
|
+
|
|
158
|
+
The 5-hour and 7-day values are nested limits inside the monthly maximum. They
|
|
159
|
+
are never added together.
|
|
160
|
+
|
|
161
|
+
What is exact, estimated, and unknown:
|
|
162
|
+
|
|
163
|
+
- **Exact.** Window limits and reset times when a control key is configured;
|
|
164
|
+
the plan tier; which models the gateway serves; each model's usable context,
|
|
165
|
+
output budget and image support.
|
|
166
|
+
- **Estimated.** Credit consumption. The router converts token counts into a
|
|
167
|
+
dollar value of model usage using the curated vendor list price, then applies
|
|
168
|
+
the published Design Plan multiplier. OpenDesign does not publish per-token
|
|
169
|
+
prices for plan models, so the underlying price is a proxy, not a quote.
|
|
170
|
+
- **Unknown.** Usage from outside OpenCode. The OpenDesign desktop app and any
|
|
171
|
+
other client share the same allowance, and the router cannot see them. The
|
|
172
|
+
estimated state is therefore a lower bound, which is why a margin is applied.
|
|
173
|
+
- **Unknown.** The multiplier for MiniMax M3.1 Flash. It is not published, so
|
|
174
|
+
the neutral 1× rate is used and flagged.
|
|
175
|
+
|
|
176
|
+
The DeepSeek V4.1 Flash off-peak promotion of 2026-10-20 to 2026-10-27 is
|
|
177
|
+
recorded but never applied: it has expired, and OpenDesign states that
|
|
178
|
+
app-only offers do not automatically apply through APIs.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 6. Spending policy
|
|
183
|
+
|
|
184
|
+
Default configuration:
|
|
185
|
+
|
|
186
|
+
- `subscriptionOnly: true`
|
|
187
|
+
- `allowExtraUsage: false`
|
|
188
|
+
|
|
189
|
+
With those set, the router will not send a request to OpenDesign when the
|
|
190
|
+
subscription allowance is exhausted, even though the gateway could fund the
|
|
191
|
+
request from purchased balance. It also refuses a request whose projected cost
|
|
192
|
+
does not fit inside what is left of the applicable window.
|
|
193
|
+
|
|
194
|
+
**This is a client-side guard, and it is not a guarantee.** OpenDesign's
|
|
195
|
+
gateway chooses the funding source per request. The upstream client removed
|
|
196
|
+
client-side wallet-zero blocking for exactly this reason, recording that the
|
|
197
|
+
gateway decides. The router cannot prevent a server-side charge; it can only
|
|
198
|
+
avoid sending a request it can already see does not fit.
|
|
199
|
+
|
|
200
|
+
You must confirm in the OpenDesign account console that automatic recharge is
|
|
201
|
+
disabled. The router cannot check that on your behalf.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 7. Credential setup
|
|
206
|
+
|
|
207
|
+
The inference key is the only credential the provider needs.
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# 1. Create a key in the OpenDesign Cloud console, API keys page.
|
|
211
|
+
# 2. Store it with owner-only permissions.
|
|
212
|
+
umask 077
|
|
213
|
+
printf '%s' 'sk-amr-your-key' > ~/.config/opencode/opendesign.key
|
|
214
|
+
chmod 600 ~/.config/opencode/opendesign.key
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The router discovers it from, in order: `$OPENDESIGN_API_KEY`, an explicit
|
|
218
|
+
`providers.opendesign.credentialPath`, `~/.config/opencode/opendesign.key`,
|
|
219
|
+
`opendesign.env`, `opendesign.json`, and finally the OpenCode provider
|
|
220
|
+
settings. Credential files must not be group- or world-readable.
|
|
221
|
+
|
|
222
|
+
Then point OpenCode at the gateway in `opencode.json`:
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"providers": {
|
|
227
|
+
"opendesign": {
|
|
228
|
+
"name": "OpenDesign Go",
|
|
229
|
+
"package": "@opencode/ai/providers/openai-compatible",
|
|
230
|
+
"options": {
|
|
231
|
+
"baseURL": "https://amr-link.open-design.ai/v1",
|
|
232
|
+
"apiKey": "{env:OPENDESIGN_API_KEY}"
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
And enable it for the router in `~/.config/opencode/smart-router.json`:
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"providers": {
|
|
244
|
+
"opendesign": {
|
|
245
|
+
"enabled": true,
|
|
246
|
+
"subscriptionOnly": true,
|
|
247
|
+
"allowExtraUsage": false,
|
|
248
|
+
"monthlyCreditsUsd": 120,
|
|
249
|
+
"fiveHourCapUsd": 10,
|
|
250
|
+
"weeklyCapUsd": 30
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The gateway base URL already contains `/v1`. The router never appends it twice.
|
|
257
|
+
|
|
258
|
+
**Protocol.** Use chat-completions, which is what the OpenAI-compatible
|
|
259
|
+
package speaks. Do not configure the Anthropic-messages protocol for DeepSeek
|
|
260
|
+
V4.1 Flash: upstream issue #8687 reports that it drops reasoning and stalls
|
|
261
|
+
agents that rely on tool calls. The router warns when it sees this combination.
|
|
262
|
+
|
|
263
|
+
Verify with `/router-opendesign`, which lists the entitled models the gateway
|
|
264
|
+
actually returned and makes no inference request.
|
|
265
|
+
|
|
266
|
+
### Getting closer to exact numbers without a control key
|
|
267
|
+
|
|
268
|
+
Re-probed against the live gateway on 2026-11-04, with a valid inference key:
|
|
269
|
+
|
|
270
|
+
| Request | Result |
|
|
271
|
+
|---|---|
|
|
272
|
+
| `GET /v1/models` | 200 |
|
|
273
|
+
| `GET /v1/usage`, `/v1/me`, `/v1/credits`, `/v1/billing`, `/v1/account`, `/v1/subscription`, `/v1/limits`, `/v1/quota`, `/v1/workspaces` | 404 |
|
|
274
|
+
| `GET https://amr-api.open-design.ai/api/v1/workspaces/current` (inference key) | 401 |
|
|
275
|
+
| any `chat/completions` response | no credit or quota headers; `usage` carries tokens only |
|
|
276
|
+
|
|
277
|
+
So the inference key still cannot see account usage, and the router still will not
|
|
278
|
+
scrape a browser session. If you can read your own account page, you can give the
|
|
279
|
+
router the number instead of letting it guess:
|
|
280
|
+
|
|
281
|
+
```json
|
|
282
|
+
{
|
|
283
|
+
"providers": {
|
|
284
|
+
"opendesign": {
|
|
285
|
+
"baseline": {
|
|
286
|
+
"at": "2026-11-04T12:00:00Z",
|
|
287
|
+
"designPlanUsedUsd": 37.8,
|
|
288
|
+
"weeklyResetsAt": "2026-11-05T03:00:00Z"
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The router adds every OpenDesign request it routes after `at` to that figure, and
|
|
296
|
+
labels the result as starting from your reading rather than from zero.
|
|
297
|
+
|
|
298
|
+
Two rules keep this honest:
|
|
299
|
+
|
|
300
|
+
- **The baseline expires when its window resets, not when it gets old.** These
|
|
301
|
+
are different. A seven-day window that resets in 23 hours means the reading
|
|
302
|
+
describes the window ending then; afterwards it describes a window that no
|
|
303
|
+
longer exists, and seeding it into the new one would attribute old usage to
|
|
304
|
+
fresh credit. Supply `weeklyResetsAt` (and `monthlyResetsAt`) from the account
|
|
305
|
+
page. Without them the router falls back to an age-based assumption and says
|
|
306
|
+
so in the panel.
|
|
307
|
+
- **Read the percentage off the window it belongs to.** On Pro, the account page
|
|
308
|
+
shows Design Plan credits as a share of the seven-day allowance ($90), not the
|
|
309
|
+
monthly one ($360). 42% of the week is $37.80, which is 10.5% of the month.
|
|
310
|
+
Entering `151.20` because "42% of 360" would overstate usage fourfold.
|
|
311
|
+
|
|
312
|
+
### What the vendor documentation settles
|
|
313
|
+
|
|
314
|
+
Read from <https://open-design.ai/model-credits/> and
|
|
315
|
+
<https://open-design.ai/help/billing/> on 2026-11-04. These are public pages, so
|
|
316
|
+
every claim below is verifiable and should be re-checked when terms change.
|
|
317
|
+
|
|
318
|
+
**The seven-day window is fixed, not rolling.** "For new subscriptions, the 7-day
|
|
319
|
+
window starts when the subscription begins and resets every 7 days… subscribe on
|
|
320
|
+
Wednesday at 10:00 AM, it resets the following Wednesday at 10:00 AM." The catalog
|
|
321
|
+
called this `rolling-7-day`, which would slide continuously and understate
|
|
322
|
+
available headroom for most of the week. Now `fixed-7-day`.
|
|
323
|
+
|
|
324
|
+
**The pools spill.** "Design Plan models use Design Plan credits first, then any
|
|
325
|
+
remaining plan model credits." The catalog already modelled the draw order
|
|
326
|
+
correctly; its prose did not (it claimed the pools are "never added together",
|
|
327
|
+
which reads as "never interact"). Go has no plan model pool, so a spent window
|
|
328
|
+
there really does mean waiting.
|
|
329
|
+
|
|
330
|
+
**Only Design Plan credits have windows.** Plan model credits have neither a
|
|
331
|
+
five-hour nor a seven-day window and reset per billing cycle. Unused subscription
|
|
332
|
+
credits do not roll over.
|
|
333
|
+
|
|
334
|
+
**Per-plan ceilings, corrected:**
|
|
335
|
+
|
|
336
|
+
| Plan | Monthly max | Plan models | Design Plan | 7-day | 5-hour |
|
|
337
|
+
|---|---|---|---|---|---|
|
|
338
|
+
| Go | $120 | $0 | $120 | $30 | $10 |
|
|
339
|
+
| Plus | $180 | $20 | $160 | **$40** | — |
|
|
340
|
+
| Pro | $480 | $120 | $360 | $90 | — |
|
|
341
|
+
| Max | $900 | $300 | $600 | $150 | — |
|
|
342
|
+
|
|
343
|
+
Plus had **no** weekly ceiling in the catalog, which made it look able to spend
|
|
344
|
+
its entire $160 in one week — a 4x understatement of the real limit.
|
|
345
|
+
|
|
346
|
+
**Multipliers are confirmed, not inferred.** The published table matches ours
|
|
347
|
+
exactly: DeepSeek V4.1 Flash 0.5×, the majority 1×, Claude Haiku 5.5 and
|
|
348
|
+
GLM-5.3 Flash-X 2×, GPT-6 Luna 4×. Also documented: MiniMax M3.1 Flash is 1×,
|
|
349
|
+
and the October off-peak 0.25× promotion expired on 2026-10-27 and applies to
|
|
350
|
+
the app only, never to API usage.
|
|
351
|
+
|
|
352
|
+
**Still no usage endpoint.** Upstream issue `nexu-io/open-design#8674` asks for
|
|
353
|
+
exactly the endpoint this router needs — per-window `used_percent` and
|
|
354
|
+
`resets_at` for API key holders — and remains open. The account host rejects an
|
|
355
|
+
inference key with 401 and bot-blocks non-browser requests. So the estimate plus
|
|
356
|
+
the user-supplied baseline remain the only path, and this is a vendor gap rather
|
|
357
|
+
than something to work around.
|
|
358
|
+
|
|
359
|
+
### Where the prices come from, and why they drifted
|
|
360
|
+
|
|
361
|
+
The model catalog stores the **published API price** per million tokens. The
|
|
362
|
+
Design Plan multiplier is a separate factor applied on top, so the credit cost of
|
|
363
|
+
a request is `published price x multiplier`. Conflating the two is the mistake the
|
|
364
|
+
catalog made for thirteen of its thirty-three models.
|
|
365
|
+
|
|
366
|
+
Checked against CoRouter's public model pages on 2026-11-04. That page is
|
|
367
|
+
machine-readable: every model panel carries `data-model-panel` with the exact API
|
|
368
|
+
model ID, so the mapping is read rather than inferred from display names.
|
|
369
|
+
|
|
370
|
+
What was wrong:
|
|
371
|
+
|
|
372
|
+
| Model | Was | Published | Effect |
|
|
373
|
+
|---|---|---|---|
|
|
374
|
+
| `deepseek-v4.1-flash` | 0.15 | **0.30** | **charged a quarter of real cost** |
|
|
375
|
+
| `deepseek-v4-pro` | 0.66 | 1.32 | half |
|
|
376
|
+
| `deepseek-v4-flash` | 0.66 | 0.44 | 1.5x too high |
|
|
377
|
+
| `glm-5.3-flash` | 0.15 | 0.075 | 2x too high |
|
|
378
|
+
| `hy4-preview` | 0.14 | 0.834 | 6x too low |
|
|
379
|
+
| `kimi-k2.7-code` | 3.00 | 0.95 | 3x too high |
|
|
380
|
+
| plus 7 more | | | |
|
|
381
|
+
|
|
382
|
+
The model in daily use was the worst case, and it looked like a double-applied
|
|
383
|
+
multiplier: 0.15 is 0.30 x 0.5. But there was no single rule, which is why this
|
|
384
|
+
was fixed model by model rather than by scaling.
|
|
385
|
+
|
|
386
|
+
Two consequences worth stating:
|
|
387
|
+
|
|
388
|
+
- **Routing changed.** With real prices, GLM-5.3 Flash ($0.075, intelligence
|
|
389
|
+
41.8) beats DeepSeek V4.1 Flash ($0.30, 39.5) for ordinary work. Several tests
|
|
390
|
+
had recorded the old winner; they were updated, and the reasoning is in the
|
|
391
|
+
diff rather than silently rebaselined.
|
|
392
|
+
- **`test/opendesign-pricing.test.ts` now pins every published price.** A failure
|
|
393
|
+
there means either the provider changed its rates (update both) or somebody
|
|
394
|
+
hand-edited the catalog.
|
|
395
|
+
|
|
396
|
+
The plan caps were checked at the same time and were already correct: Pro is
|
|
397
|
+
$360 Design Plan plus $120 plan models, which is the $480 total the pricing page
|
|
398
|
+
advertises, and Design Plan credits are consumed before plan model credits.
|
|
399
|
+
|
|
400
|
+
### Two estimator defects, and why the tests missed them
|
|
401
|
+
|
|
402
|
+
Both shipped with green tests. Recording them because the pattern matters more
|
|
403
|
+
than the bugs.
|
|
404
|
+
|
|
405
|
+
**The reported cost was thrown away.** Every completed step carries the cost the
|
|
406
|
+
host actually charged, and the ledger already stores it (`usage[].usd`).
|
|
407
|
+
`localTotals` ignored it and recomputed price from the static table, then applied
|
|
408
|
+
the Design Plan multiplier — re-deriving an approximation of a number that was
|
|
409
|
+
sitting right there. It also drifted in a specific case: a step funded from plan
|
|
410
|
+
model credits after the Design Plan allowance is spent is charged at the plain
|
|
411
|
+
rate, while the table still applied the Design Plan multiplier.
|
|
412
|
+
|
|
413
|
+
Now: a non-zero reported cost is used, with the multiplier applied only when the
|
|
414
|
+
step is funded from the Design Plan pool. A reported cost of **zero means "not
|
|
415
|
+
reported", not "free"**, so it falls back to the table.
|
|
416
|
+
|
|
417
|
+
**The fixed window never took effect.** The catalog was corrected to
|
|
418
|
+
`fixed-7-day` and a test asserted it — but the test checked the catalog, not the
|
|
419
|
+
estimator, and `localTotals` still bucketed by `now - 7 days`. The declaration
|
|
420
|
+
and the behavior disagreed, with every test passing.
|
|
421
|
+
|
|
422
|
+
Now `weeklyWindowStart()` anchors the window to the known reset time from a
|
|
423
|
+
pasted reading, so the window is the subscription's, not a sliding seven days.
|
|
424
|
+
The monthly bucket is untouched: it follows the billing cycle.
|
|
425
|
+
|
|
426
|
+
The lesson is in `test/opendesign-window-and-cost.test.ts`: assert on computed
|
|
427
|
+
figures, never on declarations. A test that checks a constant is a test that the
|
|
428
|
+
constant was typed, not that anything uses it.
|
|
429
|
+
|
|
430
|
+
### Calibration, and reading OpenCode's own history
|
|
431
|
+
|
|
432
|
+
Two sources of truth now replace most of the guessing.
|
|
433
|
+
|
|
434
|
+
**Calibration reads the charges it already handled.** Every completed step carries
|
|
435
|
+
the provider's real charge, so the router can compare its own arithmetic with the
|
|
436
|
+
truth on each request. The safety margin becomes measured rather than assumed: a
|
|
437
|
+
fresh install has no evidence and keeps the wide default; after a handful of
|
|
438
|
+
agreeing requests the margin narrows to the error actually observed. It never
|
|
439
|
+
narrows below a floor, and a systematic disagreement keeps the wide margin and
|
|
440
|
+
raises a warning — that is a catalog problem, not something to absorb.
|
|
441
|
+
|
|
442
|
+
**OpenCode's own database has the history the ledger lacks.** Everything routed
|
|
443
|
+
before the router existed is recorded per message, with its own model, tokens,
|
|
444
|
+
cost and timestamp. That is strictly better than the session-level bootstrap this
|
|
445
|
+
replaced:
|
|
446
|
+
|
|
447
|
+
| | Session summary | Message history |
|
|
448
|
+
|---|---|---|
|
|
449
|
+
| Timestamp | the session's last update | each request's own |
|
|
450
|
+
| Model | whatever is selected now | the one that served it |
|
|
451
|
+
| Re-read | never | yes, idempotently |
|
|
452
|
+
|
|
453
|
+
The per-message timestamp is what matters for a fixed seven-day window: a session
|
|
454
|
+
summary smears a week of work onto one instant, so every request in it lands in
|
|
455
|
+
whichever window that instant fell into.
|
|
456
|
+
|
|
457
|
+
Read with `readOnly: true`, always. The module must be incapable of damaging a
|
|
458
|
+
user's history, and making a write impossible is cheaper than remembering not to.
|
|
459
|
+
`node:sqlite` is experimental and may be absent; every failure returns a reason
|
|
460
|
+
and the router routes without it.
|
|
461
|
+
|
|
462
|
+
Together these remove the recurring paste. What remains impossible to see is
|
|
463
|
+
usage from *other* clients on the same account, which is why the paste survives
|
|
464
|
+
as a one-time seed.
|
|
465
|
+
|
|
466
|
+
### Reading the account: paste any console page
|
|
467
|
+
|
|
468
|
+
There is no API for usage — upstream `nexu-io/open-design#8674` asks for one and
|
|
469
|
+
remains open, the account host answers 401 to an inference key, and the gateway
|
|
470
|
+
serves no usage route. So the console is the only source, and asking a person to
|
|
471
|
+
translate it is where the errors come from.
|
|
472
|
+
|
|
473
|
+
The panel therefore accepts **either** page and works out which it was given:
|
|
474
|
+
|
|
475
|
+
| Page | What it contributes | Writes? |
|
|
476
|
+
|---|---|---|
|
|
477
|
+
| **Plan & balance** | The percentage of the seven-day allowance, its reset time, the tier and the wallet | yes — seeds the baseline |
|
|
478
|
+
| **Usage** | Per-model tokens and charged credits for a date range | no — it is evidence about the catalog |
|
|
479
|
+
| **Billing** | Same shape as Usage when it lists charges | no |
|
|
480
|
+
|
|
481
|
+
Pasting a *usage* page is the more valuable of the two, because it verifies the
|
|
482
|
+
arithmetic instead of assuming it. For one model, `charged credits / published
|
|
483
|
+
price` is the multiplier the account actually applied: a 0.5x model charged
|
|
484
|
+
$0.15 for $0.30 of usage is confirmation, and a 1x model charged twice what the
|
|
485
|
+
price implies is a catalog bug — which is exactly how ten wrong prices were found
|
|
486
|
+
on 2026-11-04.
|
|
487
|
+
|
|
488
|
+
Reconciliation is deliberately advisory. One window of one account is not
|
|
489
|
+
evidence about every customer, so a disagreement is shown with the size of the
|
|
490
|
+
gap for a human to act on; it never silently adjusts prices.
|
|
491
|
+
|
|
492
|
+
The parser is tolerant of what a clipboard actually produces: cells copied one
|
|
493
|
+
per line, or a whole row on one line, with navigation text included. Two details
|
|
494
|
+
worth knowing, both found by these tests:
|
|
495
|
+
|
|
496
|
+
- A trailing `5.5` is part of a model name (`Claude Haiku 5.5`), not a
|
|
497
|
+
measurement. An inline row must carry a dollar amount or a real token count.
|
|
498
|
+
- A second dollar figure while a row is open is the `Total` line starting a new
|
|
499
|
+
row. Without that, the total overwrites the last model's charge and every
|
|
500
|
+
comparison after it is wrong in a way that looks like a catalog error.
|
|
501
|
+
|
|
502
|
+
### The control key is not obtainable through the product
|
|
503
|
+
|
|
504
|
+
Checked on 2026-11-04 against the account's API keys page. It offers exactly one
|
|
505
|
+
kind of key, `sk-amr-…`, described as connecting tools "to model services", and
|
|
506
|
+
every listed key carries the same prefix. There is no control-key type, no
|
|
507
|
+
account-scope option and no separate privilege to grant.
|
|
508
|
+
|
|
509
|
+
So the `controlKeyPath` support below is real but unreachable for an ordinary
|
|
510
|
+
customer: it exists for a credential this product does not issue. Do not present
|
|
511
|
+
it as the fix for estimated numbers, and do not tell users to go and find one.
|
|
512
|
+
The evidence says the account-control endpoints come from a different surface
|
|
513
|
+
than the one customers can authenticate against.
|
|
514
|
+
|
|
515
|
+
A control key is optional and only changes quota reporting from estimated to
|
|
516
|
+
verified. Supply it only if you accept that it is a privileged credential:
|
|
517
|
+
|
|
518
|
+
```json
|
|
519
|
+
{ "providers": { "opendesign": { "controlKeyPath": "~/.config/opencode/opendesign-control.key" } } }
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## 8. Reliability handling (also fixes MiniMax stream drops)
|
|
525
|
+
|
|
526
|
+
The router previously recorded `provider_outage` counts but never attributed
|
|
527
|
+
them to a model, and a stream that died after the response started was
|
|
528
|
+
classified `unknown`, which does not count against a model. A model that
|
|
529
|
+
repeatedly dropped streams therefore kept its perfect reliability record and
|
|
530
|
+
kept being selected.
|
|
531
|
+
|
|
532
|
+
Three changes fix this:
|
|
533
|
+
|
|
534
|
+
1. **Classification.** Mid-stream faults are recognized even when the HTTP
|
|
535
|
+
status is 200. This covers OpenCode's `provider.internal`
|
|
536
|
+
("Streaming response failed: [500] EngineCore encountered an issue") and
|
|
537
|
+
`provider.invalid-output` ("OpenAI Chat stream ended without
|
|
538
|
+
finish_reason") signatures. User interrupts are classified `interrupted`
|
|
539
|
+
and never count against a model.
|
|
540
|
+
2. **Attribution.** `session.execution.failed` and `session.idle` outcomes are
|
|
541
|
+
recorded against the model the host is actually serving, falling back to
|
|
542
|
+
the host's session record when the router has none.
|
|
543
|
+
3. **Cooldown and demotion.** A provider fault puts the model into a cooldown
|
|
544
|
+
that lengthens with each repeat, and reliability now weights the success
|
|
545
|
+
estimate more strongly. A model that keeps dropping streams is avoided even
|
|
546
|
+
after the cooldown expires.
|
|
547
|
+
|
|
548
|
+
Verified against a simulated mid-stream fault: the fault is classified
|
|
549
|
+
`provider_outage`, attributed to the model, and the session moves elsewhere on
|
|
550
|
+
the next prompt.
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## 9. Lifecycle guarantees
|
|
555
|
+
|
|
556
|
+
OpenDesign adds no second core, no extra polling loop, and no duplicate
|
|
557
|
+
registration:
|
|
558
|
+
|
|
559
|
+
- One shared core per server process, one event stream, one quota coordinator.
|
|
560
|
+
- Model discovery is single-flight with a five-minute TTL and runs only when
|
|
561
|
+
the provider is enabled.
|
|
562
|
+
- Quota probes go through the same per-provider backoff, single-flight and
|
|
563
|
+
retry-after handling as the other two providers.
|
|
564
|
+
- The estimated quota path makes **no network request at all**.
|
|
565
|
+
- A failed discovery keeps the last good catalog instead of dropping every
|
|
566
|
+
OpenDesign route.
|
|
567
|
+
- An unknown OpenDesign state never disables an unrelated provider.
|
|
568
|
+
- Teardown and revival follow the existing shared-core lifecycle.
|
|
569
|
+
|
|
570
|
+
Verified with three concurrent project bridges: one core, one event stream,
|
|
571
|
+
correct bridge counts, zero duplicate registrations.
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## 10. Test and canary evidence
|
|
576
|
+
|
|
577
|
+
`npx vitest run` reports **330 tests passing**. `tsc --noEmit` is clean.
|
|
578
|
+
|
|
579
|
+
Three pre-existing test failures were fixed on the way. The suite read the
|
|
580
|
+
developer's real `~/.config/opencode/smart-router.json` and merged it over the
|
|
581
|
+
settings each test intended to use, so the result depended on the machine the
|
|
582
|
+
tests ran on. The suite is now hermetic.
|
|
583
|
+
|
|
584
|
+
The isolated three-provider canary ran against a local mock that mirrors the
|
|
585
|
+
live gateway's response shape byte for byte, with a disposable HOME, config,
|
|
586
|
+
data and cache:
|
|
587
|
+
|
|
588
|
+
- Three providers registered and approved.
|
|
589
|
+
- A session routed to OpenDesign, one to GOAT, one to Go Plus.
|
|
590
|
+
- Credit multiplier 0.5× visible in the decision record.
|
|
591
|
+
- Cache affinity: a large-context session stayed on its provider.
|
|
592
|
+
- A simulated mid-stream fault was classified, attributed, and the session
|
|
593
|
+
moved off the model.
|
|
594
|
+
- A verified exhausted five-hour window with a $50 wallet present moved the
|
|
595
|
+
session off OpenDesign.
|
|
596
|
+
- Informational commands made zero inference requests.
|
|
597
|
+
- One core, one event stream, correct bridge counts.
|
|
598
|
+
|
|
599
|
+
Evidence and the 30-minute soak result are recorded in
|
|
600
|
+
`docs/evidence/opendesign-2026-10-28/`.
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
## 11. Rollback
|
|
605
|
+
|
|
606
|
+
The provider is inert unless `providers.opendesign.enabled` is true. To remove
|
|
607
|
+
it entirely:
|
|
608
|
+
|
|
609
|
+
```bash
|
|
610
|
+
jq '.providers.opendesign = {"enabled": false}' \
|
|
611
|
+
~/.config/opencode/smart-router.json > /tmp/sr.json && \
|
|
612
|
+
mv /tmp/sr.json ~/.config/opencode/smart-router.json
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
Then remove the `opendesign` block from `providers` in `opencode.json` and
|
|
616
|
+
reload. The plugin directory can be restored from the pre-deployment backup, or
|
|
617
|
+
reverted to the previous release artifact:
|
|
618
|
+
|
|
619
|
+
```bash
|
|
620
|
+
shasum -a 256 dist-release/index.js
|
|
621
|
+
# 8c71c46c5f7616f950c6a3fcf5c42be2198407848cf9ffac15d01504496f8920
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
Emergency rollback scripts from the previous release remain valid.
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
## 12. What still needs your decision
|
|
629
|
+
|
|
630
|
+
- Whether to enable the provider, and whether to supply a control key.
|
|
631
|
+
- Whether to approve the unapproved OpenDesign models
|
|
632
|
+
(`deepseek-v4-pro`, `deepseek-v4-flash`, `deepseek-v4-flash-vision-exp`,
|
|
633
|
+
`glm-5.3-flashx`, `hy4-preview`, `kimi-k2.7-code`).
|
|
634
|
+
- Confirming automatic recharge is disabled in the OpenDesign console.
|
|
635
|
+
- Authorising any real-provider test. None was run.
|