@alexeiled/pi-model-router 0.6.1 → 0.6.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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.2 - 2026-09-21
4
+
5
+ - Increase the default Jev timeout from 750 ms to 1500 ms. User-level `jev.timeoutMs` now sets the total advisory budget without the previous hidden 750 ms cap or an arbitrary upper cap. Positive finite values within Node's timer range are accepted, including 4000 and 5000 ms. Existing explicit shorter timeouts remain valid.
6
+ - Fix delayed Pi thinking-display events being mistaken for user changes and overriding every routing tier. Internal display updates now preserve configured per-tier effort; explicit user overrides still apply.
7
+ - Keep privacy opt-in, confidence validation, caller cancellation, and direct baseline fallback unchanged. No additional advisory requests or retries.
8
+ - Add regression coverage for delayed valid Jev responses, configurable deadlines, internal thinking events, and Astra/Luna candidate selection. Align configuration examples and operator documentation.
9
+
3
10
  ## 0.6.1 — 2026-09-21
4
11
 
5
12
  - Add compact human-readable route provenance to the Pi footer, widget and `/router status`: `🧭 Jev ✓` means Jev selected the route, `🧭 Jev ↪ base` means Jev ran but the local baseline was used, and no marker means Jev was not involved.
package/README.md CHANGED
@@ -95,7 +95,7 @@ pi -e ./extensions/index.ts
95
95
 
96
96
  - Generation and classification use Pi's provider registry, including native/custom providers and credential-specific URLs. Only the optional Jev advisor uses separate HTTPS transport.
97
97
  - Fallbacks run only before content is emitted; cancellation does not retry. Every target must support the requested input and exact thinking level; explicit unsupported effort is not silently reduced. Omitted thinking defaults to `off` for non-reasoning targets, including fallbacks.
98
- - Jev gets at most 750 ms (or its shorter configured timeout and remaining time in the 1500 ms advisory budget), with no retry. The separate classifier-only compatibility path retains its 10-second bound and 256-token output limit. Failure or uncertainty means eligible baseline; caller cancellation stops generation.
98
+ - Jev gets a configurable total advisory budget via `jev.timeoutMs`: 1500 ms by default, with no additional routing cap or retry. The separate classifier-only compatibility path retains its 10-second bound and 256-token output limit. Failure or uncertainty means eligible baseline; caller cancellation stops generation.
99
99
  - Valid same-turn tool continuations reuse the actual prior route before either advisor. Pins, budget policy and a single eligible primary candidate also bypass advisors. Invalid continuations choose a compatible local route without advice; incompatible Google thought-signature replay fails plainly.
100
100
  - Pi owns tool execution permissions and per-request authentication. The router checks configured provider/profile identity, not which backend login is currently behind a provider. No private authentication storage is read.
101
101
  - Context trimming preserves system instructions and whole active tool turns. It is a text estimate, not a guarantee that images or a large active turn fit.
@@ -199,7 +199,7 @@ profile opt-ins, are ignored with a warning, before merging user credentials.
199
199
  "apiKey": "<rendered by chezmoi/1Password>",
200
200
  "endpoint": "https://api.typesafe.ai/v1/systemone",
201
201
  "model": "jev-1.13.0",
202
- "timeoutMs": 750,
202
+ "timeoutMs": 1500,
203
203
  "confidenceThreshold": 0.65,
204
204
  "maxStateChars": 12000,
205
205
  "mode": "advisory"
@@ -218,14 +218,22 @@ profile opt-ins, are ignored with a warning, before merging user credentials.
218
218
 
219
219
  The endpoint, model, timeout, confidence threshold, state limit and mode shown
220
220
  above are defaults. Only HTTPS endpoints without embedded credentials, query
221
- parameters or fragments are accepted. Timeout must be positive and at most
222
- 1500 ms, confidence must be 0–1, and the context limit must be 1–12000 characters.
223
- Provider routing further caps Jev at 750 ms within the fixed 1500 ms advisory
224
- budget; increasing `timeoutMs` does not extend those caps. Values above 750 ms
225
- are normalized to 750 ms with a configuration warning. The separate classifier-only
221
+ parameters or fragments are accepted. `timeoutMs` defaults to 1500 ms and must
222
+ be a positive finite number within Node's timer range (at most 2147483647 ms).
223
+ There is no product-level cap: 4000 or 5000 ms are valid if you prefer waiting
224
+ longer before falling back. It sets the total Jev advisory budget, including
225
+ request and response-body time; there is no separate 750 ms cap. Confidence must
226
+ be 0–1, and the context limit must be 1–12000 characters. The separate classifier-only
226
227
  path keeps a 10-second bound. Neither path retries or starts generation after
