@qadanm/opencode-smart-router 0.4.0 → 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.
- package/CHANGELOG.md +168 -0
- package/CONTRIBUTING.md +7 -0
- package/README.md +89 -5
- package/config.example.json +1 -2
- package/dist/npm/index.js +29 -29
- package/docs/INSTALL.md +35 -4
- package/docs/LIMITATIONS.md +19 -2
- package/docs/SHARED-CORE.md +47 -5
- package/package.json +3 -1
- package/src/core/config.ts +25 -8
- package/src/core/models/registry.ts +1 -0
- package/src/core/models/seed.ts +13 -3
- package/src/core/plans/catalog.ts +6 -0
- package/src/core/policy/eligibility.ts +53 -1
- package/src/core/providers/opendesign-models.ts +33 -7
- package/src/core/quota/ledger.ts +31 -0
- package/src/core/routing/engine.ts +61 -2
- package/src/core/types.ts +46 -5
- package/src/gui/index.tsx +5 -0
- package/src/index.ts +15 -30
- package/src/server/containment.ts +131 -0
- package/src/server/controller.ts +261 -137
- package/src/server/shared-core.ts +32 -2
- package/dist/allowance.js +0 -487
- package/dist/gui-experimental.cjs +0 -1
- package/dist/index.js +0 -11111
- package/dist/tui.js +0 -587
package/dist/tui.js
DELETED
|
@@ -1,587 +0,0 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
-
var __esm = (fn, res, err) => function __init() {
|
|
4
|
-
if (err) throw err[0];
|
|
5
|
-
try {
|
|
6
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
-
} catch (e) {
|
|
8
|
-
throw err = [e], e;
|
|
9
|
-
}
|
|
10
|
-
};
|
|
11
|
-
var __export = (target, all) => {
|
|
12
|
-
for (var name in all)
|
|
13
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// src/gui/viewmodel.ts
|
|
17
|
-
var viewmodel_exports = {};
|
|
18
|
-
__export(viewmodel_exports, {
|
|
19
|
-
formatPercent: () => formatPercent,
|
|
20
|
-
formatReset: () => formatReset,
|
|
21
|
-
formatUsd: () => formatUsd,
|
|
22
|
-
formatWindowUsed: () => formatWindowUsed,
|
|
23
|
-
keyOf: () => keyOf,
|
|
24
|
-
orderCandidateRows: () => orderCandidateRows,
|
|
25
|
-
providerGroups: () => providerGroups,
|
|
26
|
-
rankingAdjustments: () => rankingAdjustments,
|
|
27
|
-
statusLine: () => statusLine,
|
|
28
|
-
summariseDecision: () => summariseDecision,
|
|
29
|
-
toCandidateRow: () => toCandidateRow,
|
|
30
|
-
toProviderRow: () => toProviderRow
|
|
31
|
-
});
|
|
32
|
-
var sourceLabelOf, windowLabels, formatReset, toWindow, toProviderRow, formatUsd, formatPercent, formatWindowUsed, rankingAdjustments, keyOf, toCandidateRow, verdictRank, orderCandidateRows, summariseDecision, statusLine, contextLabel, providerGroups;
|
|
33
|
-
var init_viewmodel = __esm({
|
|
34
|
-
"src/gui/viewmodel.ts"() {
|
|
35
|
-
"use strict";
|
|
36
|
-
sourceLabelOf = (source) => {
|
|
37
|
-
switch (source) {
|
|
38
|
-
case "provider":
|
|
39
|
-
return "verified by provider";
|
|
40
|
-
case "estimate":
|
|
41
|
-
return "estimated \u2014 lower bound";
|
|
42
|
-
default:
|
|
43
|
-
return "not reported";
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
windowLabels = {
|
|
47
|
-
fiveHour: "5h",
|
|
48
|
-
weekly: "7d",
|
|
49
|
-
monthly: "Monthly"
|
|
50
|
-
};
|
|
51
|
-
formatReset = (at, now = Date.now()) => {
|
|
52
|
-
if (at === void 0) return void 0;
|
|
53
|
-
const delta = at - now;
|
|
54
|
-
if (delta <= 0) return "resetting";
|
|
55
|
-
const minutes = Math.round(delta / 6e4);
|
|
56
|
-
if (minutes < 60) return `in ${minutes}m`;
|
|
57
|
-
const hours = Math.round(minutes / 60);
|
|
58
|
-
if (hours < 36) return `in ${hours}h`;
|
|
59
|
-
const days = Math.round(hours / 24);
|
|
60
|
-
return `in ${days}d`;
|
|
61
|
-
};
|
|
62
|
-
toWindow = (id, window, now) => {
|
|
63
|
-
const source = window?.source ?? "unknown";
|
|
64
|
-
const usedPct = window?.usedPct ?? 0;
|
|
65
|
-
const reported = source !== "unknown" && (window?.capUsd !== void 0 || usedPct > 0);
|
|
66
|
-
return {
|
|
67
|
-
id,
|
|
68
|
-
label: windowLabels[id],
|
|
69
|
-
usedPct,
|
|
70
|
-
remainingPct: window?.remainingPct ?? Math.max(0, 100 - usedPct),
|
|
71
|
-
...window?.usedUsd !== void 0 ? { usedUsd: window.usedUsd } : {},
|
|
72
|
-
...window?.capUsd !== void 0 ? { capUsd: window.capUsd } : {},
|
|
73
|
-
...window?.resetsAt !== void 0 ? { resetsAt: window.resetsAt } : {},
|
|
74
|
-
...formatReset(window?.resetsAt, now) ? { resetLabel: formatReset(window?.resetsAt, now) } : {},
|
|
75
|
-
source,
|
|
76
|
-
sourceLabel: sourceLabelOf(source),
|
|
77
|
-
exceeded: Boolean(window?.exceeded),
|
|
78
|
-
reported,
|
|
79
|
-
lowerBound: source === "estimate"
|
|
80
|
-
};
|
|
81
|
-
};
|
|
82
|
-
toProviderRow = (snapshot, now = Date.now()) => {
|
|
83
|
-
const source = snapshot.monthly?.source === "provider" ? "provider" : snapshot.monthly?.source === "estimate" ? "estimate" : "unknown";
|
|
84
|
-
const windows = [
|
|
85
|
-
toWindow("fiveHour", snapshot.rolling, now),
|
|
86
|
-
toWindow("weekly", snapshot.weekly, now),
|
|
87
|
-
toWindow("monthly", snapshot.monthly, now)
|
|
88
|
-
];
|
|
89
|
-
const exhaustedWindow = windows.find((window) => window.exceeded);
|
|
90
|
-
const pools = (snapshot.pools ?? []).map((pool) => ({
|
|
91
|
-
id: pool.id,
|
|
92
|
-
label: pool.label,
|
|
93
|
-
source: pool.source,
|
|
94
|
-
sourceLabel: sourceLabelOf(pool.source),
|
|
95
|
-
monthlyUsd: pool.monthlyUsd,
|
|
96
|
-
usedUsd: pool.usedUsd,
|
|
97
|
-
remainingUsd: pool.remainingUsd,
|
|
98
|
-
usedPct: pool.usedPct,
|
|
99
|
-
...pool.weeklyUsedPct !== void 0 ? { weeklyPct: pool.weeklyUsedPct } : {},
|
|
100
|
-
...pool.fiveHourUsedPct !== void 0 ? { fiveHourPct: pool.fiveHourUsedPct } : {},
|
|
101
|
-
...formatReset(pool.resetsAt, now) ? { resetLabel: formatReset(pool.resetsAt, now) } : {},
|
|
102
|
-
note: pool.note
|
|
103
|
-
}));
|
|
104
|
-
return {
|
|
105
|
-
providerID: snapshot.providerID,
|
|
106
|
-
label: snapshot.label ?? snapshot.providerID,
|
|
107
|
-
plan: snapshot.plan ?? "",
|
|
108
|
-
source,
|
|
109
|
-
sourceLabel: sourceLabelOf(source),
|
|
110
|
-
...snapshot.quotaState ? { quotaState: snapshot.quotaState } : {},
|
|
111
|
-
stale: Boolean(snapshot.stale),
|
|
112
|
-
fetchedAt: snapshot.fetchedAt ?? now,
|
|
113
|
-
exhausted: Boolean(exhaustedWindow),
|
|
114
|
-
...exhaustedWindow ? { exhaustedWindow: exhaustedWindow.label } : {},
|
|
115
|
-
windows,
|
|
116
|
-
pools,
|
|
117
|
-
...snapshot.monthlyRemainingUsd !== void 0 ? { monthlyRemainingUsd: snapshot.monthlyRemainingUsd } : {},
|
|
118
|
-
...snapshot.extraCredits ? { extraCreditsUsd: snapshot.extraCredits.totalUsd } : {},
|
|
119
|
-
...snapshot.error ? { error: snapshot.error } : {},
|
|
120
|
-
notes: snapshot.notes ?? [],
|
|
121
|
-
...source === "estimate" ? { caveat: (snapshot.notes ?? [])[0] } : {}
|
|
122
|
-
};
|
|
123
|
-
};
|
|
124
|
-
formatUsd = (value) => {
|
|
125
|
-
if (!Number.isFinite(value)) return "\u2014";
|
|
126
|
-
if (value === 0) return "$0";
|
|
127
|
-
if (value < 0.01) return `$${value.toFixed(4)}`;
|
|
128
|
-
if (value < 1) return `$${value.toFixed(3)}`;
|
|
129
|
-
return `$${value.toFixed(2)}`;
|
|
130
|
-
};
|
|
131
|
-
formatPercent = (value) => Number.isFinite(value) ? `${Math.round(value)}%` : "\u2014";
|
|
132
|
-
formatWindowUsed = (window) => {
|
|
133
|
-
if (!window.reported) return "\u2014";
|
|
134
|
-
const value = formatPercent(window.usedPct);
|
|
135
|
-
return window.lowerBound ? `\u2265${value}` : value;
|
|
136
|
-
};
|
|
137
|
-
rankingAdjustments = (candidate) => {
|
|
138
|
-
const notes = [];
|
|
139
|
-
const credit = candidate.creditMultiplier;
|
|
140
|
-
if (credit !== void 0 && credit !== 1) notes.push(`${credit}x plan credits`);
|
|
141
|
-
const speed = candidate.speedFactor;
|
|
142
|
-
if (speed !== void 0 && speed !== 1) notes.push(`${speed}x measured speed`);
|
|
143
|
-
const marginal = candidate.marginalPriceFactor;
|
|
144
|
-
if (marginal !== void 0 && marginal !== 1) notes.push(`${marginal}x expiring allowance`);
|
|
145
|
-
if (candidate.switchPenaltyUsd > 0) notes.push(`${formatUsd(candidate.switchPenaltyUsd)} switch cost`);
|
|
146
|
-
return notes;
|
|
147
|
-
};
|
|
148
|
-
keyOf = (model) => `${model.providerID}/${model.modelID}`;
|
|
149
|
-
toCandidateRow = (candidate, chosen) => {
|
|
150
|
-
const key = keyOf(candidate.model);
|
|
151
|
-
return {
|
|
152
|
-
key,
|
|
153
|
-
name: candidate.name,
|
|
154
|
-
tier: candidate.tier,
|
|
155
|
-
verdict: key === chosen && candidate.eligible ? "chosen" : candidate.eligible ? "eligible" : "blocked",
|
|
156
|
-
reason: candidate.reason,
|
|
157
|
-
estimatedUsd: candidate.estimatedUsd,
|
|
158
|
-
effectiveCost: candidate.effectiveCost,
|
|
159
|
-
successProbability: candidate.successProbability,
|
|
160
|
-
adjustments: rankingAdjustments(candidate)
|
|
161
|
-
};
|
|
162
|
-
};
|
|
163
|
-
verdictRank = { chosen: 0, eligible: 1, blocked: 2 };
|
|
164
|
-
orderCandidateRows = (rows) => [...rows].sort((left, right) => {
|
|
165
|
-
const byVerdict = verdictRank[left.verdict] - verdictRank[right.verdict];
|
|
166
|
-
if (byVerdict !== 0) return byVerdict;
|
|
167
|
-
if (left.verdict === "blocked") return left.key.localeCompare(right.key);
|
|
168
|
-
return left.effectiveCost - right.effectiveCost || left.key.localeCompare(right.key);
|
|
169
|
-
});
|
|
170
|
-
summariseDecision = (decision) => {
|
|
171
|
-
if (!decision) return void 0;
|
|
172
|
-
const chosenKey = decision.chosen ? `${decision.chosen.providerID}/${decision.chosen.id}` : void 0;
|
|
173
|
-
const rows = orderCandidateRows(decision.candidates.map((candidate) => toCandidateRow(candidate, chosenKey)));
|
|
174
|
-
return {
|
|
175
|
-
id: decision.id,
|
|
176
|
-
chosen: chosenKey,
|
|
177
|
-
switched: decision.switched,
|
|
178
|
-
switchReason: decision.switchReason,
|
|
179
|
-
taskKind: decision.task?.kind ?? "unknown",
|
|
180
|
-
taskTier: decision.task?.tier ?? 0,
|
|
181
|
-
contextTokens: decision.contextTokens,
|
|
182
|
-
estimatedUsd: decision.estimatedUsd,
|
|
183
|
-
reasons: decision.reasons ?? [],
|
|
184
|
-
warnings: decision.warnings ?? [],
|
|
185
|
-
selfheal: decision.selfheal?.rejected ? `rejected ${decision.selfheal.rejected} \xB7 ${decision.selfheal.action === "noted" ? "noted for the next prompt" : decision.selfheal.action}` : void 0,
|
|
186
|
-
eligibleCount: rows.filter((row) => row.verdict !== "blocked").length,
|
|
187
|
-
blockedCount: rows.filter((row) => row.verdict === "blocked").length,
|
|
188
|
-
rows
|
|
189
|
-
};
|
|
190
|
-
};
|
|
191
|
-
statusLine = (input) => {
|
|
192
|
-
const parts = [input.runtime ?? "unknown", input.mode ?? "unknown"];
|
|
193
|
-
parts.push(input.current ? `${input.current.providerID}/${input.current.id}` : "no model yet");
|
|
194
|
-
return parts.join(" \xB7 ");
|
|
195
|
-
};
|
|
196
|
-
contextLabel = (tokens) => {
|
|
197
|
-
if (!Number.isFinite(tokens) || tokens <= 0) return "unknown";
|
|
198
|
-
return tokens >= 1e6 ? `${Number((tokens / 1e6).toFixed(1))}M` : `${Math.round(tokens / 1e3)}k`;
|
|
199
|
-
};
|
|
200
|
-
providerGroups = (input) => {
|
|
201
|
-
const byProvider = /* @__PURE__ */ new Map();
|
|
202
|
-
for (const model of input.models) {
|
|
203
|
-
const state = model.excluded ? "excluded" : model.approved ? "approved" : "not approved";
|
|
204
|
-
const row = {
|
|
205
|
-
key: model.key,
|
|
206
|
-
providerID: model.providerID,
|
|
207
|
-
name: model.name,
|
|
208
|
-
tier: model.tier,
|
|
209
|
-
intelligence: model.intelligence,
|
|
210
|
-
design: model.design,
|
|
211
|
-
contextLabel: contextLabel(model.context),
|
|
212
|
-
costLabel: `${formatUsd(model.costInput)} / ${formatUsd(model.costOutput)} per 1M`,
|
|
213
|
-
vision: model.vision,
|
|
214
|
-
state
|
|
215
|
-
};
|
|
216
|
-
const list = byProvider.get(model.providerID) ?? [];
|
|
217
|
-
list.push(row);
|
|
218
|
-
byProvider.set(model.providerID, list);
|
|
219
|
-
}
|
|
220
|
-
const quotaLabel = new Map(input.quota.map((entry) => [entry.providerID, entry.label ?? entry.providerID]));
|
|
221
|
-
return [...byProvider.entries()].map(([providerID, models]) => {
|
|
222
|
-
const active = input.activeProviders.includes(providerID);
|
|
223
|
-
const approvedCount = models.filter((model) => model.state === "approved").length;
|
|
224
|
-
return {
|
|
225
|
-
providerID,
|
|
226
|
-
label: quotaLabel.get(providerID) ?? providerID,
|
|
227
|
-
state: active ? "active" : "not configured",
|
|
228
|
-
// Approved first, then by tier and cost: the order a routing decision
|
|
229
|
-
// would consider them.
|
|
230
|
-
models: [...models].sort((left, right) => {
|
|
231
|
-
const rank = (row) => row.state === "approved" ? 0 : row.state === "excluded" ? 2 : 1;
|
|
232
|
-
return rank(left) - rank(right) || left.tier - right.tier || left.key.localeCompare(right.key);
|
|
233
|
-
}),
|
|
234
|
-
approvedCount,
|
|
235
|
-
totalCount: models.length,
|
|
236
|
-
...active ? {} : { note: "Not routing here: no credential found for this provider on this machine." }
|
|
237
|
-
};
|
|
238
|
-
}).sort((left, right) => {
|
|
239
|
-
if (left.state !== right.state) return left.state === "active" ? -1 : 1;
|
|
240
|
-
return left.label.localeCompare(right.label);
|
|
241
|
-
});
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
// src/tui/index.tsx
|
|
247
|
-
import { Plugin } from "@opencode/plugin/tui";
|
|
248
|
-
|
|
249
|
-
// src/rpc.ts
|
|
250
|
-
import { Rpc } from "@opencode/plugin/rpc";
|
|
251
|
-
var objectOutput = { type: "object", additionalProperties: true };
|
|
252
|
-
var emptyInput = { type: "object", properties: {}, additionalProperties: false };
|
|
253
|
-
var sessionInput = {
|
|
254
|
-
type: "object",
|
|
255
|
-
properties: { sessionID: { type: "string" } },
|
|
256
|
-
required: ["sessionID"],
|
|
257
|
-
additionalProperties: false
|
|
258
|
-
};
|
|
259
|
-
var method = (input) => ({ input, output: objectOutput });
|
|
260
|
-
var SmartRouterRpc = Rpc.define({
|
|
261
|
-
id: "smart-router",
|
|
262
|
-
methods: {
|
|
263
|
-
status: method({
|
|
264
|
-
type: "object",
|
|
265
|
-
properties: { sessionID: { type: "string" } },
|
|
266
|
-
additionalProperties: false
|
|
267
|
-
}),
|
|
268
|
-
preview: method({
|
|
269
|
-
type: "object",
|
|
270
|
-
properties: {
|
|
271
|
-
sessionID: { type: "string" },
|
|
272
|
-
text: { type: "string" },
|
|
273
|
-
files: { type: "array", items: { type: "object", additionalProperties: true } }
|
|
274
|
-
},
|
|
275
|
-
required: ["sessionID", "text"],
|
|
276
|
-
additionalProperties: false
|
|
277
|
-
}),
|
|
278
|
-
setMode: method({
|
|
279
|
-
type: "object",
|
|
280
|
-
properties: { mode: { type: "string", enum: ["efficiency", "balanced", "premium"] } },
|
|
281
|
-
required: ["mode"],
|
|
282
|
-
additionalProperties: false
|
|
283
|
-
}),
|
|
284
|
-
setPaused: method({
|
|
285
|
-
type: "object",
|
|
286
|
-
properties: { paused: { type: "boolean" } },
|
|
287
|
-
required: ["paused"],
|
|
288
|
-
additionalProperties: false
|
|
289
|
-
}),
|
|
290
|
-
/**
|
|
291
|
-
* Turn automatic routing on or off for one session.
|
|
292
|
-
*
|
|
293
|
-
* Not the same as disabling the plugin or pausing every session: the router
|
|
294
|
-
* stays loaded, keeps reporting quota and decisions, and simply stops
|
|
295
|
-
* changing the model. The user keeps whatever model they have.
|
|
296
|
-
*/
|
|
297
|
-
setSessionAuto: method({
|
|
298
|
-
type: "object",
|
|
299
|
-
properties: { sessionID: { type: "string" }, auto: { type: "boolean" } },
|
|
300
|
-
required: ["sessionID", "auto"],
|
|
301
|
-
additionalProperties: false
|
|
302
|
-
}),
|
|
303
|
-
setDryRun: method({
|
|
304
|
-
type: "object",
|
|
305
|
-
properties: { dryRun: { type: "boolean" } },
|
|
306
|
-
required: ["dryRun"],
|
|
307
|
-
additionalProperties: false
|
|
308
|
-
}),
|
|
309
|
-
resumeAuto: method(sessionInput),
|
|
310
|
-
pin: method({
|
|
311
|
-
type: "object",
|
|
312
|
-
properties: {
|
|
313
|
-
sessionID: { type: "string" },
|
|
314
|
-
providerID: { type: "string" },
|
|
315
|
-
modelID: { type: "string" },
|
|
316
|
-
variant: { type: "string" }
|
|
317
|
-
},
|
|
318
|
-
required: ["sessionID", "providerID", "modelID"],
|
|
319
|
-
additionalProperties: false
|
|
320
|
-
}),
|
|
321
|
-
approvePremium: method(sessionInput),
|
|
322
|
-
denyPremium: method(sessionInput),
|
|
323
|
-
excludeModel: method({
|
|
324
|
-
type: "object",
|
|
325
|
-
properties: {
|
|
326
|
-
providerID: { type: "string" },
|
|
327
|
-
modelID: { type: "string" },
|
|
328
|
-
excluded: { type: "boolean" }
|
|
329
|
-
},
|
|
330
|
-
required: ["providerID", "modelID", "excluded"],
|
|
331
|
-
additionalProperties: false
|
|
332
|
-
}),
|
|
333
|
-
setPremiumBudget: method({
|
|
334
|
-
type: "object",
|
|
335
|
-
properties: { usd: { type: "number", minimum: 0, maximum: 1e5 } },
|
|
336
|
-
required: ["usd"],
|
|
337
|
-
additionalProperties: false
|
|
338
|
-
}),
|
|
339
|
-
setReserve: method({
|
|
340
|
-
type: "object",
|
|
341
|
-
properties: {
|
|
342
|
-
providerID: { type: "string" },
|
|
343
|
-
monthlyPct: { type: "number", minimum: 0, maximum: 90 },
|
|
344
|
-
weeklyPct: { type: "number", minimum: 0, maximum: 90 },
|
|
345
|
-
fiveHourPct: { type: "number", minimum: 0, maximum: 90 }
|
|
346
|
-
},
|
|
347
|
-
required: ["providerID"],
|
|
348
|
-
additionalProperties: false
|
|
349
|
-
}),
|
|
350
|
-
/**
|
|
351
|
-
* The intelligence floor in force, and why.
|
|
352
|
-
*
|
|
353
|
-
* Calibration can only be trusted if its result is visible: a number with
|
|
354
|
-
* no explanation is indistinguishable from the hand-picked one it replaced.
|
|
355
|
-
* This previews without enabling anything.
|
|
356
|
-
*/
|
|
357
|
-
floor: method(emptyInput),
|
|
358
|
-
setCalibrateFloor: method({
|
|
359
|
-
type: "object",
|
|
360
|
-
properties: { enabled: { type: "boolean" } },
|
|
361
|
-
required: ["enabled"],
|
|
362
|
-
additionalProperties: false
|
|
363
|
-
}),
|
|
364
|
-
refreshQuota: method(emptyInput),
|
|
365
|
-
/**
|
|
366
|
-
* What this machine has, for the first-run screen.
|
|
367
|
-
*
|
|
368
|
-
* Onboarding must show evidence, not a questionnaire. The router already
|
|
369
|
-
* detects credentials and imports history at startup; this reports what it
|
|
370
|
-
* found so the first thing a new user sees is the truth about their own
|
|
371
|
-
* machine rather than a form asking them to type it in.
|
|
372
|
-
*/
|
|
373
|
-
onboarding: method(emptyInput),
|
|
374
|
-
/** Mark the first run as seen, so it does not reappear. */
|
|
375
|
-
completeOnboarding: method(emptyInput),
|
|
376
|
-
/**
|
|
377
|
-
* Record what the user's account page showed, so the estimate reflects the
|
|
378
|
-
* whole account rather than only the traffic this router routed.
|
|
379
|
-
*
|
|
380
|
-
* Takes the percentage the page displays, not a dollar figure: asking a user
|
|
381
|
-
* to divide by a cap they may not know is where the arithmetic mistakes come
|
|
382
|
-
* from. The server converts it against the window the page was describing.
|
|
383
|
-
*/
|
|
384
|
-
/**
|
|
385
|
-
* Record the account's balance page, pasted whole.
|
|
386
|
-
*
|
|
387
|
-
* One field, because that is the actual task: select the page, copy, paste.
|
|
388
|
-
* The server reads the plan, the percentage, which window it belongs to, the
|
|
389
|
-
* reset time, the billing period and the wallet out of the text. A form with
|
|
390
|
-
* four boxes asks the user to do that parsing by hand and get it right.
|
|
391
|
-
*/
|
|
392
|
-
recordPlanPage: method({
|
|
393
|
-
type: "object",
|
|
394
|
-
properties: {
|
|
395
|
-
/** The pasted page text. */
|
|
396
|
-
page: { type: "string" },
|
|
397
|
-
/** Apply it without previewing. Defaults to false: the user confirms first. */
|
|
398
|
-
confirm: { type: "boolean" }
|
|
399
|
-
},
|
|
400
|
-
required: ["page"],
|
|
401
|
-
additionalProperties: false
|
|
402
|
-
}),
|
|
403
|
-
report: method(emptyInput),
|
|
404
|
-
usage: method(emptyInput),
|
|
405
|
-
getConfig: method(emptyInput),
|
|
406
|
-
resetOverrides: method(emptyInput),
|
|
407
|
-
resetMetrics: method(emptyInput),
|
|
408
|
-
refreshIndicator: method(sessionInput),
|
|
409
|
-
diagnostics: method(emptyInput),
|
|
410
|
-
setTitleIndicator: method({
|
|
411
|
-
type: "object",
|
|
412
|
-
properties: { enabled: { type: "boolean" } },
|
|
413
|
-
required: ["enabled"],
|
|
414
|
-
additionalProperties: false
|
|
415
|
-
})
|
|
416
|
-
},
|
|
417
|
-
events: {
|
|
418
|
-
decision: {
|
|
419
|
-
schema: {
|
|
420
|
-
type: "object",
|
|
421
|
-
properties: {
|
|
422
|
-
decision: { type: "object", additionalProperties: true }
|
|
423
|
-
},
|
|
424
|
-
required: ["decision"],
|
|
425
|
-
additionalProperties: false
|
|
426
|
-
}
|
|
427
|
-
},
|
|
428
|
-
status: {
|
|
429
|
-
schema: {
|
|
430
|
-
type: "object",
|
|
431
|
-
properties: {
|
|
432
|
-
sessionID: { type: "string" },
|
|
433
|
-
summary: { type: "object", additionalProperties: true }
|
|
434
|
-
},
|
|
435
|
-
required: ["summary"],
|
|
436
|
-
additionalProperties: false
|
|
437
|
-
}
|
|
438
|
-
},
|
|
439
|
-
quota: {
|
|
440
|
-
schema: {
|
|
441
|
-
type: "object",
|
|
442
|
-
properties: {
|
|
443
|
-
providers: { type: "array", items: { type: "object", additionalProperties: true } }
|
|
444
|
-
},
|
|
445
|
-
required: ["providers"],
|
|
446
|
-
additionalProperties: false
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
// src/tui/index.tsx
|
|
453
|
-
var tick = (ok) => ok ? "\u2713" : "\xB7";
|
|
454
|
-
var formatDecision = (status, view) => {
|
|
455
|
-
if (!status) return "Smart Router: no response from the router.";
|
|
456
|
-
const head = `${status.runtime ?? "unknown"} \xB7 ${status.mode ?? "unknown"} \xB7 ${status.current ? `${status.current.providerID}/${status.current.id}` : "no model yet"}`;
|
|
457
|
-
if (!view) return `${head}
|
|
458
|
-
|
|
459
|
-
No decision yet in this session.`;
|
|
460
|
-
const lines = [head, "", `Chosen ${view.chosen ?? "nothing eligible"}`];
|
|
461
|
-
lines.push(
|
|
462
|
-
` ${view.taskKind} \xB7 tier ${view.taskTier} \xB7 ${Math.round(view.contextTokens / 1e3)}k context`
|
|
463
|
-
);
|
|
464
|
-
if (view.reasons.length > 0) {
|
|
465
|
-
lines.push("", "Why");
|
|
466
|
-
for (const reason of view.reasons.slice(0, 5)) lines.push(` \u2022 ${reason}`);
|
|
467
|
-
}
|
|
468
|
-
if (view.rows.length > 0) {
|
|
469
|
-
lines.push("", `Candidates (${view.eligibleCount} eligible, ${view.blockedCount} blocked)`);
|
|
470
|
-
for (const row of view.rows.slice(0, 10)) {
|
|
471
|
-
lines.push(` ${tick(row.verdict !== "blocked")} ${row.key}`);
|
|
472
|
-
lines.push(` ${row.reason}`);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
if (view.warnings.length > 0) {
|
|
476
|
-
lines.push("", "Warnings");
|
|
477
|
-
for (const warning of view.warnings.slice(0, 3)) lines.push(` ! ${warning}`);
|
|
478
|
-
}
|
|
479
|
-
return lines.join("\n");
|
|
480
|
-
};
|
|
481
|
-
var formatProviders = (providers) => {
|
|
482
|
-
if (providers.length === 0) return "No providers reported.";
|
|
483
|
-
const lines = [];
|
|
484
|
-
for (const provider of providers) {
|
|
485
|
-
const windows = provider.windows.filter((window) => window.reported).map((window) => `${window.label} ${window.lowerBound ? "\u2265" : ""}${window.usedPct}%`);
|
|
486
|
-
lines.push(`${provider.label}${provider.exhausted ? " (exhausted)" : ""}`);
|
|
487
|
-
lines.push(` ${windows.length > 0 ? windows.join(" \xB7 ") : "no windows reported"} \xB7 ${provider.sourceLabel}`);
|
|
488
|
-
if (provider.caveat) lines.push(` ! ${provider.caveat}`);
|
|
489
|
-
}
|
|
490
|
-
return lines.join("\n");
|
|
491
|
-
};
|
|
492
|
-
var index_default = Plugin.define({
|
|
493
|
-
/**
|
|
494
|
-
* Deliberately NOT nested under the server part's id.
|
|
495
|
-
*
|
|
496
|
-
* The GUI docs are explicit: if a plugin's id has another id nested under it,
|
|
497
|
-
* or nests under one, its GUI does not load. The TUI previously used
|
|
498
|
-
* "opencode-smart-router.tui", which nests under the server part's
|
|
499
|
-
* "opencode-smart-router" — so adding a TUI silently disabled the desktop
|
|
500
|
-
* panel, with no error beyond a generic request failure.
|
|
501
|
-
*
|
|
502
|
-
* A sibling id avoids the namespace collision entirely.
|
|
503
|
-
*/
|
|
504
|
-
id: "opencode-smart-router-tui",
|
|
505
|
-
setup(context) {
|
|
506
|
-
const stop = context.data.on("session.execution.succeeded", () => {
|
|
507
|
-
void refresh();
|
|
508
|
-
});
|
|
509
|
-
const refresh = async () => {
|
|
510
|
-
try {
|
|
511
|
-
const result = await context.client.rpc(SmartRouterRpc).status({});
|
|
512
|
-
setFooter(`${result?.runtime ?? "?"} \xB7 ${result?.mode ?? "?"}`);
|
|
513
|
-
} catch {
|
|
514
|
-
setFooter("router unreachable");
|
|
515
|
-
}
|
|
516
|
-
};
|
|
517
|
-
let setFooter = (_text) => {
|
|
518
|
-
};
|
|
519
|
-
context.ui.slot({
|
|
520
|
-
append: "prompt.footer.status",
|
|
521
|
-
render: () => {
|
|
522
|
-
const [text, set] = createTerminalSignal("router");
|
|
523
|
-
setFooter = set;
|
|
524
|
-
void refresh();
|
|
525
|
-
return /* @__PURE__ */ React.createElement("text", null, text());
|
|
526
|
-
}
|
|
527
|
-
});
|
|
528
|
-
context.keymap.layer(() => ({
|
|
529
|
-
mode: "global",
|
|
530
|
-
commands: [
|
|
531
|
-
{
|
|
532
|
-
id: "router.decision",
|
|
533
|
-
title: "Router: why this model",
|
|
534
|
-
slash: { name: "router-why" },
|
|
535
|
-
palette: true,
|
|
536
|
-
run: async () => {
|
|
537
|
-
try {
|
|
538
|
-
const result = await context.client.rpc(SmartRouterRpc).status({});
|
|
539
|
-
const { summariseDecision: summariseDecision2, toProviderRow: toProviderRow2 } = await Promise.resolve().then(() => (init_viewmodel(), viewmodel_exports));
|
|
540
|
-
const body = formatDecision(result, summariseDecision2(result?.lastDecision));
|
|
541
|
-
await context.ui.dialog.alert({ title: "Smart Router", message: body });
|
|
542
|
-
} catch (cause) {
|
|
543
|
-
await context.ui.dialog.alert({
|
|
544
|
-
title: "Smart Router",
|
|
545
|
-
message: `Could not read router status: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
},
|
|
550
|
-
{
|
|
551
|
-
id: "router.providers",
|
|
552
|
-
title: "Router: providers and allowances",
|
|
553
|
-
slash: { name: "router-quota" },
|
|
554
|
-
palette: true,
|
|
555
|
-
run: async () => {
|
|
556
|
-
try {
|
|
557
|
-
const result = await context.client.rpc(SmartRouterRpc).status({});
|
|
558
|
-
const { toProviderRow: toProviderRow2 } = await Promise.resolve().then(() => (init_viewmodel(), viewmodel_exports));
|
|
559
|
-
const rows = (result?.quota ?? []).map((snapshot) => toProviderRow2(snapshot));
|
|
560
|
-
await context.ui.dialog.alert({ title: "Providers", message: formatProviders(rows) });
|
|
561
|
-
} catch (cause) {
|
|
562
|
-
await context.ui.dialog.alert({
|
|
563
|
-
title: "Providers",
|
|
564
|
-
message: `Could not read provider status: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
],
|
|
570
|
-
bindings: ["router.decision", "router.providers"]
|
|
571
|
-
}));
|
|
572
|
-
return () => stop();
|
|
573
|
-
}
|
|
574
|
-
});
|
|
575
|
-
function createTerminalSignal(initial) {
|
|
576
|
-
let value = initial;
|
|
577
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
578
|
-
return [() => value, (next) => {
|
|
579
|
-
value = next;
|
|
580
|
-
for (const listener of listeners) listener(next);
|
|
581
|
-
}];
|
|
582
|
-
}
|
|
583
|
-
export {
|
|
584
|
-
index_default as default,
|
|
585
|
-
formatDecision,
|
|
586
|
-
formatProviders
|
|
587
|
-
};
|