pi-io-provider 1.0.3

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.
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Script to update IO Intelligence models from the API
5
+ *
6
+ * Fetches the model list from https://api.intelligence.io.solutions/api/v1/models
7
+ * and regenerates models.json and the README model table.
8
+ *
9
+ * Requires IOINTELLIGENCE_API_KEY environment variable.
10
+ * Usage: IOINTELLIGENCE_API_KEY=your-key node scripts/update-models.js
11
+ */
12
+
13
+ import https from 'https';
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import { fileURLToPath } from 'url';
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = path.dirname(__filename);
20
+
21
+ const API_BASE = 'https://api.intelligence.io.solutions/api/v1';
22
+ const MODELS_PATH = path.join(process.cwd(), 'models.json');
23
+ const PATCH_PATH = path.join(process.cwd(), 'patch.json');
24
+ const CUSTOM_MODELS_PATH = path.join(process.cwd(), 'custom-models.json');
25
+
26
+ // ─── HTTP helpers ───────────────────────────────────────────────────────────
27
+
28
+ function fetchJSON(url, headers = {}) {
29
+ return new Promise((resolve, reject) => {
30
+ const req = https.get(url, { headers }, (res) => {
31
+ let data = '';
32
+ res.on('data', (chunk) => (data += chunk));
33
+ res.on('end', () => {
34
+ try {
35
+ resolve(JSON.parse(data));
36
+ } catch (e) {
37
+ reject(new Error(`Failed to parse JSON from ${url}: ${e.message}`));
38
+ }
39
+ });
40
+ });
41
+ req.on('error', reject);
42
+ });
43
+ }
44
+
45
+ // ─── Model transformation ──────────────────────────────────────────────────
46
+
47
+ /** Clean up the display name from the API. */
48
+ function cleanName(apiName, apiId) {
49
+ // API returns names like "MoonshotAI: Kimi K2.6" or "MiniMaxAI/MiniMax-M2.5"
50
+ let name = apiName;
51
+ const colonIdx = name.indexOf(': ');
52
+ if (colonIdx > 0 && colonIdx < 25) {
53
+ name = name.substring(colonIdx + 2);
54
+ }
55
+ if (name.includes('/') && !name.includes(' ')) {
56
+ const parts = name.split('/');
57
+ name = parts[parts.length - 1].replace(/-/g, ' ');
58
+ }
59
+ if (name === 'R1 0528') name = 'DeepSeek R1 0528';
60
+ return name;
61
+ }
62
+
63
+ function convertModel(apiModel, existingModelsMap) {
64
+ const id = apiModel.id;
65
+
66
+ // Preserve existing curated data (reasoning, compat, etc.)
67
+ if (existingModelsMap[id]) {
68
+ const existing = { ...existingModelsMap[id] };
69
+ // Update mutable fields from API
70
+ const ctx = apiModel.context_window || 0;
71
+ const maxTok = apiModel.max_tokens || ctx;
72
+ const priceIn = (apiModel.input_token_price || 0) * 1_000_000;
73
+ const priceOut = (apiModel.output_token_price || 0) * 1_000_000;
74
+ const cacheRead = (apiModel.cache_read_token_price || 0) * 1_000_000;
75
+ const cacheWrite = (apiModel.cache_write_token_price || 0) * 1_000_000;
76
+ if (ctx > 0) existing.contextWindow = ctx;
77
+ if (maxTok > 0) existing.maxTokens = maxTok;
78
+ // Round to 6 decimals of $/M: normalizes float noise from the ×1e6 multiply
79
+ // and preserves sub-cent cache prices like 0.003 (cent rounding erased them).
80
+ if (priceIn > 0) existing.cost.input = Math.round(priceIn * 1e6) / 1e6;
81
+ if (priceOut > 0) existing.cost.output = Math.round(priceOut * 1e6) / 1e6;
82
+ if (cacheRead > 0) existing.cost.cacheRead = Math.round(cacheRead * 1e6) / 1e6;
83
+ if (cacheWrite > 0) existing.cost.cacheWrite = Math.round(cacheWrite * 1e6) / 1e6;
84
+ if (apiModel.supports_images_input && !existing.input.includes('image')) {
85
+ existing.input = ['text', 'image'];
86
+ }
87
+ return existing;
88
+ }
89
+
90
+ // New model — build from API data + sensible defaults
91
+ const ctx = apiModel.context_window || 0;
92
+ const maxTok = apiModel.max_tokens || ctx;
93
+ const input = ['text'];
94
+ if (apiModel.supports_images_input) input.push('image');
95
+
96
+ const priceIn = (apiModel.input_token_price || 0) * 1_000_000;
97
+ const priceOut = (apiModel.output_token_price || 0) * 1_000_000;
98
+ const cacheRead = (apiModel.cache_read_token_price || 0) * 1_000_000;
99
+ const cacheWrite = (apiModel.cache_write_token_price || 0) * 1_000_000;
100
+
101
+ return {
102
+ id,
103
+ name: cleanName(apiModel.name, id),
104
+ reasoning: false,
105
+ input,
106
+ cost: {
107
+ input: Math.round(priceIn * 1e6) / 1e6,
108
+ output: Math.round(priceOut * 1e6) / 1e6,
109
+ cacheRead: Math.round(cacheRead * 1e6) / 1e6,
110
+ cacheWrite: Math.round(cacheWrite * 1e6) / 1e6,
111
+ },
112
+ contextWindow: ctx,
113
+ maxTokens: maxTok,
114
+ };
115
+ }
116
+
117
+ // ─── Patch & Custom Models ──────────────────────────────────────────────────
118
+
119
+ function applyPatch(model, patch) {
120
+ const result = { ...model };
121
+ if (patch.name !== undefined) result.name = patch.name;
122
+ if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
123
+ if (patch.input !== undefined) result.input = patch.input;
124
+ if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
125
+ if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
126
+ if (patch.cost) {
127
+ result.cost = {
128
+ input: patch.cost.input ?? result.cost.input,
129
+ output: patch.cost.output ?? result.cost.output,
130
+ cacheRead: patch.cost.cacheRead ?? result.cost.cacheRead,
131
+ cacheWrite: patch.cost.cacheWrite ?? result.cost.cacheWrite,
132
+ };
133
+ }
134
+ if (patch.compat) {
135
+ result.compat = { ...(result.compat || {}), ...patch.compat };
136
+ }
137
+ if (!result.reasoning && result.compat?.thinkingFormat) {
138
+ delete result.compat.thinkingFormat;
139
+ }
140
+ if (result.compat && Object.keys(result.compat).length === 0) {
141
+ delete result.compat;
142
+ }
143
+ return result;
144
+ }
145
+
146
+ function buildModels(baseModels, customModels, patchData) {
147
+ const modelMap = new Map();
148
+ for (const model of baseModels) {
149
+ modelMap.set(model.id, model);
150
+ }
151
+ for (const [id, patchEntry] of Object.entries(patchData)) {
152
+ const existing = modelMap.get(id);
153
+ if (existing) {
154
+ modelMap.set(id, applyPatch(existing, patchEntry));
155
+ }
156
+ }
157
+ for (const model of customModels) {
158
+ const existing = modelMap.get(model.id);
159
+ const patchEntry = patchData[model.id];
160
+ if (existing && patchEntry) {
161
+ modelMap.set(model.id, applyPatch(model, patchEntry));
162
+ } else if (existing) {
163
+ modelMap.set(model.id, model);
164
+ } else if (patchEntry) {
165
+ modelMap.set(model.id, applyPatch(model, patchEntry));
166
+ } else {
167
+ modelMap.set(model.id, model);
168
+ }
169
+ }
170
+ return Array.from(modelMap.values());
171
+ }
172
+
173
+ // ─── README generation ──────────────────────────────────────────────────────
174
+
175
+ function formatCost(cost) {
176
+ if (cost === 0) return 'Free';
177
+ if (cost < 0.01) return `<$0.01`;
178
+ return `$${cost.toFixed(2)}`;
179
+ }
180
+
181
+ function formatCtx(num) {
182
+ if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`;
183
+ if (num >= 1_000) return `${Math.round(num / 1_000)}K`;
184
+ return num.toString();
185
+ }
186
+
187
+ function generateReadme(models) {
188
+ const sorted = [...models].sort((a, b) => {
189
+ // Sort by: reasoning first, then vision, then by cost
190
+ if (a.reasoning !== b.reasoning) return b.reasoning - a.reasoning;
191
+ const aVis = a.input.includes('image') ? 1 : 0;
192
+ const bVis = b.input.includes('image') ? 1 : 0;
193
+ if (aVis !== bVis) return bVis - aVis;
194
+ return a.id.localeCompare(b.id);
195
+ });
196
+
197
+ const rows = sorted.map(m => {
198
+ const vision = m.input.includes('image') ? '✅' : '❌';
199
+ const reasoning = m.reasoning ? '✅' : '❌';
200
+ const cache = m.cost.cacheRead > 0 ? '✅' : '❌';
201
+ const displayName = m.name;
202
+ return `| ${displayName} | \`${m.id}\` | ${formatCtx(m.contextWindow)} | ${formatCtx(m.maxTokens)} | ${vision} | ${reasoning} | ${cache} | ${formatCost(m.cost.input)} | ${formatCost(m.cost.output)} |`;
203
+ }).join('\n');
204
+
205
+ const readme = `# pi-io-provider
206
+
207
+ A [pi](https://github.com/badlogic/pi-mono) extension that adds [IO Intelligence](https://io.net) as a custom model provider.
208
+
209
+ ## Features
210
+
211
+ - **OpenAI-compatible API** — Uses IO Intelligence's \`/api/v1/chat/completions\` endpoint
212
+ - **23+ AI models** — DeepSeek, Kimi, GLM, Llama, Qwen, Mistral, and more
213
+ - **Reasoning models** — DeepSeek R1, Kimi K2 Thinking with extended reasoning
214
+ - **Vision models** — Kimi K2.5/K2.6, Llama 4 Maverick, Llama 3.2 Vision, Qwen2.5 VL, Mistral Large
215
+ - **Prompt caching** — Cache read/write support on most models
216
+ - **Confidential inference** — Verifiable TEE inference with attestation (via /private/ endpoints)
217
+ - **Streaming** — Real-time token streaming
218
+
219
+ ## Available Models
220
+
221
+ | Model | ID | Context | Max Output | Vision | Reasoning | Cache | Input $/M | Output $/M |
222
+ |-------|----|---------|------------|--------|-----------|-------|-----------|------------|
223
+ ${rows}
224
+
225
+ *Costs are per million tokens. Cache read/write pricing available on most models.*
226
+
227
+ ## Installation
228
+
229
+ ### Option 1: Using \`pi install\` (Recommended)
230
+
231
+ Install directly from GitHub:
232
+
233
+ \`\`\`bash
234
+ pi install git:github.com/monotykamary/pi-io-provider
235
+ \`\`\`
236
+
237
+ Then set your API key and run pi:
238
+ \`\`\`bash
239
+ # Recommended: add to auth.json
240
+ # See Authentication section below
241
+
242
+ # Or set as environment variable
243
+ export IOINTELLIGENCE_API_KEY=your-api-key-here
244
+
245
+ pi
246
+ \`\`\`
247
+
248
+ Get your API key from [io.net](https://io.net).
249
+
250
+ ### Option 2: Manual Clone
251
+
252
+ 1. Clone this repository:
253
+ \`\`\`bash
254
+ git clone https://github.com/monotykamary/pi-io-provider.git
255
+ cd pi-io-provider
256
+ \`\`\`
257
+
258
+ 2. Set your IO Intelligence API key:
259
+ \`\`\`bash
260
+ # Recommended: add to auth.json
261
+ # See Authentication section below
262
+
263
+ # Or set as environment variable
264
+ export IOINTELLIGENCE_API_KEY=your-api-key-here
265
+ \`\`\`
266
+
267
+ 3. Run pi with the extension:
268
+ \`\`\`bash
269
+ pi -e /path/to/pi-io-provider
270
+ \`\`\`
271
+
272
+ ## Authentication
273
+
274
+ The IO Intelligence API key can be configured in multiple ways (resolved in this order):
275
+
276
+ 1. **\`auth.json\`** (recommended) — Add to \`~/.pi/agent/auth.json\`:
277
+ \`\`\`json
278
+ { "io-intelligence": { "type": "api_key", "key": "your-api-key" } }
279
+ \`\`\`
280
+ The \`key\` field supports literal values, env var names, and shell commands (prefix with \`!\`). See [pi's auth file docs](https://github.com/badlogic/pi-mono) for details.
281
+ 2. **Runtime override** — Use the \`--api-key\` CLI flag
282
+ 3. **Environment variable** — Set \`IOINTELLIGENCE_API_KEY\`
283
+
284
+ Get your API key from [io.net](https://io.net).
285
+
286
+ ## Environment Variables
287
+
288
+ | Variable | Required | Description |
289
+ |----------|----------|-------------|
290
+ | \`IOINTELLIGENCE_API_KEY\` | No | Your IO Intelligence API key (fallback if not in auth.json) |
291
+
292
+ ## Configuration
293
+
294
+ Add to your pi configuration for automatic loading:
295
+
296
+ \`\`\`json
297
+ {
298
+ "extensions": [
299
+ "/path/to/pi-io-provider"
300
+ ]
301
+ }
302
+ \`\`\`
303
+
304
+ ## Usage
305
+
306
+ Once loaded, select a model with:
307
+
308
+ \`\`\`
309
+ /model io-intelligence deepseek-ai/DeepSeek-R1-0528
310
+ \`\`\`
311
+
312
+ Or use \`/models\` to browse all available IO Intelligence models.
313
+
314
+ ## API Documentation
315
+
316
+ - IO Intelligence Docs: https://io.net/docs/guides/confidential-inference/quick-start
317
+ - OpenAI-compatible endpoint: \`https://api.intelligence.io.solutions/api/v1\`
318
+ - Models endpoint: \`https://api.intelligence.io.solutions/api/v1/models\`
319
+ - Confidential inference: \`https://api.intelligence.io.solutions/api/v1/private/completions\`
320
+
321
+ ## License
322
+
323
+ MIT
324
+ `;
325
+
326
+ return readme;
327
+ }
328
+
329
+ // ─── Main ────────────────────────────────────────────────────────────────────
330
+
331
+ async function main() {
332
+ const apiKey = process.env.IOINTELLIGENCE_API_KEY;
333
+ if (!apiKey) {
334
+ console.error('Error: IOINTELLIGENCE_API_KEY environment variable is required');
335
+ console.error('Usage: IOINTELLIGENCE_API_KEY=your-key node scripts/update-models.js');
336
+ process.exit(1);
337
+ }
338
+
339
+ console.log('Fetching models from IO Intelligence API...\n');
340
+
341
+ try {
342
+ const data = await fetchJSON(`${API_BASE}/models`, {
343
+ Authorization: `Bearer ${apiKey}`,
344
+ });
345
+
346
+ const apiModels = data.data || [];
347
+ console.log(`Total models from API: ${apiModels.length}`);
348
+
349
+ // Load existing models.json — source of truth for curated specs
350
+ let existingModels = [];
351
+ try {
352
+ existingModels = JSON.parse(fs.readFileSync(MODELS_PATH, 'utf8'));
353
+ } catch (e) {
354
+ // File might not exist or be invalid
355
+ }
356
+ const existingModelsMap = {};
357
+ for (const m of existingModels) {
358
+ existingModelsMap[m.id] = m;
359
+ }
360
+
361
+ const models = apiModels.map(m => convertModel(m, existingModelsMap));
362
+ console.log(`Converted ${models.length} models`);
363
+
364
+ // Save models.json (pure API output, no patch/custom baked in)
365
+ fs.writeFileSync(MODELS_PATH, JSON.stringify(models, null, 2) + '\n');
366
+ console.log(`✓ Saved ${models.length} models to models.json`);
367
+
368
+ // Build full model list for README: base → patch → custom
369
+ let patchData = {};
370
+ let customModels = [];
371
+ try {
372
+ patchData = JSON.parse(fs.readFileSync(PATCH_PATH, 'utf8'));
373
+ } catch {}
374
+ try {
375
+ customModels = JSON.parse(fs.readFileSync(CUSTOM_MODELS_PATH, 'utf8'));
376
+ if (!Array.isArray(customModels)) customModels = [];
377
+ } catch {}
378
+ const readmeModels = buildModels(models, customModels, patchData);
379
+ readmeModels.sort((a, b) => a.name.localeCompare(b.name));
380
+
381
+ // Update README
382
+ const readme = generateReadme(readmeModels);
383
+ fs.writeFileSync(path.join(process.cwd(), 'README.md'), readme);
384
+ console.log(`✓ Updated README.md`);
385
+
386
+ console.log('\nDone!');
387
+ } catch (error) {
388
+ console.error('Error:', error.message);
389
+ process.exit(1);
390
+ }
391
+ }
392
+
393
+ main();