@kylebrodeur/pi-model-router 0.2.2 → 0.3.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.
@@ -27,7 +27,11 @@ import {
27
27
  formatModelRef,
28
28
  formatDecision,
29
29
  } from './ui';
30
- import { performOllamaSync } from './ollama-sync';
30
+ import {
31
+ hasModelDiscoveryCommand,
32
+ MODEL_DISCOVERY_INSTALL_COMMAND,
33
+ MODEL_DISCOVERY_SYNC_COMMAND,
34
+ } from './model-discovery';
31
35
  import {
32
36
  tryFallback,
33
37
  tryRestore,
@@ -75,7 +79,7 @@ export const registerCommands = (
75
79
  { name: 'status', desc: 'Show current router status' },
76
80
  { name: 'config', desc: 'Show or toggle feature configuration' },
77
81
  { name: 'scope', desc: 'Manage Pi scoped-models from router config' },
78
- { name: 'ollama-sync', desc: 'Sync Ollama models to models.json' },
82
+ { name: 'ollama-sync', desc: 'Delegate Ollama discovery to pi-model-discovery' },
79
83
  { name: 'fallback', desc: 'Switch to Ollama fallback model' },
80
84
  { name: 'restore', desc: 'Restore original model after fallback' },
81
85
  { name: 'profile', desc: 'Switch to a different router profile' },
@@ -229,7 +233,7 @@ export const registerCommands = (
229
233
  // ─── Fallback state (added by fork) ─────────────────────────────────
230
234
  `Fallback: ${getFallbackState().fallbackActive ? 'active 🏠' : 'inactive'}`,
231
235
  `Rate limit events: ${getRateLimitHistory().length}`,
232
- `Features: ollamaSync=${state.currentConfig.features?.ollamaSync !== false ? 'on' : 'off'} rateLimit=${state.currentConfig.features?.rateLimitFallback !== false ? 'on' : 'off'}`,
236
+ `Features: rateLimit=${state.currentConfig.features?.rateLimitFallback !== false ? 'on' : 'off'}`,
233
237
  ];
234
238
  if (state.lastDecision) {
235
239
  lines.push(
@@ -584,7 +588,6 @@ export const registerCommands = (
584
588
 
585
589
  const defaultConfig = {
586
590
  features: {
587
- ollamaSync: true,
588
591
  rateLimitFallback: true,
589
592
  scopeShim: true,
590
593
  perTurnRouting: true,
@@ -593,11 +596,6 @@ export const registerCommands = (
593
596
  phaseMemory: true,
594
597
  contextCompression: true,
595
598
  },
596
- ollamaSync: {
597
- enabled: true,
598
- onStartup: true,
599
- onReload: true,
600
- },
601
599
  rateLimitFallback: {
602
600
  enabled: true,
603
601
  shortDelayThreshold: 60,
@@ -667,18 +665,21 @@ export const registerCommands = (
667
665
  ctx.ui.notify('Usage: /router ollama-sync (no arguments)', 'error');
668
666
  return;
669
667
  }
670
- const result = await performOllamaSync(
671
- pi,
672
- (state.currentConfig.ollamaSync as Record<string, unknown>) ?? {},
673
- );
674
- if (result.success && result.added.length > 0) {
675
- ctx.ui.notify(`Added: ${result.added.join(', ')}`, 'info');
676
- ctx.ui.notify('Run /reload to see them', 'info');
677
- } else if (result.success) {
678
- ctx.ui.notify(result.message, 'info');
679
- } else {
680
- ctx.ui.notify(result.message, 'error');
668
+
669
+ if (hasModelDiscoveryCommand(pi)) {
670
+ ctx.ui.notify('Delegating Ollama model discovery to /providers sync.', 'info');
671
+ await pi.sendUserMessage(MODEL_DISCOVERY_SYNC_COMMAND, {
672
+ deliverAs: 'followUp',
673
+ });
674
+ return;
681
675
  }
676
+
677
+ ctx.ui.notify(
678
+ 'Install pi-model-discovery for Ollama model discovery: ' +
679
+ MODEL_DISCOVERY_INSTALL_COMMAND +
680
+ '. Then run /providers sync.',
681
+ 'warning',
682
+ );
682
683
  };
683
684
 
684
685
  const handleFallback = async (args: string[], ctx: ExtensionContext) => {
@@ -727,7 +728,6 @@ export const registerCommands = (
727
728
  if (args.length === 0) {
728
729
  const lines = [
729
730
  'Router Feature Configuration',
730
- ` ollamaSync: ${state.currentConfig.features?.ollamaSync !== false ? 'enabled' : 'disabled'}`,
731
731
  ` rateLimitFallback: ${state.currentConfig.features?.rateLimitFallback !== false ? 'enabled' : 'disabled'}`,
732
732
  ' perTurnRouting: (always active)',
733
733
  ` intentClassifier: ${state.currentConfig.features?.intentClassifier ? 'enabled' : 'disabled'}`,
@@ -747,14 +747,6 @@ export const registerCommands = (
747
747
  }
748
748
 
749
749
  switch (feature) {
750
- case 'ollama-sync':
751
- state.currentConfig.features.ollamaSync =
752
- !state.currentConfig.features.ollamaSync;
753
- ctx.ui.notify(
754
- `ollamaSync: ${state.currentConfig.features.ollamaSync ? 'enabled' : 'disabled'}`,
755
- 'info',
756
- );
757
- break;
758
750
  case 'rate-limit':
759
751
  state.currentConfig.features.rateLimitFallback =
760
752
  !state.currentConfig.features.rateLimitFallback;
@@ -789,7 +781,7 @@ export const registerCommands = (
789
781
  break;
790
782
  default:
791
783
  ctx.ui.notify(
792
- `Unknown feature: ${feature}. Try: ollama-sync, rate-limit, classifier, budget, respect-pi-scope`,
784
+ `Unknown feature: ${feature}. Try: rate-limit, classifier, budget, respect-pi-scope`,
793
785
  'error',
794
786
  );
795
787
  return;
@@ -911,8 +903,6 @@ export const registerCommands = (
911
903
  case 'config': {
912
904
  const configPrefix = subArgs[0] ?? '';
913
905
  const items = [
914
- 'status',
915
- 'ollama-sync',
916
906
  'rate-limit',
917
907
  'classifier',
918
908
  'budget',
@@ -1017,9 +1007,9 @@ export const registerCommands = (
1017
1007
  [
1018
1008
  'Router Subcommands:',
1019
1009
  ' status Show current status, profile, pin, cost, and last decision.',
1020
- ' config [feature] Show or toggle features (ollama-sync, rate-limit, classifier, budget).',
1010
+ ' config [feature] Show or toggle features (rate-limit, classifier, budget, respect-pi-scope).',
1021
1011
  ' scope <apply|reset|show> Manage Pi scoped-models from your router config.',
1022
- ' ollama-sync Sync new Ollama models to models.json.',
1012
+ ' ollama-sync Delegate Ollama model discovery to /providers sync.',
1023
1013
  ' fallback Switch to an Ollama model manually.',
1024
1014
  ' restore Restore the original model after fallback.',
1025
1015
  ' profile [name] Switch to a profile (enables router if off). Lists available if no name.',
@@ -20,7 +20,6 @@ export const FALLBACK_CONFIG: RouterConfig = {
20
20
  defaultProfile: 'auto',
21
21
  debug: false,
22
22
  features: {
23
- ollamaSync: true,
24
23
  rateLimitFallback: true,
25
24
  scopeShim: true,
26
25
  respectPiScope: false, // Disabled by default for backward compatibility
@@ -121,10 +120,6 @@ export const mergeConfig = (
121
120
  base.features || override.features
122
121
  ? { ...base.features, ...override.features }
123
122
  : undefined,
124
- ollamaSync:
125
- base.ollamaSync || override.ollamaSync
126
- ? { ...base.ollamaSync, ...override.ollamaSync }
127
- : undefined,
128
123
  rateLimitFallback:
129
124
  base.rateLimitFallback || override.rateLimitFallback
130
125
  ? { ...base.rateLimitFallback, ...override.rateLimitFallback }
@@ -363,10 +358,6 @@ export const normalizeConfig = (raw: RouterConfig): ConfigLoadResult => {
363
358
  typeof raw.features === 'object' && raw.features !== null
364
359
  ? (raw.features as RouterConfig['features'])
365
360
  : undefined,
366
- ollamaSync:
367
- typeof raw.ollamaSync === 'object' && raw.ollamaSync !== null
368
- ? (raw.ollamaSync as RouterConfig['ollamaSync'])
369
- : undefined,
370
361
  rateLimitFallback:
371
362
  typeof raw.rateLimitFallback === 'object' &&
372
363
  raw.rateLimitFallback !== null
@@ -24,12 +24,14 @@ import { updateStatus, formatModelRef } from './ui';
24
24
  import { registerCommands } from './commands';
25
25
  import { registerRouterProvider } from './provider';
26
26
  // ─── Feature modules (added by fork) ────────────────────────────────────────
27
- import { initializeOllamaSync } from './ollama-sync';
27
+ import {
28
+ hasModelDiscoveryCommand,
29
+ MODEL_DISCOVERY_INSTALL_COMMAND,
30
+ } from './model-discovery';
28
31
  import { initializeRateLimitFallback, checkAndRestore } from './rate-limit';
29
32
 
30
33
  // ─── Plugin Detection & Progressive Integration ──────────────────────────
31
34
  interface PluginStatus {
32
- ledger: boolean;
33
35
  agentBus: boolean;
34
36
  }
35
37
 
@@ -51,7 +53,6 @@ const detectPlugins = (pi: ExtensionAPI): PluginStatus => {
51
53
  typeof legacyTools[name] === 'function';
52
54
 
53
55
  return {
54
- ledger: hasTool('append_ledger'),
55
56
  agentBus: hasTool('link_send'),
56
57
  };
57
58
  };
@@ -64,17 +65,6 @@ const detectAndIntegratePlugins = (
64
65
  const plugins = detectPlugins(pi);
65
66
  const log = (pi as any).log || console;
66
67
 
67
- // Ledger integration: log routing decisions to qmd-ledger
68
- const shouldIntegrateLedger = features?.ledgerIntegration === true;
69
- if (shouldIntegrateLedger && plugins.ledger) {
70
- log.info(
71
- '[router] Progressive: qmd-ledger detected. Routing decisions will be logged.',
72
- );
73
- } else if (shouldIntegrateLedger && !plugins.ledger) {
74
- log.warn(
75
- '[router] ledgerIntegration enabled but qmd-ledger not detected. Install with: pi install npm:pi-qmd-ledger',
76
- );
77
- }
78
68
 
79
69
  // Agent-bus integration: broadcast model changes
80
70
  const shouldIntegrateAgentBus = features?.agentBusIntegration === true;
@@ -145,6 +135,7 @@ const routerExtension = (pi: ExtensionAPI) => {
145
135
  let lastPersistedSnapshot: string | undefined;
146
136
  let isInitialized = false;
147
137
  let isInternalModelSwitch = false;
138
+ let warnedMissingModelDiscovery = false;
148
139
 
149
140
  const setModelInternally = async (
150
141
  model: NonNullable<ExtensionContext['model']>,
@@ -367,15 +358,6 @@ const routerExtension = (pi: ExtensionAPI) => {
367
358
  const ctx = lastExtensionContext;
368
359
  const features = currentConfig.features;
369
360
 
370
- // Ollama sync
371
- const shouldSyncOllama = !features || features.ollamaSync !== false; // enabled by default
372
- if (shouldSyncOllama) {
373
- initializeOllamaSync(
374
- pi,
375
- (currentConfig.ollamaSync ?? {}) as Record<string, unknown>,
376
- );
377
- }
378
-
379
361
  // Rate limit fallback
380
362
  const shouldInitRateLimit =
381
363
  !features || features.rateLimitFallback !== false; // enabled by default
@@ -387,15 +369,26 @@ const routerExtension = (pi: ExtensionAPI) => {
387
369
  );
388
370
  }
389
371
 
372
+ const hasDiscovery = hasModelDiscoveryCommand(pi);
373
+ if (!hasDiscovery && !warnedMissingModelDiscovery && ctx) {
374
+ warnedMissingModelDiscovery = true;
375
+ ctx.ui.notify(
376
+ 'Router: install pi-model-discovery for Ollama model discovery if you use local Ollama models (' +
377
+ MODEL_DISCOVERY_INSTALL_COMMAND +
378
+ ').',
379
+ 'warning',
380
+ );
381
+ }
382
+
390
383
  // ─── Progressive Plugin Integrations ───────────────────────────────────
391
384
  detectAndIntegratePlugins(pi, features, debugEnabled);
392
385
 
393
386
  if (debugEnabled) {
394
387
  console.log(
395
- '[router] Feature sync complete - ollama:',
396
- shouldSyncOllama,
397
- 'rate-limit:',
388
+ '[router] Feature sync complete - rate-limit:',
398
389
  shouldInitRateLimit,
390
+ 'model-discovery:',
391
+ hasDiscovery,
399
392
  );
400
393
  }
401
394
  };
@@ -0,0 +1,48 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+
3
+ export const MODEL_DISCOVERY_PACKAGE = '@kylebrodeur/pi-model-discovery';
4
+ export const MODEL_DISCOVERY_INSTALL_COMMAND =
5
+ 'pi install npm:@kylebrodeur/pi-model-discovery';
6
+ export const MODEL_DISCOVERY_SYNC_COMMAND = '/providers sync';
7
+
8
+ interface PiCommandInfo {
9
+ name: string;
10
+ source?: string;
11
+ sourceInfo?: {
12
+ source?: string;
13
+ path?: string;
14
+ baseDir?: string;
15
+ scope?: string;
16
+ origin?: string;
17
+ };
18
+ }
19
+
20
+ type CommandAwareExtensionAPI = ExtensionAPI & {
21
+ getCommands?: () => PiCommandInfo[];
22
+ };
23
+
24
+ export const hasModelDiscoveryCommand = (pi: ExtensionAPI): boolean => {
25
+ const commandApi = pi as CommandAwareExtensionAPI;
26
+ try {
27
+ return commandApi.getCommands?.().some(isModelDiscoveryProvidersCommand) ?? false;
28
+ } catch {
29
+ return false;
30
+ }
31
+ };
32
+
33
+ const isModelDiscoveryProvidersCommand = (command: PiCommandInfo): boolean => {
34
+ if (command.name !== 'providers' || command.source !== 'extension') {
35
+ return false;
36
+ }
37
+ const provenance = [
38
+ command.sourceInfo?.source,
39
+ command.sourceInfo?.path,
40
+ command.sourceInfo?.baseDir,
41
+ ]
42
+ .filter((value): value is string => typeof value === 'string')
43
+ .join(' ');
44
+ return (
45
+ provenance.includes(MODEL_DISCOVERY_PACKAGE) ||
46
+ provenance.includes('pi-model-discovery')
47
+ );
48
+ };
@@ -3,18 +3,15 @@ import type { ThinkingLevel } from '@earendil-works/pi-agent-core';
3
3
  // ─── Feature Toggles (added by fork) ──────────────────────────────────────
4
4
 
5
5
  export type RouterFeature =
6
- | 'ollamaSync'
7
6
  | 'rateLimitFallback'
8
7
  | 'perTurnRouting'
9
8
  | 'intentClassifier'
10
9
  | 'costBudgeting'
11
10
  | 'phaseMemory'
12
11
  | 'contextCompression'
13
- | 'ledgerIntegration'
14
12
  | 'agentBusIntegration';
15
13
 
16
14
  export interface FeatureToggles {
17
- ollamaSync?: boolean;
18
15
  rateLimitFallback?: boolean;
19
16
  scopeShim?: boolean;
20
17
  respectPiScope?: boolean;
@@ -23,11 +20,6 @@ export interface FeatureToggles {
23
20
  costBudgeting?: boolean;
24
21
  phaseMemory?: boolean;
25
22
  contextCompression?: boolean;
26
- /**
27
- * Log routing decisions to qmd-ledger if available.
28
- * Progressive: detects pi-qmd-ledger at runtime.
29
- */
30
- ledgerIntegration?: boolean;
31
23
  /**
32
24
  * Publish model changes to pi-agent-bus MessageBus if available.
33
25
  * Progressive: detects pi-agent-bus at runtime.
@@ -35,17 +27,6 @@ export interface FeatureToggles {
35
27
  agentBusIntegration?: boolean;
36
28
  }
37
29
 
38
- export interface OllamaSyncConfig {
39
- enabled?: boolean;
40
- onStartup?: boolean;
41
- onReload?: boolean;
42
- addLaunchFlag?: boolean;
43
- visionKeywords?: string[];
44
- reasoningKeywords?: string[];
45
- preferredFamilies?: string[];
46
- defaultContextWindow?: number;
47
- largeContextWindow?: number;
48
- }
49
30
 
50
31
  export interface RateLimitFallbackConfig {
51
32
  enabled?: boolean;
@@ -94,7 +75,6 @@ export interface RouterConfig {
94
75
  profiles: Record<string, RouterProfile>;
95
76
  // ─── Feature toggles (added by fork) ──────────────────────────────
96
77
  features?: FeatureToggles;
97
- ollamaSync?: OllamaSyncConfig;
98
78
  rateLimitFallback?: RateLimitFallbackConfig;
99
79
  }
100
80
 
@@ -1,7 +1,6 @@
1
1
  {
2
- "comment": "Progressive Step 2: Router with ledger + agent-bus integration. Enables agent coordination for model selection.",
2
+ "comment": "Router with agent-bus integration for model selection coordination.",
3
3
  "features": {
4
- "ollamaSync": true,
5
4
  "rateLimitFallback": true,
6
5
  "scopeShim": true,
7
6
  "perTurnRouting": true,
@@ -9,7 +8,6 @@
9
8
  "costBudgeting": true,
10
9
  "phaseMemory": true,
11
10
  "contextCompression": true,
12
- "ledgerIntegration": true,
13
11
  "agentBusIntegration": true
14
12
  }
15
13
  }
@@ -1,7 +1,6 @@
1
1
  {
2
- "comment": "Progressive Step 3: Essential config all integrations enabled. Maximum interoperability with the Pi package ecosystem.",
2
+ "comment": "Essential router config with rate-limit fallback, scope sync, and agent-bus integration enabled.",
3
3
  "features": {
4
- "ollamaSync": true,
5
4
  "rateLimitFallback": true,
6
5
  "scopeShim": true,
7
6
  "perTurnRouting": true,
@@ -9,14 +8,8 @@
9
8
  "costBudgeting": true,
10
9
  "phaseMemory": true,
11
10
  "contextCompression": true,
12
- "ledgerIntegration": true,
13
11
  "agentBusIntegration": true
14
12
  },
15
- "ollamaSync": {
16
- "enabled": true,
17
- "onStartup": true,
18
- "onReload": true
19
- },
20
13
  "rateLimitFallback": {
21
14
  "enabled": true,
22
15
  "shortDelayThreshold": 60,
@@ -25,7 +18,6 @@
25
18
  "fallbackSequence": ["anthropic/claude-3-haiku-20240307", "ollama/*"]
26
19
  },
27
20
  "rules": [
28
- { "matches": "ledger", "tier": "low", "reason": "Ledger operations are lightweight" },
29
21
  { "matches": "agent-bus", "tier": "medium", "reason": "Agent coordination needs reliability" }
30
22
  ]
31
23
  }
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "features": {
3
- "ollamaSync": true,
4
3
  "rateLimitFallback": true,
5
4
  "scopeShim": true,
6
5
  "perTurnRouting": true,
@@ -9,11 +8,6 @@
9
8
  "phaseMemory": true,
10
9
  "contextCompression": true
11
10
  },
12
- "ollamaSync": {
13
- "enabled": true,
14
- "onStartup": true,
15
- "onReload": true
16
- },
17
11
  "rateLimitFallback": {
18
12
  "enabled": true,
19
13
  "shortDelayThreshold": 60,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kylebrodeur/pi-model-router",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Intelligent per-turn model router extension for the pi coding agent (Enhanced Fork)",
6
6
  "keywords": [
@@ -15,7 +15,7 @@
15
15
  "author": "Kyle Brodeur <kylebrodeur@example.com> (https://github.com/kylebrodeur)",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "https://github.com/kylebrodeur/pi-model-router.git"
18
+ "url": "git+https://github.com/kylebrodeur/pi-model-router.git"
19
19
  },
20
20
  "homepage": "https://github.com/kylebrodeur/pi-model-router#readme",
21
21
  "bugs": {
@@ -25,7 +25,6 @@
25
25
  "extensions/",
26
26
  "docs/",
27
27
  "model-router.example.json",
28
- "model-router.ledger.json",
29
28
  "model-router.agent-bus.json",
30
29
  "model-router.essential.json",
31
30
  "LICENSE",
@@ -44,11 +43,6 @@
44
43
  "./extensions/index.ts"
45
44
  ]
46
45
  },
47
- "scripts": {
48
- "tsc": "tsc --noEmit",
49
- "build": "tsc",
50
- "prepublishOnly": "npm run tsc"
51
- },
52
46
  "peerDependencies": {
53
47
  "@earendil-works/pi-agent-core": "^0.78.1",
54
48
  "@earendil-works/pi-ai": "^0.78.1",
@@ -60,5 +54,10 @@
60
54
  "@earendil-works/pi-coding-agent": "^0.78.1",
61
55
  "prettier": "^3.8.1",
62
56
  "typescript": "^6.0.2"
57
+ },
58
+ "scripts": {
59
+ "tsc": "tsc --noEmit",
60
+ "test": "tsc --noEmit",
61
+ "build": "tsc"
63
62
  }
64
- }
63
+ }