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.
- package/README.de.md +67 -54
- package/README.es.md +79 -66
- package/README.fr.md +70 -57
- package/README.it.md +78 -65
- package/README.md +33 -29
- package/README.zh.md +77 -64
- package/dist/locales/de.json +82 -47
- package/dist/locales/en.json +84 -49
- package/dist/locales/es.json +82 -47
- package/dist/locales/fr.json +81 -46
- package/dist/locales/it.json +82 -47
- package/dist/locales/zh.json +82 -47
- package/dist/server/commands.js +118 -178
- package/dist/server/config.d.ts +22 -6
- package/dist/server/config.js +45 -5
- package/dist/server/connect-response.js +7 -7
- package/dist/server/generate-config.js +2 -2
- package/dist/server/models/cache.d.ts +23 -10
- package/dist/server/models/cache.js +46 -24
- package/dist/server/models/fetcher.js +2 -0
- package/dist/server/models/types.d.ts +2 -0
- package/dist/server/models/worker.js +4 -4
- package/dist/server/proxy.d.ts +6 -0
- package/dist/server/proxy.js +318 -218
- package/dist/server/quota.d.ts +23 -32
- package/dist/server/quota.js +44 -184
- package/dist/server/scripts/pollinations_pricing.js +6 -3
- package/dist/server/status.js +1 -2
- package/dist/server/toast.js +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/pollinations/artifact-core.d.ts +53 -0
- package/dist/tools/pollinations/artifact-core.js +159 -0
- package/dist/tools/pollinations/beta_discovery.js +2 -1
- package/dist/tools/pollinations/cost-guard.d.ts +2 -2
- package/dist/tools/pollinations/error-parser.d.ts +38 -0
- package/dist/tools/pollinations/error-parser.js +112 -0
- package/dist/tools/pollinations/gen_3d.d.ts +17 -0
- package/dist/tools/pollinations/gen_3d.js +207 -0
- package/dist/tools/pollinations/gen_image.js +29 -10
- package/dist/tools/pollinations/gen_music.js +3 -2
- package/dist/tools/pollinations/gen_video.js +13 -2
- package/dist/tools/pollinations/polli_config.js +15 -21
- package/dist/tools/pollinations/polli_gen_confirm.js +2 -0
- package/dist/tools/pollinations/shared.d.ts +1 -1
- package/dist/tools/pollinations/shared.js +53 -142
- package/dist/tools/pollinations/timeout-policy.d.ts +80 -0
- package/dist/tools/pollinations/timeout-policy.js +124 -0
- package/dist/tools/pollinations/tool-capability-registry.d.ts +51 -0
- package/dist/tools/pollinations/tool-capability-registry.js +215 -0
- package/dist/tools/pollinations/transcribe_audio.js +5 -24
- package/package.json +64 -62
- package/dist/server/tier-info.d.ts +0 -36
- package/dist/server/tier-info.js +0 -107
package/dist/server/quota.d.ts
CHANGED
|
@@ -1,48 +1,39 @@
|
|
|
1
1
|
import { DetailedUsageEntry } from './pollinations-api.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
/**
|
|
3
|
+
* v6.5 — Quest/Paid semantics.
|
|
4
|
+
* The old tier/refill model (KNOWN_REFILLS, hourly allowance deduction,
|
|
5
|
+
* tierMetaForAllowance) has been removed: upstream deleted the hourly refill
|
|
6
|
+
* (cron disabled 2026-06, code removed 2026-07). The client cannot read a
|
|
7
|
+
* reliable Quest/Paid split from /account/balance in prod, so `questBalance`
|
|
8
|
+
* is a BEST-EFFORT estimate (claimed quest pollen minus tier-metered usage
|
|
9
|
+
* since claim). `walletBalance` is Paid pollen when exposed (pack), otherwise
|
|
10
|
+
* estimated as total minus quest. meter_source in /account/usage remains the
|
|
11
|
+
* only authoritative retrospective split.
|
|
12
|
+
*/
|
|
12
13
|
export interface QuotaStatus {
|
|
13
|
-
|
|
14
|
-
tierUsed: number;
|
|
15
|
-
tierLimit: number;
|
|
16
|
-
questStash: number;
|
|
14
|
+
questBalance: number;
|
|
17
15
|
walletBalance: number;
|
|
18
|
-
|
|
19
|
-
timeUntilReset: number;
|
|
16
|
+
totalBalance: number;
|
|
20
17
|
canUseEnterprise: boolean;
|
|
21
18
|
isUsingWallet: boolean;
|
|
22
19
|
needsAlert: boolean;
|
|
23
|
-
tier: string;
|
|
24
|
-
tierEmoji: string;
|
|
25
20
|
errorType?: 'auth_limited' | 'network' | 'unknown';
|
|
26
21
|
}
|
|
27
22
|
export declare function fetchUsageForPeriod(apiKey: string, lastReset: Date): Promise<DetailedUsageEntry[]>;
|
|
28
|
-
/** Map hourly refill amount → display meta (exported for unit tests). */
|
|
29
|
-
export declare function tierMetaForAllowance(allowance: number): {
|
|
30
|
-
label: string;
|
|
31
|
-
emoji: string;
|
|
32
|
-
};
|
|
33
|
-
/** Known hourly refill ladder (read-only, for tests / UI). */
|
|
34
|
-
export declare function getKnownRefills(): ReadonlyArray<{
|
|
35
|
-
pollen: number;
|
|
36
|
-
emoji: string;
|
|
37
|
-
label: string;
|
|
38
|
-
}>;
|
|
39
23
|
export declare function getQuotaStatus(forceRefresh?: boolean): Promise<QuotaStatus>;
|
|
40
|
-
/** Next top-of-hour UTC reset window (
|
|
41
|
-
export declare function calculateResetInfo():
|
|
24
|
+
/** Next top-of-hour UTC reset window (used for usage period windows only). */
|
|
25
|
+
export declare function calculateResetInfo(): {
|
|
26
|
+
nextReset: Date;
|
|
27
|
+
lastReset: Date;
|
|
28
|
+
};
|
|
42
29
|
export declare function formatQuotaForToast(quota: QuotaStatus): string;
|
|
30
|
+
/**
|
|
31
|
+
* Best-effort Quest balance: claimed quest pollen (tier bucket) minus
|
|
32
|
+
* tier-metered consumption since the earliest claim. This is NOT a server
|
|
33
|
+
* guarantee — the authoritative split is meter_source in /account/usage.
|
|
34
|
+
*/
|
|
43
35
|
export declare function fetchQuestStash(apiKey: string): Promise<{
|
|
44
36
|
questStash: number;
|
|
45
37
|
claimedQuestTier: number;
|
|
46
38
|
tierConsumedSinceClaim: number;
|
|
47
39
|
}>;
|
|
48
|
-
export {};
|
package/dist/server/quota.js
CHANGED
|
@@ -4,18 +4,10 @@ import { loadConfig } from './config.js';
|
|
|
4
4
|
const CACHE_TTL = 30000;
|
|
5
5
|
let cachedQuota = null;
|
|
6
6
|
let lastQuotaFetch = 0;
|
|
7
|
-
const STASH_CACHE_TTL = 5 * 60 * 1000; // 5 min —
|
|
7
|
+
const STASH_CACHE_TTL = 5 * 60 * 1000; // 5 min — Quest stash changes rarely
|
|
8
8
|
let cachedStash = null;
|
|
9
9
|
let lastStashFetch = 0;
|
|
10
10
|
const ONE_HOUR_MS = 60 * 60 * 1000;
|
|
11
|
-
const KNOWN_REFILLS = [
|
|
12
|
-
{ pollen: 0, emoji: '👤', label: 'anonymous' },
|
|
13
|
-
{ pollen: 0.01, emoji: '🍄', label: 'spore' },
|
|
14
|
-
{ pollen: 0.15, emoji: '🌱', label: 'seed' },
|
|
15
|
-
{ pollen: 0.4, emoji: '🌸', label: 'flower' },
|
|
16
|
-
{ pollen: 0.8, emoji: '🍯', label: 'nectar' },
|
|
17
|
-
{ pollen: 10, emoji: '🐝', label: 'router' },
|
|
18
|
-
];
|
|
19
11
|
// === LOGGING ===
|
|
20
12
|
import { logApi } from './logger.js';
|
|
21
13
|
function logQuota(msg) {
|
|
@@ -65,96 +57,11 @@ export async function fetchUsageForPeriod(apiKey, lastReset) {
|
|
|
65
57
|
logQuota(`SmartFetch: Retrieved ${allUsage.length} transactions for current period.`);
|
|
66
58
|
return allUsage;
|
|
67
59
|
}
|
|
68
|
-
// === ALLOWANCE DEDUCTION ===
|
|
69
|
-
async function fetchUsageForAllowance(apiKey) {
|
|
70
|
-
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * ONE_HOUR_MS);
|
|
71
|
-
const allUsage = [];
|
|
72
|
-
let cursorEventId = null;
|
|
73
|
-
let pageCount = 0;
|
|
74
|
-
const maxPages = 20;
|
|
75
|
-
while (pageCount < maxPages) {
|
|
76
|
-
let path = `/account/usage?days=7&limit=500`;
|
|
77
|
-
if (cursorEventId) {
|
|
78
|
-
path += `&before_event_id=${encodeURIComponent(cursorEventId)}`;
|
|
79
|
-
}
|
|
80
|
-
const res = await fetchAPI(path, apiKey);
|
|
81
|
-
if (!res.usage || res.usage.length === 0)
|
|
82
|
-
break;
|
|
83
|
-
let reachedCutoff = false;
|
|
84
|
-
for (const entry of res.usage) {
|
|
85
|
-
const ts = (entry.timestamp.includes('Z') ? entry.timestamp : entry.timestamp.replace(' ', 'T') + 'Z');
|
|
86
|
-
if (new Date(ts) < sevenDaysAgo) {
|
|
87
|
-
reachedCutoff = true;
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
allUsage.push(entry);
|
|
91
|
-
}
|
|
92
|
-
if (reachedCutoff || res.usage.length < 500)
|
|
93
|
-
break;
|
|
94
|
-
const last = res.usage[res.usage.length - 1];
|
|
95
|
-
cursorEventId = last?.cursor_event_id || null;
|
|
96
|
-
if (!cursorEventId)
|
|
97
|
-
break;
|
|
98
|
-
pageCount++;
|
|
99
|
-
}
|
|
100
|
-
logQuota(`deduceAllowance: fetched ${allUsage.length} records over ${pageCount + 1} pages`);
|
|
101
|
-
return allUsage;
|
|
102
|
-
}
|
|
103
|
-
async function deduceAllowanceFromApi(apiKey) {
|
|
104
|
-
try {
|
|
105
|
-
const allUsage = await fetchUsageForAllowance(apiKey);
|
|
106
|
-
if (allUsage.length === 0)
|
|
107
|
-
return 0;
|
|
108
|
-
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * ONE_HOUR_MS);
|
|
109
|
-
const hourlyBuckets = new Map();
|
|
110
|
-
for (const entry of allUsage) {
|
|
111
|
-
if (entry.meter_source !== 'tier')
|
|
112
|
-
continue;
|
|
113
|
-
const ts = entry.timestamp.includes('Z') ? entry.timestamp : entry.timestamp.replace(' ', 'T') + 'Z';
|
|
114
|
-
const entryTime = new Date(ts);
|
|
115
|
-
if (entryTime < sevenDaysAgo)
|
|
116
|
-
continue;
|
|
117
|
-
const hourKey = entryTime.getUTCFullYear() * 1000000
|
|
118
|
-
+ entryTime.getUTCMonth() * 10000
|
|
119
|
-
+ entryTime.getUTCDate() * 100
|
|
120
|
-
+ entryTime.getUTCHours();
|
|
121
|
-
hourlyBuckets.set(hourKey, (hourlyBuckets.get(hourKey) || 0) + entry.cost_usd);
|
|
122
|
-
}
|
|
123
|
-
const maxHourlyTier = Math.max(0, ...hourlyBuckets.values());
|
|
124
|
-
const match = KNOWN_REFILLS.slice().reverse().find(r => r.pollen <= maxHourlyTier + 0.02);
|
|
125
|
-
logQuota(`deduceAllowance: ${allUsage.length} records, ${hourlyBuckets.size} hourly buckets, max=${maxHourlyTier.toFixed(4)}, deduced=${match?.pollen ?? 0} (${match?.label})`);
|
|
126
|
-
return match ? match.pollen : 0;
|
|
127
|
-
}
|
|
128
|
-
catch (e) {
|
|
129
|
-
logQuota(`deduceAllowance failed: ${e}`);
|
|
130
|
-
return 0;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
function deduceAllowanceFromUsage(usage) {
|
|
134
|
-
const tierCosts = usage
|
|
135
|
-
.filter(u => u.meter_source === 'tier')
|
|
136
|
-
.map(u => u.cost_usd);
|
|
137
|
-
const maxHourlyTier = Math.max(0, ...tierCosts);
|
|
138
|
-
const match = KNOWN_REFILLS.slice().reverse().find(r => r.pollen <= maxHourlyTier + 0.02);
|
|
139
|
-
return match ? match.pollen : 0;
|
|
140
|
-
}
|
|
141
|
-
/** Map hourly refill amount → display meta (exported for unit tests). */
|
|
142
|
-
export function tierMetaForAllowance(allowance) {
|
|
143
|
-
const match = KNOWN_REFILLS.find(r => r.pollen === allowance)
|
|
144
|
-
|| KNOWN_REFILLS.findLast(r => r.pollen <= allowance);
|
|
145
|
-
return match
|
|
146
|
-
? { label: match.label, emoji: match.emoji }
|
|
147
|
-
: { label: 'unknown', emoji: '❓' };
|
|
148
|
-
}
|
|
149
|
-
/** Known hourly refill ladder (read-only, for tests / UI). */
|
|
150
|
-
export function getKnownRefills() {
|
|
151
|
-
return KNOWN_REFILLS;
|
|
152
|
-
}
|
|
153
60
|
// === MAIN QUOTA FUNCTION ===
|
|
154
61
|
export async function getQuotaStatus(forceRefresh = false) {
|
|
155
62
|
const config = loadConfig();
|
|
156
63
|
if (!config.apiKey) {
|
|
157
|
-
return createDefaultQuota(
|
|
64
|
+
return createDefaultQuota(0);
|
|
158
65
|
}
|
|
159
66
|
const now = Date.now();
|
|
160
67
|
if (!forceRefresh && cachedQuota && (now - lastQuotaFetch) < CACHE_TTL) {
|
|
@@ -163,19 +70,6 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
163
70
|
try {
|
|
164
71
|
logQuota("Fetching Quota Data...");
|
|
165
72
|
const balanceRes = await fetchAPI('/account/balance', config.apiKey);
|
|
166
|
-
const resetInfo = calculateResetInfo();
|
|
167
|
-
const periodUsage = await fetchUsageForPeriod(config.apiKey, resetInfo.lastReset);
|
|
168
|
-
const allowance = balanceRes.allowance
|
|
169
|
-
?? config.refillOverride
|
|
170
|
-
?? await deduceAllowanceFromApi(config.apiKey);
|
|
171
|
-
const tierMeta = tierMetaForAllowance(allowance);
|
|
172
|
-
const tierLimit = allowance;
|
|
173
|
-
const { tierUsed } = calculateCurrentPeriodUsage(periodUsage, resetInfo);
|
|
174
|
-
const tierRemaining = Math.max(0, tierLimit - tierUsed);
|
|
175
|
-
const cleanTierRemaining = Math.max(0, parseFloat(tierRemaining.toFixed(4)));
|
|
176
|
-
const totalBalance = balanceRes.total
|
|
177
|
-
?? balanceRes.balance
|
|
178
|
-
?? 0;
|
|
179
73
|
// Fetch quest stash BEFORE paidPollen calculation (cached 5 min)
|
|
180
74
|
const nowStash = Date.now();
|
|
181
75
|
if (!cachedStash || (nowStash - lastStashFetch) > STASH_CACHE_TTL) {
|
|
@@ -185,29 +79,33 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
185
79
|
}
|
|
186
80
|
catch { /* keep previous */ }
|
|
187
81
|
}
|
|
188
|
-
const
|
|
82
|
+
const questEstimate = cachedStash?.questStash ?? 0;
|
|
83
|
+
const totalBalance = balanceRes.total
|
|
84
|
+
?? balanceRes.balance
|
|
85
|
+
?? 0;
|
|
86
|
+
// Quest balance: native allowance when available (PR #12541 not in prod),
|
|
87
|
+
// else best-effort stash estimate.
|
|
88
|
+
const questBalance = balanceRes.allowance !== undefined
|
|
89
|
+
? Math.max(0, balanceRes.allowance)
|
|
90
|
+
: questEstimate;
|
|
91
|
+
// Paid pollen: native pack when available, else total minus quest estimate.
|
|
189
92
|
const paidPollenNative = balanceRes.pack;
|
|
190
|
-
const
|
|
191
|
-
? paidPollenNative
|
|
192
|
-
: Math.max(0, totalBalance -
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
|
|
93
|
+
const walletBalance = paidPollenNative !== undefined
|
|
94
|
+
? Math.max(0, paidPollenNative)
|
|
95
|
+
: Math.max(0, totalBalance - questBalance);
|
|
96
|
+
const cleanQuestBalance = Math.max(0, parseFloat(questBalance.toFixed(4)));
|
|
97
|
+
const cleanWalletBalance = Math.max(0, parseFloat(walletBalance.toFixed(4)));
|
|
98
|
+
// Alerts (v6.5): thresholds are absolute pollen floors, not percentages.
|
|
99
|
+
const questNeedsAlert = cleanQuestBalance > 0 && cleanQuestBalance < (config.thresholds.quest ?? 0.05);
|
|
196
100
|
const walletNeedsAlert = cleanWalletBalance > 0 && cleanWalletBalance < (config.thresholds.wallet || 0.5);
|
|
197
|
-
logQuota(`Fetch Success.
|
|
101
|
+
logQuota(`Fetch Success. Quest: ${cleanQuestBalance}, Paid: ${cleanWalletBalance}, Total: ${totalBalance}`);
|
|
198
102
|
cachedQuota = {
|
|
199
|
-
|
|
200
|
-
tierUsed,
|
|
201
|
-
tierLimit,
|
|
202
|
-
questStash,
|
|
103
|
+
questBalance: cleanQuestBalance,
|
|
203
104
|
walletBalance: cleanWalletBalance,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
needsAlert: tierNeedsAlert || walletNeedsAlert,
|
|
209
|
-
tier: tierMeta.label,
|
|
210
|
-
tierEmoji: tierMeta.emoji
|
|
105
|
+
totalBalance,
|
|
106
|
+
canUseEnterprise: cleanQuestBalance > 0.05 || cleanWalletBalance > 0.05,
|
|
107
|
+
isUsingWallet: cleanQuestBalance <= 0.05 && cleanWalletBalance > 0.05,
|
|
108
|
+
needsAlert: questNeedsAlert || walletNeedsAlert
|
|
211
109
|
};
|
|
212
110
|
lastQuotaFetch = now;
|
|
213
111
|
return cachedQuota;
|
|
@@ -219,24 +117,17 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
219
117
|
errorType = 'auth_limited';
|
|
220
118
|
else if (e.message && e.message.includes('Network Error'))
|
|
221
119
|
errorType = 'network';
|
|
222
|
-
return cachedQuota || { ...createDefaultQuota(
|
|
120
|
+
return cachedQuota || { ...createDefaultQuota(0), errorType };
|
|
223
121
|
}
|
|
224
122
|
}
|
|
225
|
-
function createDefaultQuota(
|
|
226
|
-
const meta = tierMetaForAllowance(limit);
|
|
123
|
+
function createDefaultQuota(_limit) {
|
|
227
124
|
return {
|
|
228
|
-
|
|
229
|
-
tierUsed: 0,
|
|
230
|
-
tierLimit: limit,
|
|
231
|
-
questStash: 0,
|
|
125
|
+
questBalance: 0,
|
|
232
126
|
walletBalance: 0,
|
|
233
|
-
|
|
234
|
-
timeUntilReset: 0,
|
|
127
|
+
totalBalance: 0,
|
|
235
128
|
canUseEnterprise: false,
|
|
236
129
|
isUsingWallet: false,
|
|
237
|
-
needsAlert: false
|
|
238
|
-
tier: tierName !== 'none' ? meta.label : 'none',
|
|
239
|
-
tierEmoji: meta.emoji
|
|
130
|
+
needsAlert: false
|
|
240
131
|
};
|
|
241
132
|
}
|
|
242
133
|
// === HELPERS ===
|
|
@@ -249,7 +140,7 @@ function fetchAPI(endpoint, apiKey) {
|
|
|
249
140
|
method: 'GET',
|
|
250
141
|
headers: {
|
|
251
142
|
'Authorization': `Bearer ${apiKey}`,
|
|
252
|
-
'User-Agent': 'opencode-pollinations-plugin/6.
|
|
143
|
+
'User-Agent': 'opencode-pollinations-plugin/6.5.0'
|
|
253
144
|
}
|
|
254
145
|
};
|
|
255
146
|
const req = https.request(options, (res) => {
|
|
@@ -272,62 +163,31 @@ function fetchAPI(endpoint, apiKey) {
|
|
|
272
163
|
req.on('error', (e) => {
|
|
273
164
|
reject(new Error(`Network Error: ${e.message}`));
|
|
274
165
|
});
|
|
166
|
+
// v6.5: bound the request (quota read was previously unbounded → hang risk).
|
|
167
|
+
req.setTimeout(10000, () => {
|
|
168
|
+
req.destroy(new Error('Timeout: quota API fetch exceeded 10s'));
|
|
169
|
+
});
|
|
275
170
|
req.end();
|
|
276
171
|
});
|
|
277
172
|
}
|
|
278
|
-
/** Next top-of-hour UTC reset window (
|
|
173
|
+
/** Next top-of-hour UTC reset window (used for usage period windows only). */
|
|
279
174
|
export function calculateResetInfo() {
|
|
280
175
|
const now = new Date();
|
|
281
176
|
const nextReset = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours() + 1, 0, 0, 0));
|
|
282
177
|
const lastReset = new Date(nextReset.getTime() - ONE_HOUR_MS);
|
|
283
|
-
|
|
284
|
-
const timeSinceReset = Math.max(0, now.getTime() - lastReset.getTime());
|
|
285
|
-
const progressPercent = Math.min(100, (timeSinceReset / ONE_HOUR_MS) * 100);
|
|
286
|
-
return {
|
|
287
|
-
nextReset,
|
|
288
|
-
lastReset,
|
|
289
|
-
timeUntilReset,
|
|
290
|
-
timeSinceReset,
|
|
291
|
-
resetHour: nextReset.getUTCHours(),
|
|
292
|
-
resetMinute: 0,
|
|
293
|
-
resetSecond: 0,
|
|
294
|
-
progressPercent
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
function calculateCurrentPeriodUsage(usage, resetInfo) {
|
|
298
|
-
let tierUsed = 0;
|
|
299
|
-
let packUsed = 0;
|
|
300
|
-
const entriesAfterReset = usage.filter(entry => {
|
|
301
|
-
const timestamp = entry.timestamp.replace(' ', 'T') + 'Z';
|
|
302
|
-
const entryTime = new Date(timestamp);
|
|
303
|
-
return entryTime >= resetInfo.lastReset;
|
|
304
|
-
});
|
|
305
|
-
for (const entry of entriesAfterReset) {
|
|
306
|
-
if (entry.meter_source === 'tier') {
|
|
307
|
-
tierUsed += entry.cost_usd;
|
|
308
|
-
}
|
|
309
|
-
else if (entry.meter_source === 'pack') {
|
|
310
|
-
packUsed += entry.cost_usd;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
return { tierUsed, packUsed };
|
|
178
|
+
return { nextReset, lastReset };
|
|
314
179
|
}
|
|
315
180
|
export function formatQuotaForToast(quota) {
|
|
316
181
|
if (quota.errorType === 'auth_limited') {
|
|
317
|
-
return `🔑 CLE LIMITÉE (Génération Seule) | 💎
|
|
182
|
+
return `🔑 CLE LIMITÉE (Génération Seule) | 💎 Paid: N/A | 🎁 Quest: N/A`;
|
|
318
183
|
}
|
|
319
|
-
|
|
320
|
-
? Math.round((quota.tierRemaining / quota.tierLimit) * 100)
|
|
321
|
-
: 0;
|
|
322
|
-
const ms = quota.timeUntilReset;
|
|
323
|
-
const hours = Math.floor(ms / (1000 * 60 * 60));
|
|
324
|
-
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
|
|
325
|
-
const resetIn = `${hours}h${minutes}m`;
|
|
326
|
-
const stashStr = quota.questStash > 0
|
|
327
|
-
? ` | 🎁 ~${quota.questStash.toFixed(2)} (stash)`
|
|
328
|
-
: '';
|
|
329
|
-
return `${quota.tierEmoji} ${quota.tierRemaining.toFixed(2)}/${quota.tierLimit} (${tierPercent}%)${stashStr} | 💎 $${quota.walletBalance.toFixed(2)} | ⏰ ${resetIn}`;
|
|
184
|
+
return `🎁 Quest: ~${quota.questBalance.toFixed(2)} | 💎 Paid: ~${quota.walletBalance.toFixed(2)}${quota.needsAlert ? ' | ⚠️' : ''}`;
|
|
330
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Best-effort Quest balance: claimed quest pollen (tier bucket) minus
|
|
188
|
+
* tier-metered consumption since the earliest claim. This is NOT a server
|
|
189
|
+
* guarantee — the authoritative split is meter_source in /account/usage.
|
|
190
|
+
*/
|
|
331
191
|
export async function fetchQuestStash(apiKey) {
|
|
332
192
|
let claimedQuestTier = 0;
|
|
333
193
|
let firstClaimMs = Infinity;
|
|
@@ -109,12 +109,15 @@ function getCost(m, type) {
|
|
|
109
109
|
}
|
|
110
110
|
// ─── RENDU VISUEL ────────────────────────────────────────────────────────────
|
|
111
111
|
function parseNameDesc(m) {
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
if (m.title && m.description) {
|
|
113
|
+
return { nom: m.title, desc: m.description };
|
|
114
|
+
}
|
|
115
|
+
const displayName = m.title || m.description || m.name;
|
|
116
|
+
const parts = displayName.split(" - ");
|
|
114
117
|
if (parts.length > 1) {
|
|
115
118
|
return { nom: parts[0].trim(), desc: parts.slice(1).join(" - ").trim() };
|
|
116
119
|
}
|
|
117
|
-
return { nom:
|
|
120
|
+
return { nom: displayName, desc: "" };
|
|
118
121
|
}
|
|
119
122
|
function flags(m, overrides = []) {
|
|
120
123
|
const f = [];
|
package/dist/server/status.js
CHANGED
|
@@ -5,6 +5,5 @@ export function createStatusHooks(client) {
|
|
|
5
5
|
};
|
|
6
6
|
}
|
|
7
7
|
function formatStatus(quota) {
|
|
8
|
-
|
|
9
|
-
return `${tierName} ${quota.tierRemaining.toFixed(2)}/${quota.tierLimit} 🌼 | Wallet $${quota.walletBalance.toFixed(2)}`;
|
|
8
|
+
return `🎁 Quest ~${quota.questBalance.toFixed(2)} | 💎 Paid ~${quota.walletBalance.toFixed(2)}`;
|
|
10
9
|
}
|
package/dist/server/toast.js
CHANGED
|
@@ -41,7 +41,7 @@ export function emitStatusToast(type, message, title, metadata) {
|
|
|
41
41
|
getQuotaStatus(true).then(quota => {
|
|
42
42
|
const quotaMsg = formatQuotaForToast
|
|
43
43
|
? formatQuotaForToast(quota)
|
|
44
|
-
:
|
|
44
|
+
: `🎁 Quest: ~${quota.questBalance.toFixed(2)} | 💎 Paid: ~${quota.walletBalance.toFixed(2)}`;
|
|
45
45
|
finalMessage += `\n${quotaMsg}`;
|
|
46
46
|
dispatchToast('status', type, finalMessage, title || 'Pollinations Status');
|
|
47
47
|
}).catch(() => {
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { polliGenVideoTool } from './pollinations/gen_video.js';
|
|
|
11
11
|
import { polliGenAudioTool } from './pollinations/gen_audio.js';
|
|
12
12
|
import { polliSttTool } from './pollinations/transcribe_audio.js';
|
|
13
13
|
import { polliGenMusicTool } from './pollinations/gen_music.js';
|
|
14
|
+
import { polliGen3dTool } from './pollinations/gen_3d.js';
|
|
14
15
|
import { polliWebSearchTool } from './pollinations/polli_web_search.js';
|
|
15
16
|
import { polliBetaDiscoveryTool } from './pollinations/beta_discovery.js';
|
|
16
17
|
import { polliGenConfirmTool } from './pollinations/polli_gen_confirm.js';
|
|
@@ -23,4 +24,4 @@ import { polliQuestsTool } from './pollinations/polli_quests.js';
|
|
|
23
24
|
* @returns Record<string, Tool> to be spread into the plugin's tool: {} property
|
|
24
25
|
*/
|
|
25
26
|
export declare function createToolRegistry(): Record<string, any>;
|
|
26
|
-
export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool, polliQuestsTool };
|
|
27
|
+
export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliGen3dTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool, polliQuestsTool };
|
package/dist/tools/index.js
CHANGED
|
@@ -30,6 +30,7 @@ import { polliGenVideoTool } from './pollinations/gen_video.js';
|
|
|
30
30
|
import { polliGenAudioTool } from './pollinations/gen_audio.js';
|
|
31
31
|
import { polliSttTool } from './pollinations/transcribe_audio.js';
|
|
32
32
|
import { polliGenMusicTool } from './pollinations/gen_music.js';
|
|
33
|
+
import { polliGen3dTool } from './pollinations/gen_3d.js';
|
|
33
34
|
import { polliWebSearchTool } from './pollinations/polli_web_search.js';
|
|
34
35
|
import { polliBetaDiscoveryTool } from './pollinations/beta_discovery.js';
|
|
35
36
|
import { polliGenConfirmTool } from './pollinations/polli_gen_confirm.js';
|
|
@@ -82,6 +83,7 @@ export function createToolRegistry() {
|
|
|
82
83
|
tools['polli_gen_audio'] = polliGenAudioTool;
|
|
83
84
|
tools['polli_stt'] = polliSttTool;
|
|
84
85
|
tools['polli_gen_music'] = polliGenMusicTool;
|
|
86
|
+
tools['polli_gen_3d'] = polliGen3dTool;
|
|
85
87
|
// Unified search tool
|
|
86
88
|
tools['polli_web_search'] = polliWebSearchTool;
|
|
87
89
|
// Cost Guard Confirmation tool
|
|
@@ -104,4 +106,4 @@ export function createToolRegistry() {
|
|
|
104
106
|
return tools;
|
|
105
107
|
}
|
|
106
108
|
// Re-export for convenience
|
|
107
|
-
export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool, polliQuestsTool };
|
|
109
|
+
export { polliGenImageTool, polliGenVideoTool, polliGenAudioTool, polliSttTool, polliGenMusicTool, polliGen3dTool, polliWebSearchTool, polliBetaDiscoveryTool, polliGenConfirmTool, polliStatusTool, polliConfigTool, polliQuestsTool };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact Core (v6.5) — shared input/output primitives.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the Free Tools (gen_video_free.resolveAsset/mimeFor,
|
|
5
|
+
* imgtools buildMultipart/getDims) and generalized for Pollinations tools,
|
|
6
|
+
* Free services and local operations.
|
|
7
|
+
*
|
|
8
|
+
* Pipeline: resolveArtifactInput → executeOperation → retrieve bytes
|
|
9
|
+
* → detectArtifactType (magic bytes) → persistArtifact
|
|
10
|
+
*
|
|
11
|
+
* Invariant: saved extension follows REAL detected bytes, never a
|
|
12
|
+
* caller-requested format (a b64 edit response can be JPEG while the
|
|
13
|
+
* caller assumes PNG — see Phase 2 live evidence T14).
|
|
14
|
+
*/
|
|
15
|
+
export interface ResolvedAsset {
|
|
16
|
+
buf: Buffer;
|
|
17
|
+
mime: string;
|
|
18
|
+
ext: string;
|
|
19
|
+
filename: string;
|
|
20
|
+
}
|
|
21
|
+
export interface DetectedArtifact {
|
|
22
|
+
format: string;
|
|
23
|
+
ext: string;
|
|
24
|
+
mime: string;
|
|
25
|
+
}
|
|
26
|
+
/** Magic-bytes detection. Returns null when the format is unknown. */
|
|
27
|
+
export declare function detectArtifactType(buf: Buffer): DetectedArtifact | null;
|
|
28
|
+
export declare function mimeForExt(ext: string): string;
|
|
29
|
+
export declare function mimeFor(ext: string, kind: 'image' | 'audio' | 'video' | 'model'): string;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve an asset input (local path | http(s) URL | data: URI) into a
|
|
32
|
+
* Buffer + mime + filename. URL downloads are bounded (60s default).
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveArtifactInput(input: string, kind?: 'image' | 'audio' | 'video' | 'model', timeoutMs?: number): Promise<ResolvedAsset>;
|
|
35
|
+
export declare function sanitizeFilename(name: string): string;
|
|
36
|
+
export interface PersistOptions {
|
|
37
|
+
outputDir: string;
|
|
38
|
+
filename?: string;
|
|
39
|
+
preferredExt?: string;
|
|
40
|
+
/** When true (default), real magic bytes override the preferred extension. */
|
|
41
|
+
detectExt?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface PersistedArtifact {
|
|
44
|
+
filePath: string;
|
|
45
|
+
size: number;
|
|
46
|
+
detected: DetectedArtifact | null;
|
|
47
|
+
ext: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Persist artifact bytes. The written extension follows the DETECTED magic
|
|
51
|
+
* bytes (detectExt !== false) — never a blind caller assumption.
|
|
52
|
+
*/
|
|
53
|
+
export declare function persistArtifact(buf: Buffer, opts: PersistOptions): PersistedArtifact;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact Core (v6.5) — shared input/output primitives.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the Free Tools (gen_video_free.resolveAsset/mimeFor,
|
|
5
|
+
* imgtools buildMultipart/getDims) and generalized for Pollinations tools,
|
|
6
|
+
* Free services and local operations.
|
|
7
|
+
*
|
|
8
|
+
* Pipeline: resolveArtifactInput → executeOperation → retrieve bytes
|
|
9
|
+
* → detectArtifactType (magic bytes) → persistArtifact
|
|
10
|
+
*
|
|
11
|
+
* Invariant: saved extension follows REAL detected bytes, never a
|
|
12
|
+
* caller-requested format (a b64 edit response can be JPEG while the
|
|
13
|
+
* caller assumes PNG — see Phase 2 live evidence T14).
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
/** Magic-bytes detection. Returns null when the format is unknown. */
|
|
18
|
+
export function detectArtifactType(buf) {
|
|
19
|
+
if (!buf || buf.length < 4)
|
|
20
|
+
return null;
|
|
21
|
+
// JPEG
|
|
22
|
+
if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
|
|
23
|
+
return { format: 'jpeg', ext: 'jpg', mime: 'image/jpeg' };
|
|
24
|
+
}
|
|
25
|
+
// PNG
|
|
26
|
+
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
|
|
27
|
+
return { format: 'png', ext: 'png', mime: 'image/png' };
|
|
28
|
+
}
|
|
29
|
+
// GIF
|
|
30
|
+
if (buf.toString('ascii', 0, 3) === 'GIF') {
|
|
31
|
+
return { format: 'gif', ext: 'gif', mime: 'image/gif' };
|
|
32
|
+
}
|
|
33
|
+
// WebP
|
|
34
|
+
if (buf.length > 12 && buf.toString('ascii', 0, 4) === 'RIFF' && buf.toString('ascii', 8, 12) === 'WEBP') {
|
|
35
|
+
return { format: 'webp', ext: 'webp', mime: 'image/webp' };
|
|
36
|
+
}
|
|
37
|
+
// glTF binary (GLB) — magic 'glTF' (0x676C5446)
|
|
38
|
+
if (buf.toString('ascii', 0, 4) === 'glTF') {
|
|
39
|
+
return { format: 'glb', ext: 'glb', mime: 'model/gltf-binary' };
|
|
40
|
+
}
|
|
41
|
+
// MP4 (ftyp box at offset 4)
|
|
42
|
+
if (buf.length > 12 && buf.toString('ascii', 4, 8) === 'ftyp') {
|
|
43
|
+
return { format: 'mp4', ext: 'mp4', mime: 'video/mp4' };
|
|
44
|
+
}
|
|
45
|
+
// WebM (EBML magic)
|
|
46
|
+
if (buf[0] === 0x1a && buf[1] === 0x45 && buf[2] === 0xdf && buf[3] === 0xa3) {
|
|
47
|
+
return { format: 'webm', ext: 'webm', mime: 'video/webm' };
|
|
48
|
+
}
|
|
49
|
+
// MP3 (ID3 tag or MPEG frame sync)
|
|
50
|
+
if (buf.length > 3 && (buf.toString('ascii', 0, 3) === 'ID3' || (buf[0] === 0xff && (buf[1] & 0xe0) === 0xe0))) {
|
|
51
|
+
return { format: 'mp3', ext: 'mp3', mime: 'audio/mpeg' };
|
|
52
|
+
}
|
|
53
|
+
// WAV
|
|
54
|
+
if (buf.length > 12 && buf.toString('ascii', 0, 4) === 'RIFF' && buf.toString('ascii', 8, 12) === 'WAVE') {
|
|
55
|
+
return { format: 'wav', ext: 'wav', mime: 'audio/wav' };
|
|
56
|
+
}
|
|
57
|
+
// OGG
|
|
58
|
+
if (buf.toString('ascii', 0, 4) === 'OggS') {
|
|
59
|
+
return { format: 'ogg', ext: 'ogg', mime: 'audio/ogg' };
|
|
60
|
+
}
|
|
61
|
+
// JSON (fallback for text artifacts)
|
|
62
|
+
if (buf.length > 0 && (buf[0] === 0x7b || buf[0] === 0x5b)) {
|
|
63
|
+
return { format: 'json', ext: 'json', mime: 'application/json' };
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const EXT_MIME = {
|
|
68
|
+
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp',
|
|
69
|
+
gif: 'image/gif', glb: 'model/gltf-binary', mp4: 'video/mp4', webm: 'video/webm',
|
|
70
|
+
mov: 'video/quicktime', mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg',
|
|
71
|
+
m4a: 'audio/mp4', flac: 'audio/flac', txt: 'text/plain', json: 'application/json',
|
|
72
|
+
};
|
|
73
|
+
export function mimeForExt(ext) {
|
|
74
|
+
return EXT_MIME[ext.toLowerCase()] || 'application/octet-stream';
|
|
75
|
+
}
|
|
76
|
+
export function mimeFor(ext, kind) {
|
|
77
|
+
const e = ext.toLowerCase();
|
|
78
|
+
const known = EXT_MIME[e];
|
|
79
|
+
if (known)
|
|
80
|
+
return known;
|
|
81
|
+
if (kind === 'image')
|
|
82
|
+
return 'image/jpeg';
|
|
83
|
+
if (kind === 'audio')
|
|
84
|
+
return 'audio/mpeg';
|
|
85
|
+
if (kind === 'video')
|
|
86
|
+
return 'video/mp4';
|
|
87
|
+
return 'application/octet-stream';
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolve an asset input (local path | http(s) URL | data: URI) into a
|
|
91
|
+
* Buffer + mime + filename. URL downloads are bounded (60s default).
|
|
92
|
+
*/
|
|
93
|
+
export async function resolveArtifactInput(input, kind = 'image', timeoutMs = 60000) {
|
|
94
|
+
// data: URI
|
|
95
|
+
const dataMatch = input.match(/^data:([^;]+);base64,(.+)$/);
|
|
96
|
+
if (dataMatch) {
|
|
97
|
+
const mime = dataMatch[1];
|
|
98
|
+
const ext = (mime.split('/')[1] || 'bin').toLowerCase();
|
|
99
|
+
return { buf: Buffer.from(dataMatch[2], 'base64'), mime, ext, filename: `${kind}.${ext}` };
|
|
100
|
+
}
|
|
101
|
+
// http(s) URL — bounded fetch
|
|
102
|
+
if (/^https?:\/\//i.test(input)) {
|
|
103
|
+
const res = await fetch(input, { signal: AbortSignal.timeout(timeoutMs) });
|
|
104
|
+
if (!res.ok)
|
|
105
|
+
throw new Error(`Asset download failed: HTTP ${res.status}`);
|
|
106
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
107
|
+
const urlExt = (input.split('?')[0].split('.').pop() || 'bin').toLowerCase();
|
|
108
|
+
const detected = detectArtifactType(buf);
|
|
109
|
+
const ext = detected ? detected.ext : urlExt;
|
|
110
|
+
const mime = detected ? detected.mime : mimeFor(urlExt, kind);
|
|
111
|
+
return { buf, mime, ext, filename: `${kind}.${ext}` };
|
|
112
|
+
}
|
|
113
|
+
// local file
|
|
114
|
+
if (fs.existsSync(input)) {
|
|
115
|
+
const buf = fs.readFileSync(input);
|
|
116
|
+
const detected = detectArtifactType(buf);
|
|
117
|
+
const nameExt = path.extname(input).toLowerCase().replace('.', '') || 'bin';
|
|
118
|
+
const ext = detected ? detected.ext : nameExt;
|
|
119
|
+
const mime = detected ? detected.mime : mimeFor(nameExt, kind);
|
|
120
|
+
return { buf, mime, ext, filename: path.basename(input) };
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`Asset not found: ${input}`);
|
|
123
|
+
}
|
|
124
|
+
export function sanitizeFilename(name) {
|
|
125
|
+
return name.replace(/[^\w.\-]+/g, '_').slice(0, 120);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Persist artifact bytes. The written extension follows the DETECTED magic
|
|
129
|
+
* bytes (detectExt !== false) — never a blind caller assumption.
|
|
130
|
+
*/
|
|
131
|
+
export function persistArtifact(buf, opts) {
|
|
132
|
+
const detected = opts.detectExt === false ? null : detectArtifactType(buf);
|
|
133
|
+
const ext = detected?.ext ?? opts.preferredExt ?? 'bin';
|
|
134
|
+
if (!fs.existsSync(opts.outputDir)) {
|
|
135
|
+
fs.mkdirSync(opts.outputDir, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
let filename = opts.filename ? sanitizeFilename(opts.filename) : undefined;
|
|
138
|
+
if (!filename) {
|
|
139
|
+
filename = `artifact_${Date.now()}.${ext}`;
|
|
140
|
+
}
|
|
141
|
+
else if (opts.detectExt !== false && detected) {
|
|
142
|
+
// STRICT: the written extension follows the real bytes — a caller
|
|
143
|
+
// filename like my_image.png becomes my_image.jpg for JPEG bytes.
|
|
144
|
+
// Filesystem path and returned ext always agree.
|
|
145
|
+
const dotIdx = filename.lastIndexOf('.');
|
|
146
|
+
if (dotIdx > 0) {
|
|
147
|
+
filename = filename.slice(0, dotIdx) + '.' + ext;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
filename = `${filename}.${ext}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (!filename.includes('.')) {
|
|
154
|
+
filename = `${filename}.${ext}`;
|
|
155
|
+
}
|
|
156
|
+
const filePath = path.join(opts.outputDir, filename);
|
|
157
|
+
fs.writeFileSync(filePath, buf);
|
|
158
|
+
return { filePath, size: buf.length, detected, ext };
|
|
159
|
+
}
|