pi-wafer-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.
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Update Wafer models from API
4
+ *
5
+ * Fetches models from https://pass.wafer.ai/v1/models and updates:
6
+ * - models.json: Provider model definitions (enriched with pricing & compat)
7
+ * - README.md: Model table in the Available Models section
8
+ *
9
+ * The Wafer /v1/models API returns basic model info (id, max_model_len)
10
+ * but does NOT include pricing or max output tokens.
11
+ * models.json is the source of truth for curated specs — the script preserves
12
+ * existing data and only adds new models with sensible defaults.
13
+ * Curate models.json manually after new model discovery.
14
+ *
15
+ * patch.json is applied at runtime by the provider — not baked into models.json.
16
+ *
17
+ * Requires WAFER_SERVERLESS_API_KEY (or WAFER_API_KEY) environment variable.
18
+ */
19
+
20
+ import fs from 'fs';
21
+ import path from 'path';
22
+ import { fileURLToPath } from 'url';
23
+
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+
26
+ const MODELS_API_URL = 'https://pass.wafer.ai/v1/models';
27
+ const MODELS_JSON_PATH = path.join(__dirname, '..', 'models.json');
28
+ const PATCH_JSON_PATH = path.join(__dirname, '..', 'patch.json');
29
+ const CUSTOM_MODELS_JSON_PATH = path.join(__dirname, '..', 'custom-models.json');
30
+ const README_PATH = path.join(__dirname, '..', 'README.md');
31
+
32
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
33
+
34
+ function loadJson(filePath) {
35
+ try {
36
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
37
+ } catch {
38
+ return {};
39
+ }
40
+ }
41
+
42
+ function saveJson(filePath, data) {
43
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
44
+ console.log(`✓ Saved ${path.basename(filePath)}`);
45
+ }
46
+
47
+ // ─── API fetch ───────────────────────────────────────────────────────────────
48
+
49
+ async function fetchModels() {
50
+ const apiKey = process.env.WAFER_SERVERLESS_API_KEY || process.env.WAFER_API_KEY;
51
+ if (!apiKey) {
52
+ throw new Error('WAFER_SERVERLESS_API_KEY (or WAFER_API_KEY) environment variable is required');
53
+ }
54
+
55
+ console.log(`Fetching models from ${MODELS_API_URL}...`);
56
+ const response = await fetch(MODELS_API_URL, {
57
+ headers: { 'Authorization': `Bearer ${apiKey}` },
58
+ });
59
+
60
+ if (!response.ok) {
61
+ throw new Error(`API error: ${response.status} ${response.statusText}`);
62
+ }
63
+
64
+ const data = await response.json();
65
+ const models = Array.isArray(data) ? data : (data.data || []);
66
+ console.log(`✓ Fetched ${models.length} models from API`);
67
+ return models;
68
+ }
69
+
70
+ // ─── Transform API model → models.json entry ────────────────────────────────
71
+
72
+ function transformApiModel(apiModel, existingModelsMap) {
73
+ const id = apiModel.id;
74
+
75
+ // Preserve existing curated data (pricing, reasoning, compat, etc.)
76
+ if (existingModelsMap[id]) {
77
+ const existing = { ...existingModelsMap[id] };
78
+ // Update API-derived fields if changed
79
+ if (apiModel.max_model_len) {
80
+ existing.contextWindow = apiModel.max_model_len;
81
+ }
82
+ if (apiModel.zdr_supported !== undefined) {
83
+ existing.compat = existing.compat || {};
84
+ existing.compat.supportsZdr = apiModel.zdr_supported;
85
+ }
86
+ return existing;
87
+ }
88
+
89
+ // New model — sensible defaults; curate models.json manually after discovery
90
+ const model = {
91
+ id,
92
+ name: generateDisplayName(id),
93
+ reasoning: false,
94
+ input: ['text'],
95
+ cost: {
96
+ input: 0,
97
+ output: 0,
98
+ cacheRead: 0,
99
+ cacheWrite: 0,
100
+ },
101
+ contextWindow: apiModel.max_model_len || 131072,
102
+ maxTokens: 16384,
103
+ compat: {
104
+ maxTokensField: 'max_completion_tokens',
105
+ supportsDeveloperRole: false,
106
+ supportsStore: false,
107
+ supportsZdr: apiModel.zdr_supported,
108
+ },
109
+ };
110
+
111
+ return model;
112
+ }
113
+
114
+ function generateDisplayName(id) {
115
+ // Fallback: prettify the ID
116
+ return id
117
+ .replace(/[-_]/g, ' ')
118
+ .replace(/\b\w/g, c => c.toUpperCase());
119
+ }
120
+
121
+ // ─── Patch & Custom Models ──────────────────────────────────────────────────
122
+
123
+ function applyPatch(model, patch) {
124
+ const result = { ...model };
125
+ if (patch.name !== undefined) result.name = patch.name;
126
+ if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
127
+ if (patch.input !== undefined) result.input = patch.input;
128
+ if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
129
+ if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
130
+ if (patch.cost) {
131
+ result.cost = {
132
+ input: patch.cost.input ?? result.cost.input,
133
+ output: patch.cost.output ?? result.cost.output,
134
+ cacheRead: patch.cost.cacheRead ?? result.cost.cacheRead,
135
+ cacheWrite: patch.cost.cacheWrite ?? result.cost.cacheWrite,
136
+ };
137
+ }
138
+ if (patch.compat) {
139
+ result.compat = { ...(result.compat || {}), ...patch.compat };
140
+ }
141
+ if (patch.thinkingLevelMap !== undefined) result.thinkingLevelMap = patch.thinkingLevelMap;
142
+ if (!result.reasoning && result.compat?.thinkingFormat) {
143
+ delete result.compat.thinkingFormat;
144
+ }
145
+ if (result.compat && Object.keys(result.compat).length === 0) {
146
+ delete result.compat;
147
+ }
148
+ return result;
149
+ }
150
+
151
+ function buildModels(baseModels, customModels, patchData) {
152
+ const modelMap = new Map();
153
+ for (const model of baseModels) {
154
+ modelMap.set(model.id, model);
155
+ }
156
+ for (const [id, patchEntry] of Object.entries(patchData)) {
157
+ const existing = modelMap.get(id);
158
+ if (existing) {
159
+ modelMap.set(id, applyPatch(existing, patchEntry));
160
+ }
161
+ }
162
+ for (const model of customModels) {
163
+ const existing = modelMap.get(model.id);
164
+ const patchEntry = patchData[model.id];
165
+ if (existing && patchEntry) {
166
+ modelMap.set(model.id, applyPatch(model, patchEntry));
167
+ } else if (existing) {
168
+ modelMap.set(model.id, model);
169
+ } else if (patchEntry) {
170
+ modelMap.set(model.id, applyPatch(model, patchEntry));
171
+ } else {
172
+ modelMap.set(model.id, model);
173
+ }
174
+ }
175
+ return Array.from(modelMap.values());
176
+ }
177
+
178
+ // ─── README generation ──────────────────────────────────────────────────────
179
+
180
+ function formatContext(n) {
181
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(0)}M`;
182
+ if (n >= 1000) return `${Math.round(n / 1000)}K`;
183
+ return n.toString();
184
+ }
185
+
186
+ function formatCost(cost) {
187
+ if (cost === 0) return 'Free';
188
+ if (cost === null || cost === undefined) return '-';
189
+ return `$${cost.toFixed(2)}`;
190
+ }
191
+
192
+ function generateReadmeTable(models) {
193
+ const lines = [
194
+ '| Model | Type | Context | Max Output | Input Cost | Output Cost | Cached Input |',
195
+ '|-------|------|---------|------------|------------|-------------|--------------|',
196
+ ];
197
+
198
+ for (const model of models) {
199
+ const type = model.input.includes('image') ? 'Text + Image' : 'Text';
200
+ const context = formatContext(model.contextWindow);
201
+ const maxOutput = formatContext(model.maxTokens);
202
+ const inputCost = formatCost(model.cost.input);
203
+ const outputCost = formatCost(model.cost.output);
204
+ const cacheCost = formatCost(model.cost.cacheRead);
205
+
206
+ lines.push(`| ${model.name} | ${type} | ${context} | ${maxOutput} | ${inputCost} | ${outputCost} | ${cacheCost} |`);
207
+ }
208
+
209
+ return lines.join('\n');
210
+ }
211
+
212
+ function updateReadme(models) {
213
+ let readme = fs.readFileSync(README_PATH, 'utf8');
214
+ const newTable = generateReadmeTable(models);
215
+
216
+ const tableRegex = /(## Available Models\n\n)\| Model \| Type \| Context[^\n]+\|\n\|[-| ]+\|(\n\|[^\n]+\|)*\n*/;
217
+
218
+ if (tableRegex.test(readme)) {
219
+ readme = readme.replace(tableRegex, (match, header) => `${header}${newTable}\n\n`);
220
+ fs.writeFileSync(README_PATH, readme);
221
+ console.log('✓ Updated README.md');
222
+ } else {
223
+ console.warn('⚠ Could not find model table in "## Available Models" section');
224
+ }
225
+ }
226
+
227
+ // ─── Main ────────────────────────────────────────────────────────────────────
228
+
229
+ async function main() {
230
+ try {
231
+ const apiModels = await fetchModels();
232
+
233
+ // Load existing models.json — source of truth for curated specs
234
+ const existingModels = loadJson(MODELS_JSON_PATH);
235
+ const existingModelsMap = {};
236
+ for (const m of (Array.isArray(existingModels) ? existingModels : [])) {
237
+ existingModelsMap[m.id] = m;
238
+ }
239
+
240
+ // Transform API models, preserving existing data where available
241
+ let models = apiModels.map(m =>
242
+ transformApiModel(m, existingModelsMap)
243
+ );
244
+
245
+ // Live API is authoritative — models absent from API are removed
246
+ // (embedded data is already used for enrichment in transformApiModel)
247
+
248
+ // Sort by model name
249
+ models.sort((a, b) => a.name.localeCompare(b.name));
250
+
251
+ // Save models.json (pure API output, no patch/custom baked in)
252
+ saveJson(MODELS_JSON_PATH, models);
253
+
254
+ // Build full model list for README: base → patch → custom
255
+ const patchData = loadJson(PATCH_JSON_PATH);
256
+ const customModels = loadJson(CUSTOM_MODELS_JSON_PATH);
257
+ const readmeModels = buildModels(models, Array.isArray(customModels) ? customModels : [], patchData);
258
+ readmeModels.sort((a, b) => a.name.localeCompare(b.name));
259
+
260
+ // Update README
261
+ updateReadme(readmeModels);
262
+
263
+ // Summary
264
+ const newIds = new Set(models.map(m => m.id));
265
+ const oldIds = new Set(Object.keys(existingModelsMap));
266
+ const added = [...newIds].filter(id => !oldIds.has(id));
267
+ const removed = [...oldIds].filter(id => !newIds.has(id));
268
+
269
+ console.log('\n--- Summary ---');
270
+ console.log(`Total models: ${models.length}`);
271
+ console.log(`Vision models: ${models.filter(m => m.input.includes('image')).length}`);
272
+ if (added.length > 0) console.log(`New models: ${added.join(', ')} — curate models.json manually`);
273
+ if (removed.length > 0) console.log(`Removed models: ${removed.join(', ')}`);
274
+
275
+ } catch (error) {
276
+ console.error('Error:', error.message);
277
+ process.exit(1);
278
+ }
279
+ }
280
+
281
+ main();