@qadanm/opencode-smart-router 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/CONTRIBUTING.md +96 -0
- package/LICENSE +21 -0
- package/README.md +341 -2
- package/SECURITY.md +174 -0
- package/config.example.json +108 -0
- package/dist/allowance.js +487 -0
- package/dist/gui-experimental.cjs +1 -0
- package/dist/index.js +11111 -0
- package/dist/npm/index.js +40 -0
- package/dist/tui.js +587 -0
- package/docs/ARCHITECTURE.md +104 -0
- package/docs/DEBUGGING-GUI.md +70 -0
- package/docs/INSTALL.md +120 -0
- package/docs/LIMITATIONS.md +213 -0
- package/docs/MODELS.md +124 -0
- package/docs/OPENDESIGN-INTEGRATION.md +635 -0
- package/docs/QA.md +142 -0
- package/docs/SHARED-CORE.md +72 -0
- package/install +75 -0
- package/package.json +92 -4
- package/scripts/install.sh +324 -0
- package/scripts/uninstall.sh +91 -0
- package/src/allowance.ts +135 -0
- package/src/core/classify/classifier.ts +305 -0
- package/src/core/config-writer.ts +167 -0
- package/src/core/config.ts +1030 -0
- package/src/core/context/strategy.ts +188 -0
- package/src/core/context-health.ts +39 -0
- package/src/core/failures.ts +398 -0
- package/src/core/history/opencode.ts +173 -0
- package/src/core/models/protocol.ts +22 -0
- package/src/core/models/registry.ts +203 -0
- package/src/core/models/seed.ts +771 -0
- package/src/core/offline/mode.ts +321 -0
- package/src/core/plan-page.ts +390 -0
- package/src/core/plans/catalog.ts +481 -0
- package/src/core/policy/calibration.ts +259 -0
- package/src/core/policy/eligibility.ts +578 -0
- package/src/core/providers/enablement.ts +143 -0
- package/src/core/providers/opendesign-credits.ts +206 -0
- package/src/core/providers/opendesign-models.ts +628 -0
- package/src/core/providers/opendesign.ts +474 -0
- package/src/core/quota/calibration.ts +166 -0
- package/src/core/quota/commandcode.ts +471 -0
- package/src/core/quota/credentials.ts +490 -0
- package/src/core/quota/ledger.ts +1273 -0
- package/src/core/quota/opencode-go.ts +148 -0
- package/src/core/quota/opendesign.ts +1043 -0
- package/src/core/quota/reconcile.ts +155 -0
- package/src/core/quota/report.ts +261 -0
- package/src/core/quota/service.ts +273 -0
- package/src/core/reprompt.ts +48 -0
- package/src/core/routing/engine.ts +919 -0
- package/src/core/types.ts +825 -0
- package/src/core/ui.ts +236 -0
- package/src/core/util.ts +114 -0
- package/src/gui/index.tsx +1670 -0
- package/src/gui/viewmodel.ts +456 -0
- package/src/index.ts +48 -0
- package/src/rpc.ts +204 -0
- package/src/server/controller.ts +3281 -0
- package/src/server/route-tracker.ts +60 -0
- package/src/server/shared-core.ts +671 -0
- package/src/tui/index.tsx +168 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calibrated routing threshold.
|
|
3
|
+
*
|
|
4
|
+
* The intelligence floor used to be a hand-picked constant (39.5). That made it
|
|
5
|
+
* impossible to answer the only question that matters about it: why that number
|
|
6
|
+
* and not another one. It also silently sat *below* the cheapest eligible
|
|
7
|
+
* model, so changing it did nothing at all.
|
|
8
|
+
*
|
|
9
|
+
* This module derives the threshold from the owner's own recorded traffic
|
|
10
|
+
* instead. It follows the convention RouteLLM established for the same problem:
|
|
11
|
+
* pick the model that minimizes cost subject to a measured quality target,
|
|
12
|
+
* rather than by intuition.
|
|
13
|
+
*
|
|
14
|
+
* Crucially the target is a *cost* target, not a quality one. The router's job
|
|
15
|
+
* is to keep spending at the same level it has actually been spending while
|
|
16
|
+
* preferring capable models, not to decide how good the work should be. Quality
|
|
17
|
+
* targets expressed as "95% of a stronger model" need preference data over model
|
|
18
|
+
* pairs, which is expensive and which this owner does not have.
|
|
19
|
+
*
|
|
20
|
+
* When there is not enough recorded traffic, this returns undefined and the
|
|
21
|
+
* router keeps its configured floor. A calibration built on four requests would
|
|
22
|
+
* be a fabrication dressed up as evidence.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { ModelRecord, RouterSettings } from "../types.js"
|
|
26
|
+
/**
|
|
27
|
+
* One row of recorded spend per model.
|
|
28
|
+
*
|
|
29
|
+
* Mirrors the shape `LedgerStore.usageByModel` returns. Declared here rather
|
|
30
|
+
* than imported so calibration depends on the data it consumes, not on the
|
|
31
|
+
* ledger implementation that happens to produce it.
|
|
32
|
+
*/
|
|
33
|
+
export interface ModelUsageRow {
|
|
34
|
+
readonly providerID: string
|
|
35
|
+
readonly modelID: string
|
|
36
|
+
readonly requests: number
|
|
37
|
+
readonly usd: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* How much recorded spend a model needs before its cost figure is trusted.
|
|
42
|
+
*
|
|
43
|
+
* Below this the per-model cost is dominated by whichever single request
|
|
44
|
+
* happened to be long, so including it would let one unlucky session set the
|
|
45
|
+
* baseline for everything after it.
|
|
46
|
+
*/
|
|
47
|
+
export const MIN_REQUESTS_FOR_COST_EVIDENCE = 5
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The traffic window used to calibrate.
|
|
51
|
+
*
|
|
52
|
+
* Two weeks is long enough to cover a normal working rhythm, including the
|
|
53
|
+
* occasional expensive day, and short enough that a price change or a new model
|
|
54
|
+
* is reflected rather than averaged away for months.
|
|
55
|
+
*/
|
|
56
|
+
export const CALIBRATION_WINDOW_MS = 14 * 24 * 60 * 60 * 1000
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* How much above the measured average cost a model may sit and still be chosen.
|
|
60
|
+
*
|
|
61
|
+
* A small allowance matters because the baseline includes sessions whose model
|
|
62
|
+
* was badly chosen. Clamping to exactly the average would entrench those
|
|
63
|
+
* choices; a modest margin lets a slightly pricier but more capable model win
|
|
64
|
+
* when the difference is immaterial.
|
|
65
|
+
*/
|
|
66
|
+
export const CALIBRATION_TOLERANCE = 1.05
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A hard ceiling on how far calibration may move the threshold.
|
|
70
|
+
*
|
|
71
|
+
* The measured baseline can drift low if recent traffic happened to be trivial.
|
|
72
|
+
* Without a bound, one cheap afternoon could lower the effective quality gate
|
|
73
|
+
* far enough to send substantive work to a model that cannot do it. This keeps
|
|
74
|
+
* a bad baseline from becoming a routable decision: at most this many points of
|
|
75
|
+
* intelligence may be given up relative to the configured floor.
|
|
76
|
+
*
|
|
77
|
+
* The bound is asymmetric on purpose. Calibration may never *raise* the
|
|
78
|
+
* threshold above the configured floor, because raising it would silently
|
|
79
|
+
* exclude models the owner already approved and would make the router stricter
|
|
80
|
+
* than its own settings claim.
|
|
81
|
+
*/
|
|
82
|
+
export const MAX_CALIBRATION_DROP = 12
|
|
83
|
+
|
|
84
|
+
export interface CalibrationInput {
|
|
85
|
+
/** Every approved model the router may choose, with its intelligence score. */
|
|
86
|
+
readonly models: ReadonlyArray<ModelRecord>
|
|
87
|
+
/** Recorded per-model spend for the calibration window. */
|
|
88
|
+
readonly usage: ReadonlyArray<ModelUsageRow>
|
|
89
|
+
/** The configured floor, used as the ceiling and as the evidence-free answer. */
|
|
90
|
+
readonly configuredFloor: number
|
|
91
|
+
readonly settings: RouterSettings
|
|
92
|
+
/** Injected for determinism in tests. */
|
|
93
|
+
readonly now?: number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface CalibrationResult {
|
|
97
|
+
/** The threshold routing should actually use. */
|
|
98
|
+
readonly floor: number
|
|
99
|
+
/**
|
|
100
|
+
* Whether the value was derived from recorded traffic.
|
|
101
|
+
*
|
|
102
|
+
* False means the configured floor was returned because there was not enough
|
|
103
|
+
* evidence. Callers surface this, so a user can tell the difference between
|
|
104
|
+
* "calibrated" and "not yet calibrated" instead of seeing one number and
|
|
105
|
+
* assuming it was measured.
|
|
106
|
+
*/
|
|
107
|
+
readonly calibrated: boolean
|
|
108
|
+
/** Average cost per request across the window, in USD. Null when unmeasured. */
|
|
109
|
+
readonly baselineUsdPerRequest: number | null
|
|
110
|
+
/** How the threshold was arrived at, in one line, for the status output. */
|
|
111
|
+
readonly reason: string
|
|
112
|
+
/** Cost of the model the threshold admits as the cheapest capable option. */
|
|
113
|
+
readonly admittedUsdPerRequest: number | null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The configured floor if the owner pinned it, otherwise a calibrated value.
|
|
118
|
+
*
|
|
119
|
+
* An explicit `intelligenceFloor` in settings always wins. Calibration is for
|
|
120
|
+
* the owner who asked not to maintain that number by hand, not a way to
|
|
121
|
+
* override someone who set it deliberately.
|
|
122
|
+
*/
|
|
123
|
+
export const resolveIntelligenceFloor = (
|
|
124
|
+
settings: RouterSettings,
|
|
125
|
+
input: Omit<CalibrationInput, "settings" | "configuredFloor">,
|
|
126
|
+
): CalibrationResult => {
|
|
127
|
+
const configured = settings.routing.intelligenceFloor
|
|
128
|
+
const off = offlineFloorBypassActive(settings)
|
|
129
|
+
const base: CalibrationResult = {
|
|
130
|
+
floor: configured,
|
|
131
|
+
calibrated: false,
|
|
132
|
+
baselineUsdPerRequest: null,
|
|
133
|
+
reason: "using the configured floor",
|
|
134
|
+
admittedUsdPerRequest: null,
|
|
135
|
+
}
|
|
136
|
+
if (settings.routing.calibrateFloor !== true) return base
|
|
137
|
+
const result = calibrateFloor({ ...input, settings, configuredFloor: configured })
|
|
138
|
+
// Offline mode has its own allow-list and its own exemption from the floor.
|
|
139
|
+
// Calibration is an online-cost concern, so it must not change offline
|
|
140
|
+
// behavior in either direction.
|
|
141
|
+
return off ? base : result
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const offlineFloorBypassActive = (settings: RouterSettings): boolean => settings.offline?.enabled === true
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Derive a floor from recorded spend.
|
|
148
|
+
*
|
|
149
|
+
* The algorithm is deliberately small and auditable:
|
|
150
|
+
*
|
|
151
|
+
* 1. Total recorded spend and requests across the window give an average cost
|
|
152
|
+
* per request. That average *is* the budget: it is what the owner already
|
|
153
|
+
* spends, so honoring it is not a quality trade-off, it is a consistency
|
|
154
|
+
* requirement.
|
|
155
|
+
* 2. Among approved, non-escalation models that clear the hard context gate,
|
|
156
|
+
* find the cheapest one whose cost per request does not exceed the average
|
|
157
|
+
* by more than the tolerance.
|
|
158
|
+
* 3. That model's intelligence score becomes the floor, bounded so it can only
|
|
159
|
+
* move down from the configured value and never by more than
|
|
160
|
+
* MAX_CALIBRATION_DROP.
|
|
161
|
+
*
|
|
162
|
+
* The floor is then a statement of fact about the catalog: "this is the least
|
|
163
|
+
* capable model I am willing to run, and it costs no more than I already spend."
|
|
164
|
+
*/
|
|
165
|
+
export const calibrateFloor = (input: CalibrationInput): CalibrationResult => {
|
|
166
|
+
const configured = input.configuredFloor
|
|
167
|
+
const usage = input.usage ?? []
|
|
168
|
+
|
|
169
|
+
let totalUsd = 0
|
|
170
|
+
let totalRequests = 0
|
|
171
|
+
for (const row of usage) {
|
|
172
|
+
if (!Number.isFinite(row.usd) || row.usd < 0) continue
|
|
173
|
+
if (!Number.isFinite(row.requests) || row.requests <= 0) continue
|
|
174
|
+
totalUsd += row.usd
|
|
175
|
+
totalRequests += row.requests
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (totalRequests < MIN_REQUESTS_FOR_COST_EVIDENCE) {
|
|
179
|
+
return {
|
|
180
|
+
floor: configured,
|
|
181
|
+
calibrated: false,
|
|
182
|
+
baselineUsdPerRequest: null,
|
|
183
|
+
reason: `not enough recorded traffic to calibrate (${totalRequests} of ${MIN_REQUESTS_FOR_COST_EVIDENCE} requests)`,
|
|
184
|
+
admittedUsdPerRequest: null,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const baseline = totalUsd / totalRequests
|
|
189
|
+
if (!Number.isFinite(baseline) || baseline <= 0) {
|
|
190
|
+
return {
|
|
191
|
+
floor: configured,
|
|
192
|
+
calibrated: false,
|
|
193
|
+
baselineUsdPerRequest: null,
|
|
194
|
+
reason: "recorded traffic has no measurable cost, so there is nothing to calibrate against",
|
|
195
|
+
admittedUsdPerRequest: null,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const ceiling = baseline * CALIBRATION_TOLERANCE
|
|
200
|
+
const perRequest = perRequestCosts(usage)
|
|
201
|
+
|
|
202
|
+
// Only models that actually served traffic have a measured cost. A model with
|
|
203
|
+
// no recorded requests has no evidence about *this owner's* workload, and
|
|
204
|
+
// substituting a list price would calibrate against the vendor's assumptions
|
|
205
|
+
// rather than the owner's usage.
|
|
206
|
+
const candidates = input.models
|
|
207
|
+
.filter((model) => !model.escalationOnly && !model.premium)
|
|
208
|
+
.filter((model) => model.intelligence > 0)
|
|
209
|
+
.map((model) => {
|
|
210
|
+
const measured = perRequest.get(`${model.providerID}/${model.modelID}`)
|
|
211
|
+
return measured === undefined ? undefined : { model, cost: measured }
|
|
212
|
+
})
|
|
213
|
+
.filter((entry): entry is { model: ModelRecord; cost: number } => entry !== undefined)
|
|
214
|
+
.filter((entry) => entry.cost <= ceiling)
|
|
215
|
+
.sort((a, b) => b.model.intelligence - a.model.intelligence)
|
|
216
|
+
|
|
217
|
+
if (candidates.length === 0) {
|
|
218
|
+
return {
|
|
219
|
+
floor: configured,
|
|
220
|
+
calibrated: false,
|
|
221
|
+
baselineUsdPerRequest: baseline,
|
|
222
|
+
reason:
|
|
223
|
+
"every model with recorded traffic costs more than the measured baseline, so no threshold could be met",
|
|
224
|
+
admittedUsdPerRequest: null,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// The most capable model that fits the budget is the one worth naming: it is
|
|
229
|
+
// what the threshold exists to admit.
|
|
230
|
+
const best = candidates[0]!
|
|
231
|
+
const bounded = Math.min(configured, Math.max(best.model.intelligence, configured - MAX_CALIBRATION_DROP))
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
floor: bounded,
|
|
235
|
+
calibrated: true,
|
|
236
|
+
baselineUsdPerRequest: baseline,
|
|
237
|
+
reason:
|
|
238
|
+
`${best.model.providerID}/${best.model.modelID} costs $${best.cost.toFixed(4)} per request ` +
|
|
239
|
+
`against a measured baseline of $${baseline.toFixed(4)}`,
|
|
240
|
+
admittedUsdPerRequest: best.cost,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Cost per request for each model that has recorded traffic.
|
|
246
|
+
*
|
|
247
|
+
* Requests rather than tokens, because a long cheap request and a short
|
|
248
|
+
* expensive one both occupy a turn of the owner's attention, and the budget
|
|
249
|
+
* being respected is spend for work done.
|
|
250
|
+
*/
|
|
251
|
+
const perRequestCosts = (usage: ReadonlyArray<ModelUsageRow>): Map<string, number> => {
|
|
252
|
+
const costs = new Map<string, number>()
|
|
253
|
+
for (const row of usage) {
|
|
254
|
+
if (!Number.isFinite(row.usd) || row.usd < 0) continue
|
|
255
|
+
if (!Number.isFinite(row.requests) || row.requests <= 0) continue
|
|
256
|
+
costs.set(`${row.providerID}/${row.modelID}`, row.usd / row.requests)
|
|
257
|
+
}
|
|
258
|
+
return costs
|
|
259
|
+
}
|