@qadanm/opencode-smart-router 0.0.0-stage → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +286 -0
  2. package/CONTRIBUTING.md +103 -0
  3. package/LICENSE +21 -0
  4. package/README.md +425 -2
  5. package/SECURITY.md +174 -0
  6. package/config.example.json +107 -0
  7. package/dist/npm/index.js +40 -0
  8. package/docs/ARCHITECTURE.md +104 -0
  9. package/docs/DEBUGGING-GUI.md +70 -0
  10. package/docs/INSTALL.md +151 -0
  11. package/docs/LIMITATIONS.md +230 -0
  12. package/docs/MODELS.md +124 -0
  13. package/docs/OPENDESIGN-INTEGRATION.md +635 -0
  14. package/docs/QA.md +142 -0
  15. package/docs/SHARED-CORE.md +114 -0
  16. package/install +75 -0
  17. package/package.json +94 -4
  18. package/scripts/install.sh +324 -0
  19. package/scripts/uninstall.sh +91 -0
  20. package/src/allowance.ts +135 -0
  21. package/src/core/classify/classifier.ts +305 -0
  22. package/src/core/config-writer.ts +167 -0
  23. package/src/core/config.ts +1047 -0
  24. package/src/core/context/strategy.ts +188 -0
  25. package/src/core/context-health.ts +39 -0
  26. package/src/core/failures.ts +398 -0
  27. package/src/core/history/opencode.ts +173 -0
  28. package/src/core/models/protocol.ts +22 -0
  29. package/src/core/models/registry.ts +204 -0
  30. package/src/core/models/seed.ts +781 -0
  31. package/src/core/offline/mode.ts +321 -0
  32. package/src/core/plan-page.ts +390 -0
  33. package/src/core/plans/catalog.ts +487 -0
  34. package/src/core/policy/calibration.ts +259 -0
  35. package/src/core/policy/eligibility.ts +630 -0
  36. package/src/core/providers/enablement.ts +143 -0
  37. package/src/core/providers/opendesign-credits.ts +206 -0
  38. package/src/core/providers/opendesign-models.ts +654 -0
  39. package/src/core/providers/opendesign.ts +474 -0
  40. package/src/core/quota/calibration.ts +166 -0
  41. package/src/core/quota/commandcode.ts +471 -0
  42. package/src/core/quota/credentials.ts +490 -0
  43. package/src/core/quota/ledger.ts +1304 -0
  44. package/src/core/quota/opencode-go.ts +148 -0
  45. package/src/core/quota/opendesign.ts +1043 -0
  46. package/src/core/quota/reconcile.ts +155 -0
  47. package/src/core/quota/report.ts +261 -0
  48. package/src/core/quota/service.ts +273 -0
  49. package/src/core/reprompt.ts +48 -0
  50. package/src/core/routing/engine.ts +978 -0
  51. package/src/core/types.ts +866 -0
  52. package/src/core/ui.ts +236 -0
  53. package/src/core/util.ts +114 -0
  54. package/src/gui/index.tsx +1675 -0
  55. package/src/gui/viewmodel.ts +456 -0
  56. package/src/index.ts +33 -0
  57. package/src/rpc.ts +204 -0
  58. package/src/server/containment.ts +131 -0
  59. package/src/server/controller.ts +3405 -0
  60. package/src/server/route-tracker.ts +60 -0
  61. package/src/server/shared-core.ts +701 -0
  62. 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.