opencode-pollinations-plugin 6.4.9 → 6.5.0

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 (54) hide show
  1. package/README.de.md +67 -54
  2. package/README.es.md +79 -66
  3. package/README.fr.md +70 -57
  4. package/README.it.md +78 -65
  5. package/README.md +33 -29
  6. package/README.zh.md +77 -64
  7. package/dist/locales/de.json +82 -47
  8. package/dist/locales/en.json +84 -49
  9. package/dist/locales/es.json +82 -47
  10. package/dist/locales/fr.json +81 -46
  11. package/dist/locales/it.json +82 -47
  12. package/dist/locales/zh.json +82 -47
  13. package/dist/server/commands.js +118 -178
  14. package/dist/server/config.d.ts +22 -6
  15. package/dist/server/config.js +45 -5
  16. package/dist/server/connect-response.js +7 -7
  17. package/dist/server/generate-config.js +2 -2
  18. package/dist/server/models/cache.d.ts +23 -10
  19. package/dist/server/models/cache.js +46 -24
  20. package/dist/server/models/fetcher.js +2 -0
  21. package/dist/server/models/types.d.ts +2 -0
  22. package/dist/server/models/worker.js +4 -4
  23. package/dist/server/proxy.d.ts +6 -0
  24. package/dist/server/proxy.js +318 -218
  25. package/dist/server/quota.d.ts +23 -32
  26. package/dist/server/quota.js +44 -184
  27. package/dist/server/scripts/pollinations_pricing.js +6 -3
  28. package/dist/server/status.js +1 -2
  29. package/dist/server/toast.js +1 -1
  30. package/dist/tools/index.d.ts +2 -1
  31. package/dist/tools/index.js +3 -1
  32. package/dist/tools/pollinations/artifact-core.d.ts +53 -0
  33. package/dist/tools/pollinations/artifact-core.js +159 -0
  34. package/dist/tools/pollinations/beta_discovery.js +2 -1
  35. package/dist/tools/pollinations/cost-guard.d.ts +2 -2
  36. package/dist/tools/pollinations/error-parser.d.ts +38 -0
  37. package/dist/tools/pollinations/error-parser.js +112 -0
  38. package/dist/tools/pollinations/gen_3d.d.ts +17 -0
  39. package/dist/tools/pollinations/gen_3d.js +207 -0
  40. package/dist/tools/pollinations/gen_image.js +29 -10
  41. package/dist/tools/pollinations/gen_music.js +3 -2
  42. package/dist/tools/pollinations/gen_video.js +13 -2
  43. package/dist/tools/pollinations/polli_config.js +15 -21
  44. package/dist/tools/pollinations/polli_gen_confirm.js +2 -0
  45. package/dist/tools/pollinations/shared.d.ts +1 -1
  46. package/dist/tools/pollinations/shared.js +53 -142
  47. package/dist/tools/pollinations/timeout-policy.d.ts +80 -0
  48. package/dist/tools/pollinations/timeout-policy.js +124 -0
  49. package/dist/tools/pollinations/tool-capability-registry.d.ts +51 -0
  50. package/dist/tools/pollinations/tool-capability-registry.js +215 -0
  51. package/dist/tools/pollinations/transcribe_audio.js +5 -24
  52. package/package.json +64 -62
  53. package/dist/server/tier-info.d.ts +0 -36
  54. package/dist/server/tier-info.js +0 -107
@@ -1,33 +1,11 @@
1
1
  import * as https from 'https';
2
2
  import { loadConfig, saveConfig, saveKeyToAuthJson } from './config.js';
3
- import { getQuotaStatus, fetchUsageForPeriod } from './quota.js';
3
+ import { getQuotaStatus, fetchUsageForPeriod, calculateResetInfo } from './quota.js';
4
4
  import { emitStatusToast } from './toast.js';
5
5
  import { generatePollinationsConfig } from './generate-config.js';
6
6
  import { ModelRegistry } from './models/index.js';
7
7
  import { t } from '../locales/index.js';
8
- import { formatTierTable } from './tier-info.js';
9
8
  import { buildQuestsReport } from '../tools/pollinations/polli_quests.js';
