pi-parasail-provider 1.0.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/.github/FUNDING.yml +4 -0
- package/.pi/autoresearch/session-id +1 -0
- package/AGENTS.md +56 -0
- package/LICENSE +21 -0
- package/README.md +196 -0
- package/custom-models.json +1 -0
- package/index.ts +405 -0
- package/models.json +676 -0
- package/package.json +35 -0
- package/patch.json +87 -0
- package/scripts/update-models.js +341 -0
package/patch.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"parasail-kimi-k25": {
|
|
3
|
+
"input": ["text", "image"],
|
|
4
|
+
"compat": {
|
|
5
|
+
"supportsReasoningEffort": true
|
|
6
|
+
}
|
|
7
|
+
},
|
|
8
|
+
"parasail-kimi-k26": {
|
|
9
|
+
"input": ["text", "image"],
|
|
10
|
+
"compat": {
|
|
11
|
+
"supportsReasoningEffort": true
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"parasail-kimi-k26-nvfp4": {
|
|
15
|
+
"input": ["text", "image"],
|
|
16
|
+
"compat": {
|
|
17
|
+
"supportsReasoningEffort": true
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"parasail-qwen3p5-35b-a3b": {
|
|
21
|
+
"input": ["text", "image"],
|
|
22
|
+
"compat": {
|
|
23
|
+
"supportsReasoningEffort": true
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"parasail-qwen3p6-35b-a3b": {
|
|
27
|
+
"input": ["text", "image"],
|
|
28
|
+
"compat": {
|
|
29
|
+
"supportsReasoningEffort": true
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"parasail-qwen35-397b-a17b": {
|
|
33
|
+
"compat": {
|
|
34
|
+
"supportsReasoningEffort": true
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"parasail-qwen3-coder-next": {
|
|
38
|
+
"compat": {
|
|
39
|
+
"supportsReasoningEffort": true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"parasail-qwen-3-next-80b-instruct": {
|
|
43
|
+
"compat": {
|
|
44
|
+
"supportsReasoningEffort": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"parasail-qwen3vl-8b-instruct": {
|
|
48
|
+
"compat": {
|
|
49
|
+
"supportsReasoningEffort": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"parasail-trinity-large-thinking": {
|
|
53
|
+
"compat": {
|
|
54
|
+
"supportsReasoningEffort": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"parasail-glm47": {
|
|
58
|
+
"compat": {
|
|
59
|
+
"supportsReasoningEffort": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"parasail-glm-5": {
|
|
63
|
+
"compat": {
|
|
64
|
+
"supportsReasoningEffort": true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"parasail-glm-51": {
|
|
68
|
+
"compat": {
|
|
69
|
+
"supportsReasoningEffort": true
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"parasail-minimax-m25": {
|
|
73
|
+
"compat": {
|
|
74
|
+
"supportsReasoningEffort": true
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"parasail-qwen3-235b-a22b-instruct-2507": {
|
|
78
|
+
"compat": {
|
|
79
|
+
"supportsReasoningEffort": true
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"parasail-qwen3-vl-235b-a22b-instruct": {
|
|
83
|
+
"compat": {
|
|
84
|
+
"supportsReasoningEffort": true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Update Parasail models from API
|
|
4
|
+
*
|
|
5
|
+
* Fetches models from https://api.parasail.io/v1/models and pricing from
|
|
6
|
+
* https://www.saas.parasail.io/api/v1/prices/serverlessEndpoints, then updates:
|
|
7
|
+
* - models.json: Provider model definitions (enriched with pricing & compat)
|
|
8
|
+
* - README.md: Model table in the Available Models section
|
|
9
|
+
*
|
|
10
|
+
* The /v1/models API returns basic model info (id, object, owned_by)
|
|
11
|
+
* but does NOT include pricing, context length, or max output tokens.
|
|
12
|
+
* The pricing endpoint provides inputCost, outputCost, cachedCost, contextLength,
|
|
13
|
+
* and tags (e.g. "multimodal" for vision models).
|
|
14
|
+
*
|
|
15
|
+
* patch.json and custom-models.json are applied at runtime by the provider.
|
|
16
|
+
* They are NOT baked into models.json, but ARE used to generate the README table.
|
|
17
|
+
*
|
|
18
|
+
* Requires PARASAIL_API_KEY environment variable.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'fs';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
|
|
25
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
|
|
27
|
+
const MODELS_API_URL = 'https://api.parasail.io/v1/models';
|
|
28
|
+
const PRICING_API_URL = 'https://www.saas.parasail.io/api/v1/prices/serverlessEndpoints';
|
|
29
|
+
const MODELS_JSON_PATH = path.join(__dirname, '..', 'models.json');
|
|
30
|
+
const PATCH_JSON_PATH = path.join(__dirname, '..', 'patch.json');
|
|
31
|
+
const CUSTOM_MODELS_JSON_PATH = path.join(__dirname, '..', 'custom-models.json');
|
|
32
|
+
const README_PATH = path.join(__dirname, '..', 'README.md');
|
|
33
|
+
|
|
34
|
+
// Non-LLM model prefixes to skip (embedding, TTS, UI agent models)
|
|
35
|
+
const SKIP_PREFIXES = ['parasail-bge-', 'parasail-resemble-', 'parasail-ui-tars-'];
|
|
36
|
+
|
|
37
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function loadJson(filePath) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
42
|
+
} catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function saveJson(filePath, data) {
|
|
48
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
49
|
+
console.log(`✓ Saved ${path.basename(filePath)}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── API fetch ───────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
async function fetchModels() {
|
|
55
|
+
const apiKey = process.env.PARASAIL_API_KEY;
|
|
56
|
+
if (!apiKey) {
|
|
57
|
+
throw new Error('PARASAIL_API_KEY environment variable is required');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(`Fetching models from ${MODELS_API_URL}...`);
|
|
61
|
+
const response = await fetch(MODELS_API_URL, {
|
|
62
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const data = await response.json();
|
|
70
|
+
const models = data.data || [];
|
|
71
|
+
console.log(`✓ Fetched ${models.length} models from API`);
|
|
72
|
+
return models;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function fetchPricing() {
|
|
76
|
+
console.log(`Fetching pricing from ${PRICING_API_URL}...`);
|
|
77
|
+
const response = await fetch(PRICING_API_URL);
|
|
78
|
+
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(`Pricing API error: ${response.status} ${response.statusText}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const data = await response.json();
|
|
84
|
+
const pricing = new Map();
|
|
85
|
+
for (const entry of data) {
|
|
86
|
+
const alias = entry.externalAlias;
|
|
87
|
+
if (!alias || !alias.startsWith('parasail-')) continue;
|
|
88
|
+
if (SKIP_PREFIXES.some(prefix => alias.startsWith(prefix))) continue;
|
|
89
|
+
pricing.set(alias, {
|
|
90
|
+
contextLength: entry.contextLength || 0,
|
|
91
|
+
inputCost: entry.inputCost ?? 0,
|
|
92
|
+
outputCost: entry.outputCost ?? 0,
|
|
93
|
+
cachedCost: entry.cachedCost ?? 0,
|
|
94
|
+
tags: entry.tags || [],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
console.log(`✓ Fetched pricing for ${pricing.size} models`);
|
|
98
|
+
return pricing;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Transform API model → models.json entry ────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function transformApiModel(apiModel, existingModelsMap, pricing) {
|
|
104
|
+
const id = apiModel.id;
|
|
105
|
+
|
|
106
|
+
// Skip non-LLM models
|
|
107
|
+
if (SKIP_PREFIXES.some(prefix => id.startsWith(prefix))) return null;
|
|
108
|
+
|
|
109
|
+
// Only use parasail- prefixed IDs (cleaner aliases)
|
|
110
|
+
if (!id.startsWith('parasail-')) return null;
|
|
111
|
+
|
|
112
|
+
// Start from existing model data if we have it (preserves pricing, compat, etc.)
|
|
113
|
+
if (existingModelsMap[id]) {
|
|
114
|
+
const existing = { ...existingModelsMap[id] };
|
|
115
|
+
// Update from live pricing data
|
|
116
|
+
const p = pricing.get(id);
|
|
117
|
+
if (p) {
|
|
118
|
+
existing.cost = {
|
|
119
|
+
input: p.inputCost,
|
|
120
|
+
output: p.outputCost,
|
|
121
|
+
cacheRead: p.cachedCost,
|
|
122
|
+
cacheWrite: 0,
|
|
123
|
+
};
|
|
124
|
+
if (p.contextLength) {
|
|
125
|
+
existing.contextWindow = p.contextLength;
|
|
126
|
+
}
|
|
127
|
+
if (p.tags.includes('multimodal') && !existing.input.includes('image')) {
|
|
128
|
+
existing.input = [...existing.input, 'image'];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return existing;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// New model — build from pricing data + sensible defaults
|
|
135
|
+
// models.json is the source of truth for curated specs (reasoning, thinkingFormat, etc.)
|
|
136
|
+
// New models get defaults here; curate models.json manually after discovery.
|
|
137
|
+
const p = pricing.get(id);
|
|
138
|
+
const input = p?.tags?.includes('multimodal') ? ['text', 'image'] : ['text'];
|
|
139
|
+
|
|
140
|
+
const model = {
|
|
141
|
+
id,
|
|
142
|
+
name: generateDisplayName(id),
|
|
143
|
+
reasoning: false,
|
|
144
|
+
input,
|
|
145
|
+
cost: {
|
|
146
|
+
input: p?.inputCost ?? 0,
|
|
147
|
+
output: p?.outputCost ?? 0,
|
|
148
|
+
cacheRead: p?.cachedCost ?? 0,
|
|
149
|
+
cacheWrite: 0,
|
|
150
|
+
},
|
|
151
|
+
contextWindow: p?.contextLength || 131_072,
|
|
152
|
+
maxTokens: 16_384,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Default compat settings (can be refined in models.json or patch.json)
|
|
156
|
+
model.compat = {
|
|
157
|
+
maxTokensField: 'max_completion_tokens',
|
|
158
|
+
supportsDeveloperRole: false,
|
|
159
|
+
supportsStore: false,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return model;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function generateDisplayName(id) {
|
|
166
|
+
const raw = id.replace(/^parasail-/, '');
|
|
167
|
+
return raw
|
|
168
|
+
.replace(/[-_]/g, ' ')
|
|
169
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─── Patch & Custom Models ──────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
function applyPatch(model, patch) {
|
|
175
|
+
const result = { ...model };
|
|
176
|
+
if (patch.name !== undefined) result.name = patch.name;
|
|
177
|
+
if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
|
|
178
|
+
if (patch.input !== undefined) result.input = patch.input;
|
|
179
|
+
if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
|
|
180
|
+
if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
|
|
181
|
+
if (patch.cost) {
|
|
182
|
+
result.cost = {
|
|
183
|
+
input: patch.cost.input ?? result.cost.input,
|
|
184
|
+
output: patch.cost.output ?? result.cost.output,
|
|
185
|
+
cacheRead: patch.cost.cacheRead ?? result.cost.cacheRead,
|
|
186
|
+
cacheWrite: patch.cost.cacheWrite ?? result.cost.cacheWrite,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (patch.compat) {
|
|
190
|
+
result.compat = { ...(result.compat || {}), ...patch.compat };
|
|
191
|
+
}
|
|
192
|
+
if (!result.reasoning && result.compat?.thinkingFormat) {
|
|
193
|
+
delete result.compat.thinkingFormat;
|
|
194
|
+
}
|
|
195
|
+
if (result.compat && Object.keys(result.compat).length === 0) {
|
|
196
|
+
delete result.compat;
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildModels(baseModels, customModels, patchData) {
|
|
202
|
+
const modelMap = new Map();
|
|
203
|
+
for (const model of baseModels) {
|
|
204
|
+
modelMap.set(model.id, model);
|
|
205
|
+
}
|
|
206
|
+
for (const [id, patchEntry] of Object.entries(patchData)) {
|
|
207
|
+
const existing = modelMap.get(id);
|
|
208
|
+
if (existing) {
|
|
209
|
+
modelMap.set(id, applyPatch(existing, patchEntry));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const model of customModels) {
|
|
213
|
+
const existing = modelMap.get(model.id);
|
|
214
|
+
const patchEntry = patchData[model.id];
|
|
215
|
+
if (existing && patchEntry) {
|
|
216
|
+
modelMap.set(model.id, applyPatch(model, patchEntry));
|
|
217
|
+
} else if (existing) {
|
|
218
|
+
modelMap.set(model.id, model);
|
|
219
|
+
} else if (patchEntry) {
|
|
220
|
+
modelMap.set(model.id, applyPatch(model, patchEntry));
|
|
221
|
+
} else {
|
|
222
|
+
modelMap.set(model.id, model);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return Array.from(modelMap.values());
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── README generation ──────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
function formatContext(n) {
|
|
231
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(0)}M`;
|
|
232
|
+
if (n >= 1000) return `${Math.round(n / 1000)}K`;
|
|
233
|
+
return n.toString();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function formatCost(cost) {
|
|
237
|
+
if (cost === 0) return 'Free';
|
|
238
|
+
if (cost === null || cost === undefined) return '-';
|
|
239
|
+
if (cost < 0.01) return `$${cost.toFixed(3)}`;
|
|
240
|
+
return `$${cost.toFixed(2)}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function generateReadmeTable(models) {
|
|
244
|
+
const lines = [
|
|
245
|
+
'| Model | Context | Reasoning | Input | Max Output | Input $/M | Output $/M | Cache $/M |',
|
|
246
|
+
'|-------|---------|-----------|-------|------------|-----------|------------|-----------|',
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
for (const model of models) {
|
|
250
|
+
const context = formatContext(model.contextWindow);
|
|
251
|
+
const reasoning = model.reasoning ? '✅' : '❌';
|
|
252
|
+
const input = model.input.includes('image') ? 'Text + Image' : 'Text';
|
|
253
|
+
const maxOutput = formatContext(model.maxTokens);
|
|
254
|
+
const inputCost = formatCost(model.cost.input);
|
|
255
|
+
const outputCost = formatCost(model.cost.output);
|
|
256
|
+
const cacheCost = formatCost(model.cost.cacheRead);
|
|
257
|
+
|
|
258
|
+
lines.push(`| ${model.name} | ${context} | ${reasoning} | ${input} | ${maxOutput} | ${inputCost} | ${outputCost} | ${cacheCost} |`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return lines.join('\n');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function updateReadme(models) {
|
|
265
|
+
let readme = fs.readFileSync(README_PATH, 'utf8');
|
|
266
|
+
const newTable = generateReadmeTable(models);
|
|
267
|
+
|
|
268
|
+
const tableRegex = /(## Available Models\n\n)\| Model \| Context \| Reasoning[^\n]+\|\n\|[-| ]+\|(\n\|[^\n]+\|)*\n*/;
|
|
269
|
+
|
|
270
|
+
if (tableRegex.test(readme)) {
|
|
271
|
+
readme = readme.replace(tableRegex, (match, header) => `${header}${newTable}\n\n`);
|
|
272
|
+
fs.writeFileSync(README_PATH, readme);
|
|
273
|
+
console.log('✓ Updated README.md');
|
|
274
|
+
} else {
|
|
275
|
+
console.warn('⚠ Could not find model table in "## Available Models" section');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
async function main() {
|
|
282
|
+
try {
|
|
283
|
+
const [apiModels, pricing] = await Promise.all([
|
|
284
|
+
fetchModels(),
|
|
285
|
+
fetchPricing(),
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
// Load existing models.json for compat preservation
|
|
289
|
+
const existingModels = loadJson(MODELS_JSON_PATH);
|
|
290
|
+
const existingModelsMap = {};
|
|
291
|
+
for (const m of (Array.isArray(existingModels) ? existingModels : [])) {
|
|
292
|
+
existingModelsMap[m.id] = m;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Transform API models, preserving existing data where available
|
|
296
|
+
let models = apiModels
|
|
297
|
+
.map(m => transformApiModel(m, existingModelsMap, pricing))
|
|
298
|
+
.filter(m => m !== null);
|
|
299
|
+
|
|
300
|
+
// Live API is authoritative — models absent from API are removed
|
|
301
|
+
// (embedded data is already used for enrichment in transformApiModel)
|
|
302
|
+
|
|
303
|
+
// Sort: reasoning models first, then by context window (descending), then name
|
|
304
|
+
models.sort((a, b) => {
|
|
305
|
+
if (a.reasoning !== b.reasoning) return b.reasoning - a.reasoning;
|
|
306
|
+
if (b.contextWindow !== a.contextWindow) return b.contextWindow - a.contextWindow;
|
|
307
|
+
return a.name.localeCompare(b.name);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Save models.json (pure API output, no patch/custom baked in)
|
|
311
|
+
saveJson(MODELS_JSON_PATH, models);
|
|
312
|
+
|
|
313
|
+
// Build full model list for README: base → patch → custom
|
|
314
|
+
const patchData = loadJson(PATCH_JSON_PATH);
|
|
315
|
+
const customModels = loadJson(CUSTOM_MODELS_JSON_PATH);
|
|
316
|
+
const readmeModels = buildModels(models, Array.isArray(customModels) ? customModels : [], patchData);
|
|
317
|
+
readmeModels.sort((a, b) => a.name.localeCompare(b.name));
|
|
318
|
+
|
|
319
|
+
// Update README
|
|
320
|
+
updateReadme(readmeModels);
|
|
321
|
+
|
|
322
|
+
// Summary
|
|
323
|
+
const newIds = new Set(models.map(m => m.id));
|
|
324
|
+
const oldIds = new Set(Object.keys(existingModelsMap));
|
|
325
|
+
const added = [...newIds].filter(id => !oldIds.has(id));
|
|
326
|
+
const removed = [...oldIds].filter(id => !newIds.has(id));
|
|
327
|
+
|
|
328
|
+
console.log('\n--- Summary ---');
|
|
329
|
+
console.log(`Total models: ${models.length}`);
|
|
330
|
+
console.log(`Reasoning models: ${models.filter(m => m.reasoning).length}`);
|
|
331
|
+
console.log(`Vision models: ${models.filter(m => m.input.includes('image')).length}`);
|
|
332
|
+
if (added.length > 0) console.log(`New models: ${added.join(', ')}`);
|
|
333
|
+
if (removed.length > 0) console.log(`Removed models: ${removed.join(', ')}`);
|
|
334
|
+
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error('Error:', error.message);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
main();
|