@alexeiled/pi-model-router 0.5.0 → 0.5.2
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 +23 -1
- package/README.md +22 -4
- package/extensions/classifier.ts +93 -0
- package/extensions/commands.ts +56 -31
- package/extensions/config.ts +65 -46
- package/extensions/constants.ts +0 -46
- package/extensions/context.ts +58 -0
- package/extensions/index.ts +105 -134
- package/extensions/provider.ts +160 -155
- package/extensions/routing.ts +17 -162
- package/extensions/state.ts +90 -27
- package/extensions/types.ts +73 -45
- package/extensions/ui.ts +14 -28
- package/package.json +6 -5
package/extensions/constants.ts
CHANGED
|
@@ -1,49 +1,3 @@
|
|
|
1
1
|
export const MAX_DEBUG_HISTORY = 12;
|
|
2
2
|
export const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
3
3
|
export const DEFAULT_MAX_TOKENS = 16_384;
|
|
4
|
-
|
|
5
|
-
const AUTH_HEADERS = new Set([
|
|
6
|
-
'authorization',
|
|
7
|
-
'x-api-key',
|
|
8
|
-
'cf-aig-authorization',
|
|
9
|
-
]);
|
|
10
|
-
|
|
11
|
-
export interface RegistryWithProviderAuth {
|
|
12
|
-
getProviderAuth?: (
|
|
13
|
-
provider: string,
|
|
14
|
-
) => Promise<{ auth: { baseUrl?: string } } | undefined>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const hasUsableRequestAuth = (auth: {
|
|
18
|
-
apiKey?: string;
|
|
19
|
-
headers?: Record<string, string | null | undefined>;
|
|
20
|
-
}): boolean => {
|
|
21
|
-
if (typeof auth.apiKey === 'string' && auth.apiKey.trim().length > 0) {
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return Object.entries(auth.headers ?? {}).some(
|
|
26
|
-
([name, value]) =>
|
|
27
|
-
AUTH_HEADERS.has(name.toLowerCase()) &&
|
|
28
|
-
typeof value === 'string' &&
|
|
29
|
-
value.trim().length > 0,
|
|
30
|
-
);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const resolveDelegatedModel = async <
|
|
34
|
-
TModel extends { provider: string; baseUrl: string },
|
|
35
|
-
>(
|
|
36
|
-
registry: RegistryWithProviderAuth,
|
|
37
|
-
model: TModel,
|
|
38
|
-
): Promise<TModel> => {
|
|
39
|
-
try {
|
|
40
|
-
const providerAuth = await registry.getProviderAuth?.(model.provider);
|
|
41
|
-
const authBaseUrl = providerAuth?.auth.baseUrl;
|
|
42
|
-
if (authBaseUrl && authBaseUrl !== model.baseUrl) {
|
|
43
|
-
return { ...model, baseUrl: authBaseUrl };
|
|
44
|
-
}
|
|
45
|
-
} catch {
|
|
46
|
-
// Older Pi versions and unavailable credentials use the model's static URL.
|
|
47
|
-
}
|
|
48
|
-
return model;
|
|
49
|
-
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Context, Message } from '@earendil-works/pi-ai';
|
|
2
|
+
|
|
3
|
+
export const extractTextFromContent = (
|
|
4
|
+
content: string | Message['content'],
|
|
5
|
+
): string => {
|
|
6
|
+
if (typeof content === 'string') return content;
|
|
7
|
+
return content
|
|
8
|
+
.map((part) => {
|
|
9
|
+
if (part.type === 'text') return part.text;
|
|
10
|
+
if (part.type === 'thinking') return part.thinking;
|
|
11
|
+
if (part.type === 'toolCall') {
|
|
12
|
+
return `${part.name} ${JSON.stringify(part.arguments)}`;
|
|
13
|
+
}
|
|
14
|
+
return '';
|
|
15
|
+
})
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.join('\n');
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const getLastUserText = (context: Context): string => {
|
|
21
|
+
for (let i = context.messages.length - 1; i >= 0; i -= 1) {
|
|
22
|
+
const message = context.messages[i];
|
|
23
|
+
if (message?.role === 'user') {
|
|
24
|
+
return extractTextFromContent(message.content).trim();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return '';
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const getRecentConversationText = (
|
|
31
|
+
context: Context,
|
|
32
|
+
limit = 6,
|
|
33
|
+
): string =>
|
|
34
|
+
context.messages
|
|
35
|
+
.slice(-limit)
|
|
36
|
+
.map((message) =>
|
|
37
|
+
message ? extractTextFromContent(message.content).trim() : '',
|
|
38
|
+
)
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.join('\n')
|
|
41
|
+
.toLowerCase();
|
|
42
|
+
|
|
43
|
+
export const countToolResults = (context: Context): number =>
|
|
44
|
+
context.messages.filter((message) => message?.role === 'toolResult').length;
|
|
45
|
+
|
|
46
|
+
export const countWords = (text: string): number =>
|
|
47
|
+
text.split(/\s+/).filter(Boolean).length;
|
|
48
|
+
|
|
49
|
+
export const hasImageAttachment = (context: Context): boolean =>
|
|
50
|
+
context.messages.some(
|
|
51
|
+
(message) =>
|
|
52
|
+
message &&
|
|
53
|
+
Array.isArray(message.content) &&
|
|
54
|
+
message.content.some((part) => part.type === 'image'),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
export const containsAny = (text: string, keywords: string[]): boolean =>
|
|
58
|
+
keywords.some((keyword) => text.includes(keyword));
|
package/extensions/index.ts
CHANGED
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
saveLastRouterProfile,
|
|
22
22
|
} from './state';
|
|
23
23
|
import type {
|
|
24
|
-
CustomSessionEntry,
|
|
25
24
|
RouterConfig,
|
|
26
25
|
RouterPinByProfile,
|
|
27
26
|
RouterThinkingByProfile,
|
|
@@ -58,6 +57,74 @@ const routerExtension = (pi: ExtensionAPI) => {
|
|
|
58
57
|
let isInternalThinkingChange = false;
|
|
59
58
|
let ignoreStartupThinkingEvent = false;
|
|
60
59
|
|
|
60
|
+
const runtimeState = {
|
|
61
|
+
get lastRegisteredModels() {
|
|
62
|
+
return lastRegisteredModels;
|
|
63
|
+
},
|
|
64
|
+
set lastRegisteredModels(value: string) {
|
|
65
|
+
lastRegisteredModels = value;
|
|
66
|
+
},
|
|
67
|
+
get currentConfig() {
|
|
68
|
+
return currentConfig;
|
|
69
|
+
},
|
|
70
|
+
get currentModelRegistry() {
|
|
71
|
+
return currentModelRegistry;
|
|
72
|
+
},
|
|
73
|
+
get lastExtensionContext() {
|
|
74
|
+
return lastExtensionContext;
|
|
75
|
+
},
|
|
76
|
+
get selectedProfile() {
|
|
77
|
+
return selectedProfile;
|
|
78
|
+
},
|
|
79
|
+
set selectedProfile(value: string | undefined) {
|
|
80
|
+
selectedProfile = value;
|
|
81
|
+
},
|
|
82
|
+
get routerEnabled() {
|
|
83
|
+
return routerEnabled;
|
|
84
|
+
},
|
|
85
|
+
set routerEnabled(value: boolean) {
|
|
86
|
+
routerEnabled = value;
|
|
87
|
+
},
|
|
88
|
+
get lastDecision() {
|
|
89
|
+
return lastDecision;
|
|
90
|
+
},
|
|
91
|
+
set lastDecision(value: RoutingDecision | undefined) {
|
|
92
|
+
lastDecision = value;
|
|
93
|
+
},
|
|
94
|
+
thinkingByProfile,
|
|
95
|
+
pinnedTierByProfile,
|
|
96
|
+
get accumulatedCost() {
|
|
97
|
+
return accumulatedCost;
|
|
98
|
+
},
|
|
99
|
+
set accumulatedCost(value: number) {
|
|
100
|
+
accumulatedCost = value;
|
|
101
|
+
},
|
|
102
|
+
get debugEnabled() {
|
|
103
|
+
return debugEnabled;
|
|
104
|
+
},
|
|
105
|
+
set debugEnabled(value: boolean) {
|
|
106
|
+
debugEnabled = value;
|
|
107
|
+
},
|
|
108
|
+
get widgetEnabled() {
|
|
109
|
+
return widgetEnabled;
|
|
110
|
+
},
|
|
111
|
+
set widgetEnabled(value: boolean) {
|
|
112
|
+
widgetEnabled = value;
|
|
113
|
+
},
|
|
114
|
+
get debugHistory() {
|
|
115
|
+
return debugHistory;
|
|
116
|
+
},
|
|
117
|
+
get lastNonRouterModel() {
|
|
118
|
+
return lastNonRouterModel;
|
|
119
|
+
},
|
|
120
|
+
set lastNonRouterModel(value: string | undefined) {
|
|
121
|
+
lastNonRouterModel = value;
|
|
122
|
+
},
|
|
123
|
+
get lastConfigWarnings() {
|
|
124
|
+
return lastConfigWarnings;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
61
128
|
const setModelInternally = async (
|
|
62
129
|
model: NonNullable<ExtensionContext['model']>,
|
|
63
130
|
) => {
|
|
@@ -92,7 +159,7 @@ const routerExtension = (pi: ExtensionAPI) => {
|
|
|
92
159
|
};
|
|
93
160
|
|
|
94
161
|
const persistState = () => {
|
|
95
|
-
const state = buildPersistedState(
|
|
162
|
+
const state = buildPersistedState({
|
|
96
163
|
routerEnabled,
|
|
97
164
|
selectedProfile,
|
|
98
165
|
pinnedTierByProfile,
|
|
@@ -103,7 +170,7 @@ const routerExtension = (pi: ExtensionAPI) => {
|
|
|
103
170
|
lastDecision,
|
|
104
171
|
lastNonRouterModel,
|
|
105
172
|
accumulatedCost,
|
|
106
|
-
);
|
|
173
|
+
});
|
|
107
174
|
const snapshot = JSON.stringify({
|
|
108
175
|
...state,
|
|
109
176
|
timestamp: 0,
|
|
@@ -133,18 +200,16 @@ const routerExtension = (pi: ExtensionAPI) => {
|
|
|
133
200
|
persistState,
|
|
134
201
|
syncPiThinkingLevel: setThinkingLevelInternally,
|
|
135
202
|
updateStatus: (ctx: ExtensionContext) =>
|
|
136
|
-
updateStatus(
|
|
137
|
-
ctx,
|
|
203
|
+
updateStatus(ctx, {
|
|
138
204
|
routerEnabled,
|
|
139
205
|
selectedProfile,
|
|
140
206
|
pinnedTierByProfile,
|
|
141
|
-
thinkingByProfile,
|
|
142
207
|
lastDecision,
|
|
143
208
|
lastNonRouterModel,
|
|
144
209
|
accumulatedCost,
|
|
145
210
|
widgetEnabled,
|
|
146
211
|
currentConfig,
|
|
147
|
-
),
|
|
212
|
+
}),
|
|
148
213
|
reloadConfig: (
|
|
149
214
|
ctx?: ExtensionContext,
|
|
150
215
|
options?: { preserveDebug?: boolean },
|
|
@@ -199,7 +264,6 @@ const routerExtension = (pi: ExtensionAPI) => {
|
|
|
199
264
|
|
|
200
265
|
// Ensure the provider is registered with current capacities for this profile
|
|
201
266
|
actions.registerRouterProvider();
|
|
202
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
203
267
|
|
|
204
268
|
const routerModel = ctx.modelRegistry.find('router', profileName);
|
|
205
269
|
if (!routerModel) {
|
|
@@ -222,59 +286,13 @@ const routerExtension = (pi: ExtensionAPI) => {
|
|
|
222
286
|
return true;
|
|
223
287
|
},
|
|
224
288
|
registerRouterProvider: () => {
|
|
225
|
-
registerRouterProvider(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
lastRegisteredModels = v;
|
|
233
|
-
},
|
|
234
|
-
get currentConfig() {
|
|
235
|
-
return currentConfig;
|
|
236
|
-
},
|
|
237
|
-
get currentModelRegistry() {
|
|
238
|
-
return currentModelRegistry;
|
|
239
|
-
},
|
|
240
|
-
get lastExtensionContext() {
|
|
241
|
-
return lastExtensionContext;
|
|
242
|
-
},
|
|
243
|
-
get selectedProfile() {
|
|
244
|
-
return selectedProfile;
|
|
245
|
-
},
|
|
246
|
-
set selectedProfile(v) {
|
|
247
|
-
selectedProfile = v;
|
|
248
|
-
},
|
|
249
|
-
get routerEnabled() {
|
|
250
|
-
return routerEnabled;
|
|
251
|
-
},
|
|
252
|
-
set routerEnabled(v) {
|
|
253
|
-
routerEnabled = v;
|
|
254
|
-
},
|
|
255
|
-
get lastDecision() {
|
|
256
|
-
return lastDecision;
|
|
257
|
-
},
|
|
258
|
-
set lastDecision(v) {
|
|
259
|
-
lastDecision = v;
|
|
260
|
-
},
|
|
261
|
-
thinkingByProfile,
|
|
262
|
-
pinnedTierByProfile,
|
|
263
|
-
get accumulatedCost() {
|
|
264
|
-
return accumulatedCost;
|
|
265
|
-
},
|
|
266
|
-
set accumulatedCost(v) {
|
|
267
|
-
accumulatedCost = v;
|
|
268
|
-
},
|
|
269
|
-
},
|
|
270
|
-
{
|
|
271
|
-
persistState,
|
|
272
|
-
recordDebugDecision,
|
|
273
|
-
getThinkingOverride,
|
|
274
|
-
updateStatus: actions.updateStatus,
|
|
275
|
-
syncPiThinkingLevel: setThinkingLevelInternally,
|
|
276
|
-
},
|
|
277
|
-
);
|
|
289
|
+
registerRouterProvider(pi, runtimeState, {
|
|
290
|
+
persistState,
|
|
291
|
+
recordDebugDecision,
|
|
292
|
+
getThinkingOverride,
|
|
293
|
+
updateStatus: actions.updateStatus,
|
|
294
|
+
syncPiThinkingLevel: setThinkingLevelInternally,
|
|
295
|
+
});
|
|
278
296
|
},
|
|
279
297
|
};
|
|
280
298
|
|
|
@@ -284,6 +302,7 @@ const routerExtension = (pi: ExtensionAPI) => {
|
|
|
284
302
|
ctx: ExtensionContext,
|
|
285
303
|
startReason: SessionStartEvent['reason'],
|
|
286
304
|
) => {
|
|
305
|
+
lastPersistedSnapshot = undefined;
|
|
287
306
|
ignoreStartupThinkingEvent =
|
|
288
307
|
startReason === 'startup' ||
|
|
289
308
|
startReason === 'new' ||
|
|
@@ -295,9 +314,6 @@ const routerExtension = (pi: ExtensionAPI) => {
|
|
|
295
314
|
const hasExplicitStartupModel =
|
|
296
315
|
startReason === 'startup' && hasExplicitCliModel();
|
|
297
316
|
|
|
298
|
-
// Give the registry a moment to synchronize after re-registration
|
|
299
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
300
|
-
|
|
301
317
|
routerEnabled = ctx.model?.provider === 'router';
|
|
302
318
|
selectedProfile =
|
|
303
319
|
ctx.model?.provider === 'router'
|
|
@@ -321,16 +337,16 @@ const routerExtension = (pi: ExtensionAPI) => {
|
|
|
321
337
|
|
|
322
338
|
await actions.ensureValidActiveRouterProfile(ctx);
|
|
323
339
|
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
.
|
|
327
|
-
|
|
328
|
-
|
|
340
|
+
const savedState = ctx.sessionManager
|
|
341
|
+
.getBranch()
|
|
342
|
+
.map((entry) =>
|
|
343
|
+
entry.type === 'custom' && entry.customType === 'router-state'
|
|
344
|
+
? entry.data
|
|
345
|
+
: undefined,
|
|
329
346
|
)
|
|
330
|
-
.
|
|
331
|
-
.findLast((data) => isRouterPersistedState(data));
|
|
347
|
+
.findLast(isRouterPersistedState);
|
|
332
348
|
|
|
333
|
-
if (
|
|
349
|
+
if (savedState) {
|
|
334
350
|
if (!hasExplicitStartupModel) {
|
|
335
351
|
selectedProfile = resolveProfileName(
|
|
336
352
|
currentConfig,
|
|
@@ -342,7 +358,10 @@ const routerExtension = (pi: ExtensionAPI) => {
|
|
|
342
358
|
Object.assign(pinnedTierByProfile, savedState.pinByProfile);
|
|
343
359
|
}
|
|
344
360
|
if (savedState.thinkingByProfile) {
|
|
345
|
-
Object.assign(
|
|
361
|
+
Object.assign(
|
|
362
|
+
thinkingByProfile,
|
|
363
|
+
structuredClone(savedState.thinkingByProfile),
|
|
364
|
+
);
|
|
346
365
|
}
|
|
347
366
|
if (savedState.pinTier && selectedProfile) {
|
|
348
367
|
pinnedTierByProfile[selectedProfile] = savedState.pinTier;
|
|
@@ -350,12 +369,14 @@ const routerExtension = (pi: ExtensionAPI) => {
|
|
|
350
369
|
debugEnabled = savedState.debugEnabled ?? debugEnabled;
|
|
351
370
|
widgetEnabled = savedState.widgetEnabled ?? widgetEnabled;
|
|
352
371
|
debugHistory = savedState.debugHistory
|
|
353
|
-
?
|
|
372
|
+
? structuredClone(savedState.debugHistory).slice(-MAX_DEBUG_HISTORY)
|
|
354
373
|
: [];
|
|
355
374
|
if (!hasExplicitStartupModel) {
|
|
356
375
|
lastNonRouterModel =
|
|
357
376
|
savedState.lastNonRouterModel ?? lastNonRouterModel;
|
|
358
|
-
lastDecision = savedState.lastDecision
|
|
377
|
+
lastDecision = savedState.lastDecision
|
|
378
|
+
? structuredClone(savedState.lastDecision)
|
|
379
|
+
: undefined;
|
|
359
380
|
}
|
|
360
381
|
accumulatedCost = savedState.accumulatedCost ?? 0;
|
|
361
382
|
} else if (
|
|
@@ -403,59 +424,7 @@ const routerExtension = (pi: ExtensionAPI) => {
|
|
|
403
424
|
actions.updateStatus(ctx);
|
|
404
425
|
};
|
|
405
426
|
|
|
406
|
-
registerCommands(
|
|
407
|
-
pi,
|
|
408
|
-
{
|
|
409
|
-
get currentConfig() {
|
|
410
|
-
return currentConfig;
|
|
411
|
-
},
|
|
412
|
-
get routerEnabled() {
|
|
413
|
-
return routerEnabled;
|
|
414
|
-
},
|
|
415
|
-
set routerEnabled(v) {
|
|
416
|
-
routerEnabled = v;
|
|
417
|
-
},
|
|
418
|
-
get selectedProfile() {
|
|
419
|
-
return selectedProfile;
|
|
420
|
-
},
|
|
421
|
-
set selectedProfile(v) {
|
|
422
|
-
selectedProfile = v;
|
|
423
|
-
},
|
|
424
|
-
pinnedTierByProfile,
|
|
425
|
-
thinkingByProfile,
|
|
426
|
-
get lastDecision() {
|
|
427
|
-
return lastDecision;
|
|
428
|
-
},
|
|
429
|
-
get lastNonRouterModel() {
|
|
430
|
-
return lastNonRouterModel;
|
|
431
|
-
},
|
|
432
|
-
set lastNonRouterModel(v) {
|
|
433
|
-
lastNonRouterModel = v;
|
|
434
|
-
},
|
|
435
|
-
get accumulatedCost() {
|
|
436
|
-
return accumulatedCost;
|
|
437
|
-
},
|
|
438
|
-
get debugEnabled() {
|
|
439
|
-
return debugEnabled;
|
|
440
|
-
},
|
|
441
|
-
set debugEnabled(v) {
|
|
442
|
-
debugEnabled = v;
|
|
443
|
-
},
|
|
444
|
-
get widgetEnabled() {
|
|
445
|
-
return widgetEnabled;
|
|
446
|
-
},
|
|
447
|
-
set widgetEnabled(v) {
|
|
448
|
-
widgetEnabled = v;
|
|
449
|
-
},
|
|
450
|
-
get debugHistory() {
|
|
451
|
-
return debugHistory;
|
|
452
|
-
},
|
|
453
|
-
get lastConfigWarnings() {
|
|
454
|
-
return lastConfigWarnings;
|
|
455
|
-
},
|
|
456
|
-
},
|
|
457
|
-
actions,
|
|
458
|
-
);
|
|
427
|
+
registerCommands(pi, runtimeState, actions);
|
|
459
428
|
|
|
460
429
|
pi.on('session_start', async (event, ctx) => {
|
|
461
430
|
isInitialized = true;
|
|
@@ -547,18 +516,20 @@ const routerExtension = (pi: ExtensionAPI) => {
|
|
|
547
516
|
|
|
548
517
|
// User changed pi's thinking level (e.g. via shift+tab).
|
|
549
518
|
// Apply as an all-tier thinking override for the active router profile.
|
|
550
|
-
thinkingByProfile[selectedProfile]
|
|
551
|
-
|
|
519
|
+
let overrides = thinkingByProfile[selectedProfile];
|
|
520
|
+
if (!overrides) {
|
|
521
|
+
overrides = {};
|
|
522
|
+
thinkingByProfile[selectedProfile] = overrides;
|
|
523
|
+
}
|
|
552
524
|
for (const t of ROUTER_TIERS) {
|
|
553
525
|
overrides[t] = event.level;
|
|
554
526
|
}
|
|
555
527
|
persistState();
|
|
556
528
|
actions.updateStatus(ctx);
|
|
557
529
|
if (event.level !== 'off') {
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
);
|
|
530
|
+
const activeProfile = currentConfig.profiles[selectedProfile];
|
|
531
|
+
if (!activeProfile) return;
|
|
532
|
+
const unsupported = getUnsupportedTiers(activeProfile, event.level);
|
|
562
533
|
if (unsupported.length > 0) {
|
|
563
534
|
ctx.ui.notify(
|
|
564
535
|
`Router thinking (all) set to ${event.level}. ` +
|