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
package/dist/server/proxy.js
CHANGED
|
@@ -3,20 +3,13 @@ import * as path from 'path';
|
|
|
3
3
|
import { loadConfig, saveConfig } from './config.js';
|
|
4
4
|
import { handleCommand } from './commands.js';
|
|
5
5
|
import { emitStatusToast, emitLogToast } from './toast.js';
|
|
6
|
+
import { buildConnectResponse } from './connect-response.js';
|
|
7
|
+
import { log } from './logger.js';
|
|
8
|
+
import { getConfigDir } from './config.js';
|
|
6
9
|
// --- PERSISTENCE: SIGNATURE MAP (Multi-Round Support) ---
|
|
7
|
-
const SIG_FILE = path.join(
|
|
10
|
+
const SIG_FILE = path.join(getConfigDir(), 'pollinations-signature.json');
|
|
8
11
|
let signatureMap = {};
|
|
9
12
|
let lastSignature = null; // V1 Fallback Global
|
|
10
|
-
function log(msg) {
|
|
11
|
-
try {
|
|
12
|
-
const ts = new Date().toISOString();
|
|
13
|
-
if (!fs.existsSync('/tmp/opencode_pollinations_debug.log')) {
|
|
14
|
-
fs.writeFileSync('/tmp/opencode_pollinations_debug.log', '');
|
|
15
|
-
}
|
|
16
|
-
fs.appendFileSync('/tmp/opencode_pollinations_debug.log', `[Proxy] ${ts} ${msg}\n`);
|
|
17
|
-
}
|
|
18
|
-
catch (e) { }
|
|
19
|
-
}
|
|
20
13
|
try {
|
|
21
14
|
if (fs.existsSync(SIG_FILE)) {
|
|
22
15
|
signatureMap = JSON.parse(fs.readFileSync(SIG_FILE, 'utf-8'));
|
|
@@ -265,25 +258,57 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
265
258
|
}
|
|
266
259
|
}
|
|
267
260
|
log(`Incoming Model (OpenCode ID): ${body.model}`);
|
|
268
|
-
// 0.
|
|
269
|
-
if (body.model === '
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
261
|
+
// 0. TEST 4: Virtual Model Handler for Commands
|
|
262
|
+
if (body.model === 'pollinations/pollimock-handler' || body.model === 'pollimock-handler') {
|
|
263
|
+
const mockContent = "🚀 **[TEST 4] Modèle Virtuel de Commande !**\n\nCe texte n'a jamais quitté ton ordinateur. La commande `/pollimock` a demandé à OpenCode de se brancher temporairement sur le modèle virtuel `pollimock-handler`.\nLe proxy a intercepté cet appel et répondu instantanément.\n\n✅ L'historique affiche bien le message du Chat, **mais la requête LLM est totalement court-circuitée**.\nC'est la méthode ultime pour créer des vues de configuration via commandes (`/pollinations-config` par ex) sans polluer le crédit ou les LLM tiers !";
|
|
264
|
+
res.writeHead(200, {
|
|
265
|
+
'Content-Type': 'text/event-stream',
|
|
266
|
+
'Cache-Control': 'no-cache',
|
|
267
|
+
'Connection': 'keep-alive'
|
|
268
|
+
});
|
|
269
|
+
const chunk = JSON.stringify({
|
|
270
|
+
id: 'mock-' + Date.now(),
|
|
271
|
+
object: 'chat.completion.chunk',
|
|
272
|
+
created: Math.floor(Date.now() / 1000),
|
|
273
|
+
model: body.model,
|
|
274
|
+
choices: [{ index: 0, delta: { role: 'assistant', content: mockContent }, finish_reason: null }]
|
|
275
|
+
});
|
|
276
|
+
res.write(`data: ${chunk}\n\n`);
|
|
277
|
+
const chunkEnd = JSON.stringify({
|
|
278
|
+
id: 'mock-' + Date.now(),
|
|
279
|
+
object: 'chat.completion.chunk',
|
|
273
280
|
created: Math.floor(Date.now() / 1000),
|
|
274
|
-
model:
|
|
281
|
+
model: body.model,
|
|
282
|
+
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }]
|
|
283
|
+
});
|
|
284
|
+
res.write(`data: ${chunkEnd}\n\n`);
|
|
285
|
+
res.write('data: [DONE]\n\n');
|
|
286
|
+
res.end();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// 0. SPECIAL: pollinations/connect (Guide & Status)
|
|
290
|
+
const CONNECT_MODEL_IDS = ['pollinations/connect', 'free/pollinations/connect', 'enter/pollinations/connect', 'connect-pollinations'];
|
|
291
|
+
if (CONNECT_MODEL_IDS.includes(body.model)) {
|
|
292
|
+
const guideContent = await buildConnectResponse(config);
|
|
293
|
+
res.writeHead(200, {
|
|
294
|
+
'Content-Type': 'text/event-stream',
|
|
295
|
+
'Cache-Control': 'no-cache',
|
|
296
|
+
'Connection': 'keep-alive'
|
|
297
|
+
});
|
|
298
|
+
const chunk = JSON.stringify({
|
|
299
|
+
id: 'connect-' + Date.now(),
|
|
300
|
+
object: 'chat.completion.chunk',
|
|
301
|
+
created: Math.floor(Date.now() / 1000),
|
|
302
|
+
model: 'pollinations/connect',
|
|
275
303
|
choices: [{
|
|
276
304
|
index: 0,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
};
|
|
285
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
286
|
-
res.end(JSON.stringify(connectMsg));
|
|
305
|
+
delta: { role: 'assistant', content: guideContent },
|
|
306
|
+
finish_reason: 'stop' // Instant finish
|
|
307
|
+
}]
|
|
308
|
+
});
|
|
309
|
+
res.write(`data: ${chunk}\n\n`);
|
|
310
|
+
res.write(`data: [DONE]\n\n`);
|
|
311
|
+
res.end();
|
|
287
312
|
return;
|
|
288
313
|
}
|
|
289
314
|
// 1. STRICT ROUTING & SAFETY NET LOGIC (V5)
|
package/dist/server/quota.d.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
timestamp: string;
|
|
3
|
-
type: string;
|
|
4
|
-
model: string;
|
|
5
|
-
meter_source: 'tier' | 'pack';
|
|
6
|
-
cost_usd: number;
|
|
7
|
-
requests?: number;
|
|
8
|
-
}
|
|
1
|
+
import { DetailedUsageEntry } from './pollinations-api.js';
|
|
9
2
|
export interface QuotaStatus {
|
|
10
3
|
tierRemaining: number;
|
|
11
4
|
tierUsed: number;
|
|
@@ -20,5 +13,6 @@ export interface QuotaStatus {
|
|
|
20
13
|
tierEmoji: string;
|
|
21
14
|
errorType?: 'auth_limited' | 'network' | 'unknown';
|
|
22
15
|
}
|
|
16
|
+
export declare function fetchUsageForPeriod(apiKey: string, lastReset: Date): Promise<DetailedUsageEntry[]>;
|
|
23
17
|
export declare function getQuotaStatus(forceRefresh?: boolean): Promise<QuotaStatus>;
|
|
24
18
|
export declare function formatQuotaForToast(quota: QuotaStatus): string;
|
package/dist/server/quota.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
1
|
import * as https from 'https'; // Use Native HTTPS
|
|
4
|
-
import * as crypto from 'crypto';
|
|
5
2
|
import { loadConfig } from './config.js';
|
|
6
3
|
// === CACHE & CONSTANTS ===
|
|
7
4
|
const CACHE_TTL = 30000; // 30 secondes
|
|
@@ -18,71 +15,45 @@ const TIER_LIMITS = {
|
|
|
18
15
|
nectar: { pollen: 20, emoji: '🍯' },
|
|
19
16
|
};
|
|
20
17
|
// === LOGGING ===
|
|
18
|
+
import { logApi } from './logger.js';
|
|
21
19
|
function logQuota(msg) {
|
|
22
|
-
|
|
23
|
-
fs.appendFileSync('/tmp/pollinations_quota_debug.log', `[${new Date().toISOString()}] ${msg}\n`);
|
|
24
|
-
}
|
|
25
|
-
catch (e) { }
|
|
20
|
+
logApi(`[QUOTA] ${msg}`);
|
|
26
21
|
}
|
|
27
|
-
// ===
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
// === SMART FETCH API ===
|
|
23
|
+
export async function fetchUsageForPeriod(apiKey, lastReset) {
|
|
24
|
+
let allUsage = [];
|
|
25
|
+
let offset = 0;
|
|
26
|
+
const limit = 100; // Bulk fetch
|
|
27
|
+
while (true) {
|
|
28
|
+
let usageRes;
|
|
32
29
|
try {
|
|
33
|
-
|
|
30
|
+
usageRes = await fetchAPI(`/account/usage?limit=${limit}&offset=${offset}`, apiKey);
|
|
34
31
|
}
|
|
35
|
-
catch (e) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
function computeEntrySignature(entry) {
|
|
40
|
-
// Unique signature per transaction: timestamp + model + cost + source
|
|
41
|
-
return crypto.createHash('md5').update(`${entry.timestamp}|${entry.model}|${entry.cost_usd}|${entry.meter_source}`).digest('hex');
|
|
42
|
-
}
|
|
43
|
-
function updateLocalHistory(newEntries) {
|
|
44
|
-
const filePath = getHistoryFilePath();
|
|
45
|
-
let history = [];
|
|
46
|
-
// 1. Load existing
|
|
47
|
-
try {
|
|
48
|
-
if (fs.existsSync(filePath)) {
|
|
49
|
-
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
50
|
-
history = JSON.parse(raw);
|
|
32
|
+
catch (e) {
|
|
33
|
+
logQuota(`SmartFetch failed at offset ${offset}: ${e}`);
|
|
34
|
+
break;
|
|
51
35
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
logQuota(`Failed to load history: ${e}`);
|
|
55
|
-
history = [];
|
|
56
|
-
}
|
|
57
|
-
// 2. Merge (Deduplication via Signature)
|
|
58
|
-
const existingSignatures = new Set(history.map(computeEntrySignature));
|
|
59
|
-
let addedCount = 0;
|
|
60
|
-
for (const entry of newEntries) {
|
|
61
|
-
const sig = computeEntrySignature(entry);
|
|
62
|
-
if (!existingSignatures.has(sig)) {
|
|
63
|
-
history.push(entry);
|
|
64
|
-
existingSignatures.add(sig);
|
|
65
|
-
addedCount++;
|
|
36
|
+
if (!usageRes.usage || usageRes.usage.length === 0) {
|
|
37
|
+
break; // No more records
|
|
66
38
|
}
|
|
39
|
+
let reachedCutoff = false;
|
|
40
|
+
for (const entry of usageRes.usage) {
|
|
41
|
+
const timestampStr = entry.timestamp.includes('Z') ? entry.timestamp : entry.timestamp.replace(' ', 'T') + 'Z';
|
|
42
|
+
const entryTime = new Date(timestampStr);
|
|
43
|
+
if (entryTime < lastReset) {
|
|
44
|
+
reachedCutoff = true;
|
|
45
|
+
break; // Entry is from previous period, stop.
|
|
46
|
+
}
|
|
47
|
+
allUsage.push(entry);
|
|
48
|
+
}
|
|
49
|
+
// If we found an entry older than lastReset, or if the page was not full, we reached the end.
|
|
50
|
+
if (reachedCutoff || usageRes.usage.length < limit) {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
offset += limit;
|
|
67
54
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const beforePrune = history.length;
|
|
71
|
-
history = history.filter(e => {
|
|
72
|
-
const entryTime = new Date(e.timestamp.replace(' ', 'T') + 'Z').getTime();
|
|
73
|
-
return (now - entryTime) < HISTORY_RETENTION_MS;
|
|
74
|
-
});
|
|
75
|
-
// 4. Sort (Newest first)
|
|
76
|
-
history.sort((a, b) => new Date(b.timestamp.replace(' ', 'T') + 'Z').getTime() - new Date(a.timestamp.replace(' ', 'T') + 'Z').getTime());
|
|
77
|
-
// 5. Save
|
|
78
|
-
try {
|
|
79
|
-
fs.writeFileSync(filePath, JSON.stringify(history, null, 2));
|
|
80
|
-
logQuota(`History Update: Added ${addedCount}, Pruned ${beforePrune - history.length}, Total ${history.length} entries.`);
|
|
81
|
-
}
|
|
82
|
-
catch (e) {
|
|
83
|
-
logQuota(`Failed to save history: ${e}`);
|
|
84
|
-
}
|
|
85
|
-
return history;
|
|
55
|
+
logQuota(`SmartFetch: Retrieved ${allUsage.length} transactions for current period.`);
|
|
56
|
+
return allUsage;
|
|
86
57
|
}
|
|
87
58
|
// === MAIN QUOTA FUNCTION ===
|
|
88
59
|
export async function getQuotaStatus(forceRefresh = false) {
|
|
@@ -100,17 +71,17 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
100
71
|
// SEQUENTIAL FETCH (Avoid Rate Limits)
|
|
101
72
|
const profileRes = await fetchAPI('/account/profile', config.apiKey);
|
|
102
73
|
const balanceRes = await fetchAPI('/account/balance', config.apiKey);
|
|
103
|
-
const usageRes = await fetchAPI('/account/usage', config.apiKey);
|
|
104
|
-
logQuota(`Fetch Success. Tier: ${profileRes.tier}, Balance: ${balanceRes.balance}`);
|
|
105
74
|
const profile = profileRes;
|
|
106
75
|
const balance = balanceRes.balance;
|
|
107
|
-
// 2.
|
|
108
|
-
const
|
|
76
|
+
// 2. Convertir Timezone : Obtenir instant exact du Reset
|
|
77
|
+
const resetInfo = calculateResetInfo(profile.nextResetAt);
|
|
78
|
+
logQuota(`Fetch Success. Tier: ${profile.tier}, Balance: ${balance}, Next Reset: ${profile.nextResetAt}`);
|
|
79
|
+
// 3. Smart Fetch : Récupérer uniquement les dépenses du jour (depuis lastReset)
|
|
80
|
+
const periodUsage = await fetchUsageForPeriod(config.apiKey, resetInfo.lastReset);
|
|
109
81
|
const tierInfo = TIER_LIMITS[profile.tier] || { pollen: 1, emoji: '❓' };
|
|
110
82
|
const tierLimit = tierInfo.pollen;
|
|
111
|
-
//
|
|
112
|
-
const
|
|
113
|
-
const { tierUsed } = calculateCurrentPeriodUsage(fullHistory, resetInfo);
|
|
83
|
+
// 4. Calcul Strict FreeTier / Wallet
|
|
84
|
+
const { tierUsed } = calculateCurrentPeriodUsage(periodUsage, resetInfo);
|
|
114
85
|
// 4. Calculate Balances
|
|
115
86
|
const tierRemaining = Math.max(0, tierLimit - tierUsed);
|
|
116
87
|
// Fix rounding errors
|
|
@@ -201,33 +172,20 @@ function fetchAPI(endpoint, apiKey) {
|
|
|
201
172
|
});
|
|
202
173
|
}
|
|
203
174
|
function calculateResetInfo(nextResetAt) {
|
|
204
|
-
const
|
|
175
|
+
const nextReset = new Date(nextResetAt);
|
|
176
|
+
const lastReset = new Date(nextReset.getTime() - ONE_DAY_MS);
|
|
205
177
|
const now = new Date();
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
const todayResetUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), resetHour, resetMinute, resetSecond));
|
|
210
|
-
let lastReset;
|
|
211
|
-
let nextReset;
|
|
212
|
-
if (now >= todayResetUTC) {
|
|
213
|
-
lastReset = todayResetUTC;
|
|
214
|
-
nextReset = new Date(todayResetUTC.getTime() + ONE_DAY_MS);
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
lastReset = new Date(todayResetUTC.getTime() - ONE_DAY_MS);
|
|
218
|
-
nextReset = todayResetUTC;
|
|
219
|
-
}
|
|
220
|
-
const timeUntilReset = nextReset.getTime() - now.getTime();
|
|
221
|
-
const timeSinceReset = now.getTime() - lastReset.getTime();
|
|
222
|
-
const progressPercent = (timeSinceReset / ONE_DAY_MS) * 100;
|
|
178
|
+
const timeUntilReset = Math.max(0, nextReset.getTime() - now.getTime());
|
|
179
|
+
const timeSinceReset = Math.max(0, now.getTime() - lastReset.getTime());
|
|
180
|
+
const progressPercent = Math.min(100, (timeSinceReset / ONE_DAY_MS) * 100);
|
|
223
181
|
return {
|
|
224
182
|
nextReset,
|
|
225
183
|
lastReset,
|
|
226
184
|
timeUntilReset,
|
|
227
185
|
timeSinceReset,
|
|
228
|
-
resetHour,
|
|
229
|
-
resetMinute,
|
|
230
|
-
resetSecond,
|
|
186
|
+
resetHour: nextReset.getUTCHours(),
|
|
187
|
+
resetMinute: nextReset.getUTCMinutes(),
|
|
188
|
+
resetSecond: nextReset.getUTCSeconds(),
|
|
231
189
|
progressPercent
|
|
232
190
|
};
|
|
233
191
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* pollinations_pricing.ts — CLI Pricing Table for enter.pollinations.ai
|
|
4
|
+
* * Usage: npx tsx pollinations_pricing.ts [--api-key YOUR_KEY] [--debug] [--mode normal|dynamic]
|
|
5
|
+
* * - Mode 'normal' (défaut) : Affiche le miroir exact du dashboard marketing.
|
|
6
|
+
* - Mode 'dynamic' : Calcule le vrai coût token par token (Cost Guard Estimator).
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* pollinations_pricing.ts — CLI Pricing Table for enter.pollinations.ai
|
|
4
|
+
* * Usage: npx tsx pollinations_pricing.ts [--api-key YOUR_KEY] [--debug] [--mode normal|dynamic]
|
|
5
|
+
* * - Mode 'normal' (défaut) : Affiche le miroir exact du dashboard marketing.
|
|
6
|
+
* - Mode 'dynamic' : Calcule le vrai coût token par token (Cost Guard Estimator).
|
|
7
|
+
*/
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const API_KEY = args.includes("--api-key") ? args[args.indexOf("--api-key") + 1] : process.env.POLLINATIONS_API_KEY ?? "";
|
|
10
|
+
const DEBUG = args.includes("--debug");
|
|
11
|
+
const modeIndex = args.indexOf("--mode");
|
|
12
|
+
const MODE = modeIndex !== -1 ? args[modeIndex + 1] : "normal";
|
|
13
|
+
const H = API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {};
|
|
14
|
+
async function get(url) {
|
|
15
|
+
const r = await fetch(url, { headers: H });
|
|
16
|
+
if (!r.ok)
|
|
17
|
+
throw new Error(`HTTP ${r.status} on ${url}`);
|
|
18
|
+
return r.json();
|
|
19
|
+
}
|
|
20
|
+
// ─── COST GUARD ESTIMATOR (Pour le mode dynamic) ─────────────────────────────
|
|
21
|
+
const BENCH = {
|
|
22
|
+
TEXT_IN_TOK: 300,
|
|
23
|
+
TEXT_OUT_TOK: 800,
|
|
24
|
+
IMG_TOKENS_PER_STD: 1667,
|
|
25
|
+
VIDEO_DEFAULT_SEC: 6,
|
|
26
|
+
STT_CLIP_SEC: 30,
|
|
27
|
+
TTS_CHARS: 200,
|
|
28
|
+
MUSIC_SEC: 50,
|
|
29
|
+
VIDEO_DURATION: { veo: 6, "seedance-pro": 5, seedance: 5, "ltx-2": 6, wan: 5, "grok-video": 5 },
|
|
30
|
+
VIDEO_DEFAULT_TOKENS: 50_000,
|
|
31
|
+
};
|
|
32
|
+
let globalModelStats = [];
|
|
33
|
+
function n(v) {
|
|
34
|
+
const x = Number(v);
|
|
35
|
+
return v === undefined || v === null || isNaN(x) || x === 0 ? null : x;
|
|
36
|
+
}
|
|
37
|
+
function perM(v) {
|
|
38
|
+
const x = n(v);
|
|
39
|
+
if (!x)
|
|
40
|
+
return "—";
|
|
41
|
+
const m = x * 1_000_000;
|
|
42
|
+
return m >= 100 ? `${m.toFixed(1)}/M` : m >= 10 ? `${m.toFixed(2)}/M` : m >= 1 ? `${m.toFixed(3)}/M` : `${m.toFixed(4)}/M`;
|
|
43
|
+
}
|
|
44
|
+
function flat(v, unit) {
|
|
45
|
+
const x = n(v);
|
|
46
|
+
if (!x)
|
|
47
|
+
return "—";
|
|
48
|
+
return x >= 1 ? `${x.toFixed(3)}/${unit}` : x >= 0.01 ? `${x.toFixed(4)}/${unit}` : `${x.toFixed(5)}/${unit}`;
|
|
49
|
+
}
|
|
50
|
+
function per1pollen(cost) {
|
|
51
|
+
if (!cost || cost <= 0)
|
|
52
|
+
return "—";
|
|
53
|
+
const x = 1 / cost;
|
|
54
|
+
if (x >= 1_000_000)
|
|
55
|
+
return `${(x / 1_000_000).toFixed(1)}M`;
|
|
56
|
+
if (x >= 100_000)
|
|
57
|
+
return `${Math.round(x / 1000)}K`;
|
|
58
|
+
if (x >= 10_000)
|
|
59
|
+
return `${(x / 1000).toFixed(1)}K`.replace(/\.0K$/, "K");
|
|
60
|
+
if (x >= 1_000)
|
|
61
|
+
return `${Math.round(x / 100) * 100}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
62
|
+
if (x >= 100)
|
|
63
|
+
return `${Math.round(x)}`;
|
|
64
|
+
if (x >= 10)
|
|
65
|
+
return `${Math.round(x * 10) / 10}`;
|
|
66
|
+
return `${x.toFixed(1)}`;
|
|
67
|
+
}
|
|
68
|
+
function getCost(m, type) {
|
|
69
|
+
// 1. Priorité aux Stats Tinybird si on est en mode normal
|
|
70
|
+
if (MODE === "normal") {
|
|
71
|
+
// Tente de trouver la variable native à la racine (si l'API l'expose)
|
|
72
|
+
const rootCost = n(m.realAvgCost) ?? n(m.baseCost) ?? n(m.pollenCost) ?? n(m.averageCost);
|
|
73
|
+
if (rootCost)
|
|
74
|
+
return rootCost;
|
|
75
|
+
// Fallback dynamique sur les stats Tinybird
|
|
76
|
+
const stat = globalModelStats.find(s => s.model === m.name);
|
|
77
|
+
if (stat && stat.avg_cost_usd > 0)
|
|
78
|
+
return stat.avg_cost_usd;
|
|
79
|
+
}
|
|
80
|
+
// 2. Calcul mathématique (Mode Dynamic ou si Modèle inconnu)
|
|
81
|
+
const pr = m.pricing ?? {};
|
|
82
|
+
if (type === "text") {
|
|
83
|
+
return (n(pr.promptTextTokens) ?? 0) * BENCH.TEXT_IN_TOK + (n(pr.completionTextTokens) ?? 0) * BENCH.TEXT_OUT_TOK || null;
|
|
84
|
+
}
|
|
85
|
+
if (type === "image") {
|
|
86
|
+
const outPrice = n(pr.completionImageTokens);
|
|
87
|
+
if (!outPrice)
|
|
88
|
+
return null;
|
|
89
|
+
if (pr.promptTextTokens) {
|
|
90
|
+
return (n(pr.promptTextTokens) ?? 0) * 100 + (n(pr.promptImageTokens) ?? 0) * 100 + (outPrice * BENCH.IMG_TOKENS_PER_STD);
|
|
91
|
+
}
|
|
92
|
+
return outPrice;
|
|
93
|
+
}
|
|
94
|
+
if (type === "video") {
|
|
95
|
+
if (pr.completionVideoSeconds) {
|
|
96
|
+
return ((n(pr.completionVideoSeconds) ?? 0) + (n(pr.completionAudioSeconds) ?? 0)) * (BENCH.VIDEO_DURATION[m.name] ?? BENCH.VIDEO_DEFAULT_SEC);
|
|
97
|
+
}
|
|
98
|
+
return n(pr.completionVideoTokens) ? (n(pr.completionVideoTokens) ?? 0) * BENCH.VIDEO_DEFAULT_TOKENS : null;
|
|
99
|
+
}
|
|
100
|
+
if (type === "audio") {
|
|
101
|
+
if (n(pr.promptAudioSeconds))
|
|
102
|
+
return (n(pr.promptAudioSeconds) ?? 0) * BENCH.STT_CLIP_SEC;
|
|
103
|
+
if (n(pr.completionAudioSeconds))
|
|
104
|
+
return (n(pr.completionAudioSeconds) ?? 0) * BENCH.MUSIC_SEC;
|
|
105
|
+
if (n(pr.completionAudioTokens))
|
|
106
|
+
return (n(pr.completionAudioTokens) ?? 0) * BENCH.TTS_CHARS;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
// ─── RENDU VISUEL ────────────────────────────────────────────────────────────
|
|
111
|
+
function parseNameDesc(m) {
|
|
112
|
+
const fullDesc = m.description || m.name;
|
|
113
|
+
const parts = fullDesc.split(" - ");
|
|
114
|
+
if (parts.length > 1) {
|
|
115
|
+
return { nom: parts[0].trim(), desc: parts.slice(1).join(" - ").trim() };
|
|
116
|
+
}
|
|
117
|
+
return { nom: fullDesc, desc: "" };
|
|
118
|
+
}
|
|
119
|
+
function flags(m, overrides = []) {
|
|
120
|
+
const f = [];
|
|
121
|
+
if (m.paid_only)
|
|
122
|
+
f.push("💎");
|
|
123
|
+
const allFlags = [...(m.input_modalities || []), ...(m.output_modalities || []), ...overrides, m.name];
|
|
124
|
+
const str = allFlags.join(" ").toLowerCase();
|
|
125
|
+
if (str.includes("image") || str.includes("👁️"))
|
|
126
|
+
f.push("👁️");
|
|
127
|
+
if (m.reasoning || str.includes("reasoning"))
|
|
128
|
+
f.push("🧠");
|
|
129
|
+
if (str.includes("audio") && !overrides.includes("🔊") || str.includes("whisper") || str.includes("scribe") || str.includes("🎙️"))
|
|
130
|
+
f.push("🎙️");
|
|
131
|
+
if (str.includes("search") || str.includes("sonar") || str.includes("gemini"))
|
|
132
|
+
f.push("🔍");
|
|
133
|
+
if (m.output_modalities?.includes("audio") || overrides.includes("🔊") || str.includes("tts") || str.includes("music"))
|
|
134
|
+
f.push("🔊");
|
|
135
|
+
if (str.includes("coder") || str.includes("code") || str.includes("gemini"))
|
|
136
|
+
f.push("💻");
|
|
137
|
+
if ((m.context_window ?? 0) >= 100_000)
|
|
138
|
+
f.push(`📏${((m.context_window ?? 0) / 1000).toFixed(0)}k`);
|
|
139
|
+
return f.filter((v, i, a) => a.indexOf(v) === i).join(" ");
|
|
140
|
+
}
|
|
141
|
+
function renderTable(title, models, type, extraFlags = []) {
|
|
142
|
+
if (models.length === 0)
|
|
143
|
+
return "";
|
|
144
|
+
// Tri par coût (du moins cher au plus cher), puis ordre alphabétique
|
|
145
|
+
const sorted = [...models].sort((a, b) => {
|
|
146
|
+
const costA = getCost(a, type) ?? 0;
|
|
147
|
+
const costB = getCost(b, type) ?? 0;
|
|
148
|
+
const diff = costA - costB;
|
|
149
|
+
if (diff !== 0)
|
|
150
|
+
return diff;
|
|
151
|
+
return a.name.localeCompare(b.name);
|
|
152
|
+
});
|
|
153
|
+
const lines = [
|
|
154
|
+
`\n## ${title}\n`,
|
|
155
|
+
`| Nom | ID | Capabilities | 1 pollen ≈ | Pricing |`,
|
|
156
|
+
`|---|---|---|---|---|`
|
|
157
|
+
];
|
|
158
|
+
for (const m of sorted) {
|
|
159
|
+
const pr = m.pricing ?? {};
|
|
160
|
+
const parts = [];
|
|
161
|
+
// Génération de la colonne Pricing en fonction du type
|
|
162
|
+
if (type === "text") {
|
|
163
|
+
if (n(pr.promptTextTokens))
|
|
164
|
+
parts.push(`💬 ${perM(pr.promptTextTokens)}`);
|
|
165
|
+
if (n(pr.promptCachedTokens))
|
|
166
|
+
parts.push(`💾 ${perM(pr.promptCachedTokens)}`);
|
|
167
|
+
if (n(pr.completionTextTokens))
|
|
168
|
+
parts.push(`💬 ${perM(pr.completionTextTokens)}`);
|
|
169
|
+
}
|
|
170
|
+
else if (type === "image") {
|
|
171
|
+
if (pr.promptTextTokens)
|
|
172
|
+
parts.push(`💬 ${perM(pr.promptTextTokens)}`);
|
|
173
|
+
if (pr.completionImageTokens)
|
|
174
|
+
parts.push(`🖼️ ${pr.promptTextTokens ? perM(pr.completionImageTokens) : flat(pr.completionImageTokens, "img")}`);
|
|
175
|
+
}
|
|
176
|
+
else if (type === "video") {
|
|
177
|
+
if (pr.completionVideoSeconds)
|
|
178
|
+
parts.push(`🎬 ${flat(pr.completionVideoSeconds, "sec")}`);
|
|
179
|
+
if (pr.completionVideoTokens)
|
|
180
|
+
parts.push(`🎬 ${perM(pr.completionVideoTokens)}`);
|
|
181
|
+
}
|
|
182
|
+
else if (type === "audio") {
|
|
183
|
+
if (pr.promptAudioSeconds)
|
|
184
|
+
parts.push(`🎬 ${flat(pr.promptAudioSeconds, "sec")}`);
|
|
185
|
+
if (pr.completionAudioTokens)
|
|
186
|
+
parts.push(`🔊 ${((n(pr.completionAudioTokens) ?? 0) * 1000).toFixed(2)}/1K chars`);
|
|
187
|
+
if (pr.completionAudioSeconds)
|
|
188
|
+
parts.push(`🎬 ${flat(pr.completionAudioSeconds, "sec")}`);
|
|
189
|
+
}
|
|
190
|
+
const { nom } = parseNameDesc(m);
|
|
191
|
+
lines.push(`| ${nom} | \`${m.name}\` | ${flags(m, extraFlags)} | ${per1pollen(getCost(m, type))} | ${parts.join(" · ") || "—"} |`);
|
|
192
|
+
}
|
|
193
|
+
return lines.join("\n");
|
|
194
|
+
}
|
|
195
|
+
// ─── DEBUG ULTIME ────────────────────────────────────────────────────────────
|
|
196
|
+
function debugDump(label, models) {
|
|
197
|
+
if (!DEBUG)
|
|
198
|
+
return;
|
|
199
|
+
console.error(`\n[debug] ═══ ROOT OBJECT DUMP: ${label} (${models.length} models) ═══`);
|
|
200
|
+
for (const m of models) {
|
|
201
|
+
const clone = { ...m };
|
|
202
|
+
delete clone.description; // Pour garder le terminal lisible
|
|
203
|
+
console.error(JSON.stringify(clone, null, 2));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// ─── EXECUTION MAIN ──────────────────────────────────────────────────────────
|
|
207
|
+
const BASE = "https://gen.pollinations.ai";
|
|
208
|
+
const STATS = "https://enter.pollinations.ai/api/model-stats";
|
|
209
|
+
const [text, imgRaw, audRaw, statsRaw] = await Promise.all([
|
|
210
|
+
get(`${BASE}/text/models`),
|
|
211
|
+
get(`${BASE}/image/models`),
|
|
212
|
+
get(`${BASE}/audio/models`),
|
|
213
|
+
get(STATS).catch(() => ({ data: [] }))
|
|
214
|
+
]);
|
|
215
|
+
globalModelStats = statsRaw.data;
|
|
216
|
+
// Séparation des modèles images et vidéos qui sont mixés sur le même endpoint
|
|
217
|
+
const isVid = (m) => (m.pricing?.completionVideoSeconds || m.pricing?.completionVideoTokens) !== undefined;
|
|
218
|
+
const img = imgRaw.filter(m => !isVid(m));
|
|
219
|
+
const vid = imgRaw.filter(m => isVid(m));
|
|
220
|
+
// Séparation Audio
|
|
221
|
+
const isSTT = (m) => !!m.pricing?.promptAudioSeconds;
|
|
222
|
+
const isMusic = (m) => m.name.includes("music");
|
|
223
|
+
const stt = audRaw.filter(m => isSTT(m));
|
|
224
|
+
const tts = audRaw.filter(m => !isSTT(m) && !isMusic(m));
|
|
225
|
+
const music = audRaw.filter(m => isMusic(m));
|
|
226
|
+
debugDump("IMAGE API", img);
|
|
227
|
+
debugDump("VIDEO API", vid);
|
|
228
|
+
debugDump("AUDIO API", audRaw);
|
|
229
|
+
debugDump("TEXT API", text);
|
|
230
|
+
console.log(`# 🌸 Pollinations — Live Model Pricing (Mode: ${MODE.toUpperCase()})`);
|
|
231
|
+
console.log(`> **${new Date().toISOString()}** · ${img.length} image · ${vid.length} video · ${audRaw.length} audio · ${text.length} text`);
|
|
232
|
+
console.log(renderTable("🖼️ Image", img, "image"));
|
|
233
|
+
console.log(renderTable("🎬 Video", vid, "video", ["👁️"]));
|
|
234
|
+
console.log(renderTable("🎙️ Speech-to-Text", stt, "audio", ["🎙️"]));
|
|
235
|
+
console.log(renderTable("🔊 Text-to-Speech", tts, "audio", ["🔊"]));
|
|
236
|
+
console.log(renderTable("🎵 Music", music, "audio", ["🔊"]));
|
|
237
|
+
console.log(renderTable("📝 Text", text, "text"));
|
|
238
|
+
console.log(`\n> **Capabilities** : 👁️ vision · 🧠 reasoning · 🎙️ audio in · 🔍 search · 🔊 audio out · 💻 code exec`);
|
|
239
|
+
console.log(`> **Token Types** : 💬 text · 🖼️ image · 💾 cached · 🎬 video · 🔊 audio`);
|
|
240
|
+
console.log(`> **Pricing Metrics** : /img = flat rate per image · /M = per million tokens · /sec = per second of video · /1K chars = per 1000 characters`);
|
|
241
|
+
console.log(`> **Other** : 💎 PAID ONLY (Wallet direct) · 📏 Contexte API max`);
|
|
242
|
+
console.log(`\n### 💡 How Pollen is Spent
|
|
243
|
+
1. Daily tier grants are used first
|
|
244
|
+
2. Purchased pollen is used after daily is depleted
|
|
245
|
+
⚠️ **Exception**: 💎 Paid Only models require purchased pollen only`);
|
|
246
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const API_KEY = process.env.POLLINATIONS_API_KEY || 'sk_eZbhgG1oJaaqSZKMvmy8nfVH9NNAGp0H';
|
|
2
|
+
async function logBalance(label) {
|
|
3
|
+
try {
|
|
4
|
+
const res = await fetch('https://gen.pollinations.ai/account/balance', {
|
|
5
|
+
headers: { 'Authorization': `Bearer ${API_KEY}` }
|
|
6
|
+
});
|
|
7
|
+
const bal = await res.json();
|
|
8
|
+
console.log(`\n[${label}] /account/balance :`);
|
|
9
|
+
console.log(JSON.stringify(bal, null, 2));
|
|
10
|
+
const resUsage = await fetch('https://gen.pollinations.ai/account/usage/daily', {
|
|
11
|
+
headers: { 'Authorization': `Bearer ${API_KEY}` }
|
|
12
|
+
});
|
|
13
|
+
const usg = await resUsage.json();
|
|
14
|
+
// Sum today stats
|
|
15
|
+
let costTier = 0;
|
|
16
|
+
let costPack = 0;
|
|
17
|
+
let req = 0;
|
|
18
|
+
if (usg.usage && usg.usage.length > 0) {
|
|
19
|
+
const todayDate = usg.usage[0].date;
|
|
20
|
+
for (const x of usg.usage) {
|
|
21
|
+
if (x.date === todayDate) {
|
|
22
|
+
req += x.requests;
|
|
23
|
+
if (x.meter_source === 'tier')
|
|
24
|
+
costTier += x.cost_usd;
|
|
25
|
+
else
|
|
26
|
+
costPack += x.cost_usd;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
console.log(`[${label}] /account/usage/daily (Today sum) : Requests = ${req}, TierCost = ${costTier}, PackCost = ${costPack}`);
|
|
31
|
+
return { bal, usage: { costTier, costPack, req } };
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
console.error("Error fetching balance/usage:", e.message);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function run() {
|
|
39
|
+
console.log("=== COST ENDPOINTS PROBE ===");
|
|
40
|
+
// 1. Avant
|
|
41
|
+
const stateBefore = await logBalance("BEFORE GENERATION");
|
|
42
|
+
// 2. Générer Fake Image (Flux : censé être free tier ou cheap)
|
|
43
|
+
console.log("\n🎬 Génération d'une image (modèle 'flux')...");
|
|
44
|
+
const url = 'https://gen.pollinations.ai/image/a_beautiful_red_square_cost_test?model=flux&width=512&height=512&nologo=true';
|
|
45
|
+
const start = Date.now();
|
|
46
|
+
const res = await fetch(url, { headers: { 'Authorization': `Bearer ${API_KEY}` } });
|
|
47
|
+
const buffer = await res.arrayBuffer();
|
|
48
|
+
console.log(`✅ Image reçue en ${Date.now() - start}ms, taille: ${buffer.byteLength}B`);
|
|
49
|
+
console.log(`Headers Cost:`, {
|
|
50
|
+
'x-usage-cost-usd': res.headers.get('x-usage-cost-usd'),
|
|
51
|
+
'x-usage-completion-image-tokens': res.headers.get('x-usage-completion-image-tokens')
|
|
52
|
+
});
|
|
53
|
+
// 3. Immediatement Apres
|
|
54
|
+
await logBalance("IMMEDIATELY AFTER");
|
|
55
|
+
// 4. Attente 5 secondes
|
|
56
|
+
console.log("\n⏳ Attente 5 secondes pour propagation /usage...");
|
|
57
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
58
|
+
await logBalance("5 SECONDS AFTER");
|
|
59
|
+
}
|
|
60
|
+
run();
|
|
61
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as https from 'https';
|
|
2
|
+
function fetchJson(url) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
https.get(url, (res) => {
|
|
5
|
+
let data = '';
|
|
6
|
+
res.on('data', chunk => data += chunk);
|
|
7
|
+
res.on('end', () => resolve(JSON.parse(data)));
|
|
8
|
+
}).on('error', reject);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
async function auditDynamicPricing() {
|
|
12
|
+
console.log("=== AUDIT PRICING DYNAMIQUE (TINYBIRD) ===");
|
|
13
|
+
try {
|
|
14
|
+
console.log("Fetching /api/model-stats...");
|
|
15
|
+
const statsProxy = await fetchJson('https://enter.pollinations.ai/api/model-stats');
|
|
16
|
+
const stats = statsProxy.data || [];
|
|
17
|
+
console.log(`\nReçu ${stats.length} statistiques de prix moyens (average cost) depuis Tinybird:`);
|
|
18
|
+
// On affiche les 10 premiers pour l'exemple
|
|
19
|
+
console.log(stats.slice(0, 10).map((s) => `- ${s.model}: ${parseFloat(s.avg_cost_usd).toFixed(6)}$/req (${s.request_count} reqs)`).join('\n'));
|
|
20
|
+
console.log("\nFetching /image/models...");
|
|
21
|
+
const imageModels = await fetchJson('https://gen.pollinations.ai/image/models');
|
|
22
|
+
console.log("\nCroisement Models API vs Tinybird Stats:");
|
|
23
|
+
for (const m of imageModels.slice(0, 5)) {
|
|
24
|
+
const stat = stats.find((s) => s.model === m.name);
|
|
25
|
+
console.log(`\nModèle: ${m.name}`);
|
|
26
|
+
console.log(`- API Pricing Base:`, m.pricing);
|
|
27
|
+
if (stat) {
|
|
28
|
+
console.log(`- Tinybird Real Average Cost: ${stat.avg_cost_usd}$ (Basé sur le vrai usage!)`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log(`- Tinybird Stats: Inconnu / Pas d'usage récent`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
console.error("Erreur lors de l'audit:", e.message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
auditDynamicPricing();
|