10
- // GET helper for /account/* JSON endpoints (used for quest-reward reconstruction).
11
- function fetchAccountJson(path, apiKey) {
12
- return new Promise((resolve, reject) => {
13
- const req = https.request({
14
- hostname: 'gen.pollinations.ai', path, method: 'GET',
15
- headers: { 'Authorization': `Bearer ${apiKey}`, 'User-Agent': 'opencode-pollinations-plugin' },
16
- }, (res) => {
17
- let data = '';
18
- res.on('data', c => data += c);
19
- res.on('end', () => { try {
20
- resolve(JSON.parse(data));
21
- }
22
- catch (e) {
23
- reject(e);
24
- } });
25
- });
26
- req.on('error', reject);
27
- req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout')); });
28
- req.end();
29
- });
30
- }
31
9
  function checkEndpoint(ep, key) {
32
10
  return new Promise((resolve) => {
33
11
  const req = https.request({
@@ -87,27 +65,10 @@ function formatTokens(tokens) {
87
65
  return `${(tokens / 1_000).toFixed(1)}K`;
88
66
  return tokens.toString();
89
67
  }
90
- function formatDuration(ms) {
91
- const hours = Math.floor(ms / (1000 * 60 * 60));
92
- const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
93
- return `${hours}h ${minutes}m`;
94
- }
95
- function progressBar(value, max) {
96
- const percentage = max > 0 ? Math.round((value / max) * 10) : 0;
97
- const filled = '█'.repeat(percentage);
98
- const empty = '░'.repeat(10 - percentage);
99
- return `\`${filled}${empty}\` (${(value / max * 100).toFixed(0)}%)`;
100
- }
101
68
  function parseUsageTimestamp(timestamp) {
102
69
  return new Date(timestamp.replace(' ', 'T') + 'Z');
103
70
  }
104
- function calculateResetDate(nextResetAt) {
105
- // Hourly quota system (reset at :00). The "current period" is the last hour,
106
- // matching the tier window computed in quota.ts. (Previously 24h — stale daily model.)
107
- const lastReset = new Date(nextResetAt.getTime() - 60 * 60 * 1000);
108
- return lastReset;
109
- }
110
- function calculateCurrentPeriodStats(usage, lastReset, tierLimit) {
71
+ function calculateCurrentPeriodStats(usage, lastReset) {
111
72
  let tierUsed = 0;
112
73
  let packUsed = 0;
113
74
  let totalRequests = 0;
@@ -136,7 +97,7 @@ function calculateCurrentPeriodStats(usage, lastReset, tierLimit) {
136
97
  }
137
98
  return {
138
99
  tierUsed,
139
- tierRemaining: Math.max(0, tierLimit - tierUsed),
100
+ tierRemaining: 0,
140
101
  packUsed,
141
102
  totalRequests,
142
103
  inputTokens,
@@ -203,50 +164,57 @@ async function handleModeCommand(args) {
203
164
  response: t('commands.mode.current', { mode: config.mode })
204
165
  };
205
166
  }
206
- if (!['manual', 'alwaysfree', 'pro'].includes(mode)) {
167
+ // v6.5 Quest/Paid modes + legacy aliases (alwaysfree → quest, pro → paid).
168
+ const LEGACY_MODE_ALIASES = {
169
+ 'alwaysfree': 'quest',
170
+ 'pro': 'paid',
171
+ };
172
+ const resolvedMode = LEGACY_MODE_ALIASES[mode] || mode;
173
+ if (!['manual', 'quest', 'quest_only', 'paid'].includes(resolvedMode)) {
207
174
  return {
208
175
  handled: true,
209
176
  error: t('commands.mode.invalid', { mode })
210
177
  };
211
178
  }
212
179
  const checkConfig = loadConfig();
213
- // JIT VERIFICATION for PRO and ALWAYSFREE Mode
214
- if (mode === 'pro' || mode === 'alwaysfree') {
215
- const checkConfig = loadConfig(); // Reload to be sure
180
+ // JIT VERIFICATION for Quest/Paid modes (requires a valid key)
181
+ if (resolvedMode === 'quest' || resolvedMode === 'quest_only' || resolvedMode === 'paid') {
216
182
  const key = checkConfig.apiKey;
217
183
  if (!key) {
218
- // If NO key, allow alwaysfree? Yes.
219
- // If HAS key, verify it? Yes.
220
- if (mode === 'pro')
221
- return { handled: true, error: t('commands.mode.pro_requires_key') };
222
- }
223
- emitStatusToast('info', t('commands.mode.verifying'), 'Mode Pro');
224
- try {
225
- // Force verify permissions NOW
226
- const check = await checkKeyPermissions(key);
227
- if (!check.ok) {
228
- saveConfig({ mode: 'manual', keyHasAccessToProfile: false });
229
- return {
230
- handled: true,
231
- error: t('commands.mode.denied', { status: check.status || '?', reason: check.reason || '?' })
232
- };
184
+ if (resolvedMode === 'paid' || resolvedMode === 'quest_only') {
185
+ return { handled: true, error: t('commands.mode.key_required', { mode: resolvedMode }) };
233
186
  }
234
- // Valid -> Ensure flag is true
235
- saveConfig({ keyHasAccessToProfile: true });
236
187
  }
237
- catch (e) {
238
- return { handled: true, error: t('commands.mode.verify_error', { error: e.message }) };
188
+ else {
189
+ emitStatusToast('info', t('commands.mode.verifying'), 'Mode');
190
+ try {
191
+ // Force verify permissions NOW
192
+ const check = await checkKeyPermissions(key);
193
+ if (!check.ok) {
194
+ saveConfig({ mode: 'manual', keyHasAccessToProfile: false });
195
+ return {
196
+ handled: true,
197
+ error: t('commands.mode.denied', { status: check.status || '?', reason: check.reason || '?' })
198
+ };
199
+ }
200
+ // Valid -> Ensure flag is true
201
+ saveConfig({ keyHasAccessToProfile: true });
202
+ }
203
+ catch (e) {
204
+ return { handled: true, error: t('commands.mode.verify_error', { error: e.message }) };
205
+ }
239
206
  }
240
207
  }
241
- // Allow switch (if alwaysfree or manual, or verified pro)
242
- saveConfig({ mode: mode });
208
+ // Allow switch
209
+ saveConfig({ mode: resolvedMode });
243
210
  const config = loadConfig();
244
211
  if (config.gui.status !== 'none') {
245
- emitStatusToast('success', t('commands.mode.success', { mode }), 'Pollinations Config');
212
+ emitStatusToast('success', t('commands.mode.success', { mode: resolvedMode }), 'Pollinations Config');
246
213
  }
214
+ const aliasNote = LEGACY_MODE_ALIASES[mode] ? ` ${t('commands.mode.legacy_alias', { legacy: mode, mode: resolvedMode })}` : '';
247
215
  return {
248
216
  handled: true,
249
- response: t('commands.mode.success', { mode })
217
+ response: t('commands.mode.success', { mode: resolvedMode }) + aliasNote
250
218
  };
251
219
  }
252
220
  export async function handleUsageCommand(args) {
@@ -254,70 +222,31 @@ export async function handleUsageCommand(args) {
254
222
  try {
255
223
  const quota = await getQuotaStatus(true);
256
224
  const config = loadConfig();
257
- const resetDate = quota.nextResetAt.toLocaleString('fr-FR', { hour: '2-digit', minute: '2-digit' });
258
- const timeUntilReset = quota.nextResetAt.getTime() - Date.now();
259
- const durationStr = formatDuration(Math.max(0, timeUntilReset));
225
+ const resetInfo = calculateResetInfo();
260
226
  let response = t('commands.usage.title', { mode: config.mode.toUpperCase() });
261
227
  response += t('commands.usage.resources');
262
- response += t('commands.usage.tier', { emoji: quota.tierEmoji, tier: quota.tier.toUpperCase(), limit: quota.tierLimit });
263
- response += t('commands.usage.quota', { remaining: formatPollen(quota.tierLimit - quota.tierRemaining), limit: formatPollen(quota.tierLimit) });
264
- response += t('commands.usage.usage_bar', { bar: progressBar(quota.tierLimit - quota.tierRemaining, quota.tierLimit) });
265
- // Reconstructed Quest/Paid split by CROSS-REFERENCING exact data (no magic field needed):
266
- // • claimed quest rewards (tier bucket) ← /account/quests (exact)
267
- // • tier consumption since the first claim ← /account/usage meter_source=='tier' (exact)
268
- // • current-hour floor remaining (≤ tierLimit) ← quota (the only fuzzy term, ignored)
269
- // Quest remaining ≈ claimedQuestTier − tierConsumedSinceClaim (+ floor); Paid = Total − Quest.
270
- const total = quota.tierRemaining + quota.walletBalance; // true balance
271
- let claimedQuestTier = 0;
272
- let firstClaimMs = Infinity;
273
- let tierConsumedSinceClaim = 0;
274
- if (config.apiKey && config.keyHasAccessToProfile !== false) {
275
- try {
276
- const qres = await fetchAccountJson('/account/quests', config.apiKey);
277
- for (const q of (qres?.quests || [])) {
278
- const r = q.reward;
279
- if (r && r.claimedAt && r.balanceBucket === 'tier') {
280
- claimedQuestTier += (r.pollenAmount || 0);
281
- const cms = new Date(r.claimedAt).getTime();
282
- if (!isNaN(cms) && cms < firstClaimMs)
283
- firstClaimMs = cms;
284
- }
285
- }
286
- // Sum tier-metered spend since the first claim (what actually ate the Quest stash).
287
- if (claimedQuestTier > 0 && isFinite(firstClaimMs)) {
288
- const ures = await fetchAccountJson('/account/usage?limit=100', config.apiKey);
289
- for (const e of (ures?.usage || [])) {
290
- const ts = new Date(String(e.timestamp).replace(' ', 'T') + (String(e.timestamp).includes('Z') ? '' : 'Z')).getTime();
291
- if (e.meter_source === 'tier' && !isNaN(ts) && ts >= firstClaimMs) {
292
- tierConsumedSinceClaim += (e.cost_usd || 0);
293
- }
294
- }
295
- }
296
- }
297
- catch { /* fall back gracefully if unreachable */ }
298
- }
299
- // Quest stash = claimed − consumed, plus the current hourly floor still available.
300
- const questPollen = Math.max(0, claimedQuestTier - tierConsumedSinceClaim) + quota.tierRemaining;
301
- const paidPollen = Math.max(0, total - questPollen);
228
+ const quest = quota.questBalance;
229
+ const paid = quota.walletBalance;
230
+ const total = quota.totalBalance || (quest + paid);
302
231
  response += t('commands.usage.split', {
303
- quest: questPollen.toFixed(2),
304
- paid: paidPollen.toFixed(2),
232
+ quest: quest.toFixed(2),
233
+ paid: paid.toFixed(2),
305
234
  total: total.toFixed(2),
306
235
  });
307
- response += t('commands.usage.reset', { date: resetDate, duration: durationStr });
236
+ response += t('commands.usage.quest_note');
308
237
  if (isFull && config.apiKey) {
309
238
  if (config.keyHasAccessToProfile === false) {
310
239
  response += t('commands.usage.restricted_key');
311
240
  }
312
241
  else {
313
- const lastReset = calculateResetDate(quota.nextResetAt);
242
+ const lastReset = resetInfo.lastReset;
314
243
  const usageData = await fetchUsageForPeriod(config.apiKey, lastReset);
315
244
  if (usageData && usageData.length > 0) {
316
- const stats = calculateCurrentPeriodStats(usageData, lastReset, quota.tierLimit);
245
+ const stats = calculateCurrentPeriodStats(usageData, lastReset);
317
246
  response += t('commands.usage.period_detail', { time: lastReset.toLocaleTimeString() });
318
247
  response += t('commands.usage.total_reqs', { reqs: stats.totalRequests, inTok: formatTokens(stats.inputTokens), outTok: formatTokens(stats.outputTokens) });
319
248
  // Exact consumption split by meter_source (tier = Quest Pollen, pack = Paid).
320
- // This is the ONLY reliable split the API exposes (remaining split is dashboard/cookie-only).
249
+ // This is the ONLY reliable split the API exposes.
321
250
  response += t('commands.usage.source_split', {
322
251
  tier: formatPollen(stats.tierUsed),
323
252
  pack: formatPollen(stats.packUsed),
@@ -650,20 +579,23 @@ ${t('commands.config.intro')}
650
579
  ${t('commands.config.table_headers')}
651
580
  ${t('commands.config.table_divider')}
652
581
  | **apiKey** | \`${k}\` | ${t('commands.config.api_key_role')} | \`/poll connect <key>\` |
653
- | **mode** | \`${config.mode}\` | ${t('commands.config.mode_role')} | \`/poll mode <manual/pro/alwaysfree>\` |
582
+ | **mode** | \`${config.mode}\` | ${t('commands.config.mode_role')} | \`/poll mode <quest/quest_only/paid/manual>\` |
654
583
  | **enablePaidTools**| \`${config.enablePaidTools ?? true}\` | ${t('commands.config.enablePaidTools_role')} | \`/poll config enablePaidTools <true/false>\` |
655
584
  | **costConfirmationRequired**| \`${config.costConfirmationRequired ?? true}\` | ${t('commands.config.costConfirmationRequired_role')} | \`/poll config costConfirmationRequired <true/false>\` |
656
585
  | **costThreshold**| \`${config.costThreshold ?? 0.15} 🌻\` | ${t('commands.config.costThreshold_role')} | \`/poll config costThreshold <X>\` |
657
586
  | **cost_estimator**| \`${config.costEstimator ?? true}\` | ${t('commands.config.cost_estimator_role')} | \`/poll config cost_estimator <true/false>\` |
658
- | **refillOverride**| \`${config.refillOverride ?? 'auto (déduit)'}\` | Quest Pollen refill horaire | \`/poll config refillOverride <0.01/0.15/0.4/0.8/10>\` |
659
- | **questStashInFreeMode**| \`${config.questStashInFreeMode ?? true}\` | Compte le stash dans alwaysfree | \`/poll config questStashInFreeMode <true/false>\` |
660
587
  | **fallbacks.free.main** | \`${config.fallbacks?.free?.main || 'free/mistral'}\` | ${t('commands.config.fallback_main_role')} | \`/poll fallback <main> <agent>\` |
661
588
  | **fallbacks.free.agent** | \`${config.fallbacks?.free?.agent || 'free/openai-fast'}\`| ${t('commands.config.fallback_agent_role')} | \`/poll fallback <main> <agent>\` |
662
589
  | **fallbacks.enter.agent** | \`${config.fallbacks?.enter?.agent || 'free/openai-fast'}\`| ${t('commands.config.fallback_enter_role')} | *${t('commands.config.managed_auto')}* |
663
590
  | **status_gui** | \`${config.gui?.status || 'all'}\` | ${t('commands.config.status_gui_role')} | \`/poll config status_gui <all/alert/none>\` |
664
591
  | **logs_gui** | \`${config.gui?.logs || 'error'}\` | ${t('commands.config.logs_gui_role')} | \`/poll config logs_gui <verbose/error/none>\` |
665
- | **threshold_tier** | \`${config.thresholds?.tier || 80}%\` | ${t('commands.config.threshold_tier_role')} | \`/poll config threshold_tier <1-100>\` |
666
- | **threshold_wallet** | \`${config.thresholds?.wallet || 80}%\` | ${t('commands.config.threshold_wallet_role')} | \`/poll config threshold_wallet <1-100>\` |
592
+ | **threshold_quest** | \`${config.thresholds?.quest ?? 0.05} 🌻\` | ${t('commands.config.threshold_quest_role')} | \`/poll config threshold_quest <pollen>\` |
593
+ | **threshold_wallet** | \`${config.thresholds?.wallet ?? 0.5} 🌻\` | ${t('commands.config.threshold_wallet_role')} | \`/poll config threshold_wallet <pollen>\` |
594
+ | **timeouts.default** | \`${config.timeouts?.default ?? 300}s\` | ${t('commands.config.timeouts_default_role')} | \`/poll config timeouts.default <s>\` |
595
+ | **timeouts.longRunning** | \`${config.timeouts?.longRunning ?? 900}s\` | ${t('commands.config.timeouts_long_role')} | \`/poll config timeouts.longRunning <s>\` |
596
+ | **timeouts.max** | \`${config.timeouts?.max ?? 3600}s\` | ${t('commands.config.timeouts_max_role')} | \`/poll config timeouts.max <s>\` |
597
+ | **timeouts.video** | \`${config.timeouts?.capabilities?.video ?? 1800}s\` | ${t('commands.config.timeouts_video_role')} | \`/poll config timeouts.video <s>\` |
598
+ | **timeouts.threeD** | \`${config.timeouts?.capabilities?.threeD ?? 1800}s\` | ${t('commands.config.timeouts_3d_role')} | \`/poll config timeouts.threeD <s>\` |
667
599
  | **status_bar** | \`${config.statusBar ?? true}\` | ${t('commands.config.status_bar_role')} | \`/poll config status_bar <true/false>\` |
668
600
  | **lang** | \`${config.lang || 'en'}\` | ${t('commands.config.lang_role')} | \`/poll config lang <en/fr/es/de/it>\` |`;
669
601
  return {
@@ -706,23 +638,63 @@ ${t('commands.config.table_divider')}
706
638
  saveConfig({ gui: { ...config.gui, logs: value } });
707
639
  return { handled: true, response: `✅ logs_gui = ${value}` };
708
640
  }
709
- if (key === 'threshold_tier' && value) {
710
- const threshold = parseInt(value);
711
- if (isNaN(threshold) || threshold < 0 || threshold > 100) {
712
- return { handled: true, error: 'Valeur entre 0 et 100 requise' };
641
+ if (key === 'threshold_quest' && value) {
642
+ const threshold = parseFloat(value);
643
+ if (isNaN(threshold) || threshold < 0) {
644
+ return { handled: true, error: 'Valeur numérique positive requise (en pollen). Ex: 0.05' };
713
645
  }
714
646
  const config = loadConfig();
715
- saveConfig({ thresholds: { ...config.thresholds, tier: threshold } });
716
- return { handled: true, response: `✅ threshold_tier = ${threshold}%` };
647
+ saveConfig({ thresholds: { ...config.thresholds, quest: threshold } });
648
+ return { handled: true, response: `✅ threshold_quest = ${threshold} 🌻` };
717
649
  }
718
650
  if (key === 'threshold_wallet' && value) {
719
- const threshold = parseInt(value);
720
- if (isNaN(threshold) || threshold < 0 || threshold > 100) {
721
- return { handled: true, error: 'Valeur entre 0 et 100 requise' };
651
+ const threshold = parseFloat(value);
652
+ if (isNaN(threshold) || threshold < 0) {
653
+ return { handled: true, error: 'Valeur numérique positive requise (en pollen). Ex: 0.5' };
722
654
  }
723
655
  const config = loadConfig();
724
656
  saveConfig({ thresholds: { ...config.thresholds, wallet: threshold } });
725
- return { handled: true, response: `✅ threshold_wallet = ${threshold}%` };
657
+ return { handled: true, response: `✅ threshold_wallet = ${threshold} 🌻` };
658
+ }
659
+ // v6.5 timeout hierarchy: timeouts.default / longRunning / max /
660
+ // timeouts.<capability> (image|video|audio|threeD|realtime|embed) /
661
+ // timeouts.model.<name> / reset
662
+ if (key.startsWith('timeouts') && value) {
663
+ const parts = key.split('.');
664
+ const config = loadConfig();
665
+ const timeouts = { ...(config.timeouts || {}) };
666
+ if (parts.length === 2 && ['default', 'longRunning', 'max'].includes(parts[1])) {
667
+ const seconds = parseFloat(value);
668
+ if (isNaN(seconds) || seconds < 1 || seconds > 3600) {
669
+ return { handled: true, error: 'Valeur en secondes requise (1-3600)' };
670
+ }
671
+ timeouts[parts[1]] = seconds;
672
+ saveConfig({ timeouts });
673
+ return { handled: true, response: `✅ timeouts.${parts[1]} = ${seconds}s` };
674
+ }
675
+ const CAP_KEYS = ['image', 'video', 'audio', 'threeD', 'realtime', 'embed'];
676
+ if (parts.length === 2 && CAP_KEYS.includes(parts[1])) {
677
+ const seconds = parseFloat(value);
678
+ if (isNaN(seconds) || seconds < 10 || seconds > 3600) {
679
+ return { handled: true, error: 'Valeur en secondes requise (10-3600)' };
680
+ }
681
+ timeouts.capabilities = { ...(timeouts.capabilities || {}), [parts[1]]: seconds };
682
+ saveConfig({ timeouts });
683
+ return { handled: true, response: `✅ timeouts.${parts[1]} = ${seconds}s` };
684
+ }
685
+ if (parts.length === 3 && parts[1] === 'model') {
686
+ const seconds = parseFloat(value);
687
+ if (isNaN(seconds) || seconds < 10 || seconds > 3600) {
688
+ return { handled: true, error: 'Valeur en secondes requise (10-3600)' };
689
+ }
690
+ timeouts.overrides = { ...(timeouts.overrides || {}), [parts[2]]: seconds };
691
+ saveConfig({ timeouts });
692
+ return { handled: true, response: `✅ timeouts.model.${parts[2]} = ${seconds}s` };
693
+ }
694
+ }
695
+ if (key === 'timeouts.reset') {
696
+ saveConfig({ timeouts: undefined });
697
+ return { handled: true, response: '✅ timeouts = defaults (reset)' };
726
698
  }
727
699
  if (key === 'status_bar' && value) {
728
700
  const enabled = value === 'true';
@@ -753,29 +725,9 @@ ${t('commands.config.table_divider')}
753
725
  saveConfig({ costConfirmationRequired: enabled });
754
726
  return { handled: true, response: `✅ costConfirmationRequired = ${enabled}` };
755
727
  }
756
- if (key === 'refillOverride' && value) {
757
- if (value === 'auto') {
758
- saveConfig({ refillOverride: undefined });
759
- return { handled: true, response: '✅ refillOverride = auto (déduction automatique)' };
760
- }
761
- const override = parseFloat(value);
762
- if (isNaN(override) || ![0.01, 0.15, 0.4, 0.8, 10].includes(override)) {
763
- return { handled: true, error: 'Valeurs supportées: auto, 0.01, 0.15, 0.4, 0.8, 10' };
764
- }
765
- saveConfig({ refillOverride: override });
766
- return { handled: true, response: `✅ refillOverride = ${override} 🌻/h` };
767
- }
768
- if (key === 'questStashInFreeMode' && value) {
769
- if (value !== 'true' && value !== 'false') {
770
- return { handled: true, error: 'Valeurs supportées: true, false' };
771
- }
772
- const enabled = value === 'true';
773
- saveConfig({ questStashInFreeMode: enabled });
774
- return { handled: true, response: `✅ questStashInFreeMode = ${enabled}${enabled ? ' (le stash Quest compte comme free)' : ' (seul le refill horaire compte)'}` };
775
- }
776
728
  return {
777
729
  handled: true,
778
- error: `Clé inconnue: ${key}. Clés: status_gui, logs_gui, threshold_tier, threshold_wallet, status_bar, cost_estimator, enablePaidTools, costThreshold, costConfirmationRequired, refillOverride, questStashInFreeMode, lang`
730
+ error: `Clé inconnue: ${key}. Clés: status_gui, logs_gui, threshold_quest, threshold_wallet, status_bar, cost_estimator, enablePaidTools, costThreshold, costConfirmationRequired, lang`
779
731
  };
780
732
  }
781
733
  function handleHelpCommand() {
@@ -784,15 +736,13 @@ function handleHelpCommand() {
784
736
  { key: 'lang', values: 'en, fr, es, de, it, zh', i18n: 'commands.help.config.lang' },
785
737
  { key: 'status_gui', values: 'none, alert, all', i18n: 'commands.help.config.status_gui' },
786
738
  { key: 'logs_gui', values: 'none, error, verbose', i18n: 'commands.help.config.logs_gui' },
787
- { key: 'threshold_tier', values: '0-100', i18n: 'commands.help.config.threshold_tier' },
788
- { key: 'threshold_wallet', values: '0-100', i18n: 'commands.help.config.threshold_wallet' },
739
+ { key: 'threshold_quest', values: 'pollen (e.g. 0.05)', i18n: 'commands.help.config.threshold_quest' },
740
+ { key: 'threshold_wallet', values: 'pollen (e.g. 0.5)', i18n: 'commands.help.config.threshold_wallet' },
789
741
  { key: 'status_bar', values: 'true/false', i18n: 'commands.help.config.status_bar' },
790
742
  { key: 'cost_estimator', values: 'true/false', i18n: 'commands.help.config.cost_estimator' },
791
743
  { key: 'enablePaidTools', values: 'true/false', i18n: 'commands.help.config.enablePaidTools' },
792
744
  { key: 'costThreshold', values: 'number (pollen)', i18n: 'commands.help.config.costThreshold' },
793
745
  { key: 'costConfirmationRequired', values: 'true/false', i18n: 'commands.help.config.costConfirmationRequired' },
794
- { key: 'refillOverride', values: '0.01, 0.15, 0.4, 0.8, 10, auto', i18n: 'commands.help.config.refillOverride' },
795
- { key: 'questStashInFreeMode', values: 'true/false', i18n: 'commands.help.config.questStashInFreeMode' },
796
746
  ];
797
747
  const configSection = configKeys.map(k => ` - \`${k.key}\`: ${t(k.i18n)} (\`${k.values}\`)`).join('\n');
798
748
  const help = `
@@ -810,12 +760,16 @@ ${t('commands.help.models_pricing')}
810
760
  }
811
761
  // === MODELS & PRICING COMMANDS ===
812
762
  function parseNameDesc(m) {
763
+ const displayName = m.title || m.description || m.name;
813
764
  const fullDesc = m.description || m.name;
814
- const parts = fullDesc.split(" - ");
765
+ if (m.title && m.description) {
766
+ return { nom: m.title, desc: m.description };
767
+ }
768
+ const parts = displayName.split(" - ");
815
769
  if (parts.length > 1) {
816
770
  return { nom: parts[0].trim(), desc: parts.slice(1).join(" - ").trim() };
817
771
  }
818
- return { nom: fullDesc, desc: "" };
772
+ return { nom: displayName, desc: "" };
819
773
  }
820
774
  export async function handleModelsCommand(args) {
821
775
  const filter = args[0]; // optional: image, video, audio, text
@@ -956,17 +910,7 @@ function buildOutputCost(m) {
956
910
  export async function handleInfosCommand() {
957
911
  const config = loadConfig();
958
912
  let name = "Developer";
959
- let tier = "anonymous";
960
- let tierEmoji = '👤';
961
913
  if (config.apiKey) {
962
- try {
963
- const quota = await getQuotaStatus(true);
964
- tier = quota.tier || 'anonymous';
965
- tierEmoji = quota.tierEmoji || '👤';
966
- }
967
- catch (e) {
968
- // Ignorer l'erreur réseau et garder les valeurs par défaut
969
- }
970
914
  try {
971
915
  const res = await fetch('https://gen.pollinations.ai/account/profile', {
972
916
  headers: { 'Authorization': `Bearer ${config.apiKey}` }
@@ -981,9 +925,8 @@ export async function handleInfosCommand() {
981
925
  // Ignorer
982
926
  }
983
927
  }
984
- // Get dynamic tier table based on user's language
985
- const userLang = (config.lang || 'en');
986
- const tierTable = formatTierTable(userLang);
928
+ // v6.5: Quest/Paid page (the old tier/refill table was removed upstream
929
+ // hourly refills no longer exist).
987
930
  const response = `${t('commands.infos.title', { name })}
988
931
  ${t('commands.infos.features_title')}
989
932
  ${t('commands.infos.features_free')}
@@ -992,16 +935,13 @@ ${t('commands.infos.features_pro')}
992
935
 
993
936
  ${t('commands.infos.features_config')}
994
937
 
995
- ${t('commands.infos.tiers_title', { emoji: tierEmoji, tier: tier.toUpperCase() })}
996
938
  ${t('commands.infos.get_started')}
997
939
 
998
940
  ${t('commands.infos.about')}
999
941
 
1000
- ${t('commands.infos.levels_title')}
1001
-
1002
- ${tierTable}
942
+ ${t('commands.infos.quest_paid_title')}
1003
943
 
1004
- ${t('commands.infos.hourly_note')}
944
+ ${t('commands.infos.quest_paid_body')}
1005
945
 
1006
946
  ${t('commands.infos.quests')}
1007
947
 
@@ -1,9 +1,10 @@
1
1
  export declare function getConfigDir(): string;
2
2
  export declare const CONFIG_DIR: string;
3
3
  export declare const CONFIG_FILE: string;
4
+ export type BillingMode = 'quest' | 'quest_only' | 'paid' | 'manual';
4
5
  export interface PollinationsConfigV5 {
5
6
  version: string | number;
6
- mode: 'manual' | 'alwaysfree' | 'pro';
7
+ mode: BillingMode;
7
8
  apiKey?: string;
8
9
  keyHasAccessToProfile?: boolean;
9
10
  gui: {
@@ -11,7 +12,7 @@ export interface PollinationsConfigV5 {
11
12
  logs: 'none' | 'error' | 'verbose';
12
13
  };
13
14
  thresholds: {
14
- tier: number;
15
+ quest: number;
15
16
  wallet: number;
16
17
  };
17
18
  fallbacks: {
@@ -28,14 +29,22 @@ export interface PollinationsConfigV5 {
28
29
  costConfirmationRequired: boolean;
29
30
  statusBar: boolean;
30
31
  costEstimator: boolean;
32
+ lang?: string;
33
+ timeouts?: {
34
+ default?: number;
35
+ longRunning?: number;
36
+ max?: number;
37
+ capabilities?: Record<string, number>;
38
+ overrides?: Record<string, number>;
39
+ };
31
40
  refillOverride?: number;
32
41
  questStashInFreeMode?: boolean;
33
- lang?: string;
34
42
  }
43
+ export declare function migrateV65Config(raw: any): any;
35
44
  export declare function loadConfig(): PollinationsConfigV5;
36
45
  export declare function saveConfig(updates: Partial<PollinationsConfigV5>): {
37
46
  version: string;
38
- mode: "manual" | "alwaysfree" | "pro";
47
+ mode: BillingMode;
39
48
  apiKey?: string;
40
49
  keyHasAccessToProfile?: boolean;
41
50
  gui: {
@@ -43,7 +52,7 @@ export declare function saveConfig(updates: Partial<PollinationsConfigV5>): {
43
52
  logs: "none" | "error" | "verbose";
44
53
  };
45
54
  thresholds: {
46
- tier: number;
55
+ quest: number;
47
56
  wallet: number;
48
57
  };
49
58
  fallbacks: {
@@ -60,9 +69,16 @@ export declare function saveConfig(updates: Partial<PollinationsConfigV5>): {
60
69
  costConfirmationRequired: boolean;
61
70
  statusBar: boolean;
62
71
  costEstimator: boolean;
72
+ lang?: string;
73
+ timeouts?: {
74
+ default?: number;
75
+ longRunning?: number;
76
+ max?: number;
77
+ capabilities?: Record<string, number>;
78
+ overrides?: Record<string, number>;
79
+ };
63
80
  refillOverride?: number;
64
81
  questStashInFreeMode?: boolean;
65
- lang?: string;
66
82
  };
67
83
  export declare function saveKeyToAuthJson(key: string): boolean;
68
84
  export declare function migrateLegacyConfig(): void;
@@ -69,11 +69,11 @@ catch (e) {
69
69
  }
70
70
  const DEFAULT_CONFIG_V5 = {
71
71
  version: PKG_VERSION,
72
- mode: 'manual',
72
+ mode: 'quest',
73
73
  gui: { status: 'alert', logs: 'none' },
74
- thresholds: { tier: 10, wallet: 5 },
74
+ thresholds: { quest: 0.05, wallet: 0.5 },
75
75
  fallbacks: {
76
- free: { main: 'free/mistral', agent: 'free/openai-fast' },
76
+ free: { main: 'free/openai-fast', agent: 'free/openai-fast' },
77
77
  enter: { agent: 'free/openai-fast' }
78
78
  },
79
79
  enablePaidTools: false,
@@ -82,9 +82,49 @@ const DEFAULT_CONFIG_V5 = {
82
82
  keyHasAccessToProfile: true, // Default true for legacy keys
83
83
  statusBar: true,
84
84
  costEstimator: true, // Show cost estimates by default
85
- questStashInFreeMode: true, // Count quest stash as free in alwaysfree
86
85
  lang: 'en', // Default language is English
87
86
  };
87
+ // v6.5 migration: legacy mode/threshold names → Quest/Paid semantics.
88
+ // alwaysfree → quest (QUEST_PREFERRED), pro → paid (PAID_ALLOWED).
89
+ // Legacy tier percentage thresholds are replaced by absolute pollen floors.
90
+ export function migrateV65Config(raw) {
91
+ if (!raw || typeof raw !== 'object')
92
+ return raw;
93
+ const legacyModeMap = {
94
+ 'alwaysfree': 'quest',
95
+ 'pro': 'paid',
96
+ 'manual': 'manual',
97
+ 'quest': 'quest',
98
+ 'quest_only': 'quest_only',
99
+ 'paid': 'paid',
100
+ };
101
+ if (raw.mode && legacyModeMap[raw.mode]) {
102
+ const mapped = legacyModeMap[raw.mode];
103
+ if (mapped !== raw.mode) {
104
+ logConfig(`Migrating legacy mode '${raw.mode}' → '${mapped}'`);
105
+ raw.mode = mapped;
106
+ }
107
+ }
108
+ if (raw.thresholds && typeof raw.thresholds === 'object') {
109
+ if (raw.thresholds.tier !== undefined && raw.thresholds.quest === undefined) {
110
+ // Old percentage-of-tier threshold cannot be converted to a
111
+ // meaningful absolute floor: use the v6.5 default.
112
+ logConfig('Migrating legacy thresholds.tier → thresholds.quest (default 0.05)');
113
+ raw.thresholds.quest = 0.05;
114
+ delete raw.thresholds.tier;
115
+ }
116
+ }
117
+ // Purge dead refill concepts from persisted config.
118
+ if ('refillOverride' in raw) {
119
+ logConfig('Removing deprecated refillOverride');
120
+ delete raw.refillOverride;
121
+ }
122
+ if ('questStashInFreeMode' in raw) {
123
+ logConfig('Removing deprecated questStashInFreeMode');
124
+ delete raw.questStashInFreeMode;
125
+ }
126
+ return raw;
127
+ }
88
128
  import { log as logSystem } from './logger.js';
89
129
  function logConfig(msg) {
90
130
  logSystem(`[Config] ${msg}`);
@@ -225,7 +265,7 @@ function readConfigFromDisk() {
225
265
  // Let's REMOVE this auto-downgrade too to be strictly "Decoupled".
226
266
  // If user is in PRO without key, they get "Missing Key" error, which is correct.
227
267
  }
228
- return { ...config, version: PKG_VERSION };
268
+ return migrateV65Config({ ...config, version: PKG_VERSION });
229
269
  }
230
270
  export function saveConfig(updates) {
231
271
  try {
@@ -4,21 +4,21 @@ export async function buildConnectResponse(config) {
4
4
  const hasKey = !!config.apiKey;
5
5
  const mode = config.mode;
6
6
  let name = "Developer";
7
- let tier = "anonymous";
8
- let tierEmoji = '👤';
7
+ let questEmoji = '🎁';
8
+ let questText = 'Quest/Paid';
9
9
  if (hasKey) {
10
10
  try {
11
- // Use getQuotaStatus which deduces allowance from usage (tier no longer in /account/profile)
11
+ // v6.5: Quest/Paid semantics (tier/refill model removed upstream).
12
12
  const quota = await getQuotaStatus(true);
13
- tier = quota.tier || 'anonymous';
14
- tierEmoji = quota.tierEmoji || '👤';
13
+ questText = `Quest ~${quota.questBalance.toFixed(2)} | Paid ~${quota.walletBalance.toFixed(2)}`;
15
14
  }
16
15
  catch (e) {
17
16
  // Ignorer l'erreur réseau et garder les valeurs par défaut
18
17
  }
19
18
  try {
20
19
  const res = await fetch('https://gen.pollinations.ai/account/profile', {
21
- headers: { 'Authorization': `Bearer ${config.apiKey}` }
20
+ headers: { 'Authorization': `Bearer ${config.apiKey}` },
21
+ signal: AbortSignal.timeout(5000),
22
22
  });
23
23
  if (res.ok) {
24
24
  const data = await res.json();
@@ -33,7 +33,7 @@ export async function buildConnectResponse(config) {
33
33
  if (hasKey) {
34
34
  return `${t('connect_response.title_key', { name, mode })}
35
35
 
36
- > **Your Tier:** ${tierEmoji} ${tier.toUpperCase()}
36
+ > **Your Pollen:** ${questEmoji} ${questText}
37
37
 
38
38
  ---
39
39