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.
- package/README.md +11 -6
- package/dist/index.js +40 -10
- package/dist/server/commands.d.ts +4 -0
- package/dist/server/commands.js +296 -12
- package/dist/server/config.d.ts +5 -0
- package/dist/server/config.js +163 -35
- package/dist/server/connect-response.d.ts +2 -0
- package/dist/server/connect-response.js +141 -0
- package/dist/server/generate-config.js +10 -24
- package/dist/server/logger.d.ts +8 -0
- package/dist/server/logger.js +36 -0
- package/dist/server/models/cache.d.ts +35 -0
- package/dist/server/models/cache.js +160 -0
- package/dist/server/models/fetcher.d.ts +18 -0
- package/dist/server/models/fetcher.js +150 -0
- package/dist/server/models/index.d.ts +6 -0
- package/dist/server/models/index.js +5 -0
- package/dist/server/models/manual.d.ts +15 -0
- package/dist/server/models/manual.js +92 -0
- package/dist/server/models/types.d.ts +55 -0
- package/dist/server/models/types.js +7 -0
- package/dist/server/models/worker.d.ts +21 -0
- package/dist/server/models/worker.js +97 -0
- package/dist/server/pollinations-api.js +1 -8
- package/dist/server/proxy.js +52 -27
- package/dist/server/quota.d.ts +2 -8
- package/dist/server/quota.js +47 -89
- package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
- package/dist/server/scripts/pollinations_pricing.js +246 -0
- package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
- package/dist/server/scripts/test_cost_endpoints.js +61 -0
- package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
- package/dist/server/scripts/test_dynamic_pricing.js +39 -0
- package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
- package/dist/server/scripts/test_freetier_audit.js +215 -0
- package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
- package/dist/server/scripts/test_parallel_cost.js +104 -0
- package/dist/server/toast.d.ts +4 -1
- package/dist/server/toast.js +27 -10
- package/dist/tools/ffmpeg.d.ts +24 -0
- package/dist/tools/ffmpeg.js +54 -0
- package/dist/tools/index.d.ts +10 -8
- package/dist/tools/index.js +27 -25
- package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
- package/dist/tools/pollinations/beta_discovery.js +197 -0
- package/dist/tools/pollinations/cost-guard.d.ts +38 -0
- package/dist/tools/pollinations/cost-guard.js +141 -0
- package/dist/tools/pollinations/gen_audio.d.ts +1 -1
- package/dist/tools/pollinations/gen_audio.js +65 -23
- package/dist/tools/pollinations/gen_image.d.ts +5 -7
- package/dist/tools/pollinations/gen_image.js +146 -160
- package/dist/tools/pollinations/gen_music.d.ts +1 -1
- package/dist/tools/pollinations/gen_music.js +57 -16
- package/dist/tools/pollinations/gen_video.d.ts +1 -1
- package/dist/tools/pollinations/gen_video.js +99 -65
- package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
- package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
- package/dist/tools/pollinations/polli_status.d.ts +2 -0
- package/dist/tools/pollinations/polli_status.js +31 -0
- package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
- package/dist/tools/pollinations/polli_web_search.js +164 -0
- package/dist/tools/pollinations/shared.d.ts +34 -39
- package/dist/tools/pollinations/shared.js +300 -89
- package/dist/tools/pollinations/test_estimators.d.ts +1 -0
- package/dist/tools/pollinations/test_estimators.js +22 -0
- package/dist/tools/pollinations/transcribe_audio.d.ts +5 -9
- package/dist/tools/pollinations/transcribe_audio.js +31 -72
- package/dist/tools/power/extract_audio.js +26 -27
- package/dist/tools/power/extract_frames.js +24 -27
- package/dist/tools/power/remove_background.js +2 -1
- package/dist/tools/power/rmbg_keys.js +2 -1
- package/dist/tools/shared.js +9 -3
- 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-
|
|
5
|
-
*
|
|
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
|
-
// ───
|
|
23
|
+
// ─── Model Data (Dynamic via ModelRegistry) ───────────────────────────────
|
|
23
24
|
/**
|
|
24
|
-
* FREE Image Models (image.pollinations.ai
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
*
|
|
34
|
-
*
|
|
56
|
+
* Dynamic Video Models accessor.
|
|
57
|
+
* BACKWARD COMPATIBLE: Same shape as old VIDEO_MODELS
|
|
35
58
|
*/
|
|
36
|
-
export
|
|
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,
|
|
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
|
-
|
|
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],
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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
|
|
13
|
+
export declare const polliSttTool: ToolDefinition;
|