opencode-pollinations-plugin 6.1.0-beta.9 → 6.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.de.md +130 -0
  2. package/README.es.md +130 -0
  3. package/README.fr.md +130 -0
  4. package/README.it.md +130 -0
  5. package/README.md +87 -73
  6. package/dist/index.js +52 -161
  7. package/dist/locales/de.json +374 -0
  8. package/dist/locales/en.json +373 -0
  9. package/dist/locales/es.json +374 -0
  10. package/dist/locales/fr.json +373 -0
  11. package/dist/locales/index.d.ts +1 -0
  12. package/dist/locales/index.js +37 -0
  13. package/dist/locales/it.json +374 -0
  14. package/dist/server/commands.d.ts +6 -0
  15. package/dist/server/commands.js +394 -125
  16. package/dist/server/config.d.ts +34 -23
  17. package/dist/server/config.js +200 -108
  18. package/dist/server/connect-response.d.ts +2 -0
  19. package/dist/server/connect-response.js +59 -0
  20. package/dist/server/generate-config.d.ts +3 -30
  21. package/dist/server/generate-config.js +164 -106
  22. package/dist/server/index.d.ts +2 -1
  23. package/dist/server/index.js +124 -149
  24. package/dist/server/logger.d.ts +8 -0
  25. package/dist/server/logger.js +38 -0
  26. package/dist/server/models/cache.d.ts +35 -0
  27. package/dist/server/models/cache.js +160 -0
  28. package/dist/server/models/fetcher.d.ts +18 -0
  29. package/dist/server/models/fetcher.js +194 -0
  30. package/dist/server/models/index.d.ts +6 -0
  31. package/dist/server/models/index.js +5 -0
  32. package/dist/server/models/manual.d.ts +15 -0
  33. package/dist/server/models/manual.js +92 -0
  34. package/dist/server/models/types.d.ts +55 -0
  35. package/dist/server/models/types.js +7 -0
  36. package/dist/server/models/worker.d.ts +22 -0
  37. package/dist/server/models/worker.js +174 -0
  38. package/dist/server/pollinations-api.d.ts +11 -0
  39. package/dist/server/pollinations-api.js +21 -8
  40. package/dist/server/proxy.js +222 -307
  41. package/dist/server/quota.d.ts +2 -0
  42. package/dist/server/quota.js +89 -86
  43. package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
  44. package/dist/server/scripts/pollinations_pricing.js +246 -0
  45. package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
  46. package/dist/server/scripts/test_cost_endpoints.js +61 -0
  47. package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
  48. package/dist/server/scripts/test_dynamic_pricing.js +39 -0
  49. package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
  50. package/dist/server/scripts/test_freetier_audit.js +215 -0
  51. package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
  52. package/dist/server/scripts/test_parallel_cost.js +104 -0
  53. package/dist/server/toast.d.ts +7 -1
  54. package/dist/server/toast.js +43 -10
  55. package/dist/tools/design/gen_diagram.d.ts +2 -0
  56. package/dist/tools/design/gen_diagram.js +94 -0
  57. package/dist/tools/design/gen_palette.d.ts +2 -0
  58. package/dist/tools/design/gen_palette.js +182 -0
  59. package/dist/tools/design/gen_qrcode.d.ts +2 -0
  60. package/dist/tools/design/gen_qrcode.js +50 -0
  61. package/dist/tools/ffmpeg.d.ts +24 -0
  62. package/dist/tools/ffmpeg.js +54 -0
  63. package/dist/tools/index.d.ts +25 -0
  64. package/dist/tools/index.js +86 -0
  65. package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
  66. package/dist/tools/pollinations/beta_discovery.js +201 -0
  67. package/dist/tools/pollinations/cost-guard.d.ts +38 -0
  68. package/dist/tools/pollinations/cost-guard.js +136 -0
  69. package/dist/tools/pollinations/deepsearch.d.ts +7 -0
  70. package/dist/tools/pollinations/deepsearch.js +80 -0
  71. package/dist/tools/pollinations/gen_audio.d.ts +18 -0
  72. package/dist/tools/pollinations/gen_audio.js +220 -0
  73. package/dist/tools/pollinations/gen_image.d.ts +11 -0
  74. package/dist/tools/pollinations/gen_image.js +211 -0
  75. package/dist/tools/pollinations/gen_music.d.ts +14 -0
  76. package/dist/tools/pollinations/gen_music.js +157 -0
  77. package/dist/tools/pollinations/gen_video.d.ts +16 -0
  78. package/dist/tools/pollinations/gen_video.js +249 -0
  79. package/dist/tools/pollinations/polli_config.d.ts +2 -0
  80. package/dist/tools/pollinations/polli_config.js +95 -0
  81. package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
  82. package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
  83. package/dist/tools/pollinations/polli_status.d.ts +2 -0
  84. package/dist/tools/pollinations/polli_status.js +31 -0
  85. package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
  86. package/dist/tools/pollinations/polli_web_search.js +126 -0
  87. package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
  88. package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
  89. package/dist/tools/pollinations/shared.d.ts +181 -0
  90. package/dist/tools/pollinations/shared.js +758 -0
  91. package/dist/tools/pollinations/test_estimators.d.ts +1 -0
  92. package/dist/tools/pollinations/test_estimators.js +22 -0
  93. package/dist/tools/pollinations/transcribe_audio.d.ts +13 -0
  94. package/dist/tools/pollinations/transcribe_audio.js +171 -0
  95. package/dist/tools/power/extract_audio.d.ts +2 -0
  96. package/dist/tools/power/extract_audio.js +179 -0
  97. package/dist/tools/power/extract_frames.d.ts +2 -0
  98. package/dist/tools/power/extract_frames.js +237 -0
  99. package/dist/tools/power/file_to_url.d.ts +2 -0
  100. package/dist/tools/power/file_to_url.js +217 -0
  101. package/dist/tools/power/remove_background.d.ts +2 -0
  102. package/dist/tools/power/remove_background.js +404 -0
  103. package/dist/tools/power/rmbg_keys.d.ts +2 -0
  104. package/dist/tools/power/rmbg_keys.js +79 -0
  105. package/dist/tools/shared.d.ts +30 -0
  106. package/dist/tools/shared.js +80 -0
  107. package/package.json +9 -3
  108. package/dist/server/models-seed.d.ts +0 -18
  109. package/dist/server/models-seed.js +0 -55
