@kdejaeger/pi-model-router 0.3.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/LICENSE +21 -0
- package/README.md +468 -0
- package/docs/ARCHITECTURE.md +75 -0
- package/extensions/commands.ts +452 -0
- package/extensions/config.ts +405 -0
- package/extensions/index.ts +412 -0
- package/extensions/provider.ts +442 -0
- package/extensions/routing.ts +290 -0
- package/extensions/state.ts +39 -0
- package/extensions/types.ts +74 -0
- package/extensions/ui.ts +54 -0
- package/model-router.example.json +48 -0
- package/package.json +54 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from '@earendil-works/pi-coding-agent';
|
|
5
|
+
import {
|
|
6
|
+
type RouterConfig,
|
|
7
|
+
type RoutingDecision,
|
|
8
|
+
type RouterPinByProfile,
|
|
9
|
+
type CustomSessionEntry,
|
|
10
|
+
} from './types';
|
|
11
|
+
import {
|
|
12
|
+
loadRouterConfig,
|
|
13
|
+
profileNames,
|
|
14
|
+
resolveProfileName,
|
|
15
|
+
} from './config';
|
|
16
|
+
import { isRouterPersistedState, buildPersistedState } from './state';
|
|
17
|
+
import { updateStatus } from './ui';
|
|
18
|
+
import { registerCommands } from './commands';
|
|
19
|
+
import { registerRouterProvider } from './provider';
|
|
20
|
+
|
|
21
|
+
const routerExtension = (pi: ExtensionAPI) => {
|
|
22
|
+
let currentConfig: RouterConfig = { profiles: {} };
|
|
23
|
+
let currentModelRegistry: ExtensionContext['modelRegistry'] | undefined;
|
|
24
|
+
let currentCwd = process.cwd();
|
|
25
|
+
let lastDecision: RoutingDecision | undefined;
|
|
26
|
+
let debugEnabled = false;
|
|
27
|
+
let routerEnabled = false;
|
|
28
|
+
let selectedProfile: string | undefined;
|
|
29
|
+
let lastLoadedModelKeys = '';
|
|
30
|
+
let pinnedTierByProfile: RouterPinByProfile = {};
|
|
31
|
+
let debugHistory: RoutingDecision[] = [];
|
|
32
|
+
let lastNonRouterModel: string | undefined;
|
|
33
|
+
let lastExtensionContext: ExtensionContext | undefined;
|
|
34
|
+
let lastConfigWarnings: string[] = [];
|
|
35
|
+
let lastPersistedSnapshot: string | undefined;
|
|
36
|
+
let isInitialized = false;
|
|
37
|
+
let isRouterDelegating = false;
|
|
38
|
+
|
|
39
|
+
const setModelInternally = async (
|
|
40
|
+
model: NonNullable<ExtensionContext['model']>,
|
|
41
|
+
) => {
|
|
42
|
+
isRouterDelegating = true;
|
|
43
|
+
try {
|
|
44
|
+
return await pi.setModel(model);
|
|
45
|
+
} finally {
|
|
46
|
+
isRouterDelegating = false;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const MAX_DEBUG_HISTORY = 12;
|
|
51
|
+
const recordDebugDecision = (decision: RoutingDecision) => {
|
|
52
|
+
debugHistory = [...debugHistory, decision].slice(-MAX_DEBUG_HISTORY);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const persistState = () => {
|
|
56
|
+
const state = buildPersistedState(
|
|
57
|
+
routerEnabled,
|
|
58
|
+
selectedProfile,
|
|
59
|
+
pinnedTierByProfile,
|
|
60
|
+
debugEnabled,
|
|
61
|
+
debugHistory,
|
|
62
|
+
lastDecision,
|
|
63
|
+
lastNonRouterModel,
|
|
64
|
+
);
|
|
65
|
+
const snapshot = JSON.stringify({
|
|
66
|
+
...state,
|
|
67
|
+
timestamp: 0,
|
|
68
|
+
lastDecision: state.lastDecision
|
|
69
|
+
? { ...state.lastDecision, timestamp: 0 }
|
|
70
|
+
: undefined,
|
|
71
|
+
debugHistory: state.debugHistory?.map((decision) => ({
|
|
72
|
+
...decision,
|
|
73
|
+
timestamp: 0,
|
|
74
|
+
})),
|
|
75
|
+
});
|
|
76
|
+
if (snapshot === lastPersistedSnapshot) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
pi.appendEntry('router-state', state);
|
|
80
|
+
lastPersistedSnapshot = snapshot;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const actions = {
|
|
84
|
+
persistState,
|
|
85
|
+
updateStatus: (ctx: ExtensionContext) =>
|
|
86
|
+
updateStatus(
|
|
87
|
+
ctx,
|
|
88
|
+
routerEnabled,
|
|
89
|
+
selectedProfile,
|
|
90
|
+
pinnedTierByProfile,
|
|
91
|
+
lastDecision,
|
|
92
|
+
),
|
|
93
|
+
reloadConfig: (
|
|
94
|
+
ctx?: ExtensionContext,
|
|
95
|
+
options?: { preserveDebug?: boolean },
|
|
96
|
+
) => {
|
|
97
|
+
const loaded = loadRouterConfig(currentCwd);
|
|
98
|
+
currentConfig = loaded.config;
|
|
99
|
+
lastConfigWarnings = loaded.warnings;
|
|
100
|
+
if (!options?.preserveDebug) {
|
|
101
|
+
debugEnabled = currentConfig.debug ?? false;
|
|
102
|
+
}
|
|
103
|
+
const prevSelectedProfile = selectedProfile;
|
|
104
|
+
selectedProfile = resolveProfileName(currentConfig, selectedProfile);
|
|
105
|
+
if (!selectedProfile && prevSelectedProfile && routerEnabled) {
|
|
106
|
+
ctx?.ui.notify(`Router profile "${prevSelectedProfile}" is no longer configured. Router disabled.`, 'warning');
|
|
107
|
+
routerEnabled = false;
|
|
108
|
+
}
|
|
109
|
+
actions.registerRouterProvider();
|
|
110
|
+
if (ctx) {
|
|
111
|
+
actions.updateStatus(ctx);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
ensureValidActiveRouterProfile: async (ctx: ExtensionContext) => {
|
|
115
|
+
if (ctx.model?.provider !== 'router') {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (currentConfig.profiles[ctx.model.id]) {
|
|
119
|
+
selectedProfile = ctx.model.id;
|
|
120
|
+
routerEnabled = true;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ctx.ui.notify(`Router profile "${ctx.model.id}" is no longer configured.`, 'warning');
|
|
125
|
+
routerEnabled = false;
|
|
126
|
+
selectedProfile = undefined;
|
|
127
|
+
},
|
|
128
|
+
switchToRouterProfile: async (
|
|
129
|
+
profileName: string,
|
|
130
|
+
ctx: ExtensionContext,
|
|
131
|
+
strict = true,
|
|
132
|
+
) => {
|
|
133
|
+
if (!currentConfig.profiles[profileName]) {
|
|
134
|
+
if (strict) {
|
|
135
|
+
ctx.ui.notify(`Unknown router profile: ${profileName}`, 'error');
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Ensure the provider is registered with current capacities for this profile
|
|
141
|
+
actions.registerRouterProvider();
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
143
|
+
|
|
144
|
+
const routerModel = ctx.modelRegistry.find('router', profileName);
|
|
145
|
+
if (!routerModel) {
|
|
146
|
+
ctx.ui.notify(`Unknown router profile: ${profileName}`, 'error');
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
if (ctx.model && ctx.model.provider !== 'router') {
|
|
150
|
+
lastNonRouterModel = `${ctx.model.provider}/${ctx.model.id}`;
|
|
151
|
+
}
|
|
152
|
+
const success = await setModelInternally(routerModel);
|
|
153
|
+
if (!success) {
|
|
154
|
+
ctx.ui.notify(`Failed to switch to router/${profileName}`, 'error');
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
selectedProfile = profileName;
|
|
158
|
+
routerEnabled = true;
|
|
159
|
+
persistState();
|
|
160
|
+
pi.setThinkingLevel('off');
|
|
161
|
+
actions.updateStatus(ctx);
|
|
162
|
+
return true;
|
|
163
|
+
},
|
|
164
|
+
registerRouterProvider: () => {
|
|
165
|
+
registerRouterProvider(
|
|
166
|
+
pi,
|
|
167
|
+
{
|
|
168
|
+
get lastLoadedModelKeys() {
|
|
169
|
+
return lastLoadedModelKeys;
|
|
170
|
+
},
|
|
171
|
+
set lastLoadedModelKeys(v) {
|
|
172
|
+
lastLoadedModelKeys = v;
|
|
173
|
+
},
|
|
174
|
+
get currentConfig() {
|
|
175
|
+
return currentConfig;
|
|
176
|
+
},
|
|
177
|
+
get currentModelRegistry() {
|
|
178
|
+
return currentModelRegistry;
|
|
179
|
+
},
|
|
180
|
+
get lastExtensionContext() {
|
|
181
|
+
return lastExtensionContext;
|
|
182
|
+
},
|
|
183
|
+
get selectedProfile() {
|
|
184
|
+
return selectedProfile;
|
|
185
|
+
},
|
|
186
|
+
set selectedProfile(v) {
|
|
187
|
+
selectedProfile = v;
|
|
188
|
+
},
|
|
189
|
+
get routerEnabled() {
|
|
190
|
+
return routerEnabled;
|
|
191
|
+
},
|
|
192
|
+
set routerEnabled(v) {
|
|
193
|
+
routerEnabled = v;
|
|
194
|
+
},
|
|
195
|
+
get lastDecision() {
|
|
196
|
+
return lastDecision;
|
|
197
|
+
},
|
|
198
|
+
set lastDecision(v) {
|
|
199
|
+
lastDecision = v;
|
|
200
|
+
},
|
|
201
|
+
get pinnedTierByProfile() {
|
|
202
|
+
return pinnedTierByProfile;
|
|
203
|
+
},
|
|
204
|
+
set pinnedTierByProfile(v) {
|
|
205
|
+
pinnedTierByProfile = v;
|
|
206
|
+
},
|
|
207
|
+
get debugEnabled() {
|
|
208
|
+
return debugEnabled;
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
persistState,
|
|
213
|
+
recordDebugDecision,
|
|
214
|
+
updateStatus: actions.updateStatus,
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
actions.reloadConfig();
|
|
221
|
+
|
|
222
|
+
const restoreStateFromSession = async (ctx: ExtensionContext) => {
|
|
223
|
+
lastExtensionContext = ctx;
|
|
224
|
+
currentModelRegistry = ctx.modelRegistry;
|
|
225
|
+
currentCwd = ctx.cwd;
|
|
226
|
+
actions.reloadConfig();
|
|
227
|
+
|
|
228
|
+
// Give the registry a moment to synchronize after re-registration
|
|
229
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
230
|
+
|
|
231
|
+
routerEnabled = ctx.model?.provider === 'router';
|
|
232
|
+
selectedProfile = resolveProfileName(
|
|
233
|
+
currentConfig,
|
|
234
|
+
ctx.model?.provider === 'router' ? ctx.model.id : selectedProfile,
|
|
235
|
+
);
|
|
236
|
+
pinnedTierByProfile = {};
|
|
237
|
+
debugHistory = [];
|
|
238
|
+
lastNonRouterModel =
|
|
239
|
+
ctx.model && ctx.model.provider !== 'router'
|
|
240
|
+
? `${ctx.model.provider}/${ctx.model.id}`
|
|
241
|
+
: lastNonRouterModel;
|
|
242
|
+
|
|
243
|
+
const entries = ctx.sessionManager.getBranch() as CustomSessionEntry[];
|
|
244
|
+
const savedState = entries
|
|
245
|
+
.filter(
|
|
246
|
+
(entry) =>
|
|
247
|
+
entry.type === 'custom' && entry.customType === 'router-state',
|
|
248
|
+
)
|
|
249
|
+
.map((entry) => entry.data)
|
|
250
|
+
.findLast((data) => isRouterPersistedState(data));
|
|
251
|
+
|
|
252
|
+
if (isRouterPersistedState(savedState)) {
|
|
253
|
+
selectedProfile = resolveProfileName(
|
|
254
|
+
currentConfig,
|
|
255
|
+
savedState.selectedProfile,
|
|
256
|
+
);
|
|
257
|
+
if (!selectedProfile) {
|
|
258
|
+
routerEnabled = false;
|
|
259
|
+
} else {
|
|
260
|
+
routerEnabled = savedState.enabled;
|
|
261
|
+
}
|
|
262
|
+
lastDecision = savedState.lastDecision;
|
|
263
|
+
pinnedTierByProfile = savedState.pinByProfile
|
|
264
|
+
? { ...savedState.pinByProfile }
|
|
265
|
+
: {};
|
|
266
|
+
if (savedState.pinTier && selectedProfile) {
|
|
267
|
+
pinnedTierByProfile[selectedProfile] = savedState.pinTier;
|
|
268
|
+
}
|
|
269
|
+
debugEnabled = savedState.debugEnabled ?? debugEnabled;
|
|
270
|
+
debugHistory = savedState.debugHistory
|
|
271
|
+
? [...savedState.debugHistory].slice(-MAX_DEBUG_HISTORY)
|
|
272
|
+
: [];
|
|
273
|
+
lastNonRouterModel = savedState.lastNonRouterModel ?? lastNonRouterModel;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
await actions.ensureValidActiveRouterProfile(ctx);
|
|
277
|
+
|
|
278
|
+
if (routerEnabled && selectedProfile) {
|
|
279
|
+
const routerModel = ctx.modelRegistry.find('router', selectedProfile);
|
|
280
|
+
if (routerModel) {
|
|
281
|
+
const success = await setModelInternally(routerModel);
|
|
282
|
+
if (!success) {
|
|
283
|
+
ctx.ui.notify(`Failed to restore router/${selectedProfile} after relaunch.`, 'warning');
|
|
284
|
+
routerEnabled = false;
|
|
285
|
+
}
|
|
286
|
+
} else if (selectedProfile) {
|
|
287
|
+
ctx.ui.notify(`Unable to restore router/${selectedProfile}; model is unavailable.`, 'warning');
|
|
288
|
+
routerEnabled = false;
|
|
289
|
+
ctx.ui.setHiddenThinkingLabel?.();
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
ctx.ui.setHiddenThinkingLabel?.();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
persistState();
|
|
296
|
+
actions.updateStatus(ctx);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
registerCommands(
|
|
300
|
+
pi,
|
|
301
|
+
{
|
|
302
|
+
get currentConfig() {
|
|
303
|
+
return currentConfig;
|
|
304
|
+
},
|
|
305
|
+
get routerEnabled() {
|
|
306
|
+
return routerEnabled;
|
|
307
|
+
},
|
|
308
|
+
set routerEnabled(v) {
|
|
309
|
+
routerEnabled = v;
|
|
310
|
+
},
|
|
311
|
+
get selectedProfile() {
|
|
312
|
+
return selectedProfile;
|
|
313
|
+
},
|
|
314
|
+
set selectedProfile(v) {
|
|
315
|
+
selectedProfile = v;
|
|
316
|
+
},
|
|
317
|
+
get pinnedTierByProfile() {
|
|
318
|
+
return pinnedTierByProfile;
|
|
319
|
+
},
|
|
320
|
+
set pinnedTierByProfile(v) {
|
|
321
|
+
pinnedTierByProfile = v;
|
|
322
|
+
},
|
|
323
|
+
get lastDecision() {
|
|
324
|
+
return lastDecision;
|
|
325
|
+
},
|
|
326
|
+
get lastNonRouterModel() {
|
|
327
|
+
return lastNonRouterModel;
|
|
328
|
+
},
|
|
329
|
+
set lastNonRouterModel(v) {
|
|
330
|
+
lastNonRouterModel = v;
|
|
331
|
+
},
|
|
332
|
+
get debugEnabled() {
|
|
333
|
+
return debugEnabled;
|
|
334
|
+
},
|
|
335
|
+
set debugEnabled(v) {
|
|
336
|
+
debugEnabled = v;
|
|
337
|
+
},
|
|
338
|
+
get debugHistory() {
|
|
339
|
+
return debugHistory;
|
|
340
|
+
},
|
|
341
|
+
get lastConfigWarnings() {
|
|
342
|
+
return lastConfigWarnings;
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
actions,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
349
|
+
await restoreStateFromSession(ctx);
|
|
350
|
+
isInitialized = true;
|
|
351
|
+
|
|
352
|
+
if (lastConfigWarnings.length > 0) {
|
|
353
|
+
ctx.ui.notify(`Router config warnings:\n${lastConfigWarnings.join('\n')}`, 'warning');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (debugEnabled) {
|
|
357
|
+
ctx.ui.notify(
|
|
358
|
+
`Router initialized with profiles: ${profileNames(currentConfig).join(', ')}`,
|
|
359
|
+
'info',
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
pi.on('model_select', async (event, ctx) => {
|
|
365
|
+
if (!isInitialized || isRouterDelegating) return;
|
|
366
|
+
if (event.model.provider === 'router') {
|
|
367
|
+
const profileName = resolveProfileName(currentConfig, event.model.id);
|
|
368
|
+
if (!profileName) {
|
|
369
|
+
ctx.ui.notify(`Unknown router profile: ${event.model.id}`, 'error');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// If the selected model has stale capacities (e.g. from the initial registration),
|
|
374
|
+
// re-apply the model from the registry to force a TUI refresh.
|
|
375
|
+
const registryModel = ctx.modelRegistry.find('router', profileName);
|
|
376
|
+
if (
|
|
377
|
+
registryModel &&
|
|
378
|
+
(registryModel.contextWindow !== event.model.contextWindow ||
|
|
379
|
+
registryModel.maxTokens !== event.model.maxTokens)
|
|
380
|
+
) {
|
|
381
|
+
await setModelInternally(registryModel);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
routerEnabled = true;
|
|
385
|
+
selectedProfile = profileName;
|
|
386
|
+
} else {
|
|
387
|
+
routerEnabled = false;
|
|
388
|
+
lastNonRouterModel = `${event.model.provider}/${event.model.id}`;
|
|
389
|
+
ctx.ui.setHiddenThinkingLabel?.();
|
|
390
|
+
}
|
|
391
|
+
persistState();
|
|
392
|
+
actions.updateStatus(ctx);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
pi.on('turn_end', async (_event, ctx) => {
|
|
396
|
+
if (routerEnabled && selectedProfile && ctx.model?.provider !== 'router') {
|
|
397
|
+
const routerModel = ctx.modelRegistry.find('router', selectedProfile);
|
|
398
|
+
if (routerModel) {
|
|
399
|
+
const success = await setModelInternally(routerModel);
|
|
400
|
+
if (!success) {
|
|
401
|
+
ctx.ui.notify('Failed to re-assert router model after turn. Router disabled.', 'warning');
|
|
402
|
+
routerEnabled = false;
|
|
403
|
+
selectedProfile = undefined;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
persistState();
|
|
408
|
+
actions.updateStatus(ctx);
|
|
409
|
+
});
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
export default routerExtension;
|