opencode-pollinations-plugin 6.1.0-beta.8 → 6.2.0

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.
Files changed (109) hide show
  1. package/README.de.md +130 -0
  2. package/README.es.md +130 -0
  3. package/README.fr.md +130 -0
  4. package/README.it.md +130 -0
  5. package/README.md +87 -73
  6. package/dist/index.js +52 -161
  7. package/dist/locales/de.json +374 -0
  8. package/dist/locales/en.json +373 -0
  9. package/dist/locales/es.json +374 -0
  10. package/dist/locales/fr.json +373 -0
  11. package/dist/locales/index.d.ts +1 -0
  12. package/dist/locales/index.js +37 -0
  13. package/dist/locales/it.json +374 -0
  14. package/dist/server/commands.d.ts +6 -0
  15. package/dist/server/commands.js +394 -125
  16. package/dist/server/config.d.ts +34 -23
  17. package/dist/server/config.js +200 -108
  18. package/dist/server/connect-response.d.ts +2 -0
  19. package/dist/server/connect-response.js +59 -0
  20. package/dist/server/generate-config.d.ts +3 -30
  21. package/dist/server/generate-config.js +164 -106
  22. package/dist/server/index.d.ts +2 -1
  23. package/dist/server/index.js +124 -149
  24. package/dist/server/logger.d.ts +8 -0
  25. package/dist/server/logger.js +38 -0
  26. package/dist/server/models/cache.d.ts +35 -0
  27. package/dist/server/models/cache.js +160 -0
  28. package/dist/server/models/fetcher.d.ts +18 -0
  29. package/dist/server/models/fetcher.js +194 -0
  30. package/dist/server/models/index.d.ts +6 -0
  31. package/dist/server/models/index.js +5 -0
  32. package/dist/server/models/manual.d.ts +15 -0
  33. package/dist/server/models/manual.js +92 -0
  34. package/dist/server/models/types.d.ts +55 -0
  35. package/dist/server/models/types.js +7 -0
  36. package/dist/server/models/worker.d.ts +22 -0
  37. package/dist/server/models/worker.js +174 -0
  38. package/dist/server/pollinations-api.d.ts +11 -0
  39. package/dist/server/pollinations-api.js +21 -8
  40. package/dist/server/proxy.js +222 -293
  41. package/dist/server/quota.d.ts +2 -0
  42. package/dist/server/quota.js +89 -86
  43. package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
  44. package/dist/server/scripts/pollinations_pricing.js +246 -0
  45. package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
  46. package/dist/server/scripts/test_cost_endpoints.js +61 -0
  47. package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
  48. package/dist/server/scripts/test_dynamic_pricing.js +39 -0
  49. package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
  50. package/dist/server/scripts/test_freetier_audit.js +215 -0
  51. package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
  52. package/dist/server/scripts/test_parallel_cost.js +104 -0
  53. package/dist/server/toast.d.ts +7 -1
  54. package/dist/server/toast.js +43 -10
  55. package/dist/tools/design/gen_diagram.d.ts +2 -0
  56. package/dist/tools/design/gen_diagram.js +94 -0
  57. package/dist/tools/design/gen_palette.d.ts +2 -0
  58. package/dist/tools/design/gen_palette.js +182 -0
  59. package/dist/tools/design/gen_qrcode.d.ts +2 -0
  60. package/dist/tools/design/gen_qrcode.js +50 -0
  61. package/dist/tools/ffmpeg.d.ts +24 -0
  62. package/dist/tools/ffmpeg.js +54 -0
  63. package/dist/tools/index.d.ts +25 -0
  64. package/dist/tools/index.js +86 -0
  65. package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
  66. package/dist/tools/pollinations/beta_discovery.js +201 -0
  67. package/dist/tools/pollinations/cost-guard.d.ts +38 -0
  68. package/dist/tools/pollinations/cost-guard.js +136 -0
  69. package/dist/tools/pollinations/deepsearch.d.ts +7 -0
  70. package/dist/tools/pollinations/deepsearch.js +80 -0
  71. package/dist/tools/pollinations/gen_audio.d.ts +18 -0
  72. package/dist/tools/pollinations/gen_audio.js +220 -0
  73. package/dist/tools/pollinations/gen_image.d.ts +11 -0
  74. package/dist/tools/pollinations/gen_image.js +211 -0
  75. package/dist/tools/pollinations/gen_music.d.ts +14 -0
  76. package/dist/tools/pollinations/gen_music.js +157 -0
  77. package/dist/tools/pollinations/gen_video.d.ts +16 -0
  78. package/dist/tools/pollinations/gen_video.js +249 -0
  79. package/dist/tools/pollinations/polli_config.d.ts +2 -0
  80. package/dist/tools/pollinations/polli_config.js +95 -0
  81. package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
  82. package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
  83. package/dist/tools/pollinations/polli_status.d.ts +2 -0
  84. package/dist/tools/pollinations/polli_status.js +31 -0
  85. package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
  86. package/dist/tools/pollinations/polli_web_search.js +126 -0
  87. package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
  88. package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
  89. package/dist/tools/pollinations/shared.d.ts +181 -0
  90. package/dist/tools/pollinations/shared.js +758 -0
  91. package/dist/tools/pollinations/test_estimators.d.ts +1 -0
  92. package/dist/tools/pollinations/test_estimators.js +22 -0
  93. package/dist/tools/pollinations/transcribe_audio.d.ts +13 -0
  94. package/dist/tools/pollinations/transcribe_audio.js +171 -0
  95. package/dist/tools/power/extract_audio.d.ts +2 -0
  96. package/dist/tools/power/extract_audio.js +179 -0
  97. package/dist/tools/power/extract_frames.d.ts +2 -0
  98. package/dist/tools/power/extract_frames.js +237 -0
  99. package/dist/tools/power/file_to_url.d.ts +2 -0
  100. package/dist/tools/power/file_to_url.js +217 -0
  101. package/dist/tools/power/remove_background.d.ts +2 -0
  102. package/dist/tools/power/remove_background.js +404 -0
  103. package/dist/tools/power/rmbg_keys.d.ts +2 -0
  104. package/dist/tools/power/rmbg_keys.js +79 -0
  105. package/dist/tools/shared.d.ts +30 -0
  106. package/dist/tools/shared.js +80 -0
  107. package/package.json +10 -4
  108. package/dist/server/models-seed.d.ts +0 -18
  109. package/dist/server/models-seed.js +0 -55
@@ -0,0 +1,758 @@
1
+ /**
2
+ * Shared utilities for Pollinations API tools
3
+ *
4
+ * Updated: 2026-02-18 - Sprint 2: Dynamic ModelRegistry integration
5
+ * Hardcoded model lists replaced by ModelRegistry lookups with static fallback.
6
+ */
7
+ import * as https from 'https';
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { loadConfig } from '../../server/config.js';
11
+ import { ModelRegistry } from '../../server/models/index.js';
12
+ // ─── Configuration ───────────────────────────────────────────────────────
13
+ const API_BASE = 'gen.pollinations.ai';
14
+ const FREE_IMAGE_BASE = 'image.pollinations.ai';
15
+ export function getApiKey() {
16
+ const config = loadConfig();
17
+ return config.apiKey;
18
+ }
19
+ export function hasApiKey() {
20
+ const key = getApiKey();
21
+ return !!(key && key.length > 5 && key !== 'dummy');
22
+ }
23
+ // ─── Model Data (Dynamic via ModelRegistry) ───────────────────────────────
24
+ /**
25
+ * FREE Image Models (DEPRECATED - image.pollinations.ai is dead)
26
+ */
27
+ export const FREE_IMAGE_MODELS = {};
28
+ /**
29
+ * Dynamic Paid Image Models accessor.
30
+ * Returns data from ModelRegistry if ready, otherwise falls back to static data.
31
+ *
32
+ * BACKWARD COMPATIBLE: Same shape as the old hardcoded PAID_IMAGE_MODELS
33
+ */
34
+ export function getPaidImageModels() {
35
+ if (ModelRegistry.isReady()) {
36
+ const models = ModelRegistry.list('image');
37
+ const result = {};
38
+ for (const m of models) {
39
+ const costStr = formatPricingForDisplay(m);
40
+ result[m.name] = {
41
+ desc: m.description,
42
+ cost: costStr,
43
+ t2i: true, // All image models support T2I
44
+ i2i: m.supportsI2X,
45
+ params: m.supportsI2X
46
+ ? ['width', 'height', 'image']
47
+ : ['width', 'height'],
48
+ notes: m.paid_only ? 'Paid Only' : undefined,
49
+ };
50
+ }
51
+ return result;
52
+ }
53
+ return _STATIC_PAID_IMAGE_MODELS;
54
+ }
55
+ /**
56
+ * Dynamic Video Models accessor.
57
+ * BACKWARD COMPATIBLE: Same shape as old VIDEO_MODELS
58
+ */
59
+ export function getVideoModels() {
60
+ if (ModelRegistry.isReady()) {
61
+ const models = ModelRegistry.list('video');
62
+ const result = {};
63
+ for (const m of models) {
64
+ const costStr = formatPricingForDisplay(m);
65
+ result[m.name] = {
66
+ desc: m.description,
67
+ cost: costStr,
68
+ t2v: !_STATIC_I2V_ONLY.has(m.name), // wan is I2V only
69
+ i2v: m.supportsI2X,
70
+ audio: !_STATIC_NO_AUDIO.has(m.name),
71
+ duration: m.durationRange || [1, 10],
72
+ aspectRatios: m.aspectRatios || ['16:9'],
73
+ costHeader: m.costHeader || 'x-usage-completion-video-seconds',
74
+ genTime: m.genTimeEstimate || '~30s',
75
+ };
76
+ }
77
+ return result;
78
+ }
79
+ return _STATIC_VIDEO_MODELS;
80
+ }
81
+ /**
82
+ * Dynamic Audio Models accessor.
83
+ * BACKWARD COMPATIBLE: Same shape as old AUDIO_MODELS
84
+ */
85
+ export function getAudioModels() {
86
+ if (ModelRegistry.isReady()) {
87
+ const models = ModelRegistry.list('audio');
88
+ const result = {};
89
+ for (const m of models) {
90
+ const audioType = detectAudioType(m);
91
+ result[m.name] = {
92
+ desc: m.description,
93
+ type: audioType,
94
+ endpoint: _STATIC_AUDIO_ENDPOINTS[m.name] || (audioType === 'stt' ? '/v1/audio/transcriptions' : `/audio/{text}`),
95
+ params: audioType === 'stt' ? ['file'] : ['voice', 'format'],
96
+ voices: m.voices,
97
+ notes: m.paid_only ? 'Paid Only' : undefined,
98
+ };
99
+ }
100
+ return result;
101
+ }
102
+ return _STATIC_AUDIO_MODELS;
103
+ }
104
+ /**
105
+ * Text Model accessor
106
+ * Returns text models from registry
107
+ */
108
+ export function getTextModels() {
109
+ if (ModelRegistry.isReady()) {
110
+ const models = ModelRegistry.all().filter((m) => m.category === 'text');
111
+ const result = {};
112
+ for (const m of models) {
113
+ result[m.name] = {
114
+ desc: m.description
115
+ };
116
+ }
117
+ return result;
118
+ }
119
+ return {};
120
+ }
121
+ /**
122
+ * Music Model accessor (backward compatible)
123
+ */
124
+ export function getMusicModel() {
125
+ // Check registry for elevenmusic
126
+ if (ModelRegistry.isReady()) {
127
+ const m = ModelRegistry.getByNameOrAlias('audio', 'elevenmusic');
128
+ if (m) {
129
+ return {
130
+ 'elevenmusic': {
131
+ desc: m.description,
132
+ endpoint: '/audio/{text}',
133
+ params: ['duration', 'instrumental'],
134
+ duration: [3, 300],
135
+ }
136
+ };
137
+ }
138
+ }
139
+ return _STATIC_MUSIC_MODEL;
140
+ }
141
+ // ─── Backward Compatibility ──────────────────────────────────────────────
142
+ // OLD const exports removed (caused TDZ error at module load).
143
+ // Consumers must use the function forms:
144
+ // getPaidImageModels(), getVideoModels(), getAudioModels(), getMusicModel()
145
+ // For direct model lookup: use ModelRegistry.getByNameOrAlias()
146
+ // ─── Private Static Fallback Data ─────────────────────────────────────────
147
+ // Used ONLY when ModelRegistry is not ready (startup race, offline).
148
+ const _STATIC_I2V_ONLY = new Set(); // Models that are I2V only (no T2V)
149
+ const _STATIC_NO_AUDIO = new Set(['seedance', 'seedance-pro']); // Video models without audio
150
+ const _STATIC_AUDIO_ENDPOINTS = {
151
+ 'openai-audio': '/v1/chat/completions',
152
+ 'elevenlabs': '/audio/{text}',
153
+ 'whisper': '/v1/audio/transcriptions',
154
+ 'scribe': '/v1/audio/transcriptions',
155
+ 'elevenmusic': '/audio/{text}',
156
+ };
157
+ const _STATIC_PAID_IMAGE_MODELS = {
158
+ 'flux': { desc: 'Flux Schnell', cost: '0.0002 🌻', t2i: true, i2i: false, params: ['width', 'height'] },
159
+ 'sana': { desc: 'Sana (Efficient)', cost: '0.0002 🌻', t2i: true, i2i: false, params: ['width', 'height'] },
160
+ 'zimage': { desc: 'Z-Image Turbo (6B Flux 2x)', cost: '0.0002 🌻', t2i: true, i2i: false, params: ['width', 'height'] },
161
+ 'imagen-4': { desc: 'Imagen 4 (alpha)', cost: '0.0025 🌻', t2i: true, i2i: false, params: ['width', 'height'] },
162
+ 'klein': { desc: 'FLUX.2 Klein 4B', cost: '0.008 🌻', t2i: true, i2i: true, params: ['width', 'height', 'image'] },
163
+ 'klein-large': { desc: 'FLUX.2 Klein 9B', cost: '0.012 🌻', t2i: true, i2i: true, params: ['width', 'height', 'image'] },
164
+ 'gptimage': { desc: 'GPT Image 1 Mini (OpenAI)', cost: 'tokens', t2i: true, i2i: false, params: ['width', 'height', 'quality', 'transparent'] },
165
+ 'gptimage-large': { desc: 'GPT Image 1.5 (Advanced)', cost: 'tokens', t2i: true, i2i: false, params: ['width', 'height', 'quality', 'transparent'] },
166
+ 'kontext': { desc: 'FLUX.1 Kontext', cost: '0.04 🌻 💎', t2i: true, i2i: true, params: ['width', 'height', 'image'], notes: 'In-Context Editing' },
167
+ 'seedream': { desc: 'Seedream 4.0 (ByteDance ARK)', cost: '0.03 🌻', t2i: true, i2i: true, params: ['width', 'height', 'image'] },
168
+ 'seedream-pro': { desc: 'Seedream 4.5 Pro (ARK 4K)', cost: '0.04 🌻 💎', t2i: true, i2i: true, params: ['width', 'height', 'image'], notes: '4K, Multi-Image' },
169
+ 'nanobanana': { desc: 'NanoBanana (Gemini 2.5 Flash)', cost: 'tokens', t2i: true, i2i: true, params: ['width', 'height', 'image'] },
170
+ 'nanobanana-pro': { desc: 'NanoBanana Pro (Gemini 3 Pro)', cost: 'tokens', t2i: true, i2i: true, params: ['width', 'height', 'image'], notes: 'Thinking Model' },
171
+ };
172
+ const _STATIC_VIDEO_MODELS = {
173
+ 'grok-video': {
174
+ desc: 'Grok Video (alpha)',
175
+ cost: '0.0025/sec',
176
+ t2v: true, i2v: false, audio: true,
177
+ duration: [1, 15],
178
+ aspectRatios: ['16:9', '9:16', '1:1', '4:3'],
179
+ costHeader: 'x-usage-completion-video-seconds',
180
+ genTime: '~10s'
181
+ },
182
+ 'ltx-2': {
183
+ desc: 'LTX-2 (Lightricks)',
184
+ cost: '0.01/sec',
185
+ t2v: true, i2v: false, audio: true,
186
+ duration: [5, 20],
187
+ aspectRatios: ['16:9'],
188
+ costHeader: 'x-usage-completion-video-seconds',
189
+ genTime: '~35s'
190
+ },
191
+ 'wan': {
192
+ desc: 'Wan 2.6 (Alibaba)',
193
+ cost: '0.025/sec',
194
+ t2v: false, i2v: true, audio: true,
195
+ duration: [5, 15],
196
+ aspectRatios: ['16:9', '9:16', '1:1', '4:3'],
197
+ costHeader: 'x-usage-completion-video-seconds',
198
+ genTime: '~30s'
199
+ },
200
+ 'veo': {
201
+ desc: 'Veo 3.1 Fast (Google)',
202
+ cost: '0.15/sec 💎',
203
+ t2v: true, i2v: true, audio: true,
204
+ duration: [4, 8],
205
+ aspectRatios: ['16:9', '9:16', '1:1'],
206
+ costHeader: 'x-usage-completion-video-seconds',
207
+ genTime: '~45-68s',
208
+ },
209
+ 'seedance': {
210
+ desc: 'Seedance Lite (BytePlus)',
211
+ cost: 'tokens',
212
+ t2v: true, i2v: true, audio: false,
213
+ duration: [4, 12],
214
+ aspectRatios: ['16:9', '9:16', '1:1'],
215
+ costHeader: 'x-usage-completion-video-tokens',
216
+ genTime: '~30s'
217
+ },
218
+ 'seedance-pro': {
219
+ desc: 'Seedance Pro-Fast (BytePlus)',
220
+ cost: 'tokens',
221
+ t2v: true, i2v: true, audio: false,
222
+ duration: [4, 12],
223
+ aspectRatios: ['16:9', '9:16', '1:1'],
224
+ costHeader: 'x-usage-completion-video-tokens',
225
+ genTime: '~30s'
226
+ },
227
+ };
228
+ const _STATIC_AUDIO_MODELS = {
229
+ 'openai-audio': {
230
+ desc: 'GPT-4o Audio Preview',
231
+ type: 'both',
232
+ endpoint: '/v1/chat/completions',
233
+ params: ['voice', 'format'],
234
+ voices: ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'],
235
+ notes: 'DEFAULT - least expensive'
236
+ },
237
+ 'elevenlabs': {
238
+ desc: 'ElevenLabs v3',
239
+ type: 'tts',
240
+ endpoint: '/audio/{text}',
241
+ params: ['voice', 'response_format'],
242
+ voices: ['rachel', 'domi', 'bella', 'elli', 'charlotte', 'dorothy', 'sarah', 'emily', 'lily', 'matilda', 'adam', 'antoni', 'arnold', 'josh', 'sam', 'daniel', 'charlie', 'james', 'fin', 'callum', 'liam', 'george', 'brian', 'bill', 'ash', 'ballad', 'coral', 'sage', 'verse'],
243
+ },
244
+ 'whisper': {
245
+ desc: 'OpenAI Whisper v3',
246
+ type: 'stt',
247
+ endpoint: '/v1/audio/transcriptions',
248
+ params: ['file'],
249
+ notes: 'POST ONLY (multipart)'
250
+ },
251
+ };
252
+ const _STATIC_MUSIC_MODEL = {
253
+ 'elevenmusic': {
254
+ desc: 'ElevenLabs Music',
255
+ endpoint: '/audio/{text}',
256
+ params: ['duration', 'instrumental'],
257
+ duration: [3, 300],
258
+ }
259
+ };
260
+ // ─── Private Helpers ─────────────────────────────────────────────────────
261
+ function formatPricingForDisplay(m) {
262
+ const p = m.pricing;
263
+ if (p.completionImageTokens) {
264
+ return p.completionImageTokens < 0.001
265
+ ? 'tokens'
266
+ : `${p.completionImageTokens} 🌻${m.paid_only ? ' 💎' : ''}`;
267
+ }
268
+ if (p.completionVideoSeconds) {
269
+ return `${p.completionVideoSeconds}/sec${m.paid_only ? ' 💎' : ''}`;
270
+ }
271
+ if (p.completionVideoTokens) {
272
+ return 'tokens';
273
+ }
274
+ if (p.completionAudioTokens) {
275
+ return `${p.completionAudioTokens} 🌻/tok`;
276
+ }
277
+ if (p.completionAudioSeconds) {
278
+ return `${p.completionAudioSeconds}/sec`;
279
+ }
280
+ if (p.promptAudioSeconds) {
281
+ return `${p.promptAudioSeconds}/sec`;
282
+ }
283
+ return 'tokens';
284
+ }
285
+ function detectAudioType(m) {
286
+ const hasAudioInput = m.input_modalities.includes('audio');
287
+ const hasAudioOutput = m.output_modalities.includes('audio');
288
+ if (hasAudioInput && hasAudioOutput)
289
+ return 'both';
290
+ if (hasAudioInput)
291
+ return 'stt';
292
+ return 'tts';
293
+ }
294
+ // ─── HTTP Helpers ─────────────────────────────────────────────────────────
295
+ export function httpsGet(url, headers = {}) {
296
+ return new Promise((resolve, reject) => {
297
+ const parsedUrl = new URL(url);
298
+ const options = {
299
+ hostname: parsedUrl.hostname,
300
+ path: parsedUrl.pathname + parsedUrl.search,
301
+ method: 'GET',
302
+ headers: {
303
+ 'User-Agent': 'OpenCode-Pollinations-Plugin/6.0',
304
+ ...headers,
305
+ },
306
+ };
307
+ const req = https.request(options, (res) => {
308
+ // Handle redirects
309
+ if ([301, 302, 307].includes(res.statusCode || 0) && res.headers.location) {
310
+ httpsGet(res.headers.location, headers).then(resolve).catch(reject);
311
+ return;
312
+ }
313
+ const chunks = [];
314
+ res.on('data', (chunk) => chunks.push(chunk));
315
+ res.on('end', () => {
316
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
317
+ resolve({
318
+ data: Buffer.concat(chunks),
319
+ headers: res.headers
320
+ });
321
+ }
322
+ else {
323
+ const errorBody = Buffer.concat(chunks).toString();
324
+ let errMsg = `HTTP ${res.statusCode}`;
325
+ try {
326
+ const errJson = JSON.parse(errorBody);
327
+ if (errJson.error && errJson.error.message) {
328
+ errMsg += `: ${errJson.error.message}`;
329
+ if (errJson.error.details?.fieldErrors) {
330
+ errMsg += ` - Fields: ${JSON.stringify(errJson.error.details.fieldErrors)}`;
331
+ }
332
+ }
333
+ else {
334
+ errMsg += `: ${errorBody.substring(0, 200)}`;
335
+ }
336
+ }
337
+ catch {
338
+ errMsg += `: ${errorBody.substring(0, 200)}`;
339
+ }
340
+ reject(new Error(errMsg));
341
+ }
342
+ });
343
+ });
344
+ req.on('error', reject);
345
+ req.setTimeout(300000, () => {
346
+ req.destroy();
347
+ reject(new Error('Timeout (300s)'));
348
+ });
349
+ req.end();
350
+ });
351
+ }
352
+ export function httpsPost(url, body, headers = {}) {
353
+ return new Promise((resolve, reject) => {
354
+ const parsedUrl = new URL(url);
355
+ const bodyData = typeof body === 'string' ? body : JSON.stringify(body);
356
+ const options = {
357
+ hostname: parsedUrl.hostname,
358
+ path: parsedUrl.pathname + parsedUrl.search,
359
+ method: 'POST',
360
+ headers: {
361
+ 'Content-Type': 'application/json',
362
+ 'Content-Length': Buffer.byteLength(bodyData),
363
+ 'User-Agent': 'OpenCode-Pollinations-Plugin/6.0',
364
+ ...headers,
365
+ },
366
+ };
367
+ const req = https.request(options, (res) => {
368
+ const chunks = [];
369
+ res.on('data', (chunk) => chunks.push(chunk));
370
+ res.on('end', () => {
371
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
372
+ resolve({
373
+ data: Buffer.concat(chunks),
374
+ headers: res.headers
375
+ });
376
+ }
377
+ else {
378
+ const errorBody = Buffer.concat(chunks).toString();
379
+ let errMsg = `HTTP ${res.statusCode}`;
380
+ try {
381
+ const errJson = JSON.parse(errorBody);
382
+ if (errJson.error && errJson.error.message) {
383
+ errMsg += `: ${errJson.error.message}`;
384
+ if (errJson.error.details?.fieldErrors) {
385
+ errMsg += ` - Fields: ${JSON.stringify(errJson.error.details.fieldErrors)}`;
386
+ }
387
+ }
388
+ else {
389
+ errMsg += `: ${errorBody.substring(0, 200)}`;
390
+ }
391
+ }
392
+ catch {
393
+ errMsg += `: ${errorBody.substring(0, 200)}`;
394
+ }
395
+ reject(new Error(errMsg));
396
+ }
397
+ });
398
+ });
399
+ req.on('error', reject);
400
+ req.setTimeout(300000, () => {
401
+ req.destroy();
402
+ reject(new Error('Timeout (300s)'));
403
+ });
404
+ req.write(bodyData);
405
+ req.end();
406
+ });
407
+ }
408
+ /**
409
+ * Multipart POST for file uploads (STT)
410
+ */
411
+ export function httpsPostMultipart(url, fields, headers = {}) {
412
+ return new Promise((resolve, reject) => {
413
+ const parsedUrl = new URL(url);
414
+ const boundary = `----FormBoundary${Date.now()}`;
415
+ const parts = [];
416
+ for (const [key, value] of Object.entries(fields)) {
417
+ parts.push(Buffer.from(`--${boundary}\r\n`));
418
+ if (Buffer.isBuffer(value)) {
419
+ parts.push(Buffer.from(`Content-Disposition: form-data; name="${key}"; filename="audio.mp3"\r\n`));
420
+ parts.push(Buffer.from(`Content-Type: audio/mpeg\r\n\r\n`));
421
+ parts.push(value);
422
+ parts.push(Buffer.from('\r\n'));
423
+ }
424
+ else {
425
+ parts.push(Buffer.from(`Content-Disposition: form-data; name="${key}"\r\n\r\n`));
426
+ parts.push(Buffer.from(value));
427
+ parts.push(Buffer.from('\r\n'));
428
+ }
429
+ }
430
+ parts.push(Buffer.from(`--${boundary}--\r\n`));
431
+ const bodyData = Buffer.concat(parts);
432
+ const options = {
433
+ hostname: parsedUrl.hostname,
434
+ path: parsedUrl.pathname + parsedUrl.search,
435
+ method: 'POST',
436
+ headers: {
437
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
438
+ 'Content-Length': bodyData.length,
439
+ 'User-Agent': 'OpenCode-Pollinations-Plugin/6.0',
440
+ ...headers,
441
+ },
442
+ };
443
+ const req = https.request(options, (res) => {
444
+ const chunks = [];
445
+ res.on('data', (chunk) => chunks.push(chunk));
446
+ res.on('end', () => {
447
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
448
+ resolve({
449
+ data: Buffer.concat(chunks),
450
+ headers: res.headers
451
+ });
452
+ }
453
+ else {
454
+ const errorBody = Buffer.concat(chunks).toString();
455
+ let errMsg = `HTTP ${res.statusCode}`;
456
+ try {
457
+ const errJson = JSON.parse(errorBody);
458
+ if (errJson.error && errJson.error.message) {
459
+ errMsg += `: ${errJson.error.message}`;
460
+ if (errJson.error.details?.fieldErrors) {
461
+ errMsg += ` - Fields: ${JSON.stringify(errJson.error.details.fieldErrors)}`;
462
+ }
463
+ }
464
+ else {
465
+ errMsg += `: ${errorBody.substring(0, 200)}`;
466
+ }
467
+ }
468
+ catch {
469
+ errMsg += `: ${errorBody.substring(0, 200)}`;
470
+ }
471
+ reject(new Error(errMsg));
472
+ }
473
+ });
474
+ });
475
+ req.on('error', reject);
476
+ req.setTimeout(300000, () => {
477
+ req.destroy();
478
+ reject(new Error('Timeout (300s)'));
479
+ });
480
+ req.write(bodyData);
481
+ req.end();
482
+ });
483
+ }
484
+ // ─── Model Discovery (delegated to ModelRegistry) ─────────────────────────
485
+ /**
486
+ * @deprecated Use ModelRegistry.list() directly
487
+ */
488
+ export async function fetchModels(type) {
489
+ ModelRegistry.ensureFresh();
490
+ const models = ModelRegistry.list(type);
491
+ return models.map(m => ({
492
+ name: m.name,
493
+ pricing: m.pricing,
494
+ paid_only: m.paid_only,
495
+ input_modalities: m.input_modalities,
496
+ output_modalities: m.output_modalities,
497
+ description: m.description,
498
+ }));
499
+ }
500
+ /**
501
+ * @deprecated Use ModelRegistry.get() directly
502
+ */
503
+ export async function getModelInfo(type, name) {
504
+ ModelRegistry.ensureFresh();
505
+ const m = ModelRegistry.getByNameOrAlias(type, name);
506
+ if (!m)
507
+ return undefined;
508
+ return {
509
+ name: m.name,
510
+ pricing: m.pricing,
511
+ paid_only: m.paid_only,
512
+ input_modalities: m.input_modalities,
513
+ output_modalities: m.output_modalities,
514
+ description: m.description,
515
+ };
516
+ }
517
+ // ─── Cost Estimation & Tracking ───────────────────────────────────────────
518
+ /**
519
+ * Extract cost tracking from response headers
520
+ */
521
+ export function extractCostFromHeaders(headers) {
522
+ return {
523
+ imageTokens: headers['x-usage-completion-image-tokens']
524
+ ? parseFloat(headers['x-usage-completion-image-tokens'])
525
+ : undefined,
526
+ videoSeconds: headers['x-usage-completion-video-seconds']
527
+ ? parseFloat(headers['x-usage-completion-video-seconds'])
528
+ : undefined,
529
+ videoTokens: headers['x-usage-completion-video-tokens']
530
+ ? parseFloat(headers['x-usage-completion-video-tokens'])
531
+ : undefined,
532
+ costUsd: headers['x-usage-cost-usd']
533
+ ? parseFloat(headers['x-usage-cost-usd'])
534
+ : undefined,
535
+ modelUsed: headers['x-model-used'],
536
+ requestId: headers['x-request-id'],
537
+ };
538
+ }
539
+ /**
540
+ * Fetch current Enter balance (`/account/balance`) for Real Cost calculation.
541
+ */
542
+ export async function fetchEnterBalance() {
543
+ const apiKey = getApiKey();
544
+ if (!apiKey)
545
+ return null;
546
+ try {
547
+ const url = 'https://gen.pollinations.ai/account/balance';
548
+ // Using native fetch
549
+ const res = await fetch(url, {
550
+ headers: { 'Authorization': `Bearer ${apiKey}` },
551
+ signal: AbortSignal.timeout(5000)
552
+ });
553
+ if (!res.ok)
554
+ return null;
555
+ const data = await res.json();
556
+ // The endpoint usually returns just { "balance": 9.9... }
557
+ return data.balance !== undefined ? data.balance : null;
558
+ }
559
+ catch {
560
+ return null; // Silent catch
561
+ }
562
+ }
563
+ /**
564
+ * Check if cost estimator is enabled in config
565
+ */
566
+ export function isCostEstimatorEnabled() {
567
+ const config = loadConfig();
568
+ return config.costEstimator !== false; // Default true
569
+ }
570
+ // ─── COST ESTIMATION BENCHMARKS ──────────────────────────────────────────
571
+ export function per1pollen(cost) {
572
+ if (!cost || cost <= 0)
573
+ return "—";
574
+ const x = 1 / cost;
575
+ if (x >= 1_000_000)
576
+ return `${(x / 1_000_000).toFixed(1)}M`;
577
+ if (x >= 100_000)
578
+ return `${Math.round(x / 1000)}K`;
579
+ if (x >= 10_000)
580
+ return `${(x / 1000).toFixed(1)}K`.replace(/\.0K$/, "K");
581
+ if (x >= 1_000)
582
+ return `${Math.round(x / 100) * 100}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
583
+ if (x >= 100)
584
+ return `${Math.round(x)}`;
585
+ if (x >= 10)
586
+ return `${Math.round(x * 10) / 10}`;
587
+ return `${x.toFixed(1)}`;
588
+ }
589
+ export function estimateImageCost(model) {
590
+ // Try ModelRegistry first
591
+ if (ModelRegistry.isReady()) {
592
+ const m = ModelRegistry.getByNameOrAlias('image', model);
593
+ if (m && m.averageCost !== undefined) {
594
+ return m.averageCost;
595
+ }
596
+ }
597
+ // Fallback to static
598
+ const info = _STATIC_PAID_IMAGE_MODELS[model];
599
+ if (!info)
600
+ return 0.0002;
601
+ const costMatch = info.cost.match(/[\d.]+/);
602
+ return costMatch ? parseFloat(costMatch[0]) : 0.0002;
603
+ }
604
+ export function estimateVideoCost(model, duration) {
605
+ // Try ModelRegistry first
606
+ if (ModelRegistry.isReady()) {
607
+ const m = ModelRegistry.getByNameOrAlias('video', model);
608
+ if (m && m.averageCost !== undefined) {
609
+ return m.averageCost;
610
+ }
611
+ }
612
+ // Fallback to static
613
+ const info = _STATIC_VIDEO_MODELS[model];
614
+ if (!info)
615
+ return duration * 0.01;
616
+ if (info.costHeader === 'x-usage-completion-video-tokens') {
617
+ const tokensPerSecond = 21780;
618
+ return (duration * tokensPerSecond) * 0.00001;
619
+ }
620
+ const costMatch = info.cost.match(/[\d.]+/);
621
+ const perSecond = costMatch ? parseFloat(costMatch[0]) : 0.01;
622
+ return duration * perSecond;
623
+ }
624
+ export function estimateTtsCost(textLength) {
625
+ // Try ModelRegistry first
626
+ if (ModelRegistry.isReady()) {
627
+ const m = ModelRegistry.getByNameOrAlias('audio', 'elevenlabs');
628
+ if (m && m.averageCost !== undefined) {
629
+ return m.averageCost;
630
+ }
631
+ }
632
+ return (textLength / 1000) * 0.00018;
633
+ }
634
+ export function estimateMusicCost(duration) {
635
+ // Try ModelRegistry first
636
+ if (ModelRegistry.isReady()) {
637
+ const m = ModelRegistry.getByNameOrAlias('audio', 'elevenmusic');
638
+ if (m && m.averageCost !== undefined) {
639
+ return m.averageCost;
640
+ }
641
+ }
642
+ return duration * 0.005;
643
+ }
644
+ // ─── Security & Validation Utils ─────────────────────────────────────────
645
+ /**
646
+ * Empêche le Path Traversal en s'assurant que le nom de fichier
647
+ * est restreint à son nom de base et ne contient pas de caractères malveillants.
648
+ */
649
+ export function sanitizeFilename(filename) {
650
+ if (!filename)
651
+ return '';
652
+ // Conserve uniquement le basename brut (protège contre ../../ etc)
653
+ const base = path.basename(filename);
654
+ // Optionnel : on peut restreindre les caractères si nécessaire
655
+ return base;
656
+ }
657
+ /**
658
+ * Valide qu'une URL est bien HTTP ou HTTPS et empêche les schémas dangereux (file://, javascript:).
659
+ */
660
+ export function validateHttpUrl(urlStr) {
661
+ if (!urlStr)
662
+ return false;
663
+ try {
664
+ const parsed = new URL(urlStr);
665
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
666
+ }
667
+ catch {
668
+ return false;
669
+ }
670
+ }
671
+ // ─── File Utils ──────────────────────────────────────────────────────────
672
+ export function ensureDir(dir) {
673
+ if (!fs.existsSync(dir)) {
674
+ fs.mkdirSync(dir, { recursive: true });
675
+ }
676
+ }
677
+ export function generateFilename(type, model, ext) {
678
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
679
+ return `${type}_${model}_${timestamp}.${ext}`;
680
+ }
681
+ export function getDefaultOutputDir(type) {
682
+ const home = process.env.HOME || process.env.USERPROFILE || '/tmp';
683
+ return path.join(home, 'Downloads', 'pollinations', type);
684
+ }
685
+ export function formatCost(cost) {
686
+ if (cost < 0.001)
687
+ return `${(cost * 1000).toFixed(4)} m🌻`;
688
+ if (cost < 1)
689
+ return `${cost.toFixed(4)} 🌻`;
690
+ return `${cost.toFixed(2)} 🌻`;
691
+ }
692
+ export function formatFileSize(bytes) {
693
+ if (bytes < 1024)
694
+ return `${bytes} B`;
695
+ if (bytes < 1024 * 1024)
696
+ return `${(bytes / 1024).toFixed(1)} KB`;
697
+ if (bytes < 1024 * 1024 * 1024)
698
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
699
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
700
+ }
701
+ // ─── Validation Helpers (Dynamic via ModelRegistry) ──────────────────────
702
+ /**
703
+ * Check if model supports Image-to-Image
704
+ */
705
+ export function supportsI2I(model) {
706
+ if (ModelRegistry.isReady()) {
707
+ const m = ModelRegistry.getByNameOrAlias('image', model);
708
+ return m?.supportsI2X === true;
709
+ }
710
+ const info = _STATIC_PAID_IMAGE_MODELS[model];
711
+ return info?.i2i === true;
712
+ }
713
+ /**
714
+ * Check if video model supports Image-to-Video
715
+ */
716
+ export function supportsI2V(model) {
717
+ if (ModelRegistry.isReady()) {
718
+ const m = ModelRegistry.getByNameOrAlias('video', model);
719
+ return m?.supportsI2X === true;
720
+ }
721
+ const info = _STATIC_VIDEO_MODELS[model];
722
+ return info?.i2v === true;
723
+ }
724
+ /**
725
+ * Check if video model requires Image-to-Video (no T2V)
726
+ */
727
+ export function requiresI2V(model) {
728
+ if (ModelRegistry.isReady()) {
729
+ const m = ModelRegistry.getByNameOrAlias('video', model);
730
+ if (m) {
731
+ return _STATIC_I2V_ONLY.has(m.name); // Only wan is I2V-only for now
732
+ }
733
+ }
734
+ const info = _STATIC_VIDEO_MODELS[model];
735
+ return info?.t2v === false && info?.i2v === true;
736
+ }
737
+ /**
738
+ * Validate aspect ratio for video model
739
+ */
740
+ export function validateAspectRatio(model, ratio) {
741
+ if (ModelRegistry.isReady()) {
742
+ const m = ModelRegistry.getByNameOrAlias('video', model);
743
+ return m?.aspectRatios?.includes(ratio) ?? false;
744
+ }
745
+ const info = _STATIC_VIDEO_MODELS[model];
746
+ return info?.aspectRatios.includes(ratio) ?? false;
747
+ }
748
+ /**
749
+ * Get valid duration range for video model
750
+ */
751
+ export function getDurationRange(model) {
752
+ if (ModelRegistry.isReady()) {
753
+ const m = ModelRegistry.getByNameOrAlias('video', model);
754
+ return m?.durationRange ?? [1, 10];
755
+ }
756
+ const info = _STATIC_VIDEO_MODELS[model];
757
+ return info?.duration ?? [1, 10];
758
+ }