227
228
  caller cancellation.
228
229
 
230
+ If Jev frequently falls back because requests time out, try `"timeoutMs": 3000`
231
+ in your user config. Existing explicit values such as 750 remain unchanged;
232
+ remove the field or set it to 1500 to use the new default. Increasing the timeout
233
+ does not lower the confidence threshold or guarantee a different route. After
234
+ upgrading, start a new Pi session; use `/router thinking auto` to clear any
235
+ unwanted effort override in an existing session.
236
+
229
237
  **External data:** Jev receives bounded, role-labelled recent user/assistant/tool
230
238
  text, prioritizing the latest user request within `maxStateChars`, plus candidate
231
239
  tier/model/thinking identifiers. Truncation is deterministic, with no keyword
@@ -376,10 +376,13 @@ export const normalizeTierConfig = (
376
376
  };
377
377
  };
378
378
 
379
+ // Node turns larger setTimeout delays into 1 ms rather than waiting longer.
380
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
381
+
379
382
  export const DEFAULT_JEV_CONFIG = {
380
383
  endpoint: 'https://api.typesafe.ai/v1/systemone',
381
384
  model: 'jev-1.13.0',
382
- timeoutMs: 750,
385
+ timeoutMs: 1500,
383
386
  confidenceThreshold: 0.65,
384
387
  maxStateChars: 12000,
385
388
  mode: 'advisory',
@@ -420,7 +423,7 @@ export const normalizeJevConfig = (
420
423
  typeof value.timeoutMs !== 'number' ||
421
424
  !Number.isFinite(value.timeoutMs) ||
422
425
  value.timeoutMs <= 0 ||
423
- value.timeoutMs > 1500 ||
426
+ value.timeoutMs > MAX_TIMER_DELAY_MS ||
424
427
  typeof value.confidenceThreshold !== 'number' ||
425
428
  !Number.isFinite(value.confidenceThreshold) ||
426
429
  value.confidenceThreshold < 0 ||
@@ -434,10 +437,6 @@ export const normalizeJevConfig = (
434
437
  (typeof value.apiKey !== 'string' || /[\r\n]/.test(value.apiKey)))
435
438
  )
436
439
  return invalid();
437
- if (value.timeoutMs > 750)
438
- warnings.push(
439
- 'Jev timeoutMs clamped to the effective 750 ms provider cap.',
440
- );
441
440
  const apiKey = typeof value.apiKey === 'string' ? value.apiKey.trim() : '';
442
441
  if (value.enabled === true && !apiKey) {
443
442
  warnings.push('Jev disabled: missing user-config API key.');
@@ -447,7 +446,7 @@ export const normalizeJevConfig = (
447
446
  apiKey,
448
447
  endpoint: value.endpoint,
449
448
  model: value.model,
450
- timeoutMs: Math.min(value.timeoutMs, 750),
449
+ timeoutMs: value.timeoutMs,
451
450
  confidenceThreshold: value.confidenceThreshold,
452
451
  maxStateChars: value.maxStateChars,
453
452
  mode: 'advisory',
@@ -57,6 +57,10 @@ const routerExtension = (pi: ExtensionAPI) => {
57
57
  let isInitialized = false;
58
58
  let isInternalModelSwitch = false;
59
59
  let isInternalThinkingChange = false;
60
+ const pendingThinkingChanges: Array<{
61
+ previousLevel: ThinkingLevel;
62
+ level: ThinkingLevel;
63
+ }> = [];
60
64
  let ignoreStartupThinkingEvent = false;
61
65
 
62
66
  const runtimeState = {
@@ -142,13 +146,27 @@ const routerExtension = (pi: ExtensionAPI) => {
142
146
  };
143
147
 
144
148
  const setThinkingLevelInternally = (level: ThinkingLevel) => {
149
+ let previousLevel: ThinkingLevel;
150
+ try {
151
+ previousLevel = pi.getThinkingLevel();
152
+ } catch {
153
+ return; // The session runtime may already be torn down.
154
+ }
155
+ const change = { previousLevel, level };
156
+ pendingThinkingChanges.push(change);
145
157
  isInternalThinkingChange = true;
146
158
  try {
147
159
  pi.setThinkingLevel(level);
160
+ change.level = pi.getThinkingLevel();
148
161
  } catch {
149
162
  // Extension context may be stale after session teardown.
163
+ change.level = change.previousLevel;
150
164
  } finally {
151
165
  isInternalThinkingChange = false;
166
+ if (change.level === change.previousLevel) {
167
+ const index = pendingThinkingChanges.indexOf(change);
168
+ if (index >= 0) pendingThinkingChanges.splice(index, 1);
169
+ }
152
170
  }
153
171
  };
154
172
 
@@ -513,8 +531,21 @@ const routerExtension = (pi: ExtensionAPI) => {
513
531
 
514
532
  pi.on('thinking_level_select', (event, ctx) => {
515
533
  ensureInitializedFromContext(ctx);
534
+ if (isInternalThinkingChange) {
535
+ pendingThinkingChanges.pop();
536
+ return;
537
+ }
538
+ // Pi emits without awaiting extension handlers; earlier handlers can delay this echo.
539
+ const internalChange = pendingThinkingChanges.findIndex(
540
+ (change) =>
541
+ change.previousLevel === event.previousLevel &&
542
+ change.level === event.level,
543
+ );
544
+ if (internalChange >= 0) {
545
+ pendingThinkingChanges.splice(internalChange, 1);
546
+ return;
547
+ }
516
548
  if (!isInitialized || !routerEnabled || !selectedProfile) return;
517
- if (isInternalThinkingChange) return;
518
549
  if (ignoreStartupThinkingEvent) {
519
550
  ignoreStartupThinkingEvent = false;
520
551
  return;
@@ -486,7 +486,8 @@ export const registerRouterProvider = (
486
486
  advisorConfigured
487
487
  ) {
488
488
  const started = performance.now();
489
- const routingDeadline = started + (useJev ? 1500 : 10_000);
489
+ const routingDeadline =
490
+ started + (useJev && jev ? jev.timeoutMs : 10_000);
490
491
  const candidates = primaryRoutePairs(profile, pairs).map(
491
492
  createJevCandidate,
492
493
  );
@@ -497,22 +498,16 @@ export const registerRouterProvider = (
497
498
  } else if (useJev && jev) {
498
499
  decision.advisor = 'jev';
499
500
  rememberAdvisedTurn(turn, 'jev');
500
- const advice = await runJev(
501
- {
502
- ...jev,
503
- timeoutMs: Math.min(750, jev.timeoutMs),
504
- },
505
- {
506
- taskSummary: getBoundedRecentContext(
507
- context,
508
- jev.maxStateChars,
509
- ),
510
- candidates,
511
- profile: profile.jev,
512
- routingDeadline,
513
- signal: options?.signal,
514
- },
515
- ).catch(() => undefined);
501
+ const advice = await runJev(jev, {
502
+ taskSummary: getBoundedRecentContext(
503
+ context,
504
+ jev.maxStateChars,
505
+ ),
506
+ candidates,
507
+ profile: profile.jev,
508
+ routingDeadline,
509
+ signal: options?.signal,
510
+ }).catch(() => undefined);
516
511
  options?.signal?.throwIfAborted();
517
512
  // Re-read registry capabilities after the network boundary.
518
513
  pairs = available();
@@ -5,7 +5,7 @@
5
5
  "apiKey": "<rendered by chezmoi/1Password in user config only>",
6
6
  "endpoint": "https://api.typesafe.ai/v1/systemone",
7
7
  "model": "jev-1.13.0",
8
- "timeoutMs": 750,
8
+ "timeoutMs": 1500,
9
9
  "confidenceThreshold": 0.65,
10
10
  "maxStateChars": 12000,
11
11
  "mode": "advisory"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexeiled/pi-model-router",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "extensions",