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
@@ -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(process.env.HOME || '/tmp', '.config/opencode/pollinations-signature.json');
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. SPECIAL: connect-pollinations fallback model
269
- if (body.model === 'connect-pollinations') {
270
- const connectMsg = {
271
- id: `chatcmpl-connect-${Date.now()}`,
272
- object: 'chat.completion',
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: 'connect-pollinations',
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
- message: {
278
- role: 'assistant',
279
- content: `🔗 **Se connecter à Pollinations**\n\nAccédez à 30+ modèles IA de pointe !\n\n📍 **Étapes :**\n1. Visitez https://enter.pollinations.ai\n2. Créez un compte gratuit\n3. Copiez votre API Key\n4. Exécutez: \`/pollinations config apiKey YOUR_KEY\`\n5. Redémarrez OpenCode\n\n✅ **Bénéfices :**\n• 30+ modèles avancés (GPT-5, Claude, Gemini...)\n• Crédits gratuits selon votre tier\n• Stabilité garantie`
280
- },
281
- finish_reason: 'stop'
282
- }],
283
- usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
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)
@@ -1,11 +1,4 @@
1
- export interface DetailedUsageEntry {
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;
@@ -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
- try {
23
- fs.appendFileSync('/tmp/pollinations_quota_debug.log', `[${new Date().toISOString()}] ${msg}\n`);
24
- }
25
- catch (e) { }
20
+ logApi(`[QUOTA] ${msg}`);
26
21
  }
27
- // === HISTORY MANAGER (JSON) ===
28
- function getHistoryFilePath() {
29
- const homedir = process.env.HOME || '/tmp';
30
- const historyDir = path.join(homedir, '.pollinations');
31
- if (!fs.existsSync(historyDir)) {
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
- fs.mkdirSync(historyDir, { recursive: true });
30
+ usageRes = await fetchAPI(`/account/usage?limit=${limit}&offset=${offset}`, apiKey);
34
31
  }
35
- catch (e) { }
36
- }
37
- return path.join(historyDir, 'usage_history.json');
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
- catch (e) {
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
- // 3. Prune (> 48h)
69
- const now = Date.now();
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. Update Local History (The Source of Truth)
108
- const fullHistory = updateLocalHistory(usageRes.usage || []);
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
- // 3. Calculate Reset & Usage from History
112
- const resetInfo = calculateResetInfo(profile.nextResetAt);
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 nextResetFromAPI = new Date(nextResetAt);
175
+ const nextReset = new Date(nextResetAt);
176
+ const lastReset = new Date(nextReset.getTime() - ONE_DAY_MS);
205
177
  const now = new Date();
206
- const resetHour = nextResetFromAPI.getUTCHours();
207
- const resetMinute = nextResetFromAPI.getUTCMinutes();
208
- const resetSecond = nextResetFromAPI.getUTCSeconds();
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();