@@ -1,3 +1,4 @@
1
+ import { DetailedUsageEntry } from './pollinations-api.js';
1
2
  export interface QuotaStatus {
2
3
  tierRemaining: number;
3
4
  tierUsed: number;
@@ -12,5 +13,6 @@ export interface QuotaStatus {
12
13
  tierEmoji: string;
13
14
  errorType?: 'auth_limited' | 'network' | 'unknown';
14
15
  }
16
+ export declare function fetchUsageForPeriod(apiKey: string, lastReset: Date): Promise<DetailedUsageEntry[]>;
15
17
  export declare function getQuotaStatus(forceRefresh?: boolean): Promise<QuotaStatus>;
16
18
  export declare function formatQuotaForToast(quota: QuotaStatus): string;
@@ -1,42 +1,65 @@
1
- import * as fs from 'fs';
2
1
  import * as https from 'https'; // Use Native HTTPS
3
2
  import { loadConfig } from './config.js';
4
- // === CACHE ===
3
+ // === CACHE & CONSTANTS ===
5
4
  const CACHE_TTL = 30000; // 30 secondes
6
5
  let cachedQuota = null;
7
6
  let lastQuotaFetch = 0;
7
+ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
8
+ const HISTORY_RETENTION_MS = 48 * 60 * 60 * 1000; // 48h history
8
9
  // === TIER LIMITS ===
9
10
  const TIER_LIMITS = {
11
+ microbe: { pollen: 0.1, emoji: '🦠' },
10
12
  spore: { pollen: 1, emoji: '🦠' },
11
13
  seed: { pollen: 3, emoji: '🌱' },
12
14
  flower: { pollen: 10, emoji: '🌸' },
13
15
  nectar: { pollen: 20, emoji: '🍯' },
14
16
  };
15
17
  // === LOGGING ===
18
+ import { logApi } from './logger.js';
16
19
  function logQuota(msg) {
17
- try {
18
- fs.appendFileSync('/tmp/pollinations_quota_debug.log', `[${new Date().toISOString()}] ${msg}\n`);
20
+ logApi(`[QUOTA] ${msg}`);
21
+ }
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;
29
+ try {
30
+ usageRes = await fetchAPI(`/account/usage?limit=${limit}&offset=${offset}`, apiKey);
31
+ }
32
+ catch (e) {
33
+ logQuota(`SmartFetch failed at offset ${offset}: ${e}`);
34
+ break;
35
+ }
36
+ if (!usageRes.usage || usageRes.usage.length === 0) {
37
+ break; // No more records
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;
19
54
  }
20
- catch (e) { }
55
+ logQuota(`SmartFetch: Retrieved ${allUsage.length} transactions for current period.`);
56
+ return allUsage;
21
57
  }
22
- // === FONCTIONS PRINCIPALES ===
58
+ // === MAIN QUOTA FUNCTION ===
23
59
  export async function getQuotaStatus(forceRefresh = false) {
24
60
  const config = loadConfig();
25
61
  if (!config.apiKey) {
26
- // Pas de clé = Mode manual par défaut, pas de quota
27
- return {
28
- tierRemaining: 0,
29
- tierUsed: 0,
30
- tierLimit: 0,
31
- walletBalance: 0,
32
- nextResetAt: new Date(),
33
- timeUntilReset: 0,
34
- canUseEnterprise: false,
35
- isUsingWallet: false,
36
- needsAlert: false,
37
- tier: 'none',
38
- tierEmoji: '❌'
39
- };
62
+ return createDefaultQuota('none', 0);
40
63
  }
41
64
  const now = Date.now();
42
65
  if (!forceRefresh && cachedQuota && (now - lastQuotaFetch) < CACHE_TTL) {
@@ -44,27 +67,33 @@ export async function getQuotaStatus(forceRefresh = false) {
44
67
  }
45
68
  try {
46
69
  logQuota("Fetching Quota Data...");
70
+ // 1. Fetch API
47
71
  // SEQUENTIAL FETCH (Avoid Rate Limits)
48
- // We fetch one by one. If one fails, we catch and return fallback.
49
72
  const profileRes = await fetchAPI('/account/profile', config.apiKey);
50
73
  const balanceRes = await fetchAPI('/account/balance', config.apiKey);
51
- const usageRes = await fetchAPI('/account/usage', config.apiKey);
52
- logQuota(`Fetch Success. Tier: ${profileRes.tier}, Balance: ${balanceRes.balance}`);
53
74
  const profile = profileRes;
54
75
  const balance = balanceRes.balance;
55
- const usage = usageRes.usage || [];
56
- const tierInfo = TIER_LIMITS[profile.tier] || { pollen: 1, emoji: '❓' }; // Default 1 (Spore)
57
- const tierLimit = tierInfo.pollen;
58
- // Calculer le reset
76
+ // 2. Convertir Timezone : Obtenir instant exact du Reset
59
77
  const resetInfo = calculateResetInfo(profile.nextResetAt);
60
- // Calculer l'usage de la période actuelle
61
- const { tierUsed } = calculateCurrentPeriodUsage(usage, resetInfo);
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);
81
+ const tierInfo = TIER_LIMITS[profile.tier] || { pollen: 1, emoji: '❓' };
82
+ const tierLimit = tierInfo.pollen;
83
+ // 4. Calcul Strict FreeTier / Wallet
84
+ const { tierUsed } = calculateCurrentPeriodUsage(periodUsage, resetInfo);
85
+ // 4. Calculate Balances
62
86
  const tierRemaining = Math.max(0, tierLimit - tierUsed);
63
87
  // Fix rounding errors
64
88
  const cleanTierRemaining = Math.max(0, parseFloat(tierRemaining.toFixed(4)));
65
89
  // Le wallet c'est le reste (balance totale - ce qu'il reste du tier gratuit non consommé)
90
+ // Formula: Pollinations Balance = Wallet + TierRemaining.
66
91
  const walletBalance = Math.max(0, balance - cleanTierRemaining);
67
92
  const cleanWalletBalance = Math.max(0, parseFloat(walletBalance.toFixed(4)));
93
+ // needsAlert: check BOTH tier threshold AND wallet threshold
94
+ const tierAlertPercent = tierLimit > 0 ? (cleanTierRemaining / tierLimit * 100) : 0;
95
+ const tierNeedsAlert = tierLimit > 0 && tierAlertPercent <= config.thresholds.tier;
96
+ const walletNeedsAlert = cleanWalletBalance > 0 && cleanWalletBalance < (config.thresholds.wallet || 0.5);
68
97
  cachedQuota = {
69
98
  tierRemaining: cleanTierRemaining,
70
99
  tierUsed,
@@ -74,7 +103,7 @@ export async function getQuotaStatus(forceRefresh = false) {
74
103
  timeUntilReset: resetInfo.timeUntilReset,
75
104
  canUseEnterprise: cleanTierRemaining > 0.05 || cleanWalletBalance > 0.05,
76
105
  isUsingWallet: cleanTierRemaining <= 0.05 && cleanWalletBalance > 0.05,
77
- needsAlert: tierLimit > 0 ? (cleanTierRemaining / tierLimit * 100) <= config.thresholds.tier : false,
106
+ needsAlert: tierNeedsAlert || walletNeedsAlert,
78
107
  tier: profile.tier,
79
108
  tierEmoji: tierInfo.emoji
80
109
  };
@@ -84,30 +113,29 @@ export async function getQuotaStatus(forceRefresh = false) {
84
113
  catch (e) {
85
114
  logQuota(`ERROR fetching quota: ${e.message}`);
86
115
  let errorType = 'unknown';
87
- if (e.message && e.message.includes('403')) {
116
+ if (e.message && e.message.includes('403'))
88
117
  errorType = 'auth_limited';
89
- }
90
- else if (e.message && e.message.includes('Network Error')) {
118
+ else if (e.message && e.message.includes('Network Error'))
91
119
  errorType = 'network';
92
- }
93
- // Retourner le cache ou un état par défaut safe
94
- return cachedQuota || {
95
- tierRemaining: 0,
96
- tierUsed: 0,
97
- tierLimit: 1,
98
- walletBalance: 0,
99
- nextResetAt: new Date(),
100
- timeUntilReset: 0,
101
- canUseEnterprise: false,
102
- isUsingWallet: false,
103
- needsAlert: true,
104
- tier: 'error',
105
- tierEmoji: '⚠️',
106
- errorType
107
- };
120
+ return cachedQuota || { ...createDefaultQuota('error', 1), errorType };
108
121
  }
109
122
  }
110
- // === HELPERS (Native HTTPS) ===
123
+ function createDefaultQuota(tierName, limit) {
124
+ return {
125
+ tierRemaining: 0,
126
+ tierUsed: 0,
127
+ tierLimit: limit,
128
+ walletBalance: 0,
129
+ nextResetAt: new Date(),
130
+ timeUntilReset: 0,
131
+ canUseEnterprise: false,
132
+ isUsingWallet: false,
133
+ needsAlert: false,
134
+ tier: tierName,
135
+ tierEmoji: TIER_LIMITS[tierName]?.emoji || '❌'
136
+ };
137
+ }
138
+ // === HELPERS ===
111
139
  function fetchAPI(endpoint, apiKey) {
112
140
  return new Promise((resolve, reject) => {
113
141
  const options = {
@@ -144,53 +172,30 @@ function fetchAPI(endpoint, apiKey) {
144
172
  });
145
173
  }
146
174
  function calculateResetInfo(nextResetAt) {
147
- const nextResetFromAPI = new Date(nextResetAt);
175
+ const nextReset = new Date(nextResetAt);
176
+ const lastReset = new Date(nextReset.getTime() - ONE_DAY_MS);
148
177
  const now = new Date();
149
- // Extraire l'heure de reset depuis l'API (varie par utilisateur!)
150
- const resetHour = nextResetFromAPI.getUTCHours();
151
- const resetMinute = nextResetFromAPI.getUTCMinutes();
152
- const resetSecond = nextResetFromAPI.getUTCSeconds();
153
- // Calculer le reset d'aujourd'hui à cette heure
154
- const todayResetUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), resetHour, resetMinute, resetSecond));
155
- let lastReset;
156
- let nextReset;
157
- if (now >= todayResetUTC) {
158
- // Le reset d'aujourd'hui est passé
159
- lastReset = todayResetUTC;
160
- nextReset = new Date(todayResetUTC.getTime() + 24 * 60 * 60 * 1000);
161
- }
162
- else {
163
- // Le reset d'aujourd'hui n'est pas encore passé
164
- lastReset = new Date(todayResetUTC.getTime() - 24 * 60 * 60 * 1000);
165
- nextReset = todayResetUTC;
166
- }
167
- const timeUntilReset = nextReset.getTime() - now.getTime();
168
- const timeSinceReset = now.getTime() - lastReset.getTime();
169
- const cycleDuration = 24 * 60 * 60 * 1000;
170
- const progressPercent = (timeSinceReset / cycleDuration) * 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);
171
181
  return {
172
182
  nextReset,
173
183
  lastReset,
174
184
  timeUntilReset,
175
185
  timeSinceReset,
176
- resetHour,
177
- resetMinute,
178
- resetSecond,
186
+ resetHour: nextReset.getUTCHours(),
187
+ resetMinute: nextReset.getUTCMinutes(),
188
+ resetSecond: nextReset.getUTCSeconds(),
179
189
  progressPercent
180
190
  };
181
191
  }
182
192
  function calculateCurrentPeriodUsage(usage, resetInfo) {
183
193
  let tierUsed = 0;
184
194
  let packUsed = 0;
185
- // Parser le timestamp de l'API avec Z pour UTC
186
- function parseUsageTimestamp(timestamp) {
187
- // Format: "2026-01-23 01:11:21"
188
- const isoString = timestamp.replace(' ', 'T') + 'Z';
189
- return new Date(isoString);
190
- }
191
- // FILTRER: Ne garder que les entrées APRÈS le dernier reset
192
195
  const entriesAfterReset = usage.filter(entry => {
193
- const entryTime = parseUsageTimestamp(entry.timestamp);
196
+ // Safe Parse
197
+ const timestamp = entry.timestamp.replace(' ', 'T') + 'Z';
198
+ const entryTime = new Date(timestamp);
194
199
  return entryTime >= resetInfo.lastReset;
195
200
  });
196
201
  for (const entry of entriesAfterReset) {
@@ -203,7 +208,6 @@ function calculateCurrentPeriodUsage(usage, resetInfo) {
203
208
  }
204
209
  return { tierUsed, packUsed };
205
210
  }
206
- // === EXPORT POUR LES ALERTES ===
207
211
  export function formatQuotaForToast(quota) {
208
212
  if (quota.errorType === 'auth_limited') {
209
213
  return `🔑 CLE LIMITÉE (Génération Seule) | 💎 Wallet: N/A | ⏰ Reset: N/A`;
@@ -211,7 +215,6 @@ export function formatQuotaForToast(quota) {
211
215
  const tierPercent = quota.tierLimit > 0
212
216
  ? Math.round((quota.tierRemaining / quota.tierLimit) * 100)
213
217
  : 0;
214
- // Format compact: 1h23m
215
218
  const ms = quota.timeUntilReset;
216
219
  const hours = Math.floor(ms / (1000 * 60 * 60));
217
220
  const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
@@ -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();
@@ -0,0 +1,11 @@
1
+ /**
2
+ * TEST FREETIER AUDIT — Quick & Dirty
3
+ * Objectif: Tester les modèles freetier Enter pour vérifier:
4
+ * - Latence de réponse
5
+ * - Headers de coût (x-usage-*)
6
+ * - Real cost via /account/balance diff
7
+ * - Erreurs et paramètres
8
+ *
9
+ * Usage: npx tsx src/server/scripts/test_freetier_audit.ts
10
+ */
11
+ export {};