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/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();