pi-fireworks-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/messenger/channels/memory.jsonl +1 -0
- package/.pi/messenger/session-id +1 -0
- package/AGENTS.md +56 -0
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/custom-models.json +151 -0
- package/index.ts +429 -0
- package/models.json +424 -0
- package/package.json +33 -0
- package/patch.json +418 -0
- package/scripts/update-models.js +349 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Script to update fireworks models from the Fireworks API
|
|
5
|
+
*
|
|
6
|
+
* Uses the official Fireworks Gateway REST API to discover available models:
|
|
7
|
+
* GET /v1/accounts/fireworks/models
|
|
8
|
+
*
|
|
9
|
+
* Requires FIREWORKS_API_KEY environment variable.
|
|
10
|
+
* Usage: FIREWORKS_API_KEY=your-key node scripts/update-models.js
|
|
11
|
+
*
|
|
12
|
+
* Data flow:
|
|
13
|
+
* models.json → auto-generated from Fireworks API (model discovery)
|
|
14
|
+
* patch.json → manual overrides (pricing, reasoning, limits, etc.)
|
|
15
|
+
* custom-models.json → hidden/router models not in the API
|
|
16
|
+
*
|
|
17
|
+
* The API provides: id, displayName, contextLength, supportsImageInput,
|
|
18
|
+
* supportsTools, supportsServerless, state, kind, moe, parameterCount, etc.
|
|
19
|
+
*
|
|
20
|
+
* It does NOT provide: pricing, max output tokens, reasoning mode, or
|
|
21
|
+
* interleaved thinking details. Those come from patch.json.
|
|
22
|
+
*
|
|
23
|
+
* Merge order for README: models.json → apply patch.json → merge custom-models.json
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import https from 'https';
|
|
27
|
+
import fs from 'fs';
|
|
28
|
+
import path from 'path';
|
|
29
|
+
import { fileURLToPath } from 'url';
|
|
30
|
+
|
|
31
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
32
|
+
const __dirname = path.dirname(__filename);
|
|
33
|
+
|
|
34
|
+
const FIREWORKS_API_BASE = 'https://api.fireworks.ai';
|
|
35
|
+
const ACCOUNT_ID = 'fireworks';
|
|
36
|
+
const MODELS_PATH = path.join(process.cwd(), 'models.json');
|
|
37
|
+
const CUSTOM_MODELS_PATH = path.join(process.cwd(), 'custom-models.json');
|
|
38
|
+
const PATCH_PATH = path.join(process.cwd(), 'patch.json');
|
|
39
|
+
|
|
40
|
+
// ─── HTTP helpers ───────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function fetchJSON(url, headers = {}) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const req = https.get(url, { headers }, (res) => {
|
|
45
|
+
let data = '';
|
|
46
|
+
res.on('data', (chunk) => (data += chunk));
|
|
47
|
+
res.on('end', () => {
|
|
48
|
+
try {
|
|
49
|
+
resolve(JSON.parse(data));
|
|
50
|
+
} catch (e) {
|
|
51
|
+
reject(new Error(`Failed to parse JSON from ${url}: ${e.message}`));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
req.on('error', reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Paginate through the Fireworks account models API.
|
|
61
|
+
* Returns all models across all pages.
|
|
62
|
+
*/
|
|
63
|
+
async function fetchAllFireworksModels(apiKey) {
|
|
64
|
+
const headers = {};
|
|
65
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
66
|
+
|
|
67
|
+
const allModels = [];
|
|
68
|
+
let pageToken = undefined;
|
|
69
|
+
let page = 0;
|
|
70
|
+
|
|
71
|
+
do {
|
|
72
|
+
let url = `${FIREWORKS_API_BASE}/v1/accounts/${ACCOUNT_ID}/models?pageSize=200`;
|
|
73
|
+
if (pageToken) url += `&pageToken=${pageToken}`;
|
|
74
|
+
|
|
75
|
+
const data = await fetchJSON(url, headers);
|
|
76
|
+
const models = data.models || [];
|
|
77
|
+
allModels.push(...models);
|
|
78
|
+
|
|
79
|
+
pageToken = data.nextPageToken || undefined;
|
|
80
|
+
page++;
|
|
81
|
+
console.log(` Page ${page}: fetched ${models.length} models (total so far: ${allModels.length})`);
|
|
82
|
+
} while (pageToken);
|
|
83
|
+
|
|
84
|
+
return allModels;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── File I/O ───────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
function loadJSON(filePath) {
|
|
90
|
+
try {
|
|
91
|
+
if (!fs.existsSync(filePath)) return [];
|
|
92
|
+
const data = fs.readFileSync(filePath, 'utf8');
|
|
93
|
+
const parsed = JSON.parse(data);
|
|
94
|
+
console.log(`✓ Loaded ${Array.isArray(parsed) ? parsed.length : Object.keys(parsed).length} entries from ${path.basename(filePath)}`);
|
|
95
|
+
return parsed;
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.warn(`Warning: Could not load ${path.basename(filePath)}: ${e.message}`);
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function saveJSON(filePath, data) {
|
|
103
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
104
|
+
const count = Array.isArray(data) ? data.length : Object.keys(data).length;
|
|
105
|
+
console.log(`✓ Saved ${count} entries to ${path.basename(filePath)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Model filtering & mapping ──────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Filter: only serverless chat-capable LLM base models that are READY.
|
|
112
|
+
*
|
|
113
|
+
* We only include models that are serverless (pay-per-token) because those
|
|
114
|
+
* are the ones relevant for the pi provider. Non-serverless models can only
|
|
115
|
+
* be used via on-demand deployments, which isn't what this provider targets.
|
|
116
|
+
*
|
|
117
|
+
* Exceptions: models present in the existing models.json are kept even if
|
|
118
|
+
* they lose serverless status (they may still work via routers/firepass).
|
|
119
|
+
*/
|
|
120
|
+
function isRelevantModel(m, existingIds = new Set()) {
|
|
121
|
+
const kind = m.kind || '';
|
|
122
|
+
// Only HuggingFace base models
|
|
123
|
+
if (kind !== 'HF_BASE_MODEL') return false;
|
|
124
|
+
// Must be READY
|
|
125
|
+
if (m.state !== 'READY') return false;
|
|
126
|
+
// Must have a context length
|
|
127
|
+
if (!m.contextLength || m.contextLength === 0) return false;
|
|
128
|
+
// Must be serverless, OR already exist in our curated list
|
|
129
|
+
if (!m.supportsServerless && !existingIds.has(m.name)) return false;
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Build a display name from the API displayName, falling back to the model id.
|
|
135
|
+
*/
|
|
136
|
+
function buildDisplayName(m) {
|
|
137
|
+
let name = m.displayName || m.name || '';
|
|
138
|
+
if (!name || name === m.name) {
|
|
139
|
+
name = m.name.split('/').pop() || m.name;
|
|
140
|
+
}
|
|
141
|
+
return name;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Convert a Fireworks API model to Pi-native models.json format.
|
|
146
|
+
* Only includes data the API provides — no pricing, reasoning, or output limits.
|
|
147
|
+
* Those come from patch.json.
|
|
148
|
+
*/
|
|
149
|
+
function convertModel(apiModel) {
|
|
150
|
+
const id = apiModel.name;
|
|
151
|
+
const name = buildDisplayName(apiModel);
|
|
152
|
+
const input = ['text'];
|
|
153
|
+
if (apiModel.supportsImageInput) input.push('image');
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
id,
|
|
157
|
+
name,
|
|
158
|
+
reasoning: false,
|
|
159
|
+
input,
|
|
160
|
+
cost: {
|
|
161
|
+
input: 0,
|
|
162
|
+
output: 0,
|
|
163
|
+
cacheRead: 0,
|
|
164
|
+
cacheWrite: 0,
|
|
165
|
+
},
|
|
166
|
+
contextWindow: apiModel.contextLength || 0,
|
|
167
|
+
maxTokens: 0,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Deep merge a patch into a model. Nested objects (cost) are merged
|
|
173
|
+
* field-by-field; scalar fields are replaced.
|
|
174
|
+
*/
|
|
175
|
+
function applyPatch(model, patch) {
|
|
176
|
+
const result = { ...model };
|
|
177
|
+
|
|
178
|
+
if (patch.name !== undefined) result.name = patch.name;
|
|
179
|
+
if (patch.family !== undefined) result.family = patch.family;
|
|
180
|
+
if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
|
|
181
|
+
if (patch.interleaved !== undefined) result.interleaved = patch.interleaved;
|
|
182
|
+
|
|
183
|
+
if (patch.input !== undefined) result.input = patch.input;
|
|
184
|
+
if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
|
|
185
|
+
if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
|
|
186
|
+
|
|
187
|
+
if (patch.cost) {
|
|
188
|
+
result.cost = {
|
|
189
|
+
input: patch.cost.input ?? result.cost?.input ?? 0,
|
|
190
|
+
output: patch.cost.output ?? result.cost?.output ?? 0,
|
|
191
|
+
cacheRead: patch.cost.cacheRead ?? result.cost?.cacheRead ?? 0,
|
|
192
|
+
cacheWrite: patch.cost.cacheWrite ?? result.cost?.cacheWrite ?? 0,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── README generation ──────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
function formatCost(cost) {
|
|
202
|
+
if (cost === null || cost === undefined) return '-';
|
|
203
|
+
if (cost === 0) return 'Free';
|
|
204
|
+
return `$${cost.toFixed(2)}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function formatNumber(num) {
|
|
208
|
+
if (num === null || num === undefined) return '-';
|
|
209
|
+
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
|
|
210
|
+
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`;
|
|
211
|
+
return num.toString();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function getInputTypes(inputTypes) {
|
|
215
|
+
const types = inputTypes || ['text'];
|
|
216
|
+
const hasImage = types.includes('image');
|
|
217
|
+
const hasText = types.includes('text');
|
|
218
|
+
if (hasImage && hasText) return 'Text + Image';
|
|
219
|
+
if (hasImage) return 'Image';
|
|
220
|
+
return 'Text';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function generateReadmeRow(model) {
|
|
224
|
+
const cost = model.cost || {};
|
|
225
|
+
return `| ${model.name} | ${getInputTypes(model.input)} | ${formatNumber(model.contextWindow)} | ${formatNumber(model.maxTokens)} | ${formatCost(cost.input)} | ${formatCost(cost.output)} |`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function updateReadme(models) {
|
|
229
|
+
const readmePath = path.join(process.cwd(), 'README.md');
|
|
230
|
+
let readme = fs.readFileSync(readmePath, 'utf8');
|
|
231
|
+
|
|
232
|
+
const sortedModels = [...models].sort((a, b) => {
|
|
233
|
+
const familyA = a.family || '';
|
|
234
|
+
const familyB = b.family || '';
|
|
235
|
+
if (familyA !== familyB) return familyA.localeCompare(familyB);
|
|
236
|
+
return a.name.localeCompare(b.name);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const tableRows = sortedModels.map(generateReadmeRow).join('\n');
|
|
240
|
+
const newTable = `| Model | Type | Context | Max Tokens | Input Cost | Output Cost |
|
|
241
|
+
|-------|------|---------|------------|------------|-------------|
|
|
242
|
+
${tableRows}`;
|
|
243
|
+
|
|
244
|
+
const tableRegex = /\| Model \| Type \| Context \| Max Tokens \| Input Cost \| Output Cost \|[\s\S]*?(?=\n\*Costs are per million)/;
|
|
245
|
+
readme = readme.replace(tableRegex, newTable);
|
|
246
|
+
|
|
247
|
+
readme = readme.replace(/\*\*\d+\+ AI Models\*\*/, `**${models.length}+ AI Models**`);
|
|
248
|
+
|
|
249
|
+
fs.writeFileSync(readmePath, readme);
|
|
250
|
+
console.log(`✓ Updated README.md with ${models.length} models`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
async function main() {
|
|
256
|
+
const apiKey = process.env.FIREWORKS_API_KEY;
|
|
257
|
+
if (!apiKey) {
|
|
258
|
+
console.error('Error: FIREWORKS_API_KEY environment variable is required');
|
|
259
|
+
console.error('Usage: FIREWORKS_API_KEY=your-key node scripts/update-models.js');
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
console.log('Fetching models from Fireworks API...\n');
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
// 1. Fetch all models from Fireworks API
|
|
267
|
+
const apiModels = await fetchAllFireworksModels(apiKey);
|
|
268
|
+
console.log(`\nTotal models from API: ${apiModels.length}`);
|
|
269
|
+
|
|
270
|
+
// 2. Load existing models.json and patch.json
|
|
271
|
+
const existingModels = Array.isArray(loadJSON(MODELS_PATH)) ? loadJSON(MODELS_PATH) : [];
|
|
272
|
+
const patchData = loadJSON(PATCH_PATH);
|
|
273
|
+
const existingIds = new Set(existingModels.map((m) => m.id));
|
|
274
|
+
|
|
275
|
+
// 3. Filter to relevant LLMs (serverless + previously curated)
|
|
276
|
+
const relevantApiModels = apiModels.filter((m) => isRelevantModel(m, existingIds));
|
|
277
|
+
console.log(`Relevant LLM models: ${relevantApiModels.length}`);
|
|
278
|
+
|
|
279
|
+
// 4. Convert API models to models.json format (no pricing — that comes from patch.json)
|
|
280
|
+
const newModels = relevantApiModels.map((apiModel) => convertModel(apiModel));
|
|
281
|
+
|
|
282
|
+
// Log new models (not in patch.json)
|
|
283
|
+
for (const m of newModels) {
|
|
284
|
+
if (!patchData[m.id]) {
|
|
285
|
+
console.log(` 🆕 New model: ${m.id} (${m.name}) — add to patch.json for pricing/output limits`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Live API is authoritative — models absent from API are removed
|
|
290
|
+
const allUpstreamModels = [...newModels];
|
|
291
|
+
|
|
292
|
+
// 5. Save upstream models (API-derived, no pricing)
|
|
293
|
+
saveJSON(MODELS_PATH, allUpstreamModels);
|
|
294
|
+
|
|
295
|
+
// 6. Load and process custom models
|
|
296
|
+
const customModels = Array.isArray(loadJSON(CUSTOM_MODELS_PATH)) ? loadJSON(CUSTOM_MODELS_PATH) : [];
|
|
297
|
+
|
|
298
|
+
// Find custom models that now appear in upstream (remove from custom)
|
|
299
|
+
const upstreamIds = new Set(allUpstreamModels.map((m) => m.id));
|
|
300
|
+
const duplicates = customModels.filter((m) => upstreamIds.has(m.id));
|
|
301
|
+
if (duplicates.length > 0) {
|
|
302
|
+
console.log(`\nFound ${duplicates.length} custom model(s) now available upstream:`);
|
|
303
|
+
for (const dup of duplicates) {
|
|
304
|
+
console.log(` - ${dup.id} (${dup.name})`);
|
|
305
|
+
}
|
|
306
|
+
const cleaned = customModels.filter((m) => !upstreamIds.has(m.id));
|
|
307
|
+
saveJSON(CUSTOM_MODELS_PATH, cleaned);
|
|
308
|
+
console.log(`✓ Removed ${duplicates.length} duplicate(s) from custom-models.json`);
|
|
309
|
+
customModels.length = 0;
|
|
310
|
+
customModels.push(...cleaned);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 7. Build merged models with patches applied (for README)
|
|
314
|
+
const mergedMap = new Map();
|
|
315
|
+
|
|
316
|
+
// Start with upstream models
|
|
317
|
+
for (const m of allUpstreamModels) mergedMap.set(m.id, m);
|
|
318
|
+
|
|
319
|
+
// Apply patches (enrichment: pricing, reasoning, limits, etc.)
|
|
320
|
+
for (const [id, patch] of Object.entries(patchData)) {
|
|
321
|
+
const existing = mergedMap.get(id);
|
|
322
|
+
if (existing) {
|
|
323
|
+
mergedMap.set(id, applyPatch(existing, patch));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Add/override with custom models, also applying their patches
|
|
328
|
+
for (const m of customModels) {
|
|
329
|
+
const patch = patchData[m.id];
|
|
330
|
+
mergedMap.set(m.id, patch ? applyPatch(m, patch) : m);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const allModels = Array.from(mergedMap.values());
|
|
334
|
+
|
|
335
|
+
console.log(
|
|
336
|
+
`\nTotal: ${allModels.length} models (${allUpstreamModels.length} upstream + ${customModels.length} custom, ${Object.keys(patchData).length} patches)`
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// 8. Update README
|
|
340
|
+
updateReadme(allModels);
|
|
341
|
+
|
|
342
|
+
console.log('\nDone!');
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error('Error:', error.message);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
main();
|