opencode-pollinations-plugin 6.2.7 → 6.4.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 +97 -7
- package/dist/locales/en.json +97 -7
- package/dist/locales/es.json +97 -7
- package/dist/locales/fr.json +97 -7
- package/dist/locales/index.js +3 -1
- package/dist/locales/it.json +97 -7
- package/dist/locales/zh.json +463 -0
- package/dist/server/commands.d.ts +8 -0
- package/dist/server/commands.js +272 -14
- 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,28 @@ 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';
|
|
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
|
+
}
|
|
9
31
|
function checkEndpoint(ep, key) {
|
|
10
32
|
return new Promise((resolve) => {
|
|
11
33
|
const req = https.request({
|
|
@@ -54,14 +76,6 @@ export async function checkKeyPermissions(key) {
|
|
|
54
76
|
}
|
|
55
77
|
return { ok: true };
|
|
56
78
|
}
|
|
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
79
|
// === MARKDOWN HELPERS ===
|
|
66
80
|
function formatPollen(amount) {
|
|
67
81
|
return `${amount.toFixed(2)} 🌼`;
|
|
@@ -88,8 +102,9 @@ function parseUsageTimestamp(timestamp) {
|
|
|
88
102
|
return new Date(timestamp.replace(' ', 'T') + 'Z');
|
|
89
103
|
}
|
|
90
104
|
function calculateResetDate(nextResetAt) {
|
|
91
|
-
|
|
92
|
-
|
|
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);
|
|
93
108
|
return lastReset;
|
|
94
109
|
}
|
|
95
110
|
function calculateCurrentPeriodStats(usage, lastReset, tierLimit) {
|
|
@@ -148,6 +163,8 @@ export async function handleCommand(command) {
|
|
|
148
163
|
return await handleUsageCommand(args);
|
|
149
164
|
case 'connect':
|
|
150
165
|
return await handleConnectCommand(args);
|
|
166
|
+
case 'login':
|
|
167
|
+
return await startDeviceLogin();
|
|
151
168
|
case 'fallback':
|
|
152
169
|
return handleFallbackCommand(args);
|
|
153
170
|
case 'config':
|
|
@@ -160,6 +177,8 @@ export async function handleCommand(command) {
|
|
|
160
177
|
return await handlePricingCommand();
|
|
161
178
|
case 'infos':
|
|
162
179
|
return await handleInfosCommand();
|
|
180
|
+
case 'quests':
|
|
181
|
+
return await handleQuestsCommand(args);
|
|
163
182
|
case 'addKey': // External trigger
|
|
164
183
|
// UI Pollution Fix: User hates appendPrompt.
|
|
165
184
|
// Just return a message telling them to use the tool.
|
|
@@ -243,7 +262,48 @@ export async function handleUsageCommand(args) {
|
|
|
243
262
|
response += t('commands.usage.tier', { emoji: quota.tierEmoji, tier: quota.tier.toUpperCase(), limit: quota.tierLimit });
|
|
244
263
|
response += t('commands.usage.quota', { remaining: formatPollen(quota.tierLimit - quota.tierRemaining), limit: formatPollen(quota.tierLimit) });
|
|
245
264
|
response += t('commands.usage.usage_bar', { bar: progressBar(quota.tierLimit - quota.tierRemaining, quota.tierLimit) });
|
|
246
|
-
|
|
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);
|
|
302
|
+
response += t('commands.usage.split', {
|
|
303
|
+
quest: questPollen.toFixed(2),
|
|
304
|
+
paid: paidPollen.toFixed(2),
|
|
305
|
+
total: total.toFixed(2),
|
|
306
|
+
});
|
|
247
307
|
response += t('commands.usage.reset', { date: resetDate, duration: durationStr });
|
|
248
308
|
if (isFull && config.apiKey) {
|
|
249
309
|
if (config.keyHasAccessToProfile === false) {
|
|
@@ -256,6 +316,12 @@ export async function handleUsageCommand(args) {
|
|
|
256
316
|
const stats = calculateCurrentPeriodStats(usageData, lastReset, quota.tierLimit);
|
|
257
317
|
response += t('commands.usage.period_detail', { time: lastReset.toLocaleTimeString() });
|
|
258
318
|
response += t('commands.usage.total_reqs', { reqs: stats.totalRequests, inTok: formatTokens(stats.inputTokens), outTok: formatTokens(stats.outputTokens) });
|
|
319
|
+
// 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).
|
|
321
|
+
response += t('commands.usage.source_split', {
|
|
322
|
+
tier: formatPollen(stats.tierUsed),
|
|
323
|
+
pack: formatPollen(stats.packUsed),
|
|
324
|
+
});
|
|
259
325
|
response += t('commands.usage.table_head1');
|
|
260
326
|
response += t('commands.usage.table_head2');
|
|
261
327
|
const sorted = Array.from(stats.models.entries()).sort((a, b) => b[1].cost - a[1].cost);
|
|
@@ -393,6 +459,185 @@ async function handleConnectCommand(args) {
|
|
|
393
459
|
};
|
|
394
460
|
}
|
|
395
461
|
}
|
|
462
|
+
// ─── DEVICE FLOW LOGIN (option C: background poller) ───────────────────────
|
|
463
|
+
/** Best-effort cross-platform browser open. Never throws (headless-safe). */
|
|
464
|
+
function openBrowser(url) {
|
|
465
|
+
try {
|
|
466
|
+
const cp = require('child_process');
|
|
467
|
+
const platform = process.platform;
|
|
468
|
+
const cmd = platform === 'win32' ? 'start ""'
|
|
469
|
+
: platform === 'darwin' ? 'open'
|
|
470
|
+
: 'xdg-open';
|
|
471
|
+
// Detached + ignore stdio so it never blocks the proxy process.
|
|
472
|
+
const child = cp.spawn(cmd, [url], {
|
|
473
|
+
shell: platform === 'win32',
|
|
474
|
+
detached: true,
|
|
475
|
+
stdio: 'ignore',
|
|
476
|
+
});
|
|
477
|
+
child.unref?.();
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function postJsonEnter(path, body) {
|
|
485
|
+
return new Promise((resolve, reject) => {
|
|
486
|
+
const payload = JSON.stringify(body);
|
|
487
|
+
const req = https.request({
|
|
488
|
+
hostname: 'enter.pollinations.ai',
|
|
489
|
+
path,
|
|
490
|
+
method: 'POST',
|
|
491
|
+
headers: {
|
|
492
|
+
'Content-Type': 'application/json',
|
|
493
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
494
|
+
'User-Agent': 'opencode-pollinations-plugin',
|
|
495
|
+
},
|
|
496
|
+
}, (res) => {
|
|
497
|
+
let data = '';
|
|
498
|
+
res.on('data', c => data += c);
|
|
499
|
+
res.on('end', () => {
|
|
500
|
+
try {
|
|
501
|
+
resolve(JSON.parse(data));
|
|
502
|
+
}
|
|
503
|
+
catch (e) {
|
|
504
|
+
reject(new Error(`Bad JSON: ${data.slice(0, 120)}`));
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
req.on('error', reject);
|
|
509
|
+
req.setTimeout(15000, () => { req.destroy(); reject(new Error('Timeout')); });
|
|
510
|
+
req.write(payload);
|
|
511
|
+
req.end();
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
let loginPollActive = false;
|
|
515
|
+
// Publishable app key (pk_) — embedded for BYOP attribution: the consent screen
|
|
516
|
+
// shows "plugin by fkom13" and traffic is credited to this app. Safe to ship
|
|
517
|
+
// publicly (publishable by design); earningsEnabled=false so users pay nothing extra.
|
|
518
|
+
const APP_CLIENT_ID = 'pk_sATzVHuna3I5e7Sf';
|
|
519
|
+
let loginResultPromise = null;
|
|
520
|
+
let lastLoginPrompt = null; // code+URL prompt, reused on wait timeout
|
|
521
|
+
/**
|
|
522
|
+
* Wait mode for the tool: ensures a login is running (auto-starts + opens the
|
|
523
|
+
* browser if needed), then waits up to ~90s and returns the final outcome.
|
|
524
|
+
* On timeout it returns the code/URL prompt so the agent can ask the user to
|
|
525
|
+
* finish authorizing, then be called again with wait:true.
|
|
526
|
+
*/
|
|
527
|
+
export async function awaitDeviceLogin() {
|
|
528
|
+
// Auto-start if nothing is in progress (single-call UX: open + wait + report).
|
|
529
|
+
if (!loginPollActive || !loginResultPromise) {
|
|
530
|
+
const started = await startDeviceLogin();
|
|
531
|
+
if (started.error)
|
|
532
|
+
return started.error;
|
|
533
|
+
// If it reported "already running" without a promise, fall through to wait.
|
|
534
|
+
}
|
|
535
|
+
if (!loginResultPromise) {
|
|
536
|
+
return lastLoginPrompt || t('commands.login.nothing_pending');
|
|
537
|
+
}
|
|
538
|
+
const WAIT_CAP_MS = 120000;
|
|
539
|
+
const timeout = new Promise((res) => setTimeout(() => res({ status: 'error', message: '__TIMEOUT__' }), WAIT_CAP_MS));
|
|
540
|
+
const outcome = await Promise.race([loginResultPromise, timeout]);
|
|
541
|
+
if (outcome.message === '__TIMEOUT__') {
|
|
542
|
+
// Still pending — hand back the code/URL so the user can finish, then retry.
|
|
543
|
+
return (lastLoginPrompt ? lastLoginPrompt + '\n\n' : '') + t('commands.login.still_waiting');
|
|
544
|
+
}
|
|
545
|
+
return outcome.message;
|
|
546
|
+
}
|
|
547
|
+
export async function startDeviceLogin() {
|
|
548
|
+
if (loginPollActive) {
|
|
549
|
+
return { handled: true, response: t('commands.login.already_running') };
|
|
550
|
+
}
|
|
551
|
+
let codeResp;
|
|
552
|
+
try {
|
|
553
|
+
codeResp = await postJsonEnter('/api/device/code', {
|
|
554
|
+
client_id: APP_CLIENT_ID,
|
|
555
|
+
scope: 'profile usage keys', // all scopes shown for transparency; keys (Account Admin) checked by default, user can uncheck
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
catch (e) {
|
|
559
|
+
return { handled: true, error: t('commands.login.code_error', { error: e.message }) };
|
|
560
|
+
}
|
|
561
|
+
const userCode = codeResp.user_code;
|
|
562
|
+
const deviceCode = codeResp.device_code;
|
|
563
|
+
const verifyUri = codeResp.verification_uri || 'https://enter.pollinations.ai/device';
|
|
564
|
+
// Standard device link (proven reliable). Scope is applied server-side via the
|
|
565
|
+
// /api/device/code POST body — passing budget/expiry/scope in the /authorize URL
|
|
566
|
+
// breaks submission (their form coerces empty->default and array-scope fails validation).
|
|
567
|
+
// For an unlimited key: user clears Budget + Expiry fields on the form before Authorize.
|
|
568
|
+
const verifyComplete = codeResp.verification_uri_complete || `${verifyUri}?user_code=${userCode}`;
|
|
569
|
+
const interval = (codeResp.interval || 5) * 1000;
|
|
570
|
+
const expiresIn = (codeResp.expires_in || 900) * 1000;
|
|
571
|
+
if (!userCode || !deviceCode) {
|
|
572
|
+
return { handled: true, error: t('commands.login.code_error', { error: 'no code returned' }) };
|
|
573
|
+
}
|
|
574
|
+
// Background poller — non-blocking. Resolves the shared promise on completion.
|
|
575
|
+
loginPollActive = true;
|
|
576
|
+
const deadline = Date.now() + Math.min(expiresIn, 300000); // cap 5 min for UX
|
|
577
|
+
let resolveOutcome;
|
|
578
|
+
loginResultPromise = new Promise((res) => { resolveOutcome = res; });
|
|
579
|
+
const finish = (o) => { loginPollActive = false; resolveOutcome(o); };
|
|
580
|
+
const poll = async () => {
|
|
581
|
+
if (Date.now() > deadline) {
|
|
582
|
+
const msg = t('commands.login.expired');
|
|
583
|
+
emitStatusToast('warning', msg, 'Pollinations Login');
|
|
584
|
+
finish({ status: 'expired', message: msg });
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
const tok = await postJsonEnter('/api/device/token', { device_code: deviceCode });
|
|
589
|
+
if (tok.access_token) {
|
|
590
|
+
// Got the key — validate & hot-load it (no restart needed)
|
|
591
|
+
const key = tok.access_token;
|
|
592
|
+
try {
|
|
593
|
+
await generatePollinationsConfig(key, true);
|
|
594
|
+
// Verify what the user actually granted (they choose on the consent form).
|
|
595
|
+
// Do NOT presume profile access — check it, like /poll connect does.
|
|
596
|
+
let limited = false;
|
|
597
|
+
try {
|
|
598
|
+
const check = await checkKeyPermissions(key);
|
|
599
|
+
limited = !check.ok;
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
limited = true;
|
|
603
|
+
}
|
|
604
|
+
saveConfig({ apiKey: key, keyHasAccessToProfile: !limited, ...(limited ? { mode: 'manual' } : {}) });
|
|
605
|
+
saveKeyToAuthJson(key);
|
|
606
|
+
const msg = limited
|
|
607
|
+
? t('commands.login.success_limited')
|
|
608
|
+
: t('commands.login.success_toast');
|
|
609
|
+
emitStatusToast(limited ? 'warning' : 'success', msg, 'Pollinations Login');
|
|
610
|
+
finish({ status: 'connected', message: msg });
|
|
611
|
+
}
|
|
612
|
+
catch (e) {
|
|
613
|
+
const msg = t('commands.login.validate_error', { error: e.message });
|
|
614
|
+
emitStatusToast('error', msg, 'Pollinations Login');
|
|
615
|
+
finish({ status: 'error', message: msg });
|
|
616
|
+
}
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
// pending → keep polling
|
|
620
|
+
setTimeout(poll, interval);
|
|
621
|
+
}
|
|
622
|
+
catch (e) {
|
|
623
|
+
// authorization_pending / slow_down / transient → keep polling
|
|
624
|
+
setTimeout(poll, interval);
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
setTimeout(poll, interval);
|
|
628
|
+
// Try to open the consent page automatically (headless-safe; URL shown as fallback).
|
|
629
|
+
const opened = openBrowser(verifyComplete);
|
|
630
|
+
const promptText = (opened ? t('commands.login.opened') + '\n\n' : '') + t('commands.login.prompt', {
|
|
631
|
+
code: userCode,
|
|
632
|
+
uri: verifyUri,
|
|
633
|
+
uri_complete: verifyComplete,
|
|
634
|
+
});
|
|
635
|
+
lastLoginPrompt = promptText;
|
|
636
|
+
return {
|
|
637
|
+
handled: true,
|
|
638
|
+
response: promptText,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
396
641
|
function handleConfigCommand(args) {
|
|
397
642
|
const [key, value] = args;
|
|
398
643
|
if (!key) {
|
|
@@ -425,7 +670,7 @@ ${t('commands.config.table_divider')}
|
|
|
425
670
|
};
|
|
426
671
|
}
|
|
427
672
|
if (key === 'lang' && value) {
|
|
428
|
-
if (!['en', 'fr', 'es', 'de', 'it'].includes(value)) {
|
|
673
|
+
if (!['en', 'fr', 'es', 'de', 'it', 'zh'].includes(value)) {
|
|
429
674
|
return { handled: true, error: "Valeurs supportées: en, fr, es, de, it" };
|
|
430
675
|
}
|
|
431
676
|
saveConfig({ lang: value });
|
|
@@ -687,7 +932,7 @@ export async function handleInfosCommand() {
|
|
|
687
932
|
}
|
|
688
933
|
}
|
|
689
934
|
const emojis = {
|
|
690
|
-
microbe: '🦠', spore: '🍄', seed: '🌱', flower: '🌸', nectar: '🍯', anonymous: '👤'
|
|
935
|
+
microbe: '🦠', spore: '🍄', seed: '🌱', flower: '🌸', nectar: '🍯', router: '🐝', anonymous: '👤'
|
|
691
936
|
};
|
|
692
937
|
const tierEmoji = emojis[tier] || '❓';
|
|
693
938
|
// Get dynamic tier table based on user's language
|
|
@@ -708,10 +953,12 @@ ${t('commands.infos.levels_title')}
|
|
|
708
953
|
|
|
709
954
|
${tierTable}
|
|
710
955
|
|
|
711
|
-
|
|
956
|
+
${t('commands.infos.hourly_note')}
|
|
712
957
|
|
|
713
958
|
${t('commands.infos.beta_note')}
|
|
714
959
|
|
|
960
|
+
${t('commands.infos.quests')}
|
|
961
|
+
|
|
715
962
|
${t('commands.infos.pollen_title')}
|
|
716
963
|
|
|
717
964
|
${t('commands.infos.pollen_get')}
|
|
@@ -719,6 +966,17 @@ ${t('commands.infos.pollen_get')}
|
|
|
719
966
|
${t('commands.infos.pollen_spend')}`;
|
|
720
967
|
return { handled: true, response };
|
|
721
968
|
}
|
|
969
|
+
async function handleQuestsCommand(args) {
|
|
970
|
+
const arg = (args[0] || 'all').toLowerCase();
|
|
971
|
+
const filter = arg === 'available' ? 'available' : arg === 'claimable' ? 'claimable' : 'all';
|
|
972
|
+
try {
|
|
973
|
+
const report = await buildQuestsReport(filter);
|
|
974
|
+
return { handled: true, response: report };
|
|
975
|
+
}
|
|
976
|
+
catch (e) {
|
|
977
|
+
return { handled: true, error: `Erreur: ${e.message || e}` };
|
|
978
|
+
}
|
|
979
|
+
}
|
|
722
980
|
// === INTEGRATION OPENCODE ===
|
|
723
981
|
export function createCommandHooks() {
|
|
724
982
|
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
|
}
|