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

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