opencode-pollinations-plugin 6.2.7 → 6.3.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.
- package/README.de.md +9 -1
- package/README.es.md +9 -1
- package/README.fr.md +9 -1
- package/README.it.md +9 -1
- package/README.md +35 -16
- package/README.zh.md +9 -1
- package/dist/index.js +11 -7
- package/dist/locales/de.json +94 -6
- package/dist/locales/en.json +94 -6
- package/dist/locales/es.json +94 -6
- package/dist/locales/fr.json +94 -6
- package/dist/locales/index.js +3 -1
- package/dist/locales/it.json +94 -6
- package/dist/locales/zh.json +461 -0
- package/dist/server/commands.d.ts +8 -0
- package/dist/server/commands.js +203 -13
- package/dist/server/connect-response.js +1 -1
- package/dist/server/generate-config.js +4 -6
- package/dist/server/models/cache.js +1 -1
- package/dist/server/models/fetcher.js +24 -2
- package/dist/server/quota.js +29 -8
- package/dist/server/tier-info.d.ts +9 -4
- package/dist/server/tier-info.js +29 -18
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +14 -1
- package/dist/tools/pollinations/beta_discovery.d.ts +11 -4
- package/dist/tools/pollinations/beta_discovery.js +288 -136
- package/dist/tools/pollinations/gen_edit_image_free.d.ts +14 -0
- package/dist/tools/pollinations/gen_edit_image_free.js +146 -0
- package/dist/tools/pollinations/gen_video.js +2 -2
- package/dist/tools/pollinations/gen_video_free.d.ts +19 -0
- package/dist/tools/pollinations/gen_video_free.js +246 -0
- package/dist/tools/pollinations/polli_config.js +1 -1
- package/dist/tools/pollinations/polli_login.d.ts +13 -0
- package/dist/tools/pollinations/polli_login.js +30 -0
- package/dist/tools/pollinations/polli_quests.d.ts +3 -0
- package/dist/tools/pollinations/polli_quests.js +135 -0
- package/package.json +2 -2
- package/dist/server/index.d.ts +0 -2
- package/dist/server/index.js +0 -158
- package/dist/server/scripts/test_cost_endpoints.d.ts +0 -1
- package/dist/server/scripts/test_cost_endpoints.js +0 -61
- package/dist/server/scripts/test_dynamic_pricing.d.ts +0 -1
- package/dist/server/scripts/test_dynamic_pricing.js +0 -39
- package/dist/server/scripts/test_freetier_audit.d.ts +0 -11
- package/dist/server/scripts/test_freetier_audit.js +0 -215
- package/dist/server/scripts/test_parallel_cost.d.ts +0 -1
- package/dist/server/scripts/test_parallel_cost.js +0 -104
- package/dist/tools/pollinations/deepsearch.d.ts +0 -7
- package/dist/tools/pollinations/deepsearch.js +0 -80
- package/dist/tools/pollinations/search_crawl_scrape.d.ts +0 -7
- package/dist/tools/pollinations/search_crawl_scrape.js +0 -85
- package/dist/tools/pollinations/test_estimators.d.ts +0 -1
- package/dist/tools/pollinations/test_estimators.js +0 -22
package/dist/server/commands.js
CHANGED
|
@@ -6,6 +6,7 @@ import { generatePollinationsConfig } from './generate-config.js';
|
|
|
6
6
|
import { ModelRegistry } from './models/index.js';
|
|
7
7
|
import { t } from '../locales/index.js';
|
|
8
8
|
import { formatTierTable } from './tier-info.js';
|
|
9
|
+
import { buildQuestsReport } from '../tools/pollinations/polli_quests.js';
|
|
9
10
|
function checkEndpoint(ep, key) {
|
|
10
11
|
return new Promise((resolve) => {
|
|
11
12
|
const req = https.request({
|
|
@@ -54,14 +55,6 @@ export async function checkKeyPermissions(key) {
|
|
|
54
55
|
}
|
|
55
56
|
return { ok: true };
|
|
56
57
|
}
|
|
57
|
-
// === CONSTANTS & PRICING ===
|
|
58
|
-
const TIER_LIMITS = {
|
|
59
|
-
microbe: { pollen: 0.1, emoji: '🦠' },
|
|
60
|
-
spore: { pollen: 1, emoji: '🦠' },
|
|
61
|
-
seed: { pollen: 3, emoji: '🌱' },
|
|
62
|
-
flower: { pollen: 10, emoji: '🌸' },
|
|
63
|
-
nectar: { pollen: 20, emoji: '🍯' },
|
|
64
|
-
};
|
|
65
58
|
// === MARKDOWN HELPERS ===
|
|
66
59
|
function formatPollen(amount) {
|
|
67
60
|
return `${amount.toFixed(2)} 🌼`;
|
|
@@ -88,8 +81,9 @@ function parseUsageTimestamp(timestamp) {
|
|
|
88
81
|
return new Date(timestamp.replace(' ', 'T') + 'Z');
|
|
89
82
|
}
|
|
90
83
|
function calculateResetDate(nextResetAt) {
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
// Hourly quota system (reset at :00). The "current period" is the last hour,
|
|
85
|
+
// matching the tier window computed in quota.ts. (Previously 24h — stale daily model.)
|
|
86
|
+
const lastReset = new Date(nextResetAt.getTime() - 60 * 60 * 1000);
|
|
93
87
|
return lastReset;
|
|
94
88
|
}
|
|
95
89
|
function calculateCurrentPeriodStats(usage, lastReset, tierLimit) {
|
|
@@ -148,6 +142,8 @@ export async function handleCommand(command) {
|
|
|
148
142
|
return await handleUsageCommand(args);
|
|
149
143
|
case 'connect':
|
|
150
144
|
return await handleConnectCommand(args);
|
|
145
|
+
case 'login':
|
|
146
|
+
return await startDeviceLogin();
|
|
151
147
|
case 'fallback':
|
|
152
148
|
return handleFallbackCommand(args);
|
|
153
149
|
case 'config':
|
|
@@ -160,6 +156,8 @@ export async function handleCommand(command) {
|
|
|
160
156
|
return await handlePricingCommand();
|
|
161
157
|
case 'infos':
|
|
162
158
|
return await handleInfosCommand();
|
|
159
|
+
case 'quests':
|
|
160
|
+
return await handleQuestsCommand(args);
|
|
163
161
|
case 'addKey': // External trigger
|
|
164
162
|
// UI Pollution Fix: User hates appendPrompt.
|
|
165
163
|
// Just return a message telling them to use the tool.
|
|
@@ -393,6 +391,185 @@ async function handleConnectCommand(args) {
|
|
|
393
391
|
};
|
|
394
392
|
}
|
|
395
393
|
}
|
|
394
|
+
// ─── DEVICE FLOW LOGIN (option C: background poller) ───────────────────────
|
|
395
|
+
/** Best-effort cross-platform browser open. Never throws (headless-safe). */
|
|
396
|
+
function openBrowser(url) {
|
|
397
|
+
try {
|
|
398
|
+
const cp = require('child_process');
|
|
399
|
+
const platform = process.platform;
|
|
400
|
+
const cmd = platform === 'win32' ? 'start ""'
|
|
401
|
+
: platform === 'darwin' ? 'open'
|
|
402
|
+
: 'xdg-open';
|
|
403
|
+
// Detached + ignore stdio so it never blocks the proxy process.
|
|
404
|
+
const child = cp.spawn(cmd, [url], {
|
|
405
|
+
shell: platform === 'win32',
|
|
406
|
+
detached: true,
|
|
407
|
+
stdio: 'ignore',
|
|
408
|
+
});
|
|
409
|
+
child.unref?.();
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function postJsonEnter(path, body) {
|
|
417
|
+
return new Promise((resolve, reject) => {
|
|
418
|
+
const payload = JSON.stringify(body);
|
|
419
|
+
const req = https.request({
|
|
420
|
+
hostname: 'enter.pollinations.ai',
|
|
421
|
+
path,
|
|
422
|
+
method: 'POST',
|
|
423
|
+
headers: {
|
|
424
|
+
'Content-Type': 'application/json',
|
|
425
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
426
|
+
'User-Agent': 'opencode-pollinations-plugin',
|
|
427
|
+
},
|
|
428
|
+
}, (res) => {
|
|
429
|
+
let data = '';
|
|
430
|
+
res.on('data', c => data += c);
|
|
431
|
+
res.on('end', () => {
|
|
432
|
+
try {
|
|
433
|
+
resolve(JSON.parse(data));
|
|
434
|
+
}
|
|
435
|
+
catch (e) {
|
|
436
|
+
reject(new Error(`Bad JSON: ${data.slice(0, 120)}`));
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
req.on('error', reject);
|
|
441
|
+
req.setTimeout(15000, () => { req.destroy(); reject(new Error('Timeout')); });
|
|
442
|
+
req.write(payload);
|
|
443
|
+
req.end();
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
let loginPollActive = false;
|
|
447
|
+
// Publishable app key (pk_) — embedded for BYOP attribution: the consent screen
|
|
448
|
+
// shows "plugin by fkom13" and traffic is credited to this app. Safe to ship
|
|
449
|
+
// publicly (publishable by design); earningsEnabled=false so users pay nothing extra.
|
|
450
|
+
const APP_CLIENT_ID = 'pk_sATzVHuna3I5e7Sf';
|
|
451
|
+
let loginResultPromise = null;
|
|
452
|
+
let lastLoginPrompt = null; // code+URL prompt, reused on wait timeout
|
|
453
|
+
/**
|
|
454
|
+
* Wait mode for the tool: ensures a login is running (auto-starts + opens the
|
|
455
|
+
* browser if needed), then waits up to ~90s and returns the final outcome.
|
|
456
|
+
* On timeout it returns the code/URL prompt so the agent can ask the user to
|
|
457
|
+
* finish authorizing, then be called again with wait:true.
|
|
458
|
+
*/
|
|
459
|
+
export async function awaitDeviceLogin() {
|
|
460
|
+
// Auto-start if nothing is in progress (single-call UX: open + wait + report).
|
|
461
|
+
if (!loginPollActive || !loginResultPromise) {
|
|
462
|
+
const started = await startDeviceLogin();
|
|
463
|
+
if (started.error)
|
|
464
|
+
return started.error;
|
|
465
|
+
// If it reported "already running" without a promise, fall through to wait.
|
|
466
|
+
}
|
|
467
|
+
if (!loginResultPromise) {
|
|
468
|
+
return lastLoginPrompt || t('commands.login.nothing_pending');
|
|
469
|
+
}
|
|
470
|
+
const WAIT_CAP_MS = 120000;
|
|
471
|
+
const timeout = new Promise((res) => setTimeout(() => res({ status: 'error', message: '__TIMEOUT__' }), WAIT_CAP_MS));
|
|
472
|
+
const outcome = await Promise.race([loginResultPromise, timeout]);
|
|
473
|
+
if (outcome.message === '__TIMEOUT__') {
|
|
474
|
+
// Still pending — hand back the code/URL so the user can finish, then retry.
|
|
475
|
+
return (lastLoginPrompt ? lastLoginPrompt + '\n\n' : '') + t('commands.login.still_waiting');
|
|
476
|
+
}
|
|
477
|
+
return outcome.message;
|
|
478
|
+
}
|
|
479
|
+
export async function startDeviceLogin() {
|
|
480
|
+
if (loginPollActive) {
|
|
481
|
+
return { handled: true, response: t('commands.login.already_running') };
|
|
482
|
+
}
|
|
483
|
+
let codeResp;
|
|
484
|
+
try {
|
|
485
|
+
codeResp = await postJsonEnter('/api/device/code', {
|
|
486
|
+
client_id: APP_CLIENT_ID,
|
|
487
|
+
scope: 'profile usage keys', // all scopes shown for transparency; keys (Account Admin) checked by default, user can uncheck
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
catch (e) {
|
|
491
|
+
return { handled: true, error: t('commands.login.code_error', { error: e.message }) };
|
|
492
|
+
}
|
|
493
|
+
const userCode = codeResp.user_code;
|
|
494
|
+
const deviceCode = codeResp.device_code;
|
|
495
|
+
const verifyUri = codeResp.verification_uri || 'https://enter.pollinations.ai/device';
|
|
496
|
+
// Standard device link (proven reliable). Scope is applied server-side via the
|
|
497
|
+
// /api/device/code POST body — passing budget/expiry/scope in the /authorize URL
|
|
498
|
+
// breaks submission (their form coerces empty->default and array-scope fails validation).
|
|
499
|
+
// For an unlimited key: user clears Budget + Expiry fields on the form before Authorize.
|
|
500
|
+
const verifyComplete = codeResp.verification_uri_complete || `${verifyUri}?user_code=${userCode}`;
|
|
501
|
+
const interval = (codeResp.interval || 5) * 1000;
|
|
502
|
+
const expiresIn = (codeResp.expires_in || 900) * 1000;
|
|
503
|
+
if (!userCode || !deviceCode) {
|
|
504
|
+
return { handled: true, error: t('commands.login.code_error', { error: 'no code returned' }) };
|
|
505
|
+
}
|
|
506
|
+
// Background poller — non-blocking. Resolves the shared promise on completion.
|
|
507
|
+
loginPollActive = true;
|
|
508
|
+
const deadline = Date.now() + Math.min(expiresIn, 300000); // cap 5 min for UX
|
|
509
|
+
let resolveOutcome;
|
|
510
|
+
loginResultPromise = new Promise((res) => { resolveOutcome = res; });
|
|
511
|
+
const finish = (o) => { loginPollActive = false; resolveOutcome(o); };
|
|
512
|
+
const poll = async () => {
|
|
513
|
+
if (Date.now() > deadline) {
|
|
514
|
+
const msg = t('commands.login.expired');
|
|
515
|
+
emitStatusToast('warning', msg, 'Pollinations Login');
|
|
516
|
+
finish({ status: 'expired', message: msg });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
const tok = await postJsonEnter('/api/device/token', { device_code: deviceCode });
|
|
521
|
+
if (tok.access_token) {
|
|
522
|
+
// Got the key — validate & hot-load it (no restart needed)
|
|
523
|
+
const key = tok.access_token;
|
|
524
|
+
try {
|
|
525
|
+
await generatePollinationsConfig(key, true);
|
|
526
|
+
// Verify what the user actually granted (they choose on the consent form).
|
|
527
|
+
// Do NOT presume profile access — check it, like /poll connect does.
|
|
528
|
+
let limited = false;
|
|
529
|
+
try {
|
|
530
|
+
const check = await checkKeyPermissions(key);
|
|
531
|
+
limited = !check.ok;
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
limited = true;
|
|
535
|
+
}
|
|
536
|
+
saveConfig({ apiKey: key, keyHasAccessToProfile: !limited, ...(limited ? { mode: 'manual' } : {}) });
|
|
537
|
+
saveKeyToAuthJson(key);
|
|
538
|
+
const msg = limited
|
|
539
|
+
? t('commands.login.success_limited')
|
|
540
|
+
: t('commands.login.success_toast');
|
|
541
|
+
emitStatusToast(limited ? 'warning' : 'success', msg, 'Pollinations Login');
|
|
542
|
+
finish({ status: 'connected', message: msg });
|
|
543
|
+
}
|
|
544
|
+
catch (e) {
|
|
545
|
+
const msg = t('commands.login.validate_error', { error: e.message });
|
|
546
|
+
emitStatusToast('error', msg, 'Pollinations Login');
|
|
547
|
+
finish({ status: 'error', message: msg });
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
// pending → keep polling
|
|
552
|
+
setTimeout(poll, interval);
|
|
553
|
+
}
|
|
554
|
+
catch (e) {
|
|
555
|
+
// authorization_pending / slow_down / transient → keep polling
|
|
556
|
+
setTimeout(poll, interval);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
setTimeout(poll, interval);
|
|
560
|
+
// Try to open the consent page automatically (headless-safe; URL shown as fallback).
|
|
561
|
+
const opened = openBrowser(verifyComplete);
|
|
562
|
+
const promptText = (opened ? t('commands.login.opened') + '\n\n' : '') + t('commands.login.prompt', {
|
|
563
|
+
code: userCode,
|
|
564
|
+
uri: verifyUri,
|
|
565
|
+
uri_complete: verifyComplete,
|
|
566
|
+
});
|
|
567
|
+
lastLoginPrompt = promptText;
|
|
568
|
+
return {
|
|
569
|
+
handled: true,
|
|
570
|
+
response: promptText,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
396
573
|
function handleConfigCommand(args) {
|
|
397
574
|
const [key, value] = args;
|
|
398
575
|
if (!key) {
|
|
@@ -425,7 +602,7 @@ ${t('commands.config.table_divider')}
|
|
|
425
602
|
};
|
|
426
603
|
}
|
|
427
604
|
if (key === 'lang' && value) {
|
|
428
|
-
if (!['en', 'fr', 'es', 'de', 'it'].includes(value)) {
|
|
605
|
+
if (!['en', 'fr', 'es', 'de', 'it', 'zh'].includes(value)) {
|
|
429
606
|
return { handled: true, error: "Valeurs supportées: en, fr, es, de, it" };
|
|
430
607
|
}
|
|
431
608
|
saveConfig({ lang: value });
|
|
@@ -687,7 +864,7 @@ export async function handleInfosCommand() {
|
|
|
687
864
|
}
|
|
688
865
|
}
|
|
689
866
|
const emojis = {
|
|
690
|
-
microbe: '🦠', spore: '🍄', seed: '🌱', flower: '🌸', nectar: '🍯', anonymous: '👤'
|
|
867
|
+
microbe: '🦠', spore: '🍄', seed: '🌱', flower: '🌸', nectar: '🍯', router: '🐝', anonymous: '👤'
|
|
691
868
|
};
|
|
692
869
|
const tierEmoji = emojis[tier] || '❓';
|
|
693
870
|
// Get dynamic tier table based on user's language
|
|
@@ -708,10 +885,12 @@ ${t('commands.infos.levels_title')}
|
|
|
708
885
|
|
|
709
886
|
${tierTable}
|
|
710
887
|
|
|
711
|
-
|
|
888
|
+
${t('commands.infos.hourly_note')}
|
|
712
889
|
|
|
713
890
|
${t('commands.infos.beta_note')}
|
|
714
891
|
|
|
892
|
+
${t('commands.infos.quests')}
|
|
893
|
+
|
|
715
894
|
${t('commands.infos.pollen_title')}
|
|
716
895
|
|
|
717
896
|
${t('commands.infos.pollen_get')}
|
|
@@ -719,6 +898,17 @@ ${t('commands.infos.pollen_get')}
|
|
|
719
898
|
${t('commands.infos.pollen_spend')}`;
|
|
720
899
|
return { handled: true, response };
|
|
721
900
|
}
|
|
901
|
+
async function handleQuestsCommand(args) {
|
|
902
|
+
const arg = (args[0] || 'all').toLowerCase();
|
|
903
|
+
const filter = arg === 'available' ? 'available' : arg === 'claimable' ? 'claimable' : 'all';
|
|
904
|
+
try {
|
|
905
|
+
const report = await buildQuestsReport(filter);
|
|
906
|
+
return { handled: true, response: report };
|
|
907
|
+
}
|
|
908
|
+
catch (e) {
|
|
909
|
+
return { handled: true, error: `Erreur: ${e.message || e}` };
|
|
910
|
+
}
|
|
911
|
+
}
|
|
722
912
|
// === INTEGRATION OPENCODE ===
|
|
723
913
|
export function createCommandHooks() {
|
|
724
914
|
return {
|
|
@@ -21,7 +21,7 @@ export async function buildConnectResponse(config) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
const emojis = {
|
|
24
|
-
microbe: '🦠', spore: '🍄', seed: '🌱', flower: '🌸', nectar: '🍯', anonymous: '👤'
|
|
24
|
+
microbe: '🦠', spore: '🍄', seed: '🌱', flower: '🌸', nectar: '🍯', router: '🐝', anonymous: '👤'
|
|
25
25
|
};
|
|
26
26
|
const tierEmoji = emojis[tier] || '❓';
|
|
27
27
|
if (hasKey) {
|
|
@@ -2,6 +2,7 @@ import * as https from 'https';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { loadConfig, CONFIG_FILE } from './config.js';
|
|
5
|
+
import { ModelRegistry } from './models/cache.js';
|
|
5
6
|
import { log as logSystem } from './logger.js';
|
|
6
7
|
// --- LOGGING ---
|
|
7
8
|
function log(msg) {
|
|
@@ -82,15 +83,12 @@ export async function generatePollinationsConfig(forceApiKey, forceStrict = fals
|
|
|
82
83
|
// 2. ENTERPRISE UNIVERSE
|
|
83
84
|
if (effectiveKey && effectiveKey.length > 5 && effectiveKey !== 'dummy') {
|
|
84
85
|
try {
|
|
85
|
-
//
|
|
86
|
-
const
|
|
87
|
-
'Authorization': `Bearer ${effectiveKey}`
|
|
88
|
-
});
|
|
89
|
-
const enterList = Array.isArray(enterListRaw) ? enterListRaw : (enterListRaw.data || []);
|
|
86
|
+
// Utilise le cache centralisé fusionné (V1 + Détaillé) au lieu de re-télécharger
|
|
87
|
+
const enterList = ModelRegistry.list('text');
|
|
90
88
|
const paidModels = [];
|
|
91
89
|
enterList.forEach((m) => {
|
|
92
90
|
if (m.tools === false)
|
|
93
|
-
return;
|
|
91
|
+
return; // OpenCode UI chat nécessite explicitement les tools
|
|
94
92
|
const mapped = mapModel(m, 'enter/', '');
|
|
95
93
|
modelsOutput.push(mapped);
|
|
96
94
|
if (m.paid_only) {
|
|
@@ -19,7 +19,7 @@ const STATIC_FALLBACK = [
|
|
|
19
19
|
{ name: 'klein', description: 'FLUX.2 Klein 4B', category: 'image', aliases: [], pricing: { currency: 'pollen', completionImageTokens: 0.008 }, paid_only: false, supportsI2X: true, outputType: 'image', input_modalities: ['text', 'image'], output_modalities: ['image'], costHeader: 'x-usage-completion-image-tokens' },
|
|
20
20
|
{ name: 'kontext', description: 'FLUX.1 Kontext', category: 'image', aliases: [], pricing: { currency: 'pollen', completionImageTokens: 0.04 }, paid_only: true, supportsI2X: true, outputType: 'image', input_modalities: ['text', 'image'], output_modalities: ['image'], costHeader: 'x-usage-completion-image-tokens' },
|
|
21
21
|
// Video — essential
|
|
22
|
-
{ name: 'grok-video', description: 'Grok Video', category: 'video', aliases: [], pricing: { currency: 'pollen', completionVideoSeconds: 0.0025 }, paid_only: false, supportsI2X: true, outputType: 'video', input_modalities: ['text', 'image'], output_modalities: ['video'], durationRange: [1, 15], aspectRatios: ['16:9', '9:16', '1:1', '4:3'], costHeader: 'x-usage-completion-video-seconds', genTimeEstimate: '~10s' },
|
|
22
|
+
{ name: 'grok-video-pro', description: 'Grok Video Pro', category: 'video', aliases: [], pricing: { currency: 'pollen', completionVideoSeconds: 0.0025 }, paid_only: false, supportsI2X: true, outputType: 'video', input_modalities: ['text', 'image'], output_modalities: ['video'], durationRange: [1, 15], aspectRatios: ['16:9', '9:16', '1:1', '4:3'], costHeader: 'x-usage-completion-video-seconds', genTimeEstimate: '~10s' },
|
|
23
23
|
{ name: 'veo', description: 'Veo 3.1 Fast', category: 'video', aliases: [], pricing: { currency: 'pollen', completionVideoSeconds: 0.15 }, paid_only: true, supportsI2X: true, outputType: 'video', input_modalities: ['text', 'image'], output_modalities: ['video'], durationRange: [4, 8], aspectRatios: ['16:9', '9:16', '1:1'], costHeader: 'x-usage-completion-video-seconds', genTimeEstimate: '~45-68s' },
|
|
24
24
|
// Audio — essential
|
|
25
25
|
{ name: 'elevenlabs', description: 'ElevenLabs v3 TTS', category: 'audio', aliases: [], pricing: { currency: 'pollen', completionAudioTokens: 0.00018 }, paid_only: false, supportsI2X: false, outputType: 'audio', input_modalities: ['text'], output_modalities: ['audio'] },
|
|
@@ -78,7 +78,7 @@ function mapRawToModel(raw, fallbackCategory, averageCost) {
|
|
|
78
78
|
tools: raw.tools,
|
|
79
79
|
reasoning: raw.reasoning,
|
|
80
80
|
is_specialized: raw.is_specialized,
|
|
81
|
-
context_window: raw.context_window,
|
|
81
|
+
context_window: raw.context_window || raw.context_length,
|
|
82
82
|
averageCost: averageCost !== undefined && !isNaN(averageCost) ? averageCost : undefined,
|
|
83
83
|
};
|
|
84
84
|
// Apply local patches from manual.ts
|
|
@@ -112,6 +112,7 @@ export async function fetchAllModels(apiKey) {
|
|
|
112
112
|
];
|
|
113
113
|
const statsPromise = fetchJson('https://enter.pollinations.ai/api/model-stats', headers).catch(() => ({ data: [] }));
|
|
114
114
|
const openapiPromise = fetchJson('https://enter.pollinations.ai/api/docs/open-api/generate-schema', headers).catch(() => ({}));
|
|
115
|
+
const v1ModelsPromise = fetchJson('https://gen.pollinations.ai/v1/models', headers).catch(() => ({ data: [] }));
|
|
115
116
|
const fetches = endpoints.map(async ({ url, fallbackCategory }) => {
|
|
116
117
|
try {
|
|
117
118
|
const raw = await fetchJson(url, headers);
|
|
@@ -122,9 +123,16 @@ export async function fetchAllModels(apiKey) {
|
|
|
122
123
|
return { url, fallbackCategory, raw: [] };
|
|
123
124
|
}
|
|
124
125
|
});
|
|
125
|
-
const resultsRaw = await Promise.all([...fetches, statsPromise, openapiPromise]);
|
|
126
|
+
const resultsRaw = await Promise.all([...fetches, statsPromise, openapiPromise, v1ModelsPromise]);
|
|
127
|
+
const v1ModelsRaw = resultsRaw.pop();
|
|
126
128
|
const openapiRaw = resultsRaw.pop();
|
|
127
129
|
const statsRaw = resultsRaw.pop();
|
|
130
|
+
// Index V1 endpoints to extract structural modalities and properties
|
|
131
|
+
const v1List = Array.isArray(v1ModelsRaw) ? v1ModelsRaw : (v1ModelsRaw?.data || []);
|
|
132
|
+
const v1Map = new Map();
|
|
133
|
+
for (const v of v1List) {
|
|
134
|
+
v1Map.set(v.id || v.name, v);
|
|
135
|
+
}
|
|
128
136
|
const statsList = Array.isArray(statsRaw?.data) ? statsRaw.data : [];
|
|
129
137
|
const statsMap = new Map();
|
|
130
138
|
for (const s of statsList) {
|
|
@@ -137,6 +145,20 @@ export async function fetchAllModels(apiKey) {
|
|
|
137
145
|
for (const item of list) {
|
|
138
146
|
const modelId = item.name || item.id;
|
|
139
147
|
const avgCost = statsMap.get(modelId);
|
|
148
|
+
const v1Item = v1Map.get(modelId);
|
|
149
|
+
// Merge structuraux de la V1 vers l'Item
|
|
150
|
+
if (v1Item) {
|
|
151
|
+
if (v1Item.input_modalities)
|
|
152
|
+
item.input_modalities = v1Item.input_modalities;
|
|
153
|
+
if (v1Item.output_modalities)
|
|
154
|
+
item.output_modalities = v1Item.output_modalities;
|
|
155
|
+
if (v1Item.context_length)
|
|
156
|
+
item.context_length = v1Item.context_length;
|
|
157
|
+
if (v1Item.tools !== undefined)
|
|
158
|
+
item.tools = v1Item.tools;
|
|
159
|
+
if (v1Item.reasoning !== undefined)
|
|
160
|
+
item.reasoning = v1Item.reasoning;
|
|
161
|
+
}
|
|
140
162
|
const model = mapRawToModel(item, res.fallbackCategory, avgCost);
|
|
141
163
|
const uniqueId = model.name;
|
|
142
164
|
if (!seen.has(uniqueId)) {
|
package/dist/server/quota.js
CHANGED
|
@@ -8,12 +8,16 @@ const ONE_HOUR_MS = 60 * 60 * 1000;
|
|
|
8
8
|
const HISTORY_RETENTION_MS = 48 * 60 * 60 * 1000; // 48h history
|
|
9
9
|
// === TIER LIMITS (HOURLY) ===
|
|
10
10
|
// https://pollinations.ai/pricing - Quotas refreshed every hour
|
|
11
|
+
// Source of truth mirrored from official pollinations/shared/tier-config.ts (2026-06)
|
|
12
|
+
// cadence 'none' => no hourly refill => API returns nextResetAt: null
|
|
11
13
|
const TIER_LIMITS = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
anonymous: { pollen: 0, emoji: '👤', cadence: 'none' },
|
|
15
|
+
microbe: { pollen: 0, emoji: '🦠', cadence: 'none' },
|
|
16
|
+
spore: { pollen: 0.01, emoji: '🍄', cadence: 'hourly' },
|
|
17
|
+
seed: { pollen: 0.15, emoji: '🌱', cadence: 'hourly' },
|
|
18
|
+
flower: { pollen: 0.4, emoji: '🌸', cadence: 'hourly' },
|
|
19
|
+
nectar: { pollen: 0.8, emoji: '🍯', cadence: 'hourly' },
|
|
20
|
+
router: { pollen: 10, emoji: '🐝', cadence: 'hourly' },
|
|
17
21
|
};
|
|
18
22
|
// === LOGGING ===
|
|
19
23
|
import { logApi } from './logger.js';
|
|
@@ -79,7 +83,7 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
79
83
|
logQuota(`Fetch Success. Tier: ${profile.tier}, Balance: ${balance}, Next Reset: ${profile.nextResetAt}`);
|
|
80
84
|
// 3. Smart Fetch : Récupérer uniquement les dépenses du jour (depuis lastReset)
|
|
81
85
|
const periodUsage = await fetchUsageForPeriod(config.apiKey, resetInfo.lastReset);
|
|
82
|
-
const tierInfo = TIER_LIMITS[profile.tier] || { pollen:
|
|
86
|
+
const tierInfo = TIER_LIMITS[profile.tier] || { pollen: 0, emoji: '❓', cadence: 'none' };
|
|
83
87
|
const tierLimit = tierInfo.pollen;
|
|
84
88
|
// 4. Calcul Strict FreeTier / Wallet
|
|
85
89
|
const { tierUsed } = calculateCurrentPeriodUsage(periodUsage, resetInfo);
|
|
@@ -173,9 +177,26 @@ function fetchAPI(endpoint, apiKey) {
|
|
|
173
177
|
});
|
|
174
178
|
}
|
|
175
179
|
function calculateResetInfo(nextResetAt) {
|
|
176
|
-
const nextReset = new Date(nextResetAt);
|
|
177
|
-
const lastReset = new Date(nextReset.getTime() - ONE_HOUR_MS);
|
|
178
180
|
const now = new Date();
|
|
181
|
+
// Tiers with cadence 'none' (anonymous/microbe) return nextResetAt: null.
|
|
182
|
+
// Guard also against malformed timestamps that would yield Invalid Date (NaN).
|
|
183
|
+
let nextReset = nextResetAt ? new Date(nextResetAt) : null;
|
|
184
|
+
if (!nextReset || isNaN(nextReset.getTime())) {
|
|
185
|
+
// No refill: there is no upcoming reset. Use a stable current-hour window
|
|
186
|
+
// so usage SmartFetch still has a sane lower bound, and surface 0 countdown.
|
|
187
|
+
const lastReset = new Date(now.getTime() - ONE_HOUR_MS);
|
|
188
|
+
return {
|
|
189
|
+
nextReset: now,
|
|
190
|
+
lastReset,
|
|
191
|
+
timeUntilReset: 0,
|
|
192
|
+
timeSinceReset: ONE_HOUR_MS,
|
|
193
|
+
resetHour: now.getUTCHours(),
|
|
194
|
+
resetMinute: now.getUTCMinutes(),
|
|
195
|
+
resetSecond: now.getUTCSeconds(),
|
|
196
|
+
progressPercent: 100
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const lastReset = new Date(nextReset.getTime() - ONE_HOUR_MS);
|
|
179
200
|
const timeUntilReset = Math.max(0, nextReset.getTime() - now.getTime());
|
|
180
201
|
const timeSinceReset = Math.max(0, now.getTime() - lastReset.getTime());
|
|
181
202
|
const progressPercent = Math.min(100, (timeSinceReset / ONE_HOUR_MS) * 100);
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tier Information - Central Configuration
|
|
3
3
|
*
|
|
4
|
-
* Hourly quota system (Pollinations API
|
|
5
|
-
*
|
|
4
|
+
* Hourly quota system (Pollinations API). Quotas refill every hour at :00.
|
|
5
|
+
* Tiers mirror the official pollinations/shared/tier-config.ts.
|
|
6
|
+
*
|
|
7
|
+
* NOTE (2026-06): The legacy account-level upgrade paths (dev-points → Seed,
|
|
8
|
+
* publish-app → Flower, admin tier-update) were removed upstream. Pollen is now
|
|
9
|
+
* primarily earned by completing Quests. The `condition` fields below describe
|
|
10
|
+
* the broad tier profile, not an automated upgrade trigger.
|
|
6
11
|
*/
|
|
7
12
|
export interface TierInfo {
|
|
8
13
|
name: string;
|
|
@@ -24,8 +29,8 @@ export declare function getAllTiers(): TierInfo[];
|
|
|
24
29
|
/**
|
|
25
30
|
* Format tier list for display (markdown table)
|
|
26
31
|
*/
|
|
27
|
-
export declare function formatTierTable(lang?: 'en' | 'fr' | 'es' | 'de' | 'it'): string;
|
|
32
|
+
export declare function formatTierTable(lang?: 'en' | 'fr' | 'es' | 'de' | 'it' | 'zh'): string;
|
|
28
33
|
/**
|
|
29
34
|
* Get dynamic tier description with hourly rates
|
|
30
35
|
*/
|
|
31
|
-
export declare function getTierDescription(lang?: 'en' | 'fr' | 'es' | 'de' | 'it'): string;
|
|
36
|
+
export declare function getTierDescription(lang?: 'en' | 'fr' | 'es' | 'de' | 'it' | 'zh'): string;
|
package/dist/server/tier-info.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tier Information - Central Configuration
|
|
3
3
|
*
|
|
4
|
-
* Hourly quota system (Pollinations API
|
|
5
|
-
*
|
|
4
|
+
* Hourly quota system (Pollinations API). Quotas refill every hour at :00.
|
|
5
|
+
* Tiers mirror the official pollinations/shared/tier-config.ts.
|
|
6
|
+
*
|
|
7
|
+
* NOTE (2026-06): The legacy account-level upgrade paths (dev-points → Seed,
|
|
8
|
+
* publish-app → Flower, admin tier-update) were removed upstream. Pollen is now
|
|
9
|
+
* primarily earned by completing Quests. The `condition` fields below describe
|
|
10
|
+
* the broad tier profile, not an automated upgrade trigger.
|
|
6
11
|
*/
|
|
12
|
+
import { t } from '../locales/index.js';
|
|
7
13
|
export const TIER_INFO = {
|
|
8
14
|
microbe: {
|
|
9
15
|
name: 'Microbe',
|
|
10
16
|
emoji: '🦠',
|
|
11
|
-
hourlyPollen: 0
|
|
12
|
-
dailyEstimate: 0
|
|
17
|
+
hourlyPollen: 0,
|
|
18
|
+
dailyEstimate: 0,
|
|
13
19
|
condition: 'Just register!',
|
|
14
20
|
conditionKey: 'tier.condition.signup',
|
|
15
21
|
},
|
|
@@ -26,8 +32,8 @@ export const TIER_INFO = {
|
|
|
26
32
|
emoji: '🌱',
|
|
27
33
|
hourlyPollen: 0.15,
|
|
28
34
|
dailyEstimate: 3.6,
|
|
29
|
-
condition: '
|
|
30
|
-
conditionKey: 'tier.condition.
|
|
35
|
+
condition: 'Active community member',
|
|
36
|
+
conditionKey: 'tier.condition.community',
|
|
31
37
|
},
|
|
32
38
|
flower: {
|
|
33
39
|
name: 'Flower',
|
|
@@ -42,8 +48,16 @@ export const TIER_INFO = {
|
|
|
42
48
|
emoji: '🍯',
|
|
43
49
|
hourlyPollen: 0.8,
|
|
44
50
|
dailyEstimate: 19.2,
|
|
45
|
-
condition: '
|
|
46
|
-
conditionKey: 'tier.condition.
|
|
51
|
+
condition: 'Top contributor',
|
|
52
|
+
conditionKey: 'tier.condition.top_contributor',
|
|
53
|
+
},
|
|
54
|
+
router: {
|
|
55
|
+
name: 'Router',
|
|
56
|
+
emoji: '🐝',
|
|
57
|
+
hourlyPollen: 10,
|
|
58
|
+
dailyEstimate: 240,
|
|
59
|
+
condition: 'Special / invite-only',
|
|
60
|
+
conditionKey: 'tier.condition.special',
|
|
47
61
|
},
|
|
48
62
|
};
|
|
49
63
|
/**
|
|
@@ -69,28 +83,25 @@ export function formatTierTable(lang = 'en') {
|
|
|
69
83
|
es: '| Nivel | Por hora | Diario (est.) | Condición |',
|
|
70
84
|
de: '| Stufe | Pro Stunde | Täglich (ca.) | Bedingung |',
|
|
71
85
|
it: '| Livello | Orario | Giornaliero (stima) | Condizione |',
|
|
86
|
+
zh: '| 等级 | 每小时 | 每日 (估算) | 条件 |',
|
|
72
87
|
};
|
|
73
88
|
const separator = '|------|---------|----------------|-----------|';
|
|
74
89
|
const rows = tiers.map(tier => {
|
|
75
|
-
const conditionText =
|
|
90
|
+
const conditionText = t(tier.conditionKey);
|
|
76
91
|
return `| ${tier.emoji} **${tier.name}** | **${tier.hourlyPollen} pollen/h** | ~${tier.dailyEstimate}/day | ${conditionText} |`;
|
|
77
92
|
});
|
|
78
|
-
return [headers[lang], separator, ...rows].join('\n');
|
|
93
|
+
return [headers[lang] || headers.en, separator, ...rows].join('\n');
|
|
79
94
|
}
|
|
80
95
|
/**
|
|
81
96
|
* Get dynamic tier description with hourly rates
|
|
82
97
|
*/
|
|
83
98
|
export function getTierDescription(lang = 'en') {
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const perHour = isFrench ? '/heure' : '/hour';
|
|
87
|
-
const perDay = isFrench ? '/jour (est.)' : '/day (est.)';
|
|
99
|
+
const perHour = lang === 'fr' ? '/heure' : '/hour';
|
|
100
|
+
const perDay = lang === 'fr' ? '/jour (est.)' : '/day (est.)';
|
|
88
101
|
const tiers = getAllTiers();
|
|
89
102
|
const lines = tiers.map(tier => {
|
|
90
|
-
const conditionText =
|
|
91
|
-
|
|
92
|
-
tier.condition;
|
|
93
|
-
return `- ${tier.emoji} **${tier.name}** (**${tier.hourlyPollen} ${pollenWord}${perHour}** ≈ ~${tier.dailyEstimate}${perDay}) : ${conditionText}`;
|
|
103
|
+
const conditionText = t(tier.conditionKey);
|
|
104
|
+
return `- ${tier.emoji} **${tier.name}** (**${tier.hourlyPollen} Pollen${perHour}** ≈ ~${tier.dailyEstimate}${perDay}) : ${conditionText}`;
|
|
94
105
|
});
|
|
95
106
|
return lines.join('\n');
|
|
96
107
|
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -16,10 +16,11 @@ import { polliBetaDiscoveryTool } from './pollinations/beta_discovery.js';
|
|
|
16
16
|
import { polliGenConfirmTool } from './pollinations/polli_gen_confirm.js';
|
|
17
17
|
import { polliStatusTool } from './pollinations/polli_status.js';
|
|
18
18
|
import { polliConfigTool } from './pollinations/polli_config.js';
|
|
19
|
+
import { polliQuestsTool } from './pollinations/polli_quests.js';
|
|
19
20
|
/**
|
|
20
21
|
* Build the tool registry based on user's access level
|
|
21
22
|
*
|
|
22
23
|
* @returns Record<string, Tool> to be spread into the plugin's tool: {} property
|
|
23
24
|
*/
|
|
24
25
|
export declare function createToolRegistry(): Record<string, any>;
|
|
25
|
-
export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool };
|
|
26
|
+
export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool, polliQuestsTool };
|
package/dist/tools/index.js
CHANGED
|
@@ -16,6 +16,10 @@ import { removeBackgroundTool } from './power/remove_background.js';
|
|
|
16
16
|
import { extractFramesTool } from './power/extract_frames.js';
|
|
17
17
|
import { extractAudioTool } from './power/extract_audio.js';
|
|
18
18
|
import { rmbgKeysTool } from './power/rmbg_keys.js';
|
|
19
|
+
// === FREE BONUS: standalone always-free image gen/edit + video (no key, no Pollen) ===
|
|
20
|
+
import { genEditImageFreeTool } from './pollinations/gen_edit_image_free.js';
|
|
21
|
+
import { genVideoFreeTool } from './pollinations/gen_video_free.js';
|
|
22
|
+
import { polliLoginTool } from './pollinations/polli_login.js';
|
|
19
23
|
// === ENTER TOOLS (Require API key) ===
|
|
20
24
|
import { polliGenImageTool } from './pollinations/gen_image.js';
|
|
21
25
|
import { polliGenVideoTool } from './pollinations/gen_video.js';
|
|
@@ -27,6 +31,7 @@ import { polliBetaDiscoveryTool } from './pollinations/beta_discovery.js';
|
|
|
27
31
|
import { polliGenConfirmTool } from './pollinations/polli_gen_confirm.js';
|
|
28
32
|
import { polliStatusTool } from './pollinations/polli_status.js';
|
|
29
33
|
import { polliConfigTool } from './pollinations/polli_config.js';
|
|
34
|
+
import { polliQuestsTool } from './pollinations/polli_quests.js';
|
|
30
35
|
import { log } from '../server/logger.js';
|
|
31
36
|
/**
|
|
32
37
|
* Detect if a valid API key is present
|
|
@@ -54,6 +59,12 @@ export function createToolRegistry() {
|
|
|
54
59
|
tools['extract_frames'] = extractFramesTool;
|
|
55
60
|
tools['extract_audio'] = extractAudioTool;
|
|
56
61
|
tools['rmbg_keys'] = rmbgKeysTool;
|
|
62
|
+
// Bonus tool: always-free image gen/edit (works without a Pollinations key)
|
|
63
|
+
tools['gen_edit_image_free'] = genEditImageFreeTool;
|
|
64
|
+
// Bonus tool: always-free video generation (works without a Pollinations key)
|
|
65
|
+
tools['gen_video_free'] = genVideoFreeTool;
|
|
66
|
+
// Login tool: device-flow login, callable by any model (no key needed to run)
|
|
67
|
+
tools['polli_login'] = polliLoginTool;
|
|
57
68
|
log(`Free tools injected: ${Object.keys(tools).length}`);
|
|
58
69
|
// === ENTER UNIVERSE: Only with valid API key (+6 tools) ===
|
|
59
70
|
if (keyPresent) {
|
|
@@ -73,6 +84,8 @@ export function createToolRegistry() {
|
|
|
73
84
|
tools['polli_config'] = polliConfigTool;
|
|
74
85
|
// Plugin Status / Info / Pricing helper map
|
|
75
86
|
tools['polli_status'] = polliStatusTool;
|
|
87
|
+
// Quests — read-only quest status & claimable Pollen nudge
|
|
88
|
+
tools['polli_quests'] = polliQuestsTool;
|
|
76
89
|
log(`Enter tools injected (key detected). Total: ${Object.keys(tools).length}`);
|
|
77
90
|
}
|
|
78
91
|
else {
|
|
@@ -83,4 +96,4 @@ export function createToolRegistry() {
|
|
|
83
96
|
return tools;
|
|
84
97
|
}
|
|
85
98
|
// Re-export for convenience
|
|
86
|
-
export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool };
|
|
99
|
+
export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool, polliQuestsTool };
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* beta_discovery Tool (API Explorer
|
|
2
|
+
* beta_discovery Tool (API Explorer V4 — Defense-in-Depth)
|
|
3
3
|
*
|
|
4
|
-
* Combines reading the official OpenAPI Specification with
|
|
5
|
-
* blackbox
|
|
6
|
-
*
|
|
4
|
+
* Combines reading the official OpenAPI Specification with hardened
|
|
5
|
+
* blackbox fuzzing that GUARANTEES HTTP 400 responses by forcefully
|
|
6
|
+
* injecting invalid values. The AI agent NEVER controls the actual
|
|
7
|
+
* values sent to the API.
|
|
8
|
+
*
|
|
9
|
+
* Security Layers:
|
|
10
|
+
* 1. Command Whitelist — only 4 commands exist
|
|
11
|
+
* 2. Endpoint Whitelist — only 4 API routes are probeable
|
|
12
|
+
* 3. Value Injection — fuzz values are hardcoded, AI cannot override
|
|
13
|
+
* 4. Payload Sabotage — fuzz corrupts, probe_missing removes keys
|
|
7
14
|
*/
|
|
8
15
|
import { type ToolDefinition } from '@opencode-ai/plugin/tool';
|
|
9
16
|
export declare const polliBetaDiscoveryTool: ToolDefinition;
|