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