@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.
@@ -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));
@@ -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
- pi,
227
- {
228
- get lastRegisteredModels() {
229
- return lastRegisteredModels;
230
- },
231
- set lastRegisteredModels(v) {
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 entries = ctx.sessionManager.getBranch() as CustomSessionEntry[];
325
- const savedState = entries
326
- .filter(
327
- (entry) =>
328
- entry.type === 'custom' && entry.customType === 'router-state',
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
- .map((entry) => entry.data)
331
- .findLast((data) => isRouterPersistedState(data));
347
+ .findLast(isRouterPersistedState);
332
348
 
333
- if (isRouterPersistedState(savedState)) {
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(thinkingByProfile, savedState.thinkingByProfile);
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
- ? [...savedState.debugHistory].slice(-MAX_DEBUG_HISTORY)
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
- const overrides = thinkingByProfile[selectedProfile];
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 unsupported = getUnsupportedTiers(
559
- currentConfig.profiles[selectedProfile],
560
- event.level,
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}. ` +