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.
- package/.github/FUNDING.yml +4 -0
- package/AGENTS.md +56 -0
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/custom-models.json +1 -0
- package/index.ts +331 -0
- package/models.json +470 -0
- package/npm-shrinkwrap.json +13 -0
- package/package.json +35 -0
- package/patch.json +16 -0
- package/scripts/update-models.js +393 -0
|
@@ -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();
|