@kylebrodeur/pi-model-router 0.2.0 → 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.
- package/CHANGELOG.md +17 -1
- package/CONTRIBUTING.md +19 -20
- package/LEARNINGS.md +2 -3
- package/QUICKSTART.md +19 -26
- package/README.md +15 -9
- package/TESTING.md +35 -100
- package/docs/pi-model-router-refactor-prompt.md +99 -0
- package/extensions/commands.ts +24 -34
- package/extensions/config.ts +0 -9
- package/extensions/index.ts +19 -26
- package/extensions/model-discovery.ts +48 -0
- package/extensions/types.ts +0 -20
- package/model-router.agent-bus.json +1 -3
- package/model-router.essential.json +1 -9
- package/model-router.example.json +0 -6
- package/package.json +13 -14
- package/extensions/ollama-sync.ts +0 -254
- package/model-router.ledger.json +0 -15
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ollama Sync Feature
|
|
3
|
-
*
|
|
4
|
-
* Auto-detects and registers new Ollama models in models.json.
|
|
5
|
-
* NOTE: Pi 0.67+ recommended for full support.
|
|
6
|
-
*/
|
|
7
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { homedir } from 'node:os';
|
|
10
|
-
import type {
|
|
11
|
-
ExtensionAPI,
|
|
12
|
-
ExtensionContext,
|
|
13
|
-
} from '@earendil-works/pi-coding-agent';
|
|
14
|
-
|
|
15
|
-
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
interface OllamaListEntry {
|
|
18
|
-
name: string;
|
|
19
|
-
id: string;
|
|
20
|
-
size: string;
|
|
21
|
-
modified: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface ModelsJsonEntry {
|
|
25
|
-
id: string;
|
|
26
|
-
name?: string;
|
|
27
|
-
reasoning?: boolean;
|
|
28
|
-
input?: string[];
|
|
29
|
-
contextWindow?: number;
|
|
30
|
-
maxTokens?: number;
|
|
31
|
-
cost?: { input: number; output: number };
|
|
32
|
-
_launch?: boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface ModelsJson {
|
|
36
|
-
providers: {
|
|
37
|
-
ollama?: {
|
|
38
|
-
api: string;
|
|
39
|
-
apiKey: string;
|
|
40
|
-
baseUrl: string;
|
|
41
|
-
models: ModelsJsonEntry[];
|
|
42
|
-
};
|
|
43
|
-
[key: string]: unknown;
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ─── Config ─────────────────────────────────────────────────────────────────
|
|
48
|
-
|
|
49
|
-
export interface OllamaSyncConfig {
|
|
50
|
-
enabled: boolean;
|
|
51
|
-
onStartup: boolean;
|
|
52
|
-
onReload: boolean;
|
|
53
|
-
addLaunchFlag: boolean;
|
|
54
|
-
visionKeywords: string[];
|
|
55
|
-
reasoningKeywords: string[];
|
|
56
|
-
preferredFamilies: string[];
|
|
57
|
-
defaultContextWindow: number;
|
|
58
|
-
largeContextWindow: number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export const DEFAULT_OLLAMA_CONFIG: OllamaSyncConfig = {
|
|
62
|
-
enabled: true,
|
|
63
|
-
onStartup: true,
|
|
64
|
-
onReload: true,
|
|
65
|
-
addLaunchFlag: false,
|
|
66
|
-
visionKeywords: ['vl', 'vision', 'ocr', 'image'],
|
|
67
|
-
reasoningKeywords: ['thinking', 'reason', 'cascade', 'deepseek'],
|
|
68
|
-
preferredFamilies: ['gemma4', 'qwen3', 'kimi', 'llama3'],
|
|
69
|
-
defaultContextWindow: 128000,
|
|
70
|
-
largeContextWindow: 262144,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
const getModelsJsonPath = (): string => {
|
|
76
|
-
return join(homedir(), '.pi', 'agent', 'models.json');
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const loadModelsJson = (): ModelsJson | null => {
|
|
80
|
-
const path = getModelsJsonPath();
|
|
81
|
-
if (!existsSync(path)) return null;
|
|
82
|
-
try {
|
|
83
|
-
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
84
|
-
} catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const saveModelsJson = (data: ModelsJson): void => {
|
|
90
|
-
writeFileSync(getModelsJsonPath(), JSON.stringify(data, null, 2));
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const parseOllamaList = (output: string): OllamaListEntry[] => {
|
|
94
|
-
const lines = output.trim().split('\n');
|
|
95
|
-
const models: OllamaListEntry[] = [];
|
|
96
|
-
for (let i = 1; i < lines.length; i++) {
|
|
97
|
-
const line = lines[i].trim();
|
|
98
|
-
if (!line) continue;
|
|
99
|
-
const parts = line.split(/\s{2,}/);
|
|
100
|
-
if (parts.length >= 3) {
|
|
101
|
-
models.push({
|
|
102
|
-
name: parts[0],
|
|
103
|
-
id: parts[1],
|
|
104
|
-
size: parts[2],
|
|
105
|
-
modified: parts.slice(3).join(' '),
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return models;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const inferCapabilities = (
|
|
113
|
-
modelName: string,
|
|
114
|
-
cfg: OllamaSyncConfig,
|
|
115
|
-
): Partial<ModelsJsonEntry> => {
|
|
116
|
-
const lower = modelName.toLowerCase();
|
|
117
|
-
const entry: Partial<ModelsJsonEntry> = {
|
|
118
|
-
contextWindow: cfg.defaultContextWindow,
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
if (cfg.visionKeywords.some((kw) => lower.includes(kw))) {
|
|
122
|
-
entry.input = ['text', 'image'];
|
|
123
|
-
} else {
|
|
124
|
-
entry.input = ['text'];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (cfg.reasoningKeywords.some((kw) => lower.includes(kw))) {
|
|
128
|
-
entry.reasoning = true;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
lower.includes('kimi') ||
|
|
133
|
-
lower.includes('gemma4') ||
|
|
134
|
-
lower.includes('deepseek')
|
|
135
|
-
) {
|
|
136
|
-
entry.contextWindow = cfg.largeContextWindow;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return entry;
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
143
|
-
|
|
144
|
-
export interface OllamaSyncResult {
|
|
145
|
-
added: string[];
|
|
146
|
-
message: string;
|
|
147
|
-
success: boolean;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export const performOllamaSync = async (
|
|
151
|
-
pi: ExtensionAPI,
|
|
152
|
-
userConfig: Partial<OllamaSyncConfig>,
|
|
153
|
-
): Promise<OllamaSyncResult> => {
|
|
154
|
-
const config = { ...DEFAULT_OLLAMA_CONFIG, ...userConfig };
|
|
155
|
-
|
|
156
|
-
let output: string;
|
|
157
|
-
try {
|
|
158
|
-
const result = await pi.exec('ollama', ['list'], { timeout: 10000 });
|
|
159
|
-
if (result.code !== 0) {
|
|
160
|
-
return { added: [], message: 'Ollama not available', success: false };
|
|
161
|
-
}
|
|
162
|
-
output = result.stdout;
|
|
163
|
-
} catch {
|
|
164
|
-
return { added: [], message: 'Ollama not available', success: false };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const ollamaModels = parseOllamaList(output);
|
|
168
|
-
if (ollamaModels.length === 0) {
|
|
169
|
-
return { added: [], message: 'No Ollama models found', success: true };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const modelsJson = loadModelsJson();
|
|
173
|
-
if (!modelsJson) {
|
|
174
|
-
return { added: [], message: 'models.json not found', success: false };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (!modelsJson.providers.ollama) {
|
|
178
|
-
modelsJson.providers.ollama = {
|
|
179
|
-
api: 'openai-completions',
|
|
180
|
-
apiKey: 'ollama',
|
|
181
|
-
baseUrl: 'http://127.0.0.1:11434/v1',
|
|
182
|
-
models: [],
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const existingIds = new Set(
|
|
187
|
-
modelsJson.providers.ollama.models.map((m) => m.id),
|
|
188
|
-
);
|
|
189
|
-
const added: string[] = [];
|
|
190
|
-
|
|
191
|
-
for (const model of ollamaModels) {
|
|
192
|
-
if (!existingIds.has(model.name)) {
|
|
193
|
-
const caps = inferCapabilities(model.name, config);
|
|
194
|
-
const entry: ModelsJsonEntry = { id: model.name, ...caps };
|
|
195
|
-
|
|
196
|
-
if (
|
|
197
|
-
config.addLaunchFlag &&
|
|
198
|
-
config.preferredFamilies.some((f) =>
|
|
199
|
-
model.name.toLowerCase().includes(f),
|
|
200
|
-
)
|
|
201
|
-
) {
|
|
202
|
-
entry._launch = true;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
modelsJson.providers.ollama.models.push(entry);
|
|
206
|
-
added.push(model.name);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (added.length > 0) {
|
|
211
|
-
saveModelsJson(modelsJson);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return {
|
|
215
|
-
added,
|
|
216
|
-
message:
|
|
217
|
-
added.length > 0 ? `Added ${added.length} model(s)` : 'No new models',
|
|
218
|
-
success: true,
|
|
219
|
-
};
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
// ─── Extension Integration ──────────────────────────────────────────────────
|
|
223
|
-
|
|
224
|
-
export const initializeOllamaSync = (
|
|
225
|
-
pi: ExtensionAPI,
|
|
226
|
-
rawConfig: Record<string, unknown>,
|
|
227
|
-
): void => {
|
|
228
|
-
const merged = { ...DEFAULT_OLLAMA_CONFIG };
|
|
229
|
-
for (const key of Object.keys(merged) as Array<keyof typeof merged>) {
|
|
230
|
-
if (rawConfig[key] !== undefined) merged[key] = rawConfig[key] as never;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (!merged.enabled) {
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Auto-sync on session start/reload
|
|
238
|
-
pi.on('session_start', async (event, ctx) => {
|
|
239
|
-
const shouldSync =
|
|
240
|
-
merged.enabled &&
|
|
241
|
-
((event.reason === 'startup' && merged.onStartup) ||
|
|
242
|
-
(event.reason === 'reload' && merged.onReload));
|
|
243
|
-
|
|
244
|
-
if (shouldSync) {
|
|
245
|
-
const result = await performOllamaSync(pi, merged);
|
|
246
|
-
if (result.success && result.added.length > 0) {
|
|
247
|
-
ctx.ui.notify(`[Router] Added ${result.added.length} model(s)`, 'info');
|
|
248
|
-
ctx.ui.notify(`Run /reload to see: ${result.added.join(', ')}`, 'info');
|
|
249
|
-
} else if (!result.success) {
|
|
250
|
-
ctx.ui.notify(`Ollama sync failed: ${result.message}`, 'warning');
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
};
|
package/model-router.ledger.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"comment": "Progressive Step 1: Basic router with qmd-ledger integration. Enables logging routing decisions to ledger.",
|
|
3
|
-
"features": {
|
|
4
|
-
"ollamaSync": true,
|
|
5
|
-
"rateLimitFallback": true,
|
|
6
|
-
"scopeShim": true,
|
|
7
|
-
"perTurnRouting": true,
|
|
8
|
-
"intentClassifier": false,
|
|
9
|
-
"costBudgeting": true,
|
|
10
|
-
"phaseMemory": true,
|
|
11
|
-
"contextCompression": true,
|
|
12
|
-
"ledgerIntegration": true,
|
|
13
|
-
"agentBusIntegration": false
|
|
14
|
-
}
|
|
15
|
-
}
|