@alexeiled/pi-model-router 0.6.5 → 0.7.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 +27 -0
- package/README.md +56 -251
- package/extensions/classifier.ts +4 -5
- package/extensions/commands.ts +278 -576
- package/extensions/config.ts +52 -8
- package/extensions/constants.ts +7 -0
- package/extensions/context.ts +6 -2
- package/extensions/jev.ts +325 -66
- package/extensions/provider.ts +30 -4
- package/extensions/state.ts +29 -2
- package/extensions/types.ts +50 -1
- package/extensions/ui.ts +49 -7
- package/model-router.example.json +2 -0
- package/package.json +1 -1
package/extensions/commands.ts
CHANGED
|
@@ -7,7 +7,6 @@ import type { AutocompleteItem } from '@earendil-works/pi-tui';
|
|
|
7
7
|
import {
|
|
8
8
|
getUnsupportedTiers,
|
|
9
9
|
isRouterPinValue,
|
|
10
|
-
isRouterTier,
|
|
11
10
|
isThinkingLevel,
|
|
12
11
|
parseCanonicalModelRef,
|
|
13
12
|
profileNames,
|
|
@@ -21,7 +20,6 @@ import type {
|
|
|
21
20
|
RouterConfig,
|
|
22
21
|
RouterPinByProfile,
|
|
23
22
|
RouterThinkingByProfile,
|
|
24
|
-
RouterTier,
|
|
25
23
|
RoutingDecision,
|
|
26
24
|
} from './types';
|
|
27
25
|
import {
|
|
@@ -34,6 +32,42 @@ import {
|
|
|
34
32
|
formatThinkingSummary,
|
|
35
33
|
} from './ui';
|
|
36
34
|
|
|
35
|
+
/** One verb per concern; state is shown by the verb that changes it. */
|
|
36
|
+
const VERBS = [
|
|
37
|
+
{ name: 'pin', desc: 'Pin the active profile to a tier, or auto' },
|
|
38
|
+
{ name: 'thinking', desc: 'Override thinking for every tier, or auto' },
|
|
39
|
+
{ name: 'log', desc: 'Recent decisions and Jev stats; on, off or clear' },
|
|
40
|
+
{ name: 'widget', desc: 'Toggle the status widget' },
|
|
41
|
+
{ name: 'off', desc: 'Leave the router and restore the previous model' },
|
|
42
|
+
{ name: 'reload', desc: 'Reload model-router.json' },
|
|
43
|
+
{ name: 'help', desc: 'Show usage' },
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
/** Removed verbs answer with the replacement instead of acting. */
|
|
47
|
+
const RETIRED_VERBS: Record<string, string> = {
|
|
48
|
+
status: '/router',
|
|
49
|
+
profile: '/router <profile>',
|
|
50
|
+
disable: '/router off',
|
|
51
|
+
fix: '/router pin <tier>',
|
|
52
|
+
debug: '/router log',
|
|
53
|
+
'?': '/router help',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const LOG_ACTIONS = ['on', 'off', 'clear'] as const;
|
|
57
|
+
const THINKING_VALUES = ['auto', ...THINKING_LEVELS] as const;
|
|
58
|
+
|
|
59
|
+
const USAGE = [
|
|
60
|
+
'/router status',
|
|
61
|
+
'/router <profile> switch profile (enables the router)',
|
|
62
|
+
'/router off leave the router; restore the previous model',
|
|
63
|
+
'/router pin <tier|auto> pin the active profile to high|medium|low|micro, or clear',
|
|
64
|
+
'/router thinking <level|auto> override thinking for every tier, or clear',
|
|
65
|
+
'/router log [on|off|clear] recent decisions and Jev stats; control collection',
|
|
66
|
+
'/router widget toggle the status widget',
|
|
67
|
+
'/router reload reload model-router.json',
|
|
68
|
+
'/router help this text',
|
|
69
|
+
].join('\n');
|
|
70
|
+
|
|
37
71
|
export const registerCommands = (
|
|
38
72
|
pi: ExtensionAPI,
|
|
39
73
|
state: {
|
|
@@ -66,503 +100,224 @@ export const registerCommands = (
|
|
|
66
100
|
syncPiThinkingLevel: (level: ThinkingLevel) => void;
|
|
67
101
|
},
|
|
68
102
|
) => {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
{ name: 'profile', desc: 'Switch to a different router profile' },
|
|
72
|
-
{ name: 'pin', desc: 'Pin routing for a profile to a specific tier' },
|
|
73
|
-
{ name: 'thinking', desc: 'Override thinking level for a tier or profile' },
|
|
74
|
-
{ name: 'disable', desc: 'Disable the router and restore last model' },
|
|
75
|
-
{
|
|
76
|
-
name: 'fix',
|
|
77
|
-
desc: 'Correct the last routing decision and pin that tier',
|
|
78
|
-
},
|
|
79
|
-
{ name: 'widget', desc: 'Toggle the router status widget' },
|
|
80
|
-
{
|
|
81
|
-
name: 'debug',
|
|
82
|
-
desc: 'Inspect Jev stats or control router debug history',
|
|
83
|
-
},
|
|
84
|
-
{ name: 'reload', desc: 'Reload the model router configuration' },
|
|
85
|
-
{ name: 'help', desc: 'Show usage help for subcommands' },
|
|
86
|
-
];
|
|
87
|
-
|
|
88
|
-
const getSubcommandCompletions = (
|
|
89
|
-
prefix: string,
|
|
90
|
-
): AutocompleteItem[] | null => {
|
|
91
|
-
const items = SUBCOMMAND_DETAILS.filter((s) =>
|
|
92
|
-
s.name.startsWith(prefix),
|
|
93
|
-
).map((s) => ({
|
|
94
|
-
value: s.name,
|
|
95
|
-
label: s.name,
|
|
96
|
-
description: s.desc,
|
|
97
|
-
}));
|
|
98
|
-
return items.length > 0 ? items : null;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const getPinCompletions = (args: string[]): AutocompleteItem[] | null => {
|
|
102
|
-
// pin <tier|auto>
|
|
103
|
-
if (args.length <= 1) {
|
|
104
|
-
const token = args[0] ?? '';
|
|
105
|
-
const items = ROUTER_PIN_VALUES.filter((value) =>
|
|
106
|
-
value.startsWith(token),
|
|
107
|
-
).map((value) => ({
|
|
108
|
-
value,
|
|
109
|
-
label: value,
|
|
110
|
-
description:
|
|
111
|
-
value === 'auto'
|
|
112
|
-
? 'Restore auto-routing (clear pin) for the active profile'
|
|
113
|
-
: `Pin active profile to ${value} tier`,
|
|
114
|
-
}));
|
|
115
|
-
return items.length > 0 ? items : null;
|
|
116
|
-
}
|
|
117
|
-
return null;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const getThinkingCompletions = (
|
|
121
|
-
args: string[],
|
|
122
|
-
): AutocompleteItem[] | null => {
|
|
123
|
-
// thinking [tier] <level|auto>
|
|
124
|
-
const tierValues: RouterTier[] = [...ROUTER_TIERS];
|
|
125
|
-
const levelValues = ['auto', ...THINKING_LEVELS];
|
|
126
|
-
|
|
127
|
-
if (args.length <= 1) {
|
|
128
|
-
const token = args[0] ?? '';
|
|
129
|
-
return [
|
|
130
|
-
...levelValues
|
|
131
|
-
.filter((v) => v.startsWith(token))
|
|
132
|
-
.map((v) => ({
|
|
133
|
-
value: v,
|
|
134
|
-
label: v,
|
|
135
|
-
description:
|
|
136
|
-
v === 'auto'
|
|
137
|
-
? 'Restore default thinking level'
|
|
138
|
-
: `Set thinking level to ${v}`,
|
|
139
|
-
})),
|
|
140
|
-
...tierValues
|
|
141
|
-
.filter((v) => v.startsWith(token))
|
|
142
|
-
.map((v) => ({
|
|
143
|
-
value: v,
|
|
144
|
-
label: v,
|
|
145
|
-
description: `Override thinking for ${v} tier`,
|
|
146
|
-
})),
|
|
147
|
-
];
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const tier = args[0];
|
|
151
|
-
if (isRouterTier(tier)) {
|
|
152
|
-
const levelPrefix = args[1] ?? '';
|
|
153
|
-
return levelValues
|
|
154
|
-
.filter((v) => v.startsWith(levelPrefix))
|
|
155
|
-
.map((v) => ({
|
|
156
|
-
value: `${tier} ${v}`,
|
|
157
|
-
label: `${tier} ${v}`,
|
|
158
|
-
description:
|
|
159
|
-
v === 'auto'
|
|
160
|
-
? `Restore default thinking level for ${tier} tier`
|
|
161
|
-
: `Set thinking level to ${v} for ${tier} tier`,
|
|
162
|
-
}));
|
|
163
|
-
}
|
|
103
|
+
const usage = (ctx: ExtensionContext, line: string) =>
|
|
104
|
+
ctx.ui.notify(`Usage: ${line}`, 'error');
|
|
164
105
|
|
|
165
|
-
|
|
106
|
+
const activeProfile = (ctx: ExtensionContext): string | undefined => {
|
|
107
|
+
if (!state.selectedProfile)
|
|
108
|
+
ctx.ui.notify(
|
|
109
|
+
'No router profile is active. Run /router <profile> first.',
|
|
110
|
+
'error',
|
|
111
|
+
);
|
|
112
|
+
return state.selectedProfile;
|
|
166
113
|
};
|
|
167
114
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
115
|
+
const showStatus = (ctx: ExtensionContext) => {
|
|
116
|
+
const profile = state.selectedProfile;
|
|
117
|
+
const config = state.currentConfig;
|
|
118
|
+
const jev = config.jev;
|
|
119
|
+
const context = jev?.context ?? DEFAULT_JEV_CONTEXT;
|
|
120
|
+
const cost =
|
|
121
|
+
`$${state.accumulatedCost.toFixed(4)}` +
|
|
122
|
+
(config.maxSessionBudget
|
|
123
|
+
? ` / $${config.maxSessionBudget.toFixed(2)}`
|
|
124
|
+
: '');
|
|
176
125
|
const lines = [
|
|
177
|
-
'
|
|
178
|
-
`
|
|
179
|
-
`
|
|
180
|
-
`Selected profile pin: ${state.selectedProfile ? (state.pinnedTierByProfile[state.selectedProfile] ?? 'auto') : 'none'}`,
|
|
181
|
-
`Pins by profile: ${formatPinSummary(state.pinnedTierByProfile)}`,
|
|
182
|
-
`Thinking overrides: ${formatThinkingSummary(state.thinkingByProfile)}`,
|
|
183
|
-
`Widget: ${state.widgetEnabled ? 'on' : 'off'}`,
|
|
184
|
-
`Status line: ${state.currentConfig.ui?.statusLine ?? 'compact'}`,
|
|
126
|
+
`Router: ${state.routerEnabled ? 'on' : 'off'} · profile ${profile ?? 'none'} · available: ${profileNames(config).join(', ')}`,
|
|
127
|
+
`Pin: ${formatPinSummary(state.pinnedTierByProfile)} · thinking override: ${formatThinkingSummary(state.thinkingByProfile)}`,
|
|
128
|
+
`Baseline: ${profile ? (config.profiles[profile]?.baselineTier ?? 'automatic') : 'none'} · cost: ${cost} · widget: ${state.widgetEnabled ? 'on' : 'off'} · log: ${state.debugEnabled ? 'on' : 'off'} (${state.debugHistory.length} decisions)`,
|
|
185
129
|
jev
|
|
186
|
-
? `Jev
|
|
187
|
-
: 'Jev
|
|
188
|
-
`
|
|
189
|
-
'Jev confidence measures classification certainty, not model success.',
|
|
190
|
-
`Session cost: $${state.accumulatedCost.toFixed(4)}` +
|
|
191
|
-
(state.currentConfig.maxSessionBudget
|
|
192
|
-
? ` / $${state.currentConfig.maxSessionBudget.toFixed(2)}`
|
|
193
|
-
: ''),
|
|
194
|
-
`Available profiles: ${names}`,
|
|
195
|
-
`Last non-router model: ${formatModelRef(state.lastNonRouterModel)}`,
|
|
196
|
-
`Debug: ${state.debugEnabled ? 'on' : 'off'}`,
|
|
197
|
-
`Debug history: ${state.debugHistory.length} decisions`,
|
|
198
|
-
`Baseline preference: ${state.selectedProfile ? (state.currentConfig.profiles[state.selectedProfile]?.baselineTier ?? 'automatic') : 'none'} (eligibility and budget still apply)`,
|
|
130
|
+
? `Jev: ${jev.enabled ? 'enabled' : 'disabled'} · profile opt-in: ${profile && config.profiles[profile]?.jev?.enabled ? 'yes' : 'no'} · budget ${jev.timeoutMs}ms · context ${context.previousTurns} turns / ≈${context.maxHistoryTokens} history / ${context.toolResults} ≈${context.maxToolTokens} tool / ≈${jev.maxStateTokens} state tokens`
|
|
131
|
+
: 'Jev: not configured',
|
|
132
|
+
`Previous model: ${formatModelRef(state.lastNonRouterModel)}`,
|
|
199
133
|
...formatJevStats(state.debugHistory),
|
|
200
134
|
];
|
|
201
|
-
|
|
202
|
-
|
|
135
|
+
const last = state.lastDecision;
|
|
136
|
+
if (last) {
|
|
137
|
+
const source = formatDecisionSource(last);
|
|
138
|
+
const advisor = formatAdvisorDetail(last);
|
|
203
139
|
lines.push(
|
|
204
|
-
`Last
|
|
205
|
-
|
|
206
|
-
`Last model: ${state.lastDecision.targetProvider}/${state.lastDecision.targetModelId} (${state.lastDecision.thinking})`,
|
|
207
|
-
...(formatDecisionSource(state.lastDecision)
|
|
208
|
-
? [`Reason: ${formatDecisionSource(state.lastDecision)}`]
|
|
209
|
-
: []),
|
|
210
|
-
...(advisorDetail ? [advisorDetail] : []),
|
|
140
|
+
`Last: ${last.tier} → ${last.targetProvider}/${last.targetModelId} (${last.thinking})${source ? ` · ${source}` : ''}`,
|
|
141
|
+
...(advisor ? [advisor] : []),
|
|
211
142
|
);
|
|
212
143
|
}
|
|
213
|
-
if (state.lastConfigWarnings
|
|
144
|
+
if (state.lastConfigWarnings.length > 0)
|
|
214
145
|
lines.push(
|
|
215
146
|
'',
|
|
216
|
-
'⚠️ Configuration
|
|
217
|
-
...state.lastConfigWarnings.map((
|
|
147
|
+
'⚠️ Configuration warnings:',
|
|
148
|
+
...state.lastConfigWarnings.map((warning) => ` - ${warning}`),
|
|
218
149
|
);
|
|
219
|
-
}
|
|
220
150
|
ctx.ui.notify(lines.join('\n'), 'info');
|
|
221
151
|
actions.updateStatus(ctx);
|
|
222
152
|
};
|
|
223
153
|
|
|
224
|
-
const handleProfile = async (
|
|
225
|
-
if (
|
|
226
|
-
ctx.ui.notify(
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
const profileName = args[0];
|
|
230
|
-
if (!profileName) {
|
|
231
|
-
ctx.ui.notify(
|
|
232
|
-
`Current profile: ${state.selectedProfile}. Available: ${profileNames(state.currentConfig).join(', ')}`,
|
|
233
|
-
'info',
|
|
234
|
-
);
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
const success = await actions.switchToRouterProfile(profileName, ctx);
|
|
238
|
-
if (success) {
|
|
239
|
-
ctx.ui.notify(
|
|
240
|
-
`Switched to router profile: ${state.selectedProfile}`,
|
|
241
|
-
'info',
|
|
242
|
-
);
|
|
243
|
-
}
|
|
154
|
+
const handleProfile = async (name: string, ctx: ExtensionContext) => {
|
|
155
|
+
if (await actions.switchToRouterProfile(name, ctx))
|
|
156
|
+
ctx.ui.notify(`Router profile: ${state.selectedProfile}`, 'info');
|
|
244
157
|
};
|
|
245
158
|
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
if (!currentProfile) {
|
|
159
|
+
const handleOff = async (ctx: ExtensionContext) => {
|
|
160
|
+
if (!state.lastNonRouterModel) {
|
|
249
161
|
ctx.ui.notify(
|
|
250
|
-
'No router
|
|
251
|
-
'
|
|
162
|
+
'No previous non-router model recorded. Use /model to pick one.',
|
|
163
|
+
'warning',
|
|
252
164
|
);
|
|
253
165
|
return;
|
|
254
166
|
}
|
|
255
|
-
|
|
167
|
+
const { provider, modelId } = parseCanonicalModelRef(
|
|
168
|
+
state.lastNonRouterModel,
|
|
169
|
+
);
|
|
170
|
+
const target = ctx.modelRegistry.find(provider, modelId);
|
|
171
|
+
if (!target) {
|
|
256
172
|
ctx.ui.notify(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
`Pinned tier: ${state.pinnedTierByProfile[currentProfile] ?? 'auto'}`,
|
|
260
|
-
`Usage: /router pin <high|medium|low|micro|auto>`,
|
|
261
|
-
].join('\n'),
|
|
262
|
-
'info',
|
|
173
|
+
`Previous model is unavailable: ${state.lastNonRouterModel}`,
|
|
174
|
+
'error',
|
|
263
175
|
);
|
|
264
|
-
actions.updateStatus(ctx);
|
|
265
176
|
return;
|
|
266
177
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
ctx.ui.notify('Usage: /router pin <high|medium|low|micro|auto>', 'error');
|
|
178
|
+
if (!(await pi.setModel(target))) {
|
|
179
|
+
ctx.ui.notify(`Failed to switch to ${state.lastNonRouterModel}`, 'error');
|
|
270
180
|
return;
|
|
271
181
|
}
|
|
182
|
+
state.routerEnabled = false;
|
|
183
|
+
actions.persistState();
|
|
184
|
+
actions.updateStatus(ctx);
|
|
185
|
+
ctx.ui.notify(`Router off. Restored ${state.lastNonRouterModel}`, 'info');
|
|
186
|
+
};
|
|
272
187
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (!
|
|
188
|
+
const handlePin = (args: string[], ctx: ExtensionContext) => {
|
|
189
|
+
const profile = activeProfile(ctx);
|
|
190
|
+
if (!profile) return;
|
|
191
|
+
const value = args[0]?.toLowerCase();
|
|
192
|
+
if (args.length === 0) {
|
|
276
193
|
ctx.ui.notify(
|
|
277
|
-
`
|
|
278
|
-
'
|
|
194
|
+
`Pin: ${state.pinnedTierByProfile[profile] ?? 'auto'} (profile ${profile})`,
|
|
195
|
+
'info',
|
|
279
196
|
);
|
|
280
197
|
return;
|
|
281
198
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (nextTier) {
|
|
286
|
-
state.pinnedTierByProfile[currentProfile] = nextTier;
|
|
287
|
-
} else {
|
|
288
|
-
delete state.pinnedTierByProfile[currentProfile];
|
|
199
|
+
if (args.length > 1 || !isRouterPinValue(value)) {
|
|
200
|
+
usage(ctx, `/router pin <${ROUTER_PIN_VALUES.join('|')}>`);
|
|
201
|
+
return;
|
|
289
202
|
}
|
|
203
|
+
if (value === 'auto') delete state.pinnedTierByProfile[profile];
|
|
204
|
+
else state.pinnedTierByProfile[profile] = value;
|
|
290
205
|
actions.persistState();
|
|
291
206
|
actions.updateStatus(ctx);
|
|
292
207
|
ctx.ui.notify(
|
|
293
|
-
|
|
294
|
-
?
|
|
295
|
-
: `Router
|
|
208
|
+
value === 'auto'
|
|
209
|
+
? 'Router pin cleared; baseline routing restored'
|
|
210
|
+
: `Router pinned to ${value}`,
|
|
296
211
|
'info',
|
|
297
212
|
);
|
|
298
213
|
};
|
|
299
214
|
|
|
300
|
-
const handleThinking =
|
|
301
|
-
const
|
|
302
|
-
if (!
|
|
303
|
-
|
|
304
|
-
'No router profile is active. Select a router model first.',
|
|
305
|
-
'error',
|
|
306
|
-
);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
215
|
+
const handleThinking = (args: string[], ctx: ExtensionContext) => {
|
|
216
|
+
const profile = activeProfile(ctx);
|
|
217
|
+
if (!profile) return;
|
|
218
|
+
const value = args[0]?.toLowerCase();
|
|
309
219
|
if (args.length === 0) {
|
|
310
220
|
ctx.ui.notify(
|
|
311
|
-
|
|
312
|
-
`Profile: ${currentProfile}`,
|
|
313
|
-
`Thinking overrides: ${JSON.stringify(state.thinkingByProfile[currentProfile] ?? {})}`,
|
|
314
|
-
'Usage: /router thinking <level|auto> (applies to all tiers)',
|
|
315
|
-
' or: /router thinking <tier> <level|auto> (applies to one tier)',
|
|
316
|
-
'Note: not all tier models may support every thinking level.',
|
|
317
|
-
].join('\n'),
|
|
221
|
+
`Thinking override: ${formatThinkingSummary(state.thinkingByProfile)}`,
|
|
318
222
|
'info',
|
|
319
223
|
);
|
|
320
224
|
return;
|
|
321
225
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
let tier: RouterTier | 'all' | undefined;
|
|
329
|
-
let levelValue = '';
|
|
330
|
-
|
|
331
|
-
const levelValues = ['auto', ...THINKING_LEVELS];
|
|
332
|
-
|
|
333
|
-
if (args.length === 1) {
|
|
334
|
-
const level = args[0];
|
|
335
|
-
if (!level) return;
|
|
336
|
-
levelValue = level;
|
|
337
|
-
tier = 'all';
|
|
338
|
-
} else if (args.length === 2) {
|
|
339
|
-
const requestedTier = args[0];
|
|
340
|
-
const requestedLevel = args[1];
|
|
341
|
-
if (!requestedTier || !requestedLevel) return;
|
|
342
|
-
if (isRouterTier(requestedTier) || requestedTier === 'all') {
|
|
343
|
-
tier = requestedTier === 'all' ? 'all' : requestedTier;
|
|
344
|
-
levelValue = requestedLevel;
|
|
345
|
-
} else {
|
|
346
|
-
ctx.ui.notify(
|
|
347
|
-
`Invalid tier: ${args[0]}. Use high, medium, low, or micro.`,
|
|
348
|
-
'error',
|
|
349
|
-
);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (tier !== 'all' && !tier) {
|
|
355
|
-
ctx.ui.notify(
|
|
356
|
-
`Invalid tier: ${tier}. Use high, medium, low, or micro.`,
|
|
357
|
-
'error',
|
|
358
|
-
);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
if (!levelValues.includes(levelValue)) {
|
|
362
|
-
ctx.ui.notify(
|
|
363
|
-
`Invalid thinking level: ${levelValue}. Use auto or: ${THINKING_LEVELS.join(', ')}`,
|
|
364
|
-
'error',
|
|
365
|
-
);
|
|
226
|
+
if (
|
|
227
|
+
args.length > 1 ||
|
|
228
|
+
!value ||
|
|
229
|
+
!THINKING_VALUES.some((level) => level === value)
|
|
230
|
+
) {
|
|
231
|
+
usage(ctx, `/router thinking <${THINKING_VALUES.join('|')}>`);
|
|
366
232
|
return;
|
|
367
233
|
}
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
levelValue === 'auto'
|
|
371
|
-
? undefined
|
|
372
|
-
: isThinkingLevel(levelValue)
|
|
373
|
-
? levelValue
|
|
374
|
-
: undefined;
|
|
375
|
-
const overrides = { ...state.thinkingByProfile[currentProfile] };
|
|
376
|
-
const tiers = tier === 'all' ? ROUTER_TIERS : [tier];
|
|
377
|
-
for (const targetTier of tiers) {
|
|
378
|
-
if (nextLevel) overrides[targetTier] = nextLevel;
|
|
379
|
-
else delete overrides[targetTier];
|
|
380
|
-
}
|
|
381
|
-
const activeProfile = state.currentConfig.profiles[currentProfile];
|
|
234
|
+
const level = isThinkingLevel(value) ? value : undefined;
|
|
235
|
+
const config = state.currentConfig.profiles[profile];
|
|
382
236
|
if (
|
|
383
|
-
|
|
384
|
-
|
|
237
|
+
level &&
|
|
238
|
+
config &&
|
|
385
239
|
preservesRouteCoverage(
|
|
386
|
-
|
|
240
|
+
config,
|
|
387
241
|
(provider, id) => ctx.modelRegistry.find(provider, id),
|
|
388
|
-
|
|
242
|
+
Object.fromEntries(ROUTER_TIERS.map((tier) => [tier, level])),
|
|
389
243
|
) === false
|
|
390
244
|
) {
|
|
391
245
|
ctx.ui.notify(
|
|
392
|
-
`Router thinking unchanged: '${
|
|
246
|
+
`Router thinking unchanged: '${level}' leaves no eligible route.`,
|
|
393
247
|
'warning',
|
|
394
248
|
);
|
|
395
249
|
return;
|
|
396
250
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
actions.persistState();
|
|
403
|
-
actions.updateStatus(ctx);
|
|
404
|
-
if (nextLevel) {
|
|
405
|
-
actions.syncPiThinkingLevel(nextLevel);
|
|
406
|
-
} else if (state.lastDecision) {
|
|
407
|
-
actions.syncPiThinkingLevel(state.lastDecision.thinking);
|
|
408
|
-
}
|
|
409
|
-
// Only warn when the level isn't supported by some tiers; skip for 'off' and 'auto'
|
|
410
|
-
if (nextLevel && nextLevel !== 'off') {
|
|
411
|
-
const activeProfile = state.currentConfig.profiles[currentProfile];
|
|
412
|
-
if (!activeProfile) return;
|
|
413
|
-
const unsupported = getUnsupportedTiers(activeProfile, nextLevel);
|
|
414
|
-
if (unsupported.length > 0) {
|
|
415
|
-
ctx.ui.notify(
|
|
416
|
-
`Router thinking (${tier}) set to ${nextLevel}. ` +
|
|
417
|
-
`${unsupported.join(', ')} tier${unsupported.length > 1 ? 's' : ''} may not support '${nextLevel}' and will be skipped when unsupported.`,
|
|
418
|
-
'warning',
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
const handleDisable = async (args: string[], ctx: ExtensionContext) => {
|
|
425
|
-
if (args.length > 0) {
|
|
426
|
-
ctx.ui.notify('Usage: /router disable (no arguments)', 'error');
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
if (!state.lastNonRouterModel) {
|
|
430
|
-
ctx.ui.notify(
|
|
431
|
-
'No previous non-router model recorded. Use /model to pick a concrete model.',
|
|
432
|
-
'warning',
|
|
433
|
-
);
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
const { provider, modelId } = parseCanonicalModelRef(
|
|
437
|
-
state.lastNonRouterModel,
|
|
438
|
-
);
|
|
439
|
-
const targetModel = ctx.modelRegistry.find(provider, modelId);
|
|
440
|
-
if (!targetModel) {
|
|
441
|
-
ctx.ui.notify(
|
|
442
|
-
`Recorded non-router model is unavailable: ${state.lastNonRouterModel}`,
|
|
443
|
-
'error',
|
|
251
|
+
if (level)
|
|
252
|
+
state.thinkingByProfile[profile] = Object.fromEntries(
|
|
253
|
+
ROUTER_TIERS.map((tier) => [tier, level]),
|
|
444
254
|
);
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
const success = await pi.setModel(targetModel);
|
|
448
|
-
if (!success) {
|
|
449
|
-
ctx.ui.notify(`Failed to switch to ${state.lastNonRouterModel}`, 'error');
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
state.routerEnabled = false;
|
|
255
|
+
else delete state.thinkingByProfile[profile];
|
|
453
256
|
actions.persistState();
|
|
454
257
|
actions.updateStatus(ctx);
|
|
258
|
+
if (level) actions.syncPiThinkingLevel(level);
|
|
259
|
+
else if (state.lastDecision)
|
|
260
|
+
actions.syncPiThinkingLevel(state.lastDecision.thinking);
|
|
261
|
+
const unsupported =
|
|
262
|
+
level && level !== 'off' && config
|
|
263
|
+
? getUnsupportedTiers(config, level)
|
|
264
|
+
: [];
|
|
455
265
|
ctx.ui.notify(
|
|
456
|
-
|
|
457
|
-
|
|
266
|
+
level
|
|
267
|
+
? `Router thinking set to ${level}${unsupported.length > 0 ? `; ${unsupported.join(', ')} may not support it and will be skipped when unsupported` : ''}`
|
|
268
|
+
: 'Router thinking override cleared',
|
|
269
|
+
unsupported.length > 0 ? 'warning' : 'info',
|
|
458
270
|
);
|
|
459
271
|
};
|
|
460
272
|
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
273
|
+
const handleLog = (args: string[], ctx: ExtensionContext) => {
|
|
274
|
+
const action = args[0]?.toLowerCase();
|
|
275
|
+
if (args.length > 1 || (action && !LOG_ACTIONS.some((a) => a === action))) {
|
|
276
|
+
usage(ctx, `/router log [${LOG_ACTIONS.join('|')}]`);
|
|
464
277
|
return;
|
|
465
278
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
279
|
+
if (action === 'on' || action === 'off') {
|
|
280
|
+
state.debugEnabled = action === 'on';
|
|
281
|
+
actions.persistState();
|
|
282
|
+
ctx.ui.notify(`Router log ${action}`, 'info');
|
|
469
283
|
return;
|
|
470
284
|
}
|
|
471
|
-
if (
|
|
472
|
-
|
|
285
|
+
if (action === 'clear') {
|
|
286
|
+
state.debugHistory.length = 0;
|
|
287
|
+
actions.persistState();
|
|
288
|
+
ctx.ui.notify('Router log cleared', 'info');
|
|
473
289
|
return;
|
|
474
290
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
291
|
+
const header = state.debugEnabled
|
|
292
|
+
? 'Log: on'
|
|
293
|
+
: 'Log: off; /router log on collects new decisions';
|
|
294
|
+
const history = state.debugHistory.map(
|
|
295
|
+
(decision) =>
|
|
296
|
+
`[${new Date(decision.timestamp).toLocaleTimeString()}] ${formatDecision(decision)}`,
|
|
481
297
|
);
|
|
482
|
-
};
|
|
483
|
-
|
|
484
|
-
const handleWidget = async (args: string[], ctx: ExtensionContext) => {
|
|
485
|
-
if (args.length > 1) {
|
|
486
|
-
ctx.ui.notify('Usage: /router widget <on|off|toggle>', 'error');
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
const cmd = args[0]?.toLowerCase();
|
|
490
|
-
if (cmd && !['on', 'off', 'toggle'].includes(cmd)) {
|
|
491
|
-
ctx.ui.notify('Usage: /router widget <on|off|toggle>', 'error');
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
if (cmd === 'on') state.widgetEnabled = true;
|
|
495
|
-
else if (cmd === 'off') state.widgetEnabled = false;
|
|
496
|
-
else state.widgetEnabled = !state.widgetEnabled;
|
|
497
|
-
actions.persistState();
|
|
498
|
-
actions.updateStatus(ctx);
|
|
499
298
|
ctx.ui.notify(
|
|
500
|
-
|
|
299
|
+
[
|
|
300
|
+
header,
|
|
301
|
+
...formatJevStats(state.debugHistory),
|
|
302
|
+
...(history.length > 0
|
|
303
|
+
? ['Recent decisions:', ...history]
|
|
304
|
+
: ['No recent routing decisions.']),
|
|
305
|
+
].join('\n'),
|
|
501
306
|
'info',
|
|
502
307
|
);
|
|
503
308
|
};
|
|
504
309
|
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
ctx.ui.notify('Usage: /router debug <on|off|show|stats|clear>', 'error');
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
const cmd = args[0]?.toLowerCase();
|
|
511
|
-
if (
|
|
512
|
-
cmd &&
|
|
513
|
-
!['on', 'off', 'toggle', 'clear', 'show', 'stats'].includes(cmd)
|
|
514
|
-
) {
|
|
515
|
-
ctx.ui.notify(
|
|
516
|
-
'Usage: /router debug <on|off|toggle|show|stats|clear>',
|
|
517
|
-
'error',
|
|
518
|
-
);
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
if (cmd === 'on') state.debugEnabled = true;
|
|
522
|
-
else if (cmd === 'off') state.debugEnabled = false;
|
|
523
|
-
else if (cmd === 'clear') state.debugHistory.length = 0;
|
|
524
|
-
else if (cmd === 'stats') {
|
|
525
|
-
ctx.ui.notify(
|
|
526
|
-
[
|
|
527
|
-
state.debugEnabled
|
|
528
|
-
? 'Debug collection: on'
|
|
529
|
-
: 'Debug collection: off; use /router debug on to collect new decisions.',
|
|
530
|
-
...formatJevStats(state.debugHistory),
|
|
531
|
-
].join('\n'),
|
|
532
|
-
'info',
|
|
533
|
-
);
|
|
534
|
-
return;
|
|
535
|
-
} else if (cmd === 'show') {
|
|
536
|
-
if (state.debugHistory.length === 0) {
|
|
537
|
-
ctx.ui.notify('No recent routing decisions.', 'info');
|
|
538
|
-
} else {
|
|
539
|
-
const history = state.debugHistory
|
|
540
|
-
.map(
|
|
541
|
-
(d) =>
|
|
542
|
-
`[${new Date(d.timestamp).toLocaleTimeString()}] ${formatDecision(d)}`,
|
|
543
|
-
)
|
|
544
|
-
.join('\n');
|
|
545
|
-
ctx.ui.notify(
|
|
546
|
-
`${formatJevStats(state.debugHistory).join('\n')}\nRecent Routing Decisions:\n${history}`,
|
|
547
|
-
'info',
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
return;
|
|
551
|
-
} else {
|
|
552
|
-
state.debugEnabled = !state.debugEnabled;
|
|
553
|
-
}
|
|
310
|
+
const handleWidget = (ctx: ExtensionContext) => {
|
|
311
|
+
state.widgetEnabled = !state.widgetEnabled;
|
|
554
312
|
actions.persistState();
|
|
313
|
+
actions.updateStatus(ctx);
|
|
555
314
|
ctx.ui.notify(
|
|
556
|
-
`Router
|
|
315
|
+
`Router widget ${state.widgetEnabled ? 'on' : 'off'}`,
|
|
557
316
|
'info',
|
|
558
317
|
);
|
|
559
318
|
};
|
|
560
319
|
|
|
561
|
-
const handleReload = async (
|
|
562
|
-
if (args.length > 0) {
|
|
563
|
-
ctx.ui.notify('Usage: /router reload (no arguments)', 'error');
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
320
|
+
const handleReload = async (ctx: ExtensionContext) => {
|
|
566
321
|
actions.reloadConfig(ctx, { preserveDebug: true });
|
|
567
322
|
await actions.ensureValidActiveRouterProfile(ctx);
|
|
568
323
|
ctx.ui.notify(
|
|
@@ -571,186 +326,133 @@ export const registerCommands = (
|
|
|
571
326
|
);
|
|
572
327
|
};
|
|
573
328
|
|
|
329
|
+
const items = (
|
|
330
|
+
values: readonly string[],
|
|
331
|
+
token: string,
|
|
332
|
+
describe: (value: string) => string,
|
|
333
|
+
prefix = '',
|
|
334
|
+
): AutocompleteItem[] | null => {
|
|
335
|
+
const matches = values
|
|
336
|
+
.filter((value) => value.startsWith(token))
|
|
337
|
+
.map((value) => ({
|
|
338
|
+
value: `${prefix}${value}`,
|
|
339
|
+
label: value,
|
|
340
|
+
description: describe(value),
|
|
341
|
+
}));
|
|
342
|
+
return matches.length > 0 ? matches : null;
|
|
343
|
+
};
|
|
344
|
+
|
|
574
345
|
pi.registerCommand('router', {
|
|
575
|
-
description: 'Model router
|
|
346
|
+
description: 'Model router: profile, pin, thinking, log',
|
|
576
347
|
getArgumentCompletions: (prefix) => {
|
|
577
|
-
const
|
|
578
|
-
const
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
subArgs.push('');
|
|
348
|
+
const text = prefix.trimStart();
|
|
349
|
+
const parts = text.length > 0 ? text.split(/\s+/) : [];
|
|
350
|
+
const trailing = /\s$/.test(prefix);
|
|
351
|
+
if (parts.length === 0 || (parts.length === 1 && !trailing)) {
|
|
352
|
+
const token = parts[0] ?? '';
|
|
353
|
+
const profiles = items(
|
|
354
|
+
profileNames(state.currentConfig),
|
|
355
|
+
token,
|
|
356
|
+
(name) => `Switch to profile ${name}`,
|
|
357
|
+
);
|
|
358
|
+
const verbs = items(
|
|
359
|
+
VERBS.map((verb) => verb.name),
|
|
360
|
+
token,
|
|
361
|
+
(name) => VERBS.find((verb) => verb.name === name)?.desc ?? name,
|
|
362
|
+
);
|
|
363
|
+
const all = [...(profiles ?? []), ...(verbs ?? [])];
|
|
364
|
+
return all.length > 0 ? all : null;
|
|
595
365
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
case 'pin': {
|
|
610
|
-
const completions = getPinCompletions(subArgs);
|
|
611
|
-
return (
|
|
612
|
-
completions?.map((c) => ({
|
|
613
|
-
...c,
|
|
614
|
-
value: `pin ${c.value}`,
|
|
615
|
-
description: c.description ?? `Pin routing to ${c.label}`,
|
|
616
|
-
})) ?? null
|
|
366
|
+
const [verb, ...rest] = parts;
|
|
367
|
+
const token = trailing && rest.length === 0 ? '' : (rest[0] ?? '');
|
|
368
|
+
if (rest.length > 1) return null;
|
|
369
|
+
switch (verb) {
|
|
370
|
+
case 'pin':
|
|
371
|
+
return items(
|
|
372
|
+
ROUTER_PIN_VALUES,
|
|
373
|
+
token,
|
|
374
|
+
(value) =>
|
|
375
|
+
value === 'auto'
|
|
376
|
+
? 'Clear the pin for the active profile'
|
|
377
|
+
: `Pin the active profile to ${value}`,
|
|
378
|
+
'pin ',
|
|
617
379
|
);
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
380
|
+
case 'thinking':
|
|
381
|
+
return items(
|
|
382
|
+
THINKING_VALUES,
|
|
383
|
+
token,
|
|
384
|
+
(value) =>
|
|
385
|
+
value === 'auto'
|
|
386
|
+
? 'Clear the thinking override'
|
|
387
|
+
: `Set thinking to ${value} for every tier`,
|
|
388
|
+
'thinking ',
|
|
627
389
|
);
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const items = ['on', 'off', 'toggle']
|
|
643
|
-
.filter((v) => v.startsWith(widgetPrefix))
|
|
644
|
-
.map((v) => ({
|
|
645
|
-
value: `widget ${v}`,
|
|
646
|
-
label: v,
|
|
647
|
-
description: `Set widget to ${v}`,
|
|
648
|
-
}));
|
|
649
|
-
return items.length > 0 ? items : null;
|
|
650
|
-
}
|
|
651
|
-
case 'debug': {
|
|
652
|
-
const debugPrefix = subArgs[0] ?? '';
|
|
653
|
-
const items = ['on', 'off', 'toggle', 'clear', 'show', 'stats']
|
|
654
|
-
.filter((v) => v.startsWith(debugPrefix))
|
|
655
|
-
.map((v) => ({
|
|
656
|
-
value: `debug ${v}`,
|
|
657
|
-
label: v,
|
|
658
|
-
description: `Router debug: ${v}`,
|
|
659
|
-
}));
|
|
660
|
-
return items.length > 0 ? items : null;
|
|
661
|
-
}
|
|
390
|
+
case 'log':
|
|
391
|
+
return items(
|
|
392
|
+
LOG_ACTIONS,
|
|
393
|
+
token,
|
|
394
|
+
(value) =>
|
|
395
|
+
({
|
|
396
|
+
on: 'Collect decisions',
|
|
397
|
+
off: 'Stop collecting decisions',
|
|
398
|
+
clear: 'Forget collected decisions',
|
|
399
|
+
})[value] ?? value,
|
|
400
|
+
'log ',
|
|
401
|
+
);
|
|
402
|
+
default:
|
|
403
|
+
return null;
|
|
662
404
|
}
|
|
663
|
-
|
|
664
|
-
return null;
|
|
665
405
|
},
|
|
666
406
|
handler: async (args, ctx) => {
|
|
667
|
-
const parts = args?.trim().split(/\s+/) ?? [];
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
await handleStatus(subArgs, ctx);
|
|
407
|
+
const parts = args?.trim().split(/\s+/).filter(Boolean) ?? [];
|
|
408
|
+
const [verb, ...rest] = parts;
|
|
409
|
+
if (!verb) {
|
|
410
|
+
showStatus(ctx);
|
|
672
411
|
return;
|
|
673
412
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
413
|
+
const noArgs = (line: string) => {
|
|
414
|
+
if (rest.length > 0) {
|
|
415
|
+
usage(ctx, line);
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
return true;
|
|
419
|
+
};
|
|
420
|
+
switch (verb) {
|
|
679
421
|
case 'pin':
|
|
680
|
-
|
|
681
|
-
|
|
422
|
+
handlePin(rest, ctx);
|
|
423
|
+
return;
|
|
682
424
|
case 'thinking':
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
case '
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
case 'fix':
|
|
689
|
-
await handleFix(subArgs, ctx);
|
|
690
|
-
break;
|
|
425
|
+
handleThinking(rest, ctx);
|
|
426
|
+
return;
|
|
427
|
+
case 'log':
|
|
428
|
+
handleLog(rest, ctx);
|
|
429
|
+
return;
|
|
691
430
|
case 'widget':
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
case '
|
|
695
|
-
await
|
|
696
|
-
|
|
431
|
+
if (noArgs('/router widget')) handleWidget(ctx);
|
|
432
|
+
return;
|
|
433
|
+
case 'off':
|
|
434
|
+
if (noArgs('/router off')) await handleOff(ctx);
|
|
435
|
+
return;
|
|
697
436
|
case 'reload':
|
|
698
|
-
await handleReload(
|
|
699
|
-
|
|
700
|
-
case 'status':
|
|
701
|
-
await handleStatus(subArgs, ctx);
|
|
702
|
-
break;
|
|
437
|
+
if (noArgs('/router reload')) await handleReload(ctx);
|
|
438
|
+
return;
|
|
703
439
|
case 'help':
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
ctx.ui.notify('Usage: /router help (no arguments)', 'error');
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
ctx.ui.notify(
|
|
710
|
-
[
|
|
711
|
-
'Router Subcommands:',
|
|
712
|
-
' status Show current status, profile, pin, cost, and last decision.',
|
|
713
|
-
' profile [name] Switch to a profile (enables router if off). Lists available if no name.',
|
|
714
|
-
' pin <tier|auto> Force a tier (high|medium|low|micro) or set to auto.',
|
|
715
|
-
' thinking [tier] <level> Override thinking level (off|minimal|...|max|auto). Not all tier models may support every level.',
|
|
716
|
-
' disable Disable the router and restore the last used non-router model.',
|
|
717
|
-
' fix <tier> Correct the last routing decision and pin that tier for the current profile.',
|
|
718
|
-
' widget <on|off|toggle> Control the persistent status widget visibility.',
|
|
719
|
-
' debug <on|off|show|stats|clear> Control decision history; stats summarize unique Jev requests.',
|
|
720
|
-
' reload Hot-reload the configuration JSON from .pi/model-router.json.',
|
|
721
|
-
' help, ? Show this help message.',
|
|
722
|
-
].join('\n'),
|
|
723
|
-
'info',
|
|
724
|
-
);
|
|
725
|
-
break;
|
|
440
|
+
if (noArgs('/router help')) ctx.ui.notify(USAGE, 'info');
|
|
441
|
+
return;
|
|
726
442
|
default:
|
|
727
|
-
if (subcommand) {
|
|
728
|
-
// Check if subcommand is actually a profile name (backwards compatible-ish with /router-on)
|
|
729
|
-
if (state.currentConfig.profiles[subcommand]) {
|
|
730
|
-
if (subArgs.length > 0) {
|
|
731
|
-
ctx.ui.notify(
|
|
732
|
-
`Usage: /router ${subcommand} (no extra arguments allowed)`,
|
|
733
|
-
'error',
|
|
734
|
-
);
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
if (await actions.switchToRouterProfile(subcommand, ctx)) {
|
|
738
|
-
ctx.ui.notify(
|
|
739
|
-
`Router enabled with profile: ${state.selectedProfile}`,
|
|
740
|
-
'info',
|
|
741
|
-
);
|
|
742
|
-
}
|
|
743
|
-
} else {
|
|
744
|
-
ctx.ui.notify(
|
|
745
|
-
`Unknown router subcommand: ${subcommand}. Try /router help`,
|
|
746
|
-
'error',
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
} else {
|
|
750
|
-
await handleStatus(subArgs, ctx);
|
|
751
|
-
}
|
|
752
443
|
break;
|
|
753
444
|
}
|
|
445
|
+
if (Object.hasOwn(state.currentConfig.profiles, verb)) {
|
|
446
|
+
if (noArgs(`/router ${verb}`)) await handleProfile(verb, ctx);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const replacement = RETIRED_VERBS[verb];
|
|
450
|
+
ctx.ui.notify(
|
|
451
|
+
replacement
|
|
452
|
+
? `/router ${verb} was removed; use ${replacement}`
|
|
453
|
+
: `Unknown router command: ${verb}. Try /router help`,
|
|
454
|
+
'error',
|
|
455
|
+
);
|
|
754
456
|
},
|
|
755
457
|
});
|
|
756
458
|
};
|