@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,866 @@
1
+ /**
2
+ * Shared domain types for the router.
3
+ *
4
+ * These types are intentionally provider-neutral. Provider adapters translate
5
+ * live OpenCode and provider metadata into these records.
6
+ */
7
+
8
+ import type { OfflineSettings } from "./offline/mode.js"
9
+
10
+ export type Mode = "efficiency" | "balanced" | "premium"
11
+ export type TaskTier = 0 | 1 | 2 | 3
12
+ export type RuntimeMode = "automatic" | "manual" | "paused"
13
+
14
+ /**
15
+ * How the router treats conversation size.
16
+ *
17
+ * - `native` never compacts except to avoid a verified overflow.
18
+ * - `conservative` preserves history; compacts only to prevent overflow.
19
+ * - `budget-optimized` the legacy aggressive strategy: compact early to keep
20
+ * a session on the cheapest provider. Kept for small
21
+ * plans, metered APIs and small-window models.
22
+ */
23
+ export type ContextManagementMode = "native" | "conservative" | "budget-optimized"
24
+
25
+ export type TaskKind =
26
+ | "deterministic"
27
+ | "mechanical"
28
+ | "feature"
29
+ | "debug"
30
+ | "refactor"
31
+ | "design"
32
+ | "architecture"
33
+ | "security"
34
+ | "test"
35
+ | "docs"
36
+ | "unknown"
37
+
38
+ export type FailureClass =
39
+ | "context_overflow"
40
+ | "rate_limit"
41
+ | "network"
42
+ | "auth"
43
+ | "provider_outage"
44
+ | "invalid_request"
45
+ | "image_rejected"
46
+ | "tool"
47
+ | "capability"
48
+ | "interrupted"
49
+ /**
50
+ * The provider refused because the account has no allowance left.
51
+ *
52
+ * Distinguished from `auth` (the credential is wrong) and from
53
+ * `rate_limit` (too fast): here the credential is fine and the request is
54
+ * well formed, there is simply no credit. It matters because it is the one
55
+ * reliable signal available for a provider whose quota cannot be read —
56
+ * retrying cannot help, and the provider should be left alone until its
57
+ * window resets.
58
+ */
59
+ | "quota_exhausted"
60
+ /**
61
+ * The provider rejected the request shape itself (an Anthropic-protocol
62
+ * discriminator failure is the observed case) before generating anything.
63
+ * Retrying is useless; the router repairs, or fails the turn over to the
64
+ * next best choice.
65
+ */
66
+ | "prompt_rejected"
67
+ | "unknown"
68
+
69
+ export interface CostTier {
70
+ readonly input: number
71
+ readonly output: number
72
+ readonly cache: { readonly read: number; readonly write: number }
73
+ readonly tier?: { readonly type: "context"; readonly size: number }
74
+ }
75
+
76
+ export interface PeakPricing {
77
+ /** Multiplier applied to every token rate inside the peak windows. */
78
+ readonly multiplier: number
79
+ /** UTC hour ranges [startHour, endHour); 0-24. */
80
+ readonly windowsUtc: ReadonlyArray<{ readonly startHour: number; readonly endHour: number }>
81
+ /** When true, peak pricing applies Monday-Friday only. */
82
+ readonly weekdaysOnly: boolean
83
+ }
84
+
85
+ export interface ModelRecord {
86
+ readonly key: string
87
+ readonly providerID: string
88
+ readonly modelID: string
89
+ readonly name: string
90
+ readonly context: number
91
+ readonly maxOutput: number
92
+ readonly inputModalities: ReadonlyArray<string>
93
+ readonly outputModalities: ReadonlyArray<string>
94
+ readonly tools: boolean
95
+ readonly cost: ReadonlyArray<CostTier>
96
+ /** Documented time-of-day pricing, when the provider changes rates. */
97
+ readonly peakPricing?: PeakPricing
98
+ /** Curated aggregate coding-intelligence score. Higher is better. */
99
+ readonly intelligence: number
100
+ /** Curated front-end and UX quality score. */
101
+ readonly design: number
102
+ /**
103
+ * Per-kind capability evidence, when measured benchmarks exist. Absent
104
+ * axes are unknown, never zero: an unscored model stays neutral in
105
+ * ranking rather than being treated as incapable.
106
+ */
107
+ readonly capabilities?: {
108
+ /** Agentic coding benchmark, percent. */
109
+ readonly coding?: number
110
+ /** Complex-debugging / automation benchmark, percent. */
111
+ readonly debugging?: number
112
+ /** Long-horizon autonomous-execution benchmark, percent. */
113
+ readonly agentic?: number
114
+ /** Curated architectural-reasoning score, same 0-100 scale. */
115
+ readonly architecture?: number
116
+ }
117
+ readonly designEvidence: "benchmark" | "curated" | "observed" | "unverified"
118
+ /**
119
+ * Credit multiplier applied to this model's usage value. One for every
120
+ * provider that bills in plain USD. OpenDesign uses it to convert the
121
+ * dollar value of model usage into Design Plan credits.
122
+ */
123
+ readonly creditMultiplier?: number
124
+ /** True when the model is funded from a pool reserved for escalation. */
125
+ readonly escalationOnly?: boolean
126
+ readonly premium: boolean
127
+ readonly training: boolean
128
+ readonly free: boolean
129
+ readonly tier: 1 | 2 | 3
130
+ /** False for discovered models that have not been qualified. */
131
+ readonly approved: boolean
132
+ readonly approvalSource: "seed" | "user" | "discovered"
133
+ /** Monthly included usage allowance in US dollars at public list prices. */
134
+ readonly monthlyAllowanceUsd: number
135
+ /**
136
+ * Which wire protocol the provider's serving path speaks. Seeded records
137
+ * may state it; `src/core/models/protocol.ts` otherwise infers it from the
138
+ * model family. Only the Anthropic Messages protocol forces prompt repair.
139
+ */
140
+ readonly protocol?: "anthropic" | "openai-compat"
141
+ readonly notes?: string
142
+ }
143
+
144
+ export interface ModelRef {
145
+ readonly providerID: string
146
+ readonly id: string
147
+ readonly variant?: string
148
+ }
149
+
150
+ export interface ModelKey {
151
+ readonly providerID: string
152
+ readonly modelID: string
153
+ }
154
+
155
+ export interface TaskScope {
156
+ readonly files: number
157
+ readonly codeBlocks: number
158
+ readonly longestCodeBlockLines: number
159
+ readonly hasStackTrace: boolean
160
+ readonly hasImage: boolean
161
+ readonly hasScreenshot: boolean
162
+ readonly promptChars: number
163
+ readonly promptTokens: number
164
+ }
165
+
166
+ export interface TaskConstraints {
167
+ /** The user explicitly asked to avoid premium or expensive models. */
168
+ readonly avoidPremium: boolean
169
+ /** The user explicitly asked for the strongest model. */
170
+ readonly requestPremium: boolean
171
+ /** The user explicitly asked for a cheap/economy model. */
172
+ readonly requestEconomy: boolean
173
+ }
174
+
175
+ export interface TaskClass {
176
+ readonly tier: TaskTier
177
+ readonly kind: TaskKind
178
+ readonly requiresVision: boolean
179
+ readonly designIntensive: boolean
180
+ readonly sophisticatedDesign: boolean
181
+ readonly needsLongContext: boolean
182
+ readonly confidence: "low" | "medium" | "high"
183
+ readonly signals: ReadonlyArray<string>
184
+ readonly reasons: ReadonlyArray<string>
185
+ readonly constraints: TaskConstraints
186
+ readonly scope: TaskScope
187
+ }
188
+
189
+ export interface CandidateEvaluation {
190
+ readonly model: ModelKey
191
+ readonly name: string
192
+ readonly tier: 1 | 2 | 3
193
+ readonly eligible: boolean
194
+ readonly reason: string
195
+ readonly estimatedUsd: number
196
+ readonly effectiveCost: number
197
+ readonly successProbability: number
198
+ readonly switchPenaltyUsd: number
199
+ readonly quotaPressure: number
200
+ readonly retained: boolean
201
+ /** Credit multiplier applied to the estimate. One for plain USD providers. */
202
+ readonly creditMultiplier?: number
203
+ /** Multiplier applied for measured generation speed. One when neutral. */
204
+ readonly speedFactor?: number
205
+ /**
206
+ * Multiplier applied to the price because the provider's prepaid allowance is
207
+ * expiring. One when the allowance is fresh money that will be consumed.
208
+ */
209
+ readonly marginalPriceFactor?: number
210
+ }
211
+
212
+ export interface QuotaWindow {
213
+ readonly usedPct: number
214
+ readonly remainingPct: number
215
+ readonly usedUsd?: number
216
+ readonly capUsd?: number
217
+ readonly resetsAt?: number
218
+ readonly exceeded?: boolean
219
+ readonly source: "provider" | "estimate" | "unknown"
220
+ }
221
+
222
+ export interface ModelQuotaSnapshot {
223
+ readonly model: ModelKey
224
+ readonly monthlyAllowanceUsd: number
225
+ readonly monthlyUsedUsd: number
226
+ readonly monthlyRemainingUsd: number
227
+ readonly monthlyUsedPct: number
228
+ readonly fiveHourUsedUsd: number
229
+ readonly fiveHourCapUsd: number
230
+ readonly fiveHourUsedPct: number
231
+ readonly weeklyUsedUsd: number
232
+ readonly weeklyCapUsd: number
233
+ readonly weeklyUsedPct: number
234
+ readonly blocked: boolean
235
+ readonly blockReason?: string
236
+ readonly source: "provider" | "estimate" | "unknown"
237
+ }
238
+
239
+ export interface ExtraCredits {
240
+ readonly purchasedUsd: number
241
+ readonly freeUsd: number
242
+ readonly totalUsd: number
243
+ }
244
+
245
+ /**
246
+ * One funding pool belonging to a plan.
247
+ *
248
+ * Pools are separate allowances. `monthlyUsd` is the pool's own ceiling; the
249
+ * weekly and five-hour values nest inside it and are never added to it. A
250
+ * pool with no window values is metered per billing cycle only.
251
+ */
252
+ export interface CreditPoolSnapshot {
253
+ readonly id: string
254
+ readonly label: string
255
+ readonly source: "provider" | "estimate" | "unknown"
256
+ readonly monthlyUsd: number
257
+ readonly usedUsd: number
258
+ readonly remainingUsd: number
259
+ readonly usedPct: number
260
+ readonly weeklyUsd?: number
261
+ readonly weeklyUsedPct?: number
262
+ readonly fiveHourUsd?: number
263
+ readonly fiveHourUsedPct?: number
264
+ readonly resetKind: "billing-cycle" | "fixed-7-day" | "rolling-7-day"
265
+ readonly resetsAt?: number
266
+ readonly note: string
267
+ }
268
+
269
+ export interface ProviderQuotaSnapshot {
270
+ readonly providerID: string
271
+ readonly label: string
272
+ readonly plan: string
273
+ readonly source: "provider" | "estimate" | "unknown"
274
+ readonly fetchedAt: number
275
+ readonly stale: boolean
276
+ readonly rolling: QuotaWindow
277
+ readonly weekly: QuotaWindow
278
+ readonly monthly: QuotaWindow
279
+ readonly monthlyGrantUsd?: number
280
+ readonly monthlyRemainingUsd?: number
281
+ readonly extraCredits?: ExtraCredits
282
+ /** Every funding pool the plan exposes, in draw order. */
283
+ readonly pools?: ReadonlyArray<CreditPoolSnapshot>
284
+ readonly planStatus?: string
285
+ readonly periodStart?: number
286
+ readonly periodEnd?: number
287
+ readonly models: Readonly<Record<string, ModelQuotaSnapshot>>
288
+ readonly notes: ReadonlyArray<string>
289
+ readonly error?: string
290
+ /** Provider-supplied retry delay on a rate-limited probe. */
291
+ readonly retryAfterMs?: number
292
+ /**
293
+ * Explicit confidence state. `provider` and `estimate` map onto `source`;
294
+ * this field also distinguishes an exhausted allowance from an unavailable
295
+ * service, which `source` alone cannot express.
296
+ */
297
+ readonly quotaState?: "verified" | "estimated" | "stale" | "unknown" | "exhausted" | "unavailable"
298
+ }
299
+
300
+ export interface PremiumState {
301
+ readonly mode: "off" | "ask" | "allowed" | "auto"
302
+ readonly monthlyBudgetUsd: number
303
+ readonly spentUsd: number
304
+ readonly remainingUsd: number
305
+ /**
306
+ * Remaining plan-model pool allowance for the month. In auto mode this, not
307
+ * `remainingUsd`, is what escalation draws against; undefined when the
308
+ * caller has no pool identity for the model in play.
309
+ */
310
+ readonly planPoolRemainingUsd?: number
311
+ readonly oneShotGranted: boolean
312
+ readonly oneShotGrantedAt?: number
313
+ }
314
+
315
+ export interface RoutingDecision {
316
+ readonly id: string
317
+ readonly at: number
318
+ readonly sessionID: string
319
+ readonly mode: Mode
320
+ readonly dryRun: boolean
321
+ readonly runtime: RuntimeMode
322
+ readonly task: TaskClass
323
+ readonly current?: ModelRef
324
+ readonly chosen?: ModelRef
325
+ readonly switched: boolean
326
+ readonly switchReason: string
327
+ readonly reasons: ReadonlyArray<string>
328
+ readonly warnings: ReadonlyArray<string>
329
+ readonly candidates: ReadonlyArray<CandidateEvaluation>
330
+ readonly premium: PremiumState
331
+ readonly quota: ReadonlyArray<ProviderQuotaSnapshot>
332
+ readonly contextTokens: number
333
+ readonly cachedTokens: number
334
+ readonly estimatedUsd: number
335
+ readonly needsPremiumApproval: boolean
336
+ readonly manualOverride: boolean
337
+ readonly error?: string
338
+ /**
339
+ * Present when the router recovered a deterministic provider rejection for
340
+ * this prompt: which model rejected the request and what happened next.
341
+ */
342
+ readonly selfheal?: { readonly rejected: string; readonly action: string }
343
+ }
344
+
345
+ export interface ModelFailureRecord {
346
+ readonly class: FailureClass
347
+ readonly at: number
348
+ readonly message: string
349
+ }
350
+
351
+ export interface SessionState {
352
+ readonly sessionID: string
353
+ readonly parentID?: string
354
+ readonly projectID?: string
355
+ readonly directory?: string
356
+ manualOverride: boolean
357
+ manualModel?: ModelRef
358
+ pinnedModel?: ModelRef
359
+ premiumOneShot: boolean
360
+ premiumOneShotAt?: number
361
+ paused: boolean
362
+ lastModel?: ModelRef
363
+ lastRoutedModel?: ModelRef
364
+ lastDecisionID?: string
365
+ /** Original user or generated title, without the router status prefix. */
366
+ baseTitle?: string
367
+ /** Last title written by the router, used to avoid update loops. */
368
+ indicatorTitle?: string
369
+ contextTokens: number
370
+ cacheReadTokens: number
371
+ stepCount: number
372
+ status: "idle" | "busy" | "unknown"
373
+ failures: ReadonlyArray<ModelFailureRecord>
374
+ retries: number
375
+ createdAt: number
376
+ updatedAt: number
377
+ title?: string
378
+ }
379
+
380
+ export interface UsageEvent {
381
+ readonly id: string
382
+ readonly at: number
383
+ readonly providerID: string
384
+ readonly modelID: string
385
+ readonly sessionID?: string
386
+ readonly usd: number
387
+ readonly input: number
388
+ readonly output: number
389
+ readonly cacheRead: number
390
+ readonly cacheWrite: number
391
+ readonly source: "step" | "bootstrap" | "manual"
392
+ }
393
+
394
+ export interface ModelMetrics {
395
+ requests: number
396
+ input: number
397
+ output: number
398
+ cacheRead: number
399
+ cacheWrite: number
400
+ usd: number
401
+ failures: number
402
+ /** Output tokens produced per second of generation, when measurable. */
403
+ outputTokensPerSecond: number
404
+ /** Milliseconds from request start to a completed response, when measurable. */
405
+ latencyMsTotal: number
406
+ latencySamples: number
407
+ timeoutFailures: number
408
+ }
409
+
410
+ export interface Metrics {
411
+ startedAt: number
412
+ requests: number
413
+ inputTokens: number
414
+ cacheReadTokens: number
415
+ cacheWriteTokens: number
416
+ outputTokens: number
417
+ estimatedUsd: number
418
+ switches: number
419
+ escalations: number
420
+ retries: number
421
+ preventedPremium: number
422
+ manualOverrides: number
423
+ dryRunDecisions: number
424
+ premiumRequests: number
425
+ premiumUsd: number
426
+ /** Lifetime spend against the plan-model pool (escalation allowance). */
427
+ planPoolUsd: number
428
+ latencyMsTotal: number
429
+ latencySamples: number
430
+ lastLatencyMs?: number
431
+ failures: Record<FailureClass, number>
432
+ byModel: Record<string, ModelMetrics>
433
+ /** Request-level latency samples, separate from per-model aggregates. */
434
+ timeouts: number
435
+ }
436
+
437
+ /**
438
+ * A durable per-hour usage bucket.
439
+ *
440
+ * Retaining thousands of raw events in the ledger does not scale: 2,000
441
+ * events covered under seven hours of heavy use, which is useless for weekly
442
+ * or monthly forecasting. Events are folded into these buckets as they age,
443
+ * so long-horizon aggregation costs a bounded number of rows instead of
444
+ * growing without limit, and the totals survive restarts.
445
+ */
446
+ export interface UsageBucket {
447
+ /** Hour start, epoch milliseconds, truncated to the hour. */
448
+ readonly hour: number
449
+ readonly providerID: string
450
+ readonly modelID: string
451
+ /** Billing period the bucket belongs to, so plan changes are separable. */
452
+ readonly periodID: string
453
+ requests: number
454
+ input: number
455
+ output: number
456
+ cacheRead: number
457
+ cacheWrite: number
458
+ usd: number
459
+ failures: number
460
+ latencyMsTotal: number
461
+ latencySamples: number
462
+ }
463
+
464
+ export interface UsageRollup {
465
+ readonly version: 1
466
+ buckets: UsageBucket[]
467
+ /** Raw events newer than the fold horizon, retained for exact per-request detail. */
468
+ recent: UsageEvent[]
469
+ updatedAt: number
470
+ }
471
+
472
+ export interface Ledger {
473
+ version: 1
474
+ bootstrappedAt?: number
475
+ bootstrapSource?: string
476
+ usage: UsageEvent[]
477
+ /** Hourly aggregates for anything older than the raw-event horizon. */
478
+ usageRollup?: UsageRollup
479
+ premiumSpend: Array<{ month: string; usd: number }>
480
+ /**
481
+ * Monthly spend against the plan-model pool (the escalation allowance),
482
+ * separate from `premiumSpend` so the pool gate reads exactly what the
483
+ * pool funded. The hydrator always materializes this array, so ledgers
484
+ * written before the field existed read as zero spend.
485
+ */
486
+ planPoolSpend: Array<{ month: string; usd: number }>
487
+ sessions: Record<string, SessionState>
488
+ decisions: RoutingDecision[]
489
+ metrics: Metrics
490
+ /** Model key to the epoch millisecond when its failure cooldown ends. */
491
+ cooldowns?: Record<string, number>
492
+ /** Provider id to the epoch millisecond when its outage backoff ends. */
493
+ providerCooldowns?: Record<string, number>
494
+ updatedAt: number
495
+ }
496
+
497
+ export interface ProviderSettings {
498
+ readonly enabled: boolean
499
+ /**
500
+ * Plan identifier resolved against the plan catalog. A plan is described as
501
+ * data, so a different user's tier needs no code change.
502
+ */
503
+ readonly plan: string
504
+ readonly reserveMonthlyPct: number
505
+ readonly reserveWeeklyPct: number
506
+ readonly reserveFiveHourPct: number
507
+ readonly allowExtraUsage: boolean
508
+ readonly probeUsage: boolean
509
+ /**
510
+ * Explicit caps. Each overrides the plan catalog value when set, which is
511
+ * how a user can correct a stale catalog entry without a code change.
512
+ */
513
+ readonly monthlyCreditsUsd?: number
514
+ readonly fiveHourCapUsd?: number
515
+ readonly weeklyCapUsd?: number
516
+ /**
517
+ * When set, every pool is treated as having no window ceiling of the given
518
+ * kind, regardless of what the catalog says. Used to model a plan that has
519
+ * advertised the removal of a window.
520
+ */
521
+ readonly ignoreFiveHourWindow?: boolean
522
+ readonly ignoreWeeklyWindow?: boolean
523
+ /** Optional explicit credential file for a read-only quota probe. */
524
+ readonly credentialPath?: string
525
+ /**
526
+ * A reading of the account's own usage, supplied by the user, that the router
527
+ * adds its own traffic to.
528
+ *
529
+ * This exists because OpenDesign publishes no quota endpoint for an inference
530
+ * key and the router refuses to scrape a browser session. Without a control
531
+ * key the best available number was "traffic this router routed", which
532
+ * under-reports badly on any account that was used before the router was
533
+ * installed — one user saw 5% while the account was 42% used.
534
+ *
535
+ * A baseline is not a guess: it is a figure the user read off their own
536
+ * account, with the time they read it. The router labels the result as
537
+ * "reported by your account at <time> plus this router's traffic" and drops it
538
+ * once the relevant window has reset, because a stale reading inside a fresh
539
+ * window would be a new lie in the other direction.
540
+ */
541
+ readonly baseline?: {
542
+ /** Epoch ms when the user read these figures. */
543
+ readonly at: number
544
+ /** Design Plan credits used at that moment, in USD-equivalent. */
545
+ readonly designPlanUsedUsd?: number
546
+ /**
547
+ * Design Plan credits used at that moment, as the percentage the account
548
+ * page displays.
549
+ *
550
+ * The page shows a percentage, not dollars, so asking for a dollar figure
551
+ * asks the user to do conversion arithmetic against a cap they may not know.
552
+ * Taking the percentage and converting it here removes that step and the
553
+ * mistake it invites — reading "42%" as 42% of the month when the page means
554
+ * 42% of the week overstates usage fourfold.
555
+ */
556
+ readonly designPlanUsedPct?: number
557
+ /** Plan model credits used at that moment, as the percentage shown. */
558
+ readonly planModelsUsedPct?: number
559
+ /**
560
+ * Which window a supplied percentage refers to. Defaults to `weekly`,
561
+ * because that is the window OpenDesign's account page reports.
562
+ */
563
+ readonly pctOf?: "weekly" | "monthly"
564
+ /** Plan model credits used at that moment, in USD-equivalent. */
565
+ readonly planModelsUsedUsd?: number
566
+ /**
567
+ * When the seven-day window resets, if the account page showed it.
568
+ *
569
+ * Without it the router has to guess, and the guess is wrong in a way that
570
+ * matters: "the reading is younger than seven days" is not the same as "the
571
+ * window has not reset". A weekly window can reset an hour after you read
572
+ * the page, after which your 42% describes a window that no longer exists
573
+ * and seeding it into the new one overstates usage.
574
+ */
575
+ readonly weeklyResetsAt?: number
576
+ /** When the monthly allowance resets, if the account page showed it. */
577
+ readonly monthlyResetsAt?: number
578
+ /** Where the reading came from, for display. Defaults to the account page. */
579
+ readonly source?: string
580
+ }
581
+ /**
582
+ * Whether the router may read well-known credential files without being
583
+ * told exactly where. `explicit` (the default) reads only configured paths
584
+ * and the environment; `auto` also scans the provider's own files; `off`
585
+ * reads nothing from disk.
586
+ *
587
+ * The default is deliberately conservative: automatically harvesting
588
+ * secrets from predictable paths is not something a router should do
589
+ * without the operator saying so.
590
+ */
591
+ readonly credentialSource?: "explicit" | "auto" | "off"
592
+ /**
593
+ * Gateway base URL. Only providers with a configurable endpoint use it.
594
+ * OpenDesign accepts the OpenAI-compatible gateway root; a path that
595
+ * already contains the version is never extended again.
596
+ */
597
+ readonly baseURL?: string
598
+ /** Account-control base URL, used only for an explicitly supplied control key. */
599
+ readonly controlBaseURL?: string
600
+ /** Optional explicit file holding an account-control key. */
601
+ readonly controlKeyPath?: string
602
+ /**
603
+ * Refuse to route when the subscription allowance is exhausted, so the
604
+ * gateway cannot fall back to purchased wallet balance.
605
+ */
606
+ readonly subscriptionOnly?: boolean
607
+ /** Credits per USD, when the provider denominates its allowance in credits. */
608
+ readonly creditsPerUsd?: number
609
+ /** Safety margin applied to locally estimated usage, in percent. */
610
+ readonly uncertaintyMarginPct?: number
611
+ /** What to do when the provider's quota state cannot be established. */
612
+ readonly unknownQuotaPolicy?: "route" | "block" | "ask"
613
+ /** Explicit per-model credit multiplier overrides. */
614
+ readonly multiplierOverrides?: Readonly<Record<string, number>>
615
+ /**
616
+ * True when the provider's allowance is one shared pool rather than a
617
+ * per-model allowance. Per-model quota snapshots are skipped in that case,
618
+ * because they would count the same pool more than once.
619
+ */
620
+ readonly providerScopedQuota?: boolean
621
+ }
622
+
623
+ export interface PrivacySettings {
624
+ readonly allowTrainingModels: boolean
625
+ readonly allowFreeModels: boolean
626
+ readonly requireZdr: boolean
627
+ }
628
+
629
+ export interface PremiumSettings {
630
+ readonly mode: "off" | "ask" | "allowed" | "auto"
631
+ readonly monthlyBudgetUsd: number
632
+ readonly oneShotApproval: boolean
633
+ }
634
+
635
+ export interface RoutingSettings {
636
+ readonly intelligenceFloor: number
637
+ readonly designFloor: number
638
+ readonly sophisticatedDesignFloor: number
639
+ readonly switchHysteresisPct: number
640
+ readonly largeContextTokens: number
641
+ readonly largeContextHysteresisPct: number
642
+ readonly maxEscalationsPerTask: number
643
+ readonly preferSameProvider: boolean
644
+ /**
645
+ * Block a prompt when this plugin cannot produce a routing decision.
646
+ *
647
+ * Default false, and that default is the point: routing is an optimization,
648
+ * and a defect inside the router must degrade to "do not route", never to
649
+ * "do not work". An internal error leaves the current model in place and lets
650
+ * the prompt through.
651
+ *
652
+ * This does NOT govern quota safety. A provider that is configured to be
653
+ * probed but has no reading is always refused, because that is a spending
654
+ * decision about a provider the router cannot see, not a response to its own
655
+ * failure. Set this true only if you would rather be stopped than pay for an
656
+ * unrouted request.
657
+ */
658
+ readonly failClosed: boolean
659
+ /**
660
+ * Refuse to route a task to a model funded from a pool reserved for
661
+ * escalation while a design-plan-funded model can serve it. This keeps a
662
+ * plan-model pool from being drained by routine work.
663
+ */
664
+ readonly protectEscalationPool: boolean
665
+ /** Minimum model window considered adequate when a task needs long context. */
666
+ readonly longContextFloorTokens: number
667
+ /**
668
+ * Derive the intelligence floor from recorded spend instead of trusting the
669
+ * hand-picked `intelligenceFloor`.
670
+ *
671
+ * The configured floor remains the ceiling: calibration may only lower it.
672
+ * This is opt-in because it changes which model serves real work, and a
673
+ * router that silently re-derives its own quality gate on upgrade would be
674
+ * surprising.
675
+ */
676
+ readonly calibrateFloor?: boolean
677
+ }
678
+
679
+ export interface QuotaSettings {
680
+ readonly probeOpenCodeGo: boolean
681
+ readonly probeCommandCode: boolean
682
+ readonly probeOpenDesign: boolean
683
+ readonly probeIntervalMinutes: number
684
+ readonly bootstrapSessions: number
685
+ readonly bootstrapDays: number
686
+ }
687
+
688
+ export interface ContextHealthSettings {
689
+ readonly enabled: boolean
690
+ readonly warnAtTokens: number
691
+ readonly recommendAtTokens: number
692
+ }
693
+
694
+ /**
695
+ * Router-driven context management.
696
+ *
697
+ * `compactAtTokens` is only consulted in `budget-optimized` mode. It exists so
698
+ * a budget-conscious user can set an explicit early threshold; it is
699
+ * deliberately ignored in the other two modes so a leftover value from an old
700
+ * configuration can never reintroduce eager compaction.
701
+ */
702
+ export interface ContextManagementSettings {
703
+ readonly mode: ContextManagementMode
704
+ readonly enabled: boolean
705
+ readonly compactAtTokens: number
706
+ /** Minimum time between router-initiated compactions for one session. */
707
+ readonly cooldownMinutes: number
708
+ /**
709
+ * When `native` mode hits the verified window, allow one emergency
710
+ * compaction rather than failing the turn. Disabling it means the host's own
711
+ * handling is the only recovery.
712
+ */
713
+ readonly allowEmergencyCompaction: boolean
714
+ }
715
+
716
+ /** Measured-speed awareness in routing. */
717
+ export interface SpeedSettings {
718
+ readonly enabled: boolean
719
+ /**
720
+ * How much a slow route is penalised relative to the fastest eligible one.
721
+ * Zero treats speed as irrelevant; one doubles the effective cost of a
722
+ * route that is twice as slow as the fastest.
723
+ */
724
+ readonly weightPct: number
725
+ /** Samples required before measured speed is trusted over a neutral prior. */
726
+ readonly minSamples: number
727
+ /** A route slower than this multiple of the fastest is heavily penalised. */
728
+ readonly slowRouteMultiple: number
729
+ }
730
+
731
+ export interface UiSettings {
732
+ /** Send a native macOS notification when the provider or model changes. */
733
+ readonly notifications: boolean
734
+ /** Use a shorter, denser indicator title and UI chrome (compact mode). */
735
+ readonly compactMode: boolean
736
+ /** Alias for compactMode. */
737
+ readonly compact: boolean
738
+ }
739
+
740
+ export interface LoggingSettings {
741
+ readonly storePromptText: boolean
742
+ readonly decisionLogSize: number
743
+ readonly diagnostics: boolean
744
+ }
745
+
746
+ export interface ModelDefinition {
747
+ readonly key: string
748
+ readonly providerID: string
749
+ readonly modelID: string
750
+ readonly name: string
751
+ readonly context: number
752
+ readonly maxOutput: number
753
+ readonly input: ReadonlyArray<string>
754
+ readonly cost: ReadonlyArray<CostTier>
755
+ readonly intelligence: number
756
+ readonly design: number
757
+ readonly premium?: boolean
758
+ readonly training?: boolean
759
+ readonly free?: boolean
760
+ readonly tier: 1 | 2 | 3
761
+ readonly approved?: boolean
762
+ readonly monthlyAllowanceUsd: number
763
+ }
764
+
765
+ export interface ModelSettings {
766
+ readonly approved: ReadonlyArray<string>
767
+ readonly excluded: ReadonlyArray<string>
768
+ readonly tierOverrides: Readonly<Record<string, 1 | 2 | 3>>
769
+ /** User-defined metadata for qualifying models that are not in the seed. */
770
+ readonly definitions: ReadonlyArray<ModelDefinition>
771
+ }
772
+
773
+ export interface RouterSettings {
774
+ readonly enabled: boolean
775
+ readonly mode: Mode
776
+ readonly dryRun: boolean
777
+ /** Top-level alias for ui.compactMode (compact mode toggle). */
778
+ readonly compactMode?: boolean
779
+ /** Top-level alias for ui.compactMode (compact mode toggle). */
780
+ readonly compact?: boolean
781
+ readonly providers: Readonly<Record<string, ProviderSettings>>
782
+ readonly privacy: PrivacySettings
783
+ readonly premium: PremiumSettings
784
+ readonly routing: RoutingSettings
785
+ readonly quota: QuotaSettings
786
+ readonly contextHealth: ContextHealthSettings
787
+ readonly contextManagement: ContextManagementSettings
788
+ readonly speed: SpeedSettings
789
+ readonly ui: UiSettings
790
+ readonly logging: LoggingSettings
791
+ readonly models: ModelSettings
792
+ /**
793
+ * Offline development mode. Absent means the defaults, which keep it off.
794
+ *
795
+ * This is a separate top-level block rather than a field inside `routing`
796
+ * so that enabling offline mode cannot alter a single online routing value.
797
+ */
798
+ readonly offline?: OfflineSettings
799
+ }
800
+
801
+ export interface ContextHealth {
802
+ readonly tokens: number
803
+ readonly cacheRead: number
804
+ readonly cacheSharePct: number
805
+ readonly state: "healthy" | "watch" | "recommend_new_session"
806
+ readonly message: string
807
+ }
808
+
809
+ export interface RouterStatus {
810
+ readonly enabled: boolean
811
+ readonly mode: Mode
812
+ readonly runtime: RuntimeMode
813
+ readonly dryRun: boolean
814
+ readonly sessionID?: string
815
+ readonly current?: ModelRef
816
+ readonly currentTier?: 1 | 2 | 3
817
+ readonly manualOverride: boolean
818
+ readonly pinnedModel?: ModelRef
819
+ readonly premium: PremiumState
820
+ readonly quota: ReadonlyArray<ProviderQuotaSnapshot>
821
+ readonly context: ContextHealth
822
+ readonly indicator: {
823
+ readonly state: "AUTO" | "MANUAL" | "PAUSED" | "DRY RUN" | "BLOCKED"
824
+ readonly title?: string
825
+ readonly provider?: string
826
+ readonly model?: string
827
+ readonly modelLabel?: string
828
+ }
829
+ readonly lastDecision?: RoutingDecision
830
+ readonly lastDecisions: ReadonlyArray<RoutingDecision>
831
+ readonly metrics: Metrics
832
+ readonly approvedModels: ReadonlyArray<{
833
+ key: string
834
+ tier: 1 | 2 | 3
835
+ name: string
836
+ intelligence: number
837
+ design: number
838
+ vision: boolean
839
+ premium: boolean
840
+ }>
841
+ /**
842
+ * Every model in the catalog, with the facts a routing decision turns on.
843
+ *
844
+ * The panel needs to answer "why is this model not being used" without the
845
+ * user reading the source. That means carrying approval and exclusion, not
846
+ * just the approved subset, plus the cost and window that drove the ranking.
847
+ */
848
+ readonly models: ReadonlyArray<{
849
+ readonly key: string
850
+ readonly providerID: string
851
+ readonly name: string
852
+ readonly tier: 1 | 2 | 3
853
+ readonly intelligence: number
854
+ readonly design: number
855
+ readonly vision: boolean
856
+ readonly premium: boolean
857
+ readonly context: number
858
+ readonly costInput: number
859
+ readonly costOutput: number
860
+ readonly approved: boolean
861
+ readonly excluded: boolean
862
+ /** Why it is not approved, when that is a decision rather than a gap. */
863
+ readonly approvalNote?: string
864
+ }>
865
+ readonly warnings: ReadonlyArray<string>
866
+ }