opencode-pollinations-plugin 6.1.0-beta.12 → 6.1.0-beta.22

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 (73) hide show
  1. package/README.md +11 -6
  2. package/dist/index.js +40 -10
  3. package/dist/server/commands.d.ts +4 -0
  4. package/dist/server/commands.js +296 -12
  5. package/dist/server/config.d.ts +5 -0
  6. package/dist/server/config.js +163 -35
  7. package/dist/server/connect-response.d.ts +2 -0
  8. package/dist/server/connect-response.js +141 -0
  9. package/dist/server/generate-config.js +10 -24
  10. package/dist/server/logger.d.ts +8 -0
  11. package/dist/server/logger.js +36 -0
  12. package/dist/server/models/cache.d.ts +35 -0
  13. package/dist/server/models/cache.js +160 -0
  14. package/dist/server/models/fetcher.d.ts +18 -0
  15. package/dist/server/models/fetcher.js +150 -0
  16. package/dist/server/models/index.d.ts +6 -0
  17. package/dist/server/models/index.js +5 -0
  18. package/dist/server/models/manual.d.ts +15 -0
  19. package/dist/server/models/manual.js +92 -0
  20. package/dist/server/models/types.d.ts +55 -0
  21. package/dist/server/models/types.js +7 -0
  22. package/dist/server/models/worker.d.ts +21 -0
  23. package/dist/server/models/worker.js +97 -0
  24. package/dist/server/pollinations-api.js +1 -8
  25. package/dist/server/proxy.js +52 -27
  26. package/dist/server/quota.d.ts +2 -8
  27. package/dist/server/quota.js +47 -89
  28. package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
  29. package/dist/server/scripts/pollinations_pricing.js +246 -0
  30. package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
  31. package/dist/server/scripts/test_cost_endpoints.js +61 -0
  32. package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
  33. package/dist/server/scripts/test_dynamic_pricing.js +39 -0
  34. package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
  35. package/dist/server/scripts/test_freetier_audit.js +215 -0
  36. package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
  37. package/dist/server/scripts/test_parallel_cost.js +104 -0
  38. package/dist/server/toast.d.ts +4 -1
  39. package/dist/server/toast.js +27 -10
  40. package/dist/tools/ffmpeg.d.ts +24 -0
  41. package/dist/tools/ffmpeg.js +54 -0
  42. package/dist/tools/index.d.ts +10 -8
  43. package/dist/tools/index.js +27 -25
  44. package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
  45. package/dist/tools/pollinations/beta_discovery.js +197 -0
  46. package/dist/tools/pollinations/cost-guard.d.ts +38 -0
  47. package/dist/tools/pollinations/cost-guard.js +141 -0
  48. package/dist/tools/pollinations/gen_audio.d.ts +1 -1
  49. package/dist/tools/pollinations/gen_audio.js +65 -23
  50. package/dist/tools/pollinations/gen_image.d.ts +5 -7
  51. package/dist/tools/pollinations/gen_image.js +146 -160
  52. package/dist/tools/pollinations/gen_music.d.ts +1 -1
  53. package/dist/tools/pollinations/gen_music.js +57 -16
  54. package/dist/tools/pollinations/gen_video.d.ts +1 -1
  55. package/dist/tools/pollinations/gen_video.js +99 -65
  56. package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
  57. package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
  58. package/dist/tools/pollinations/polli_status.d.ts +2 -0
  59. package/dist/tools/pollinations/polli_status.js +31 -0
  60. package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
  61. package/dist/tools/pollinations/polli_web_search.js +164 -0
  62. package/dist/tools/pollinations/shared.d.ts +34 -39
  63. package/dist/tools/pollinations/shared.js +300 -89
  64. package/dist/tools/pollinations/test_estimators.d.ts +1 -0
  65. package/dist/tools/pollinations/test_estimators.js +22 -0
  66. package/dist/tools/pollinations/transcribe_audio.d.ts +5 -9
  67. package/dist/tools/pollinations/transcribe_audio.js +31 -72
  68. package/dist/tools/power/extract_audio.js +26 -27
  69. package/dist/tools/power/extract_frames.js +24 -27
  70. package/dist/tools/power/remove_background.js +2 -1
  71. package/dist/tools/power/rmbg_keys.js +2 -1
  72. package/dist/tools/shared.js +9 -3
  73. package/package.json +2 -2
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Shared utilities for Pollinations API tools
3
3
  *
4
- * Updated: 2026-02-12 - Verified API Reference
5
- * Tests: 18/18 passed
4
+ * Updated: 2026-02-18 - Sprint 2: Dynamic ModelRegistry integration
5
+ * Hardcoded model lists replaced by ModelRegistry lookups with static fallback.
6
6
  */
7
7
  import * as https from 'https';
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
10
10
  import { loadConfig } from '../../server/config.js';
11
+ import { ModelRegistry } from '../../server/models/index.js';
11
12
  // ─── Configuration ───────────────────────────────────────────────────────
12
13
  const API_BASE = 'gen.pollinations.ai';
13
14
  const FREE_IMAGE_BASE = 'image.pollinations.ai';
@@ -19,22 +20,126 @@ export function hasApiKey() {
19
20
  const key = getApiKey();
20
21
  return !!(key && key.length > 5 && key !== 'dummy');
21
22
  }
22
- // ─── Verified Model Data (2026-02-12) ─────────────────────────────────────
23
+ // ─── Model Data (Dynamic via ModelRegistry) ───────────────────────────────
23
24
  /**
24
- * FREE Image Models (image.pollinations.ai/models)
25
- * WARNING: flux removed from free, turbo broken (shows notice)
25
+ * FREE Image Models (DEPRECATED - image.pollinations.ai is dead)
26
26
  */
27
- export const FREE_IMAGE_MODELS = {
28
- sana: { desc: 'Default free model', fileSize: '~60KB', reliable: true },
29
- zimage: { desc: 'Alias sana/turbo low qual', fileSize: '~35KB', reliable: true },
30
- turbo: { desc: 'DEPRECATED - shows notice', fileSize: '~4.1MB', reliable: false },
31
- };
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
+ }
32
55
  /**
33
- * Paid Image Models (gen.pollinations.ai)
34
- * I2I = Image-to-Image support
56
+ * Dynamic Video Models accessor.
57
+ * BACKWARD COMPATIBLE: Same shape as old VIDEO_MODELS
35
58
  */
36
- export const PAID_IMAGE_MODELS = {
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
+ * Music Model accessor (backward compatible)
106
+ */
107
+ export function getMusicModel() {
108
+ // Check registry for elevenmusic
109
+ if (ModelRegistry.isReady()) {
110
+ const m = ModelRegistry.getByNameOrAlias('audio', 'elevenmusic');
111
+ if (m) {
112
+ return {
113
+ 'elevenmusic': {
114
+ desc: m.description,
115
+ endpoint: '/audio/{text}',
116
+ params: ['duration', 'instrumental'],
117
+ duration: [3, 300],
118
+ }
119
+ };
120
+ }
121
+ }
122
+ return _STATIC_MUSIC_MODEL;
123
+ }
124
+ // ─── Backward Compatibility ──────────────────────────────────────────────
125
+ // OLD const exports removed (caused TDZ error at module load).
126
+ // Consumers must use the function forms:
127
+ // getPaidImageModels(), getVideoModels(), getAudioModels(), getMusicModel()
128
+ // For direct model lookup: use ModelRegistry.getByNameOrAlias()
129
+ // ─── Private Static Fallback Data ─────────────────────────────────────────
130
+ // Used ONLY when ModelRegistry is not ready (startup race, offline).
131
+ const _STATIC_I2V_ONLY = new Set(); // Models that are I2V only (no T2V)
132
+ const _STATIC_NO_AUDIO = new Set(['seedance', 'seedance-pro']); // Video models without audio
133
+ const _STATIC_AUDIO_ENDPOINTS = {
134
+ 'openai-audio': '/v1/chat/completions',
135
+ 'elevenlabs': '/audio/{text}',
136
+ 'whisper': '/v1/audio/transcriptions',
137
+ 'scribe': '/v1/audio/transcriptions',
138
+ 'elevenmusic': '/audio/{text}',
139
+ };
140
+ const _STATIC_PAID_IMAGE_MODELS = {
37
141
  'flux': { desc: 'Flux Schnell', cost: '0.0002 🌻', t2i: true, i2i: false, params: ['width', 'height'] },
142
+ 'sana': { desc: 'Sana (Efficient)', cost: '0.0002 🌻', t2i: true, i2i: false, params: ['width', 'height'] },
38
143
  'zimage': { desc: 'Z-Image Turbo (6B Flux 2x)', cost: '0.0002 🌻', t2i: true, i2i: false, params: ['width', 'height'] },
39
144
  'imagen-4': { desc: 'Imagen 4 (alpha)', cost: '0.0025 🌻', t2i: true, i2i: false, params: ['width', 'height'] },
40
145
  'klein': { desc: 'FLUX.2 Klein 4B', cost: '0.008 🌻', t2i: true, i2i: true, params: ['width', 'height', 'image'] },
@@ -47,17 +152,11 @@ export const PAID_IMAGE_MODELS = {
47
152
  'nanobanana': { desc: 'NanoBanana (Gemini 2.5 Flash)', cost: 'tokens', t2i: true, i2i: true, params: ['width', 'height', 'image'] },
48
153
  'nanobanana-pro': { desc: 'NanoBanana Pro (Gemini 3 Pro)', cost: 'tokens', t2i: true, i2i: true, params: ['width', 'height', 'image'], notes: 'Thinking Model' },
49
154
  };
50
- /**
51
- * Video Models (gen.pollinations.ai)
52
- * T2V = Text-to-Video, I2V = Image-to-Video
53
- */
54
- export const VIDEO_MODELS = {
155
+ const _STATIC_VIDEO_MODELS = {
55
156
  'grok-video': {
56
157
  desc: 'Grok Video (alpha)',
57
158
  cost: '0.0025/sec',
58
- t2v: true,
59
- i2v: false,
60
- audio: true,
159
+ t2v: true, i2v: false, audio: true,
61
160
  duration: [1, 15],
62
161
  aspectRatios: ['16:9', '9:16', '1:1', '4:3'],
63
162
  costHeader: 'x-usage-completion-video-seconds',
@@ -66,9 +165,7 @@ export const VIDEO_MODELS = {
66
165
  'ltx-2': {
67
166
  desc: 'LTX-2 (Lightricks)',
68
167
  cost: '0.01/sec',
69
- t2v: true,
70
- i2v: false,
71
- audio: true,
168
+ t2v: true, i2v: false, audio: true,
72
169
  duration: [5, 20],
73
170
  aspectRatios: ['16:9'],
74
171
  costHeader: 'x-usage-completion-video-seconds',
@@ -77,9 +174,7 @@ export const VIDEO_MODELS = {
77
174
  'wan': {
78
175
  desc: 'Wan 2.6 (Alibaba)',
79
176
  cost: '0.025/sec',
80
- t2v: false, // I2V ONLY!
81
- i2v: true,
82
- audio: true,
177
+ t2v: false, i2v: true, audio: true,
83
178
  duration: [5, 15],
84
179
  aspectRatios: ['16:9', '9:16', '1:1', '4:3'],
85
180
  costHeader: 'x-usage-completion-video-seconds',
@@ -88,10 +183,8 @@ export const VIDEO_MODELS = {
88
183
  'veo': {
89
184
  desc: 'Veo 3.1 Fast (Google)',
90
185
  cost: '0.15/sec 💎',
91
- t2v: true,
92
- i2v: true,
93
- audio: true,
94
- duration: [4, 8], // 4, 6, or 8 seconds
186
+ t2v: true, i2v: true, audio: true,
187
+ duration: [4, 8],
95
188
  aspectRatios: ['16:9', '9:16', '1:1'],
96
189
  costHeader: 'x-usage-completion-video-seconds',
97
190
  genTime: '~45-68s',
@@ -99,9 +192,7 @@ export const VIDEO_MODELS = {
99
192
  'seedance': {
100
193
  desc: 'Seedance Lite (BytePlus)',
101
194
  cost: 'tokens',
102
- t2v: true,
103
- i2v: true,
104
- audio: false,
195
+ t2v: true, i2v: true, audio: false,
105
196
  duration: [4, 12],
106
197
  aspectRatios: ['16:9', '9:16', '1:1'],
107
198
  costHeader: 'x-usage-completion-video-tokens',
@@ -110,20 +201,14 @@ export const VIDEO_MODELS = {
110
201
  'seedance-pro': {
111
202
  desc: 'Seedance Pro-Fast (BytePlus)',
112
203
  cost: 'tokens',
113
- t2v: true,
114
- i2v: true,
115
- audio: false,
204
+ t2v: true, i2v: true, audio: false,
116
205
  duration: [4, 12],
117
206
  aspectRatios: ['16:9', '9:16', '1:1'],
118
207
  costHeader: 'x-usage-completion-video-tokens',
119
208
  genTime: '~30s'
120
209
  },
121
210
  };
122
- /**
123
- * Audio Models
124
- * TTS = Text-to-Speech, STT = Speech-to-Text
125
- */
126
- export const AUDIO_MODELS = {
211
+ const _STATIC_AUDIO_MODELS = {
127
212
  'openai-audio': {
128
213
  desc: 'GPT-4o Audio Preview',
129
214
  type: 'both',
@@ -147,17 +232,48 @@ export const AUDIO_MODELS = {
147
232
  notes: 'POST ONLY (multipart)'
148
233
  },
149
234
  };
150
- /**
151
- * Music Model (separate tool)
152
- */
153
- export const MUSIC_MODEL = {
235
+ const _STATIC_MUSIC_MODEL = {
154
236
  'elevenmusic': {
155
237
  desc: 'ElevenLabs Music',
156
238
  endpoint: '/audio/{text}',
157
239
  params: ['duration', 'instrumental'],
158
- duration: [3, 300], // 3-300 seconds
240
+ duration: [3, 300],
159
241
  }
160
242
  };
243
+ // ─── Private Helpers ─────────────────────────────────────────────────────
244
+ function formatPricingForDisplay(m) {
245
+ const p = m.pricing;
246
+ if (p.completionImageTokens) {
247
+ return p.completionImageTokens < 0.001
248
+ ? 'tokens'
249
+ : `${p.completionImageTokens} 🌻${m.paid_only ? ' 💎' : ''}`;
250
+ }
251
+ if (p.completionVideoSeconds) {
252
+ return `${p.completionVideoSeconds}/sec${m.paid_only ? ' 💎' : ''}`;
253
+ }
254
+ if (p.completionVideoTokens) {
255
+ return 'tokens';
256
+ }
257
+ if (p.completionAudioTokens) {
258
+ return `${p.completionAudioTokens} 🌻/tok`;
259
+ }
260
+ if (p.completionAudioSeconds) {
261
+ return `${p.completionAudioSeconds}/sec`;
262
+ }
263
+ if (p.promptAudioSeconds) {
264
+ return `${p.promptAudioSeconds}/sec`;
265
+ }
266
+ return 'tokens';
267
+ }
268
+ function detectAudioType(m) {
269
+ const hasAudioInput = m.input_modalities.includes('audio');
270
+ const hasAudioOutput = m.output_modalities.includes('audio');
271
+ if (hasAudioInput && hasAudioOutput)
272
+ return 'both';
273
+ if (hasAudioInput)
274
+ return 'stt';
275
+ return 'tts';
276
+ }
161
277
  // ─── HTTP Helpers ─────────────────────────────────────────────────────────
162
278
  export function httpsGet(url, headers = {}) {
163
279
  return new Promise((resolve, reject) => {
@@ -299,38 +415,38 @@ export function httpsPostMultipart(url, fields, headers = {}) {
299
415
  req.end();
300
416
  });
301
417
  }
302
- // ─── Model Discovery ─────────────────────────────────────────────────────
303
- const MODEL_CACHE = {
304
- image: [],
305
- audio: [],
306
- text: [],
307
- };
308
- let CACHE_TIME = 0;
309
- const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
418
+ // ─── Model Discovery (delegated to ModelRegistry) ─────────────────────────
419
+ /**
420
+ * @deprecated Use ModelRegistry.list() directly
421
+ */
310
422
  export async function fetchModels(type) {
311
- const now = Date.now();
312
- if (MODEL_CACHE[type].length > 0 && now - CACHE_TIME < CACHE_TTL) {
313
- return MODEL_CACHE[type];
314
- }
315
- const apiKey = getApiKey();
316
- const headers = {};
317
- if (apiKey) {
318
- headers['Authorization'] = `Bearer ${apiKey}`;
319
- }
320
- try {
321
- const { data } = await httpsGet(`https://${API_BASE}/${type}/models`, headers);
322
- MODEL_CACHE[type] = JSON.parse(data.toString());
323
- CACHE_TIME = now;
324
- return MODEL_CACHE[type];
325
- }
326
- catch (err) {
327
- console.error(`Failed to fetch ${type} models:`, err);
328
- return [];
329
- }
423
+ ModelRegistry.ensureFresh();
424
+ const models = ModelRegistry.list(type);
425
+ return models.map(m => ({
426
+ name: m.name,
427
+ pricing: m.pricing,
428
+ paid_only: m.paid_only,
429
+ input_modalities: m.input_modalities,
430
+ output_modalities: m.output_modalities,
431
+ description: m.description,
432
+ }));
330
433
  }
434
+ /**
435
+ * @deprecated Use ModelRegistry.get() directly
436
+ */
331
437
  export async function getModelInfo(type, name) {
332
- const models = await fetchModels(type);
333
- return models.find(m => m.name === name);
438
+ ModelRegistry.ensureFresh();
439
+ const m = ModelRegistry.getByNameOrAlias(type, name);
440
+ if (!m)
441
+ return undefined;
442
+ return {
443
+ name: m.name,
444
+ pricing: m.pricing,
445
+ paid_only: m.paid_only,
446
+ input_modalities: m.input_modalities,
447
+ output_modalities: m.output_modalities,
448
+ description: m.description,
449
+ };
334
450
  }
335
451
  // ─── Cost Estimation & Tracking ───────────────────────────────────────────
336
452
  /**
@@ -347,10 +463,37 @@ export function extractCostFromHeaders(headers) {
347
463
  videoTokens: headers['x-usage-completion-video-tokens']
348
464
  ? parseFloat(headers['x-usage-completion-video-tokens'])
349
465
  : undefined,
466
+ costUsd: headers['x-usage-cost-usd']
467
+ ? parseFloat(headers['x-usage-cost-usd'])
468
+ : undefined,
350
469
  modelUsed: headers['x-model-used'],
351
470
  requestId: headers['x-request-id'],
352
471
  };
353
472
  }
473
+ /**
474
+ * Fetch current Enter balance (`/account/balance`) for Real Cost calculation.
475
+ */
476
+ export async function fetchEnterBalance() {
477
+ const apiKey = getApiKey();
478
+ if (!apiKey)
479
+ return null;
480
+ try {
481
+ const url = 'https://gen.pollinations.ai/account/balance';
482
+ // Using native fetch
483
+ const res = await fetch(url, {
484
+ headers: { 'Authorization': `Bearer ${apiKey}` },
485
+ signal: AbortSignal.timeout(5000)
486
+ });
487
+ if (!res.ok)
488
+ return null;
489
+ const data = await res.json();
490
+ // The endpoint usually returns just { "balance": 9.9... }
491
+ return data.balance !== undefined ? data.balance : null;
492
+ }
493
+ catch {
494
+ return null; // Silent catch
495
+ }
496
+ }
354
497
  /**
355
498
  * Check if cost estimator is enabled in config
356
499
  */
@@ -358,33 +501,79 @@ export function isCostEstimatorEnabled() {
358
501
  const config = loadConfig();
359
502
  return config.costEstimator !== false; // Default true
360
503
  }
504
+ // ─── COST ESTIMATION BENCHMARKS ──────────────────────────────────────────
505
+ export function per1pollen(cost) {
506
+ if (!cost || cost <= 0)
507
+ return "—";
508
+ const x = 1 / cost;
509
+ if (x >= 1_000_000)
510
+ return `${(x / 1_000_000).toFixed(1)}M`;
511
+ if (x >= 100_000)
512
+ return `${Math.round(x / 1000)}K`;
513
+ if (x >= 10_000)
514
+ return `${(x / 1000).toFixed(1)}K`.replace(/\.0K$/, "K");
515
+ if (x >= 1_000)
516
+ return `${Math.round(x / 100) * 100}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
517
+ if (x >= 100)
518
+ return `${Math.round(x)}`;
519
+ if (x >= 10)
520
+ return `${Math.round(x * 10) / 10}`;
521
+ return `${x.toFixed(1)}`;
522
+ }
361
523
  export function estimateImageCost(model) {
362
- const info = PAID_IMAGE_MODELS[model];
524
+ // Try ModelRegistry first
525
+ if (ModelRegistry.isReady()) {
526
+ const m = ModelRegistry.getByNameOrAlias('image', model);
527
+ if (m && m.averageCost !== undefined) {
528
+ return m.averageCost;
529
+ }
530
+ }
531
+ // Fallback to static
532
+ const info = _STATIC_PAID_IMAGE_MODELS[model];
363
533
  if (!info)
364
534
  return 0.0002;
365
535
  const costMatch = info.cost.match(/[\d.]+/);
366
536
  return costMatch ? parseFloat(costMatch[0]) : 0.0002;
367
537
  }
368
538
  export function estimateVideoCost(model, duration) {
369
- const info = VIDEO_MODELS[model];
539
+ // Try ModelRegistry first
540
+ if (ModelRegistry.isReady()) {
541
+ const m = ModelRegistry.getByNameOrAlias('video', model);
542
+ if (m && m.averageCost !== undefined) {
543
+ return m.averageCost;
544
+ }
545
+ }
546
+ // Fallback to static
547
+ const info = _STATIC_VIDEO_MODELS[model];
370
548
  if (!info)
371
549
  return duration * 0.01;
372
550
  if (info.costHeader === 'x-usage-completion-video-tokens') {
373
- // Token-based: 108900 tokens for 5s video
374
551
  const tokensPerSecond = 21780;
375
- return (duration * tokensPerSecond) * 0.00001; // Approximate
552
+ return (duration * tokensPerSecond) * 0.00001;
376
553
  }
377
- // Second-based
378
554
  const costMatch = info.cost.match(/[\d.]+/);
379
555
  const perSecond = costMatch ? parseFloat(costMatch[0]) : 0.01;
380
556
  return duration * perSecond;
381
557
  }
382
558
  export function estimateTtsCost(textLength) {
383
- // Approximate: 1 char ≈ 1 token
559
+ // Try ModelRegistry first
560
+ if (ModelRegistry.isReady()) {
561
+ const m = ModelRegistry.getByNameOrAlias('audio', 'elevenlabs');
562
+ if (m && m.averageCost !== undefined) {
563
+ return m.averageCost;
564
+ }
565
+ }
384
566
  return (textLength / 1000) * 0.00018;
385
567
  }
386
568
  export function estimateMusicCost(duration) {
387
- return duration * 0.005; // ~0.005/sec
569
+ // Try ModelRegistry first
570
+ if (ModelRegistry.isReady()) {
571
+ const m = ModelRegistry.getByNameOrAlias('audio', 'elevenmusic');
572
+ if (m && m.averageCost !== undefined) {
573
+ return m.averageCost;
574
+ }
575
+ }
576
+ return duration * 0.005;
388
577
  }
389
578
  // ─── File Utils ──────────────────────────────────────────────────────────
390
579
  export function ensureDir(dir) {
@@ -416,39 +605,61 @@ export function formatFileSize(bytes) {
416
605
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
417
606
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
418
607
  }
419
- // ─── Validation Helpers ──────────────────────────────────────────────────
608
+ // ─── Validation Helpers (Dynamic via ModelRegistry) ──────────────────────
420
609
  /**
421
610
  * Check if model supports Image-to-Image
422
611
  */
423
612
  export function supportsI2I(model) {
424
- const info = PAID_IMAGE_MODELS[model];
613
+ if (ModelRegistry.isReady()) {
614
+ const m = ModelRegistry.getByNameOrAlias('image', model);
615
+ return m?.supportsI2X === true;
616
+ }
617
+ const info = _STATIC_PAID_IMAGE_MODELS[model];
425
618
  return info?.i2i === true;
426
619
  }
427
620
  /**
428
621
  * Check if video model supports Image-to-Video
429
622
  */
430
623
  export function supportsI2V(model) {
431
- const info = VIDEO_MODELS[model];
624
+ if (ModelRegistry.isReady()) {
625
+ const m = ModelRegistry.getByNameOrAlias('video', model);
626
+ return m?.supportsI2X === true;
627
+ }
628
+ const info = _STATIC_VIDEO_MODELS[model];
432
629
  return info?.i2v === true;
433
630
  }
434
631
  /**
435
632
  * Check if video model requires Image-to-Video (no T2V)
436
633
  */
437
634
  export function requiresI2V(model) {
438
- const info = VIDEO_MODELS[model];
635
+ if (ModelRegistry.isReady()) {
636
+ const m = ModelRegistry.getByNameOrAlias('video', model);
637
+ if (m) {
638
+ return _STATIC_I2V_ONLY.has(m.name); // Only wan is I2V-only for now
639
+ }
640
+ }
641
+ const info = _STATIC_VIDEO_MODELS[model];
439
642
  return info?.t2v === false && info?.i2v === true;
440
643
  }
441
644
  /**
442
645
  * Validate aspect ratio for video model
443
646
  */
444
647
  export function validateAspectRatio(model, ratio) {
445
- const info = VIDEO_MODELS[model];
648
+ if (ModelRegistry.isReady()) {
649
+ const m = ModelRegistry.getByNameOrAlias('video', model);
650
+ return m?.aspectRatios?.includes(ratio) ?? false;
651
+ }
652
+ const info = _STATIC_VIDEO_MODELS[model];
446
653
  return info?.aspectRatios.includes(ratio) ?? false;
447
654
  }
448
655
  /**
449
656
  * Get valid duration range for video model
450
657
  */
451
658
  export function getDurationRange(model) {
452
- const info = VIDEO_MODELS[model];
659
+ if (ModelRegistry.isReady()) {
660
+ const m = ModelRegistry.getByNameOrAlias('video', model);
661
+ return m?.durationRange ?? [1, 10];
662
+ }
663
+ const info = _STATIC_VIDEO_MODELS[model];
453
664
  return info?.duration ?? [1, 10];
454
665
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { estimateImageCost, estimateVideoCost, estimateTtsCost, per1pollen } from './shared.js';
2
+ import { ModelRegistry } from '../../server/models/index.js';
3
+ async function testEstimators() {
4
+ console.log("Loading models...");
5
+ await ModelRegistry.ensureFresh();
6
+ const imageTests = ['flux', 'flux-pro', 'turbo'];
7
+ console.log("\n=== IMAGE ESTIMATIONS ===");
8
+ for (const model of imageTests) {
9
+ const cost = estimateImageCost(model);
10
+ console.log(`[${model}]: Cost = ${cost}, 1 pollen ≈ ${per1pollen(cost)} images`);
11
+ }
12
+ const videoTests = ['ltx-2', 'wan', 'veo'];
13
+ console.log("\n=== VIDEO ESTIMATIONS (6s) ===");
14
+ for (const model of videoTests) {
15
+ const cost = estimateVideoCost(model, 6);
16
+ console.log(`[${model}]: Cost = ${cost}, 1 pollen ≈ ${per1pollen(cost)} vidéos`);
17
+ }
18
+ console.log("\n=== TTS ESTIMATIONS (200 chars) ===");
19
+ const ttsCost = estimateTtsCost(200);
20
+ console.log(`[elevenlabs]: Cost = ${ttsCost}, 1 pollen ≈ ${per1pollen(ttsCost)} generations`);
21
+ }
22
+ testEstimators();
@@ -3,15 +3,11 @@
3
3
  *
4
4
  * Updated: 2026-02-12 - Verified API Reference
5
5
  *
6
- * Two STT options:
7
- * 1. openai-audio (DEFAULT): GPT-4o Audio Preview - uses /v1/chat/completions with modalities
8
- * - Least expensive option
9
- * - Can handle both audio input and output
6
+ * 1. whisper-large-v3 (DEFAULT): High accuracy Whisper model
7
+ * 2. whisper-1: Standard Whisper model
8
+ * 3. scribe: ElevenLabs Scribe v2
10
9
  *
11
- * 2. whisper: OpenAI Whisper v3 - uses /v1/audio/transcriptions
12
- * - POST ONLY with multipart/form-data
13
- * - Specialized for transcription
14
- * - Higher accuracy for long audio
10
+ * All models use /v1/audio/transcriptions (POST multipart)
15
11
  */
16
12
  import { type ToolDefinition } from '@opencode-ai/plugin/tool';
17
- export declare const transcribeAudioTool: ToolDefinition;
13
+ export declare const polliSttTool: ToolDefinition;