opencode-pollinations-plugin 6.1.0-beta.3 β 6.1.0-beta.30
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.md +257 -61
- package/dist/index.js +53 -161
- package/dist/server/commands.d.ts +6 -0
- package/dist/server/commands.js +404 -73
- package/dist/server/config.d.ts +32 -23
- package/dist/server/config.js +183 -104
- package/dist/server/connect-response.d.ts +2 -0
- package/dist/server/connect-response.js +141 -0
- package/dist/server/generate-config.d.ts +3 -30
- package/dist/server/generate-config.js +164 -106
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +124 -149
- package/dist/server/logger.d.ts +8 -0
- package/dist/server/logger.js +36 -0
- package/dist/server/models/cache.d.ts +35 -0
- package/dist/server/models/cache.js +160 -0
- package/dist/server/models/fetcher.d.ts +18 -0
- package/dist/server/models/fetcher.js +150 -0
- package/dist/server/models/index.d.ts +6 -0
- package/dist/server/models/index.js +5 -0
- package/dist/server/models/manual.d.ts +15 -0
- package/dist/server/models/manual.js +92 -0
- package/dist/server/models/types.d.ts +55 -0
- package/dist/server/models/types.js +7 -0
- package/dist/server/models/worker.d.ts +21 -0
- package/dist/server/models/worker.js +97 -0
- package/dist/server/pollinations-api.d.ts +11 -0
- package/dist/server/pollinations-api.js +21 -8
- package/dist/server/proxy.js +195 -160
- package/dist/server/quota.d.ts +2 -0
- package/dist/server/quota.js +89 -86
- package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
- package/dist/server/scripts/pollinations_pricing.js +246 -0
- package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
- package/dist/server/scripts/test_cost_endpoints.js +61 -0
- package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
- package/dist/server/scripts/test_dynamic_pricing.js +39 -0
- package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
- package/dist/server/scripts/test_freetier_audit.js +215 -0
- package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
- package/dist/server/scripts/test_parallel_cost.js +104 -0
- package/dist/server/toast.d.ts +7 -1
- package/dist/server/toast.js +43 -10
- package/dist/tools/design/gen_diagram.d.ts +2 -0
- package/dist/tools/design/gen_diagram.js +94 -0
- package/dist/tools/design/gen_palette.d.ts +2 -0
- package/dist/tools/design/gen_palette.js +182 -0
- package/dist/tools/design/gen_qrcode.d.ts +2 -0
- package/dist/tools/design/gen_qrcode.js +50 -0
- package/dist/tools/ffmpeg.d.ts +24 -0
- package/dist/tools/ffmpeg.js +54 -0
- package/dist/tools/index.d.ts +25 -0
- package/dist/tools/index.js +86 -0
- package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
- package/dist/tools/pollinations/beta_discovery.js +197 -0
- package/dist/tools/pollinations/cost-guard.d.ts +38 -0
- package/dist/tools/pollinations/cost-guard.js +141 -0
- package/dist/tools/pollinations/deepsearch.d.ts +7 -0
- package/dist/tools/pollinations/deepsearch.js +80 -0
- package/dist/tools/pollinations/gen_audio.d.ts +18 -0
- package/dist/tools/pollinations/gen_audio.js +246 -0
- package/dist/tools/pollinations/gen_image.d.ts +11 -0
- package/dist/tools/pollinations/gen_image.js +225 -0
- package/dist/tools/pollinations/gen_music.d.ts +14 -0
- package/dist/tools/pollinations/gen_music.js +180 -0
- package/dist/tools/pollinations/gen_video.d.ts +16 -0
- package/dist/tools/pollinations/gen_video.js +256 -0
- package/dist/tools/pollinations/polli_config.d.ts +2 -0
- package/dist/tools/pollinations/polli_config.js +88 -0
- package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
- package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
- package/dist/tools/pollinations/polli_status.d.ts +2 -0
- package/dist/tools/pollinations/polli_status.js +31 -0
- package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
- package/dist/tools/pollinations/polli_web_search.js +164 -0
- package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
- package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
- package/dist/tools/pollinations/shared.d.ts +165 -0
- package/dist/tools/pollinations/shared.js +665 -0
- package/dist/tools/pollinations/test_estimators.d.ts +1 -0
- package/dist/tools/pollinations/test_estimators.js +22 -0
- package/dist/tools/pollinations/transcribe_audio.d.ts +13 -0
- package/dist/tools/pollinations/transcribe_audio.js +194 -0
- package/dist/tools/power/extract_audio.d.ts +2 -0
- package/dist/tools/power/extract_audio.js +179 -0
- package/dist/tools/power/extract_frames.d.ts +2 -0
- package/dist/tools/power/extract_frames.js +237 -0
- package/dist/tools/power/file_to_url.d.ts +2 -0
- package/dist/tools/power/file_to_url.js +217 -0
- package/dist/tools/power/remove_background.d.ts +2 -0
- package/dist/tools/power/remove_background.js +392 -0
- package/dist/tools/power/rmbg_keys.d.ts +2 -0
- package/dist/tools/power/rmbg_keys.js +79 -0
- package/dist/tools/shared.d.ts +30 -0
- package/dist/tools/shared.js +80 -0
- package/package.json +10 -4
- package/dist/server/models-seed.d.ts +0 -18
- package/dist/server/models-seed.js +0 -55
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen_video Tool - Pollinations Video Generation
|
|
3
|
+
*
|
|
4
|
+
* Updated: 2026-02-12 - Verified API Reference
|
|
5
|
+
*
|
|
6
|
+
* Video models with different capabilities:
|
|
7
|
+
* - T2V (Text-to-Video): grok-video, ltx-2, veo, seedance, seedance-pro
|
|
8
|
+
* - I2V (Image-to-Video): wan (I2V ONLY!), veo, seedance, seedance-pro
|
|
9
|
+
* - Veo Interpolation: Uses image=url1,url2 for transitions
|
|
10
|
+
*
|
|
11
|
+
* Response headers for cost tracking:
|
|
12
|
+
* - x-usage-completion-video-seconds (grok, ltx-2, veo, wan)
|
|
13
|
+
* - x-usage-completion-video-tokens (seedance, seedance-pro)
|
|
14
|
+
*/
|
|
15
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { getApiKey, httpsGet, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, estimateVideoCost, extractCostFromHeaders, isCostEstimatorEnabled, supportsI2V, requiresI2V, validateAspectRatio, getDurationRange, getVideoModels, fetchEnterBalance, } from './shared.js';
|
|
19
|
+
import { loadConfig } from '../../server/config.js';
|
|
20
|
+
import { checkCostControl, isTokenBased } from './cost-guard.js';
|
|
21
|
+
import { emitStatusToast } from '../../server/toast.js';
|
|
22
|
+
// βββ Constants βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
23
|
+
const CHEAPEST_MODEL = 'grok-video';
|
|
24
|
+
const DEFAULT_DURATION = 3;
|
|
25
|
+
const DEFAULT_ASPECT_RATIO = '16:9';
|
|
26
|
+
// βββ Tool Definition ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
27
|
+
export const polliGenVideoTool = tool({
|
|
28
|
+
description: `Generate a video from a text prompt or image using Pollinations AI.
|
|
29
|
+
|
|
30
|
+
π‘ **ModΓ¨les VidΓ©o Dynamiques** :
|
|
31
|
+
L'API vidéo Pollinations évolue constamment. Les modèles disponibles (T2V/I2V), leurs limites de durée, options d'aspect ratios et tarifs (tokens ou USD) sont injectés ci-dessous en temps réel.
|
|
32
|
+
|
|
33
|
+
**Exemples d'options communes** :
|
|
34
|
+
- \`veo\` interpolation: Utilisez \`reference_image=url1,url2\` pour les transitions.
|
|
35
|
+
- L'outil gΓ©rera le "costGuard" si l'utilisateur doit confirmer.`,
|
|
36
|
+
args: {
|
|
37
|
+
prompt: tool.schema.string().describe('Description of the video to generate'),
|
|
38
|
+
model: tool.schema.string().optional().describe(`Video model (default: ${CHEAPEST_MODEL})`),
|
|
39
|
+
duration: tool.schema.number().min(1).max(20).optional().describe('Duration in seconds (default: 3, varies by model)'),
|
|
40
|
+
aspect_ratio: tool.schema.enum(['16:9', '9:16', '1:1', '4:3']).optional().describe('Aspect ratio (default: 16:9, varies by model)'),
|
|
41
|
+
reference_image: tool.schema.string().optional().describe('URL for I2V (required for wan) or comma-separated URLs for veo interpolation'),
|
|
42
|
+
seed: tool.schema.number().optional().describe('Seed for reproducibility (-1 for random)'),
|
|
43
|
+
save_to: tool.schema.string().optional().describe('Custom output directory'),
|
|
44
|
+
filename: tool.schema.string().optional().describe('Custom filename (without extension)'),
|
|
45
|
+
},
|
|
46
|
+
async execute(args, context) {
|
|
47
|
+
const apiKey = getApiKey();
|
|
48
|
+
if (!apiKey) {
|
|
49
|
+
return `β La gΓ©nΓ©ration vidΓ©o nΓ©cessite une clΓ© API Pollinations.
|
|
50
|
+
π§ Connectez votre clΓ© avec /pollinations connect`;
|
|
51
|
+
}
|
|
52
|
+
const model = args.model || CHEAPEST_MODEL;
|
|
53
|
+
const aspectRatio = args.aspect_ratio || DEFAULT_ASPECT_RATIO;
|
|
54
|
+
// Get model config from dynamic registry
|
|
55
|
+
const videoModels = getVideoModels();
|
|
56
|
+
const modelConfig = videoModels[model];
|
|
57
|
+
const isBetaModel = !modelConfig;
|
|
58
|
+
if (isBetaModel) {
|
|
59
|
+
emitStatusToast('warning', `ModΓ¨le "${model}" non rΓ©fΓ©rencΓ© β mode (beta)`, 'π¬ gen_video');
|
|
60
|
+
}
|
|
61
|
+
// Validate duration (for known models; beta models use defaults)
|
|
62
|
+
const [minDuration, maxDuration] = isBetaModel ? [1, 20] : getDurationRange(model);
|
|
63
|
+
const duration = args.duration || Math.min(DEFAULT_DURATION, maxDuration);
|
|
64
|
+
if (duration < minDuration || duration > maxDuration) {
|
|
65
|
+
return `β DurΓ©e invalide pour ${model}: ${duration}s
|
|
66
|
+
π‘ DurΓ©e supportΓ©e: ${minDuration}-${maxDuration}s`;
|
|
67
|
+
}
|
|
68
|
+
// Validate aspect ratio (for known models; beta models accept any)
|
|
69
|
+
if (!isBetaModel && !validateAspectRatio(model, aspectRatio)) {
|
|
70
|
+
return `β Aspect ratio non supportΓ© par ${model}: ${aspectRatio}
|
|
71
|
+
π‘ Ratios supportΓ©s: ${modelConfig.aspectRatios.join(', ')}`;
|
|
72
|
+
}
|
|
73
|
+
// Check I2V requirements
|
|
74
|
+
const requiresReferenceImage = !isBetaModel && requiresI2V(model);
|
|
75
|
+
const supportsReferenceImage = isBetaModel || supportsI2V(model);
|
|
76
|
+
if (requiresReferenceImage && !args.reference_image) {
|
|
77
|
+
return `β Le modΓ¨le "${model}" nΓ©cessite une image de dΓ©part (I2V ONLY).
|
|
78
|
+
π‘ Ajoutez --reference_image <url>
|
|
79
|
+
π‘ Pour du T2V, utilisez: grok-video, ltx-2, veo, seedance`;
|
|
80
|
+
}
|
|
81
|
+
if (args.reference_image && !supportsReferenceImage) {
|
|
82
|
+
return `β οΈ Le modΓ¨le "${model}" ne supporte pas l'I2V.
|
|
83
|
+
π‘ ModΓ¨les I2V: ${Object.entries(videoModels)
|
|
84
|
+
.filter(([, info]) => info.i2v)
|
|
85
|
+
.map(([name]) => name)
|
|
86
|
+
.join(', ')}`;
|
|
87
|
+
}
|
|
88
|
+
// Estimate cost
|
|
89
|
+
const estimatedCost = estimateVideoCost(model, duration);
|
|
90
|
+
// Cost Guard check V2
|
|
91
|
+
const costCheck = checkCostControl('polli_gen_video', args, model, estimatedCost, 'video');
|
|
92
|
+
if (!costCheck.allowed) {
|
|
93
|
+
return costCheck.message || 'β OpΓ©ration bloquΓ©e par le Cost Guard.';
|
|
94
|
+
}
|
|
95
|
+
// Emit start toast
|
|
96
|
+
const config = loadConfig();
|
|
97
|
+
const argsStr = config.gui?.logs === 'verbose' ? `\nParameters: ${JSON.stringify(args)}` : '';
|
|
98
|
+
emitStatusToast('info', `GΓ©nΓ©ration vidΓ©o: ${model} (${duration}s)${argsStr}`, 'π¬ polli_gen_video');
|
|
99
|
+
// Metadata
|
|
100
|
+
context.metadata({ title: `π¬ Video: ${model}${isBetaModel ? ' (beta)' : ''} (${duration}s)` });
|
|
101
|
+
try {
|
|
102
|
+
// Build URL
|
|
103
|
+
const params = new URLSearchParams({
|
|
104
|
+
model: model,
|
|
105
|
+
nologo: 'true',
|
|
106
|
+
private: 'true',
|
|
107
|
+
});
|
|
108
|
+
// Duration parameter
|
|
109
|
+
params.set('duration', String(duration));
|
|
110
|
+
// Aspect ratio - convert to width/height for API
|
|
111
|
+
const aspectToSize = {
|
|
112
|
+
'16:9': { w: 1920, h: 1080 },
|
|
113
|
+
'9:16': { w: 1080, h: 1920 },
|
|
114
|
+
'1:1': { w: 1024, h: 1024 },
|
|
115
|
+
'4:3': { w: 1440, h: 1080 },
|
|
116
|
+
};
|
|
117
|
+
const size = aspectToSize[aspectRatio] || aspectToSize['16:9'];
|
|
118
|
+
params.set('width', String(size.w));
|
|
119
|
+
params.set('height', String(size.h));
|
|
120
|
+
// I2V: reference image(s)
|
|
121
|
+
if (args.reference_image) {
|
|
122
|
+
// Veo interpolation: comma-separated URLs
|
|
123
|
+
// Other I2V models: single URL
|
|
124
|
+
params.set('image', args.reference_image);
|
|
125
|
+
}
|
|
126
|
+
// Seed for reproducibility
|
|
127
|
+
if (args.seed !== undefined) {
|
|
128
|
+
params.set('seed', String(args.seed));
|
|
129
|
+
}
|
|
130
|
+
const promptEncoded = encodeURIComponent(args.prompt);
|
|
131
|
+
const url = `https://gen.pollinations.ai/image/${promptEncoded}?${params}`;
|
|
132
|
+
const headers = {
|
|
133
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
134
|
+
};
|
|
135
|
+
// 1. Fetch balance avant gΓ©nΓ©ration
|
|
136
|
+
const balBefore = await fetchEnterBalance();
|
|
137
|
+
// Video generation takes time (30-70 seconds depending on model)
|
|
138
|
+
const result = await httpsGet(url, headers);
|
|
139
|
+
const videoData = result.data;
|
|
140
|
+
const responseHeaders = result.headers;
|
|
141
|
+
// Save video
|
|
142
|
+
let outputDir = getDefaultOutputDir('videos');
|
|
143
|
+
let filename = args.filename;
|
|
144
|
+
if (args.save_to) {
|
|
145
|
+
if (args.save_to.match(/\.(mp4|webm|mov|avi)$/i)) {
|
|
146
|
+
outputDir = path.dirname(args.save_to);
|
|
147
|
+
filename = path.basename(args.save_to);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
outputDir = args.save_to;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
ensureDir(outputDir);
|
|
154
|
+
filename = filename || generateFilename('video', model, 'mp4');
|
|
155
|
+
const filePath = path.join(outputDir, filename.endsWith('.mp4') ? filename : `${filename}.mp4`);
|
|
156
|
+
fs.writeFileSync(filePath, videoData);
|
|
157
|
+
const fileSize = fs.statSync(filePath).size;
|
|
158
|
+
// Extract actual cost from headers as fallback
|
|
159
|
+
const costTracking = extractCostFromHeaders(responseHeaders);
|
|
160
|
+
// 2. Fetch balance après génération (delay for API sync)
|
|
161
|
+
let balAfter = null;
|
|
162
|
+
let realCost;
|
|
163
|
+
if (balBefore !== null) {
|
|
164
|
+
await new Promise(r => setTimeout(r, 1000)); // Laisse le temps au ledger
|
|
165
|
+
balAfter = await fetchEnterBalance();
|
|
166
|
+
if (balAfter !== null) {
|
|
167
|
+
realCost = Math.round((balBefore - balAfter) * 10000) / 10000;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Build result
|
|
171
|
+
const lines = [];
|
|
172
|
+
// Inject costWarning at top if present
|
|
173
|
+
if (costCheck.message && !costCheck.allowed) { // Assuming costWarning should come from costCheck if not allowed
|
|
174
|
+
lines.push(costCheck.message);
|
|
175
|
+
lines.push('');
|
|
176
|
+
}
|
|
177
|
+
lines.push(`π¬ VidΓ©o GΓ©nΓ©rΓ©e`);
|
|
178
|
+
lines.push(`ββββββββββββββββββ`);
|
|
179
|
+
lines.push(`Prompt: ${args.prompt.substring(0, 80)}${args.prompt.length > 80 ? '...' : ''}`);
|
|
180
|
+
lines.push(`ModΓ¨le: ${model}${isBetaModel ? ' (beta)' : ''}${modelConfig?.cost?.includes('π') ? ' π' : ''}`);
|
|
181
|
+
lines.push(`DurΓ©e: ~${duration}s`);
|
|
182
|
+
lines.push(`Aspect: ${aspectRatio}`);
|
|
183
|
+
// Add I2V info if used
|
|
184
|
+
if (args.reference_image) {
|
|
185
|
+
const isInterpolation = model === 'veo' && args.reference_image.includes(',');
|
|
186
|
+
lines.push(`I2V Mode: ${isInterpolation ? 'Interpolation (multi-image)' : 'Single image'}`);
|
|
187
|
+
lines.push(`Source: ${args.reference_image.substring(0, 50)}...`);
|
|
188
|
+
}
|
|
189
|
+
// Audio info (known models only)
|
|
190
|
+
if (modelConfig?.audio) {
|
|
191
|
+
lines.push(`Audio: β
GΓ©nΓ©rΓ© automatiquement`);
|
|
192
|
+
}
|
|
193
|
+
else if (!isBetaModel) {
|
|
194
|
+
lines.push(`Audio: β Non supportΓ© par ce modΓ¨le`);
|
|
195
|
+
}
|
|
196
|
+
lines.push(`Fichier: ${filePath}`);
|
|
197
|
+
lines.push(`Taille: ${formatFileSize(fileSize)}`);
|
|
198
|
+
// Pricing details (EstimΓ© vs RΓ©el)
|
|
199
|
+
if (isCostEstimatorEnabled()) {
|
|
200
|
+
const maxCost = estimatedCost * 3;
|
|
201
|
+
lines.push(`\nπ° **Rapport Financier :**`);
|
|
202
|
+
if (isTokenBased('video', model)) {
|
|
203
|
+
lines.push(`- CoΓ»t EstimΓ© : ${formatCost(estimatedCost)} (Max thΓ©orique: ${formatCost(maxCost)})`);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
lines.push(`- CoΓ»t EstimΓ© : ${formatCost(estimatedCost)}`);
|
|
207
|
+
}
|
|
208
|
+
if (realCost !== undefined) {
|
|
209
|
+
lines.push(`- CoΓ»t RΓ©el : **${formatCost(realCost)}** (via Solde Wallet)`);
|
|
210
|
+
}
|
|
211
|
+
else if (costTracking.costUsd !== undefined) {
|
|
212
|
+
lines.push(`- CoΓ»t RΓ©el : **${formatCost(costTracking.costUsd)}** (via Headers API)`);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
lines.push(`- CoΓ»t RΓ©el : Inconnu (API injoignable)`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (responseHeaders['x-model-used']) {
|
|
219
|
+
lines.push(`Modèle utilisé: ${responseHeaders['x-model-used']}`);
|
|
220
|
+
}
|
|
221
|
+
if (responseHeaders['x-request-id']) {
|
|
222
|
+
lines.push(`Request ID: ${responseHeaders['x-request-id']}`);
|
|
223
|
+
}
|
|
224
|
+
// Gen time estimate (known models only)
|
|
225
|
+
if (modelConfig?.genTime) {
|
|
226
|
+
lines.push(`β±οΈ Temps de gΓ©nΓ©ration: ${modelConfig.genTime}`);
|
|
227
|
+
}
|
|
228
|
+
// Emit success toast
|
|
229
|
+
emitStatusToast('success', `VidΓ©o gΓ©nΓ©rΓ©e β (${model}, ${duration}s)`, 'π¬ gen_video');
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
emitStatusToast('error', `Erreur: ${err.message?.substring(0, 60)}`, 'π¬ gen_video');
|
|
234
|
+
if (err.message?.includes('402') || err.message?.includes('Payment')) {
|
|
235
|
+
return `β CrΓ©dits pollen insuffisants.
|
|
236
|
+
π‘ Essayez \`grok-video\` (le moins cher: 0.0025/sec)`;
|
|
237
|
+
}
|
|
238
|
+
if (err.message?.includes('400')) {
|
|
239
|
+
if (requiresI2V(model) && !args.reference_image) {
|
|
240
|
+
return `β Le modΓ¨le "${model}" est I2V ONLY.
|
|
241
|
+
π‘ Ajoutez --reference_image <url>`;
|
|
242
|
+
}
|
|
243
|
+
return `β ParamΓ¨tres invalides: ${err.message}`;
|
|
244
|
+
}
|
|
245
|
+
if (err.message?.includes('520') && model === 'ltx-2') {
|
|
246
|
+
return `β οΈ LTX-2 a retournΓ© une erreur 520 (intermittent).
|
|
247
|
+
π‘ RΓ©essayez dans quelques secondes.`;
|
|
248
|
+
}
|
|
249
|
+
if (err.message?.includes('Timeout')) {
|
|
250
|
+
return `β Timeout - La gΓ©nΓ©ration vidΓ©o a pris trop de temps.
|
|
251
|
+
π‘ RΓ©essayez avec une durΓ©e plus courte.`;
|
|
252
|
+
}
|
|
253
|
+
return `β Erreur gΓ©nΓ©ration vidΓ©o: ${err.message}`;
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
|
+
import { loadConfig, saveConfig } from '../../server/config.js';
|
|
3
|
+
import { emitStatusToast } from '../../server/toast.js';
|
|
4
|
+
export const polliConfigTool = tool({
|
|
5
|
+
description: `[CRITICAL TOOL FOR ASSISTANT] View or modify the Pollinations plugin configuration.
|
|
6
|
+
You must strictly understand the 3 INDEPENDENT categories of settings before explaining or changing them:
|
|
7
|
+
|
|
8
|
+
=== 1. CHAT MODELS & FALLBACKS (Applies ONLY to conversational chat models) ===
|
|
9
|
+
- mode: Dictates fallback rules for the chat.
|
|
10
|
+
* 'manual': No automatic rules.
|
|
11
|
+
* 'alwaysfree': Free tiers first. If 'thresholdsTier' is reached -> fallbacks to Free Universe. NEVER uses Wallet.
|
|
12
|
+
* 'pro': Uses Wallet. If 'thresholdsWallet' is reached -> fallbacks to Free Universe.
|
|
13
|
+
- thresholdsTier: WARNING PERCENTAGE (e.g. 10 for 10%) that triggers chat fallback in 'alwaysfree' mode.
|
|
14
|
+
- thresholdsWallet: WARNING PERCENTAGE (e.g. 50 for 50%) that triggers chat fallback in 'pro' mode.
|
|
15
|
+
*Note: 'enter.agent' or 'free.agent' are fallback conversational models for logic reasoning, THEY DO NOT GENERATE IMAGES OR VIDEOS!*
|
|
16
|
+
|
|
17
|
+
=== 2. TOOLS PROTECTION (Applies ONLY to independent 'polli_' tools like image, video, search) ===
|
|
18
|
+
- enablePaidTools: Allow tools to execute models that consume 'Wallet' pollen. If false, tools can only use models that consume 'Freetier' pollen.
|
|
19
|
+
- costConfirmationRequired: Safety lock for tools. If true, the user MUST manually confirm BEFORE executing ANY tool whose cost estimate exceeds the 'costThreshold'.
|
|
20
|
+
- costThreshold: USD/πΌ limit (cost of the tool execution) that triggers the confirmation lock.
|
|
21
|
+
- costEstimator: Shows live cost estimates IN TOOL OUTPUTS (false = Silent Mode).
|
|
22
|
+
|
|
23
|
+
=== 3. UI & NOTIFICATIONS (General display) ===
|
|
24
|
+
- statusBar: Show/Hide the floating status bar notification in the OpenCode UI.
|
|
25
|
+
|
|
26
|
+
Use 'action=update' to change these. NEVER confuse Chat Mode with Tools Protection!`,
|
|
27
|
+
args: {
|
|
28
|
+
action: tool.schema.enum(['view', 'update'])
|
|
29
|
+
.describe('Action to perform: "view" to see current configuration, "update" to modify it.'),
|
|
30
|
+
mode: tool.schema.enum(['manual', 'alwaysfree', 'pro']).optional().describe('CHAT ONLY: Dictates automatic fallback rules (manual/alwaysfree/pro).'),
|
|
31
|
+
costEstimator: tool.schema.boolean().optional().describe('Set to true to show cost estimates auto. Set to false for "Manual Mode" (hide estimates).'),
|
|
32
|
+
statusBar: tool.schema.boolean().optional().describe('Enable/disable status bar visibility (true/false)'),
|
|
33
|
+
costConfirmationRequired: tool.schema.boolean().optional().describe('Safety Lock: Set to true to ask user confirmation before spending money. Set to false to spend automatically.'),
|
|
34
|
+
enablePaidTools: tool.schema.boolean().optional().describe('Allow execution of paid or premium models using user Wallet balance (true/false)'),
|
|
35
|
+
costThreshold: tool.schema.number().optional().describe('Cost threshold in USD/πΌ above which confirmation is required'),
|
|
36
|
+
thresholdsTier: tool.schema.number().optional().describe('Warning threshold PERCENTAGE (e.g. 10 for 10%) for Free Tier.'),
|
|
37
|
+
thresholdsWallet: tool.schema.number().optional().describe('Warning threshold PERCENTAGE (e.g. 50 for 50%) for Wallet balance.')
|
|
38
|
+
},
|
|
39
|
+
async execute(args, context) {
|
|
40
|
+
if (args.action === 'view') {
|
|
41
|
+
const current = loadConfig();
|
|
42
|
+
// Obfuscate API key for safety in logs/UI
|
|
43
|
+
const safeConfig = { ...current };
|
|
44
|
+
if (safeConfig.apiKey && safeConfig.apiKey.length > 10) {
|
|
45
|
+
safeConfig.apiKey = safeConfig.apiKey.substring(0, 5) + '...[REDACTED]';
|
|
46
|
+
}
|
|
47
|
+
return `Current Plugin Configuration:\n\n${JSON.stringify(safeConfig, null, 2)}`;
|
|
48
|
+
}
|
|
49
|
+
if (args.action === 'update') {
|
|
50
|
+
const currentConfig = loadConfig();
|
|
51
|
+
const updates = {};
|
|
52
|
+
if (args.mode !== undefined)
|
|
53
|
+
updates.mode = args.mode;
|
|
54
|
+
if (args.costEstimator !== undefined)
|
|
55
|
+
updates.costEstimator = args.costEstimator;
|
|
56
|
+
if (args.statusBar !== undefined)
|
|
57
|
+
updates.statusBar = args.statusBar;
|
|
58
|
+
if (args.costConfirmationRequired !== undefined)
|
|
59
|
+
updates.costConfirmationRequired = args.costConfirmationRequired;
|
|
60
|
+
if (args.enablePaidTools !== undefined)
|
|
61
|
+
updates.enablePaidTools = args.enablePaidTools;
|
|
62
|
+
if (args.costThreshold !== undefined)
|
|
63
|
+
updates.costThreshold = args.costThreshold;
|
|
64
|
+
if (args.thresholdsTier !== undefined || args.thresholdsWallet !== undefined) {
|
|
65
|
+
updates.thresholds = { ...currentConfig.thresholds };
|
|
66
|
+
if (args.thresholdsTier !== undefined)
|
|
67
|
+
updates.thresholds.tier = args.thresholdsTier;
|
|
68
|
+
if (args.thresholdsWallet !== undefined)
|
|
69
|
+
updates.thresholds.wallet = args.thresholdsWallet;
|
|
70
|
+
}
|
|
71
|
+
if (Object.keys(updates).length === 0) {
|
|
72
|
+
return "No configuration values provided to update. Please specify at least one setting via the arguments.";
|
|
73
|
+
}
|
|
74
|
+
saveConfig(updates);
|
|
75
|
+
const newConfig = loadConfig();
|
|
76
|
+
if (newConfig.statusBar) {
|
|
77
|
+
const changedDetails = Object.keys(updates).map(k => `${k}=${updates[k]}`).join(", ");
|
|
78
|
+
let toastMsg = "βοΈ Configuration modifiΓ©e par l'Agent";
|
|
79
|
+
if (changedDetails.length > 0) {
|
|
80
|
+
toastMsg += ` (${changedDetails})`;
|
|
81
|
+
}
|
|
82
|
+
emitStatusToast('info', toastMsg, 'Config Update');
|
|
83
|
+
}
|
|
84
|
+
return `Configuration successfully updated.\nApplied changes:\n${JSON.stringify(updates, null, 2)}\n\n(Note: Verify with polli_status if you need to know model prefixes).`;
|
|
85
|
+
}
|
|
86
|
+
return "Invalid action. Use 'view' or 'update'.";
|
|
87
|
+
}
|
|
88
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
|
+
import { getPendingRequest, removePendingRequest } from './cost-guard.js';
|
|
3
|
+
// Import tools to re-execute them
|
|
4
|
+
import { polliGenImageTool } from './gen_image.js';
|
|
5
|
+
import { polliGenVideoTool } from './gen_video.js';
|
|
6
|
+
import { polliGenAudioTool } from './gen_audio.js';
|
|
7
|
+
import { polliGenMusicTool } from './gen_music.js';
|
|
8
|
+
import { polliWebSearchTool } from './polli_web_search.js';
|
|
9
|
+
import { formatCost } from './shared.js';
|
|
10
|
+
export const polliGenConfirmTool = tool({
|
|
11
|
+
description: `Valide et exΓ©cute (ou annule) une requΓͺte Pollinations prΓ©cΓ©demment suspendue par le Cost Guard.
|
|
12
|
+
Cet outil doit Γͺtre appelΓ© lorsque l'utilisateur a explicitement donnΓ© son accord (ou refusΓ©) pour dΓ©penser le montant estimΓ©.`,
|
|
13
|
+
args: {
|
|
14
|
+
request_id: tool.schema.string().describe('L\'identifiant de la requΓͺte (req_xxxx) retournΓ© par l\'outil bloquΓ©.'),
|
|
15
|
+
action: tool.schema.enum(['confirm', 'cancel']).describe('L\'action Γ effectuer : confirm pour lancer la gΓ©nΓ©ration, cancel pour l\'annuler dΓ©finitivement.'),
|
|
16
|
+
},
|
|
17
|
+
async execute(args, context) {
|
|
18
|
+
const reqId = args.request_id;
|
|
19
|
+
const action = args.action;
|
|
20
|
+
const pendingReq = getPendingRequest(reqId);
|
|
21
|
+
if (!pendingReq) {
|
|
22
|
+
return `β Session introuvable ou expirΓ©e pour l'ID: ${reqId}. Veuillez relancer la gΓ©nΓ©ration initiale.`;
|
|
23
|
+
}
|
|
24
|
+
if (action === 'cancel') {
|
|
25
|
+
removePendingRequest(reqId);
|
|
26
|
+
return `β
La requΓͺte ${reqId} a Γ©tΓ© annulΓ©e et supprimΓ©e de la file d'attente. Action abandonnΓ©e.`;
|
|
27
|
+
}
|
|
28
|
+
const toolRegistry = {
|
|
29
|
+
'polli_gen_image': polliGenImageTool,
|
|
30
|
+
'polli_gen_video': polliGenVideoTool,
|
|
31
|
+
'polli_gen_audio': polliGenAudioTool,
|
|
32
|
+
'polli_gen_music': polliGenMusicTool,
|
|
33
|
+
'polli_web_search': polliWebSearchTool,
|
|
34
|
+
};
|
|
35
|
+
const targetTool = toolRegistry[pendingReq.toolName];
|
|
36
|
+
if (!targetTool) {
|
|
37
|
+
return `β Outil cible inconnu: ${pendingReq.toolName}`;
|
|
38
|
+
}
|
|
39
|
+
// Add a bypass flag to arguments
|
|
40
|
+
const executionArgs = { ...pendingReq.args };
|
|
41
|
+
const CONFIRM_SYMBOL = Symbol.for('polli_confirmed');
|
|
42
|
+
executionArgs[CONFIRM_SYMBOL] = true;
|
|
43
|
+
context.metadata({ title: `β
Confirmed: ${pendingReq.toolName} (${formatCost(pendingReq.estimatedCost)})` });
|
|
44
|
+
// Execute original tool and clean up
|
|
45
|
+
removePendingRequest(reqId);
|
|
46
|
+
return await targetTool.execute(executionArgs, context);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
|
+
import { handleUsageCommand, handleInfosCommand, handlePricingCommand, handleModelsCommand } from '../../server/commands.js';
|
|
3
|
+
export const polliStatusTool = tool({
|
|
4
|
+
description: `Check the current status, available models, live pricing, and account usage/tiers for the Pollinations AI plugin.
|
|
5
|
+
Use this tool to understand the universe of models, account balance, and usage. NOT for discovering hidden parameters.`,
|
|
6
|
+
args: {
|
|
7
|
+
info_type: tool.schema.enum(['usage', 'pricing', 'models', 'infos', 'all'])
|
|
8
|
+
.describe('Type of information to retrieve: usage=quota/wallet, pricing=model costs, models=list of models, infos=tier details, all=everything')
|
|
9
|
+
},
|
|
10
|
+
async execute(args, context) {
|
|
11
|
+
let results = [];
|
|
12
|
+
const type = args.info_type || 'all';
|
|
13
|
+
if (type === 'usage' || type === 'all') {
|
|
14
|
+
const res = await handleUsageCommand(['full']);
|
|
15
|
+
results.push(res.response || String(res.error));
|
|
16
|
+
}
|
|
17
|
+
if (type === 'pricing' || type === 'all') {
|
|
18
|
+
const res = await handlePricingCommand();
|
|
19
|
+
results.push(res.response || String(res.error));
|
|
20
|
+
}
|
|
21
|
+
if (type === 'models' || type === 'all') {
|
|
22
|
+
const res = await handleModelsCommand([]);
|
|
23
|
+
results.push(res.response || String(res.error));
|
|
24
|
+
}
|
|
25
|
+
if (type === 'infos' || type === 'all') {
|
|
26
|
+
const res = await handleInfosCommand();
|
|
27
|
+
results.push(res.response || String(res.error));
|
|
28
|
+
}
|
|
29
|
+
return results.join('\n\n======================================================\n\n');
|
|
30
|
+
}
|
|
31
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* polli_web_search Tool - Unified Web Search via Pollinations AI
|
|
3
|
+
*
|
|
4
|
+
* Replaces: deepsearch.ts + search_crawl_scrape.ts
|
|
5
|
+
*
|
|
6
|
+
* Three modes mapped to API models:
|
|
7
|
+
* - rapid: Fast web search (perplexity-fast or Google Search model)
|
|
8
|
+
* - medium: Standard web search with sources (perplexity-fast)
|
|
9
|
+
* - deep: Deep research with reasoning (perplexity-reasoning)
|
|
10
|
+
*
|
|
11
|
+
* Models are resolved dynamically from /text/models registry.
|
|
12
|
+
* Integrated with Cost Guard and Toast notifications.
|
|
13
|
+
*/
|
|
14
|
+
import { type ToolDefinition } from '@opencode-ai/plugin/tool';
|
|
15
|
+
export declare const polliWebSearchTool: ToolDefinition;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* polli_web_search Tool - Unified Web Search via Pollinations AI
|
|
3
|
+
*
|
|
4
|
+
* Replaces: deepsearch.ts + search_crawl_scrape.ts
|
|
5
|
+
*
|
|
6
|
+
* Three modes mapped to API models:
|
|
7
|
+
* - rapid: Fast web search (perplexity-fast or Google Search model)
|
|
8
|
+
* - medium: Standard web search with sources (perplexity-fast)
|
|
9
|
+
* - deep: Deep research with reasoning (perplexity-reasoning)
|
|
10
|
+
*
|
|
11
|
+
* Models are resolved dynamically from /text/models registry.
|
|
12
|
+
* Integrated with Cost Guard and Toast notifications.
|
|
13
|
+
*/
|
|
14
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
15
|
+
import { getApiKey, httpsPost, formatCost, extractCostFromHeaders, } from './shared.js';
|
|
16
|
+
import { loadConfig } from '../../server/config.js';
|
|
17
|
+
import { checkCostControl, isTokenBased } from './cost-guard.js';
|
|
18
|
+
import { emitStatusToast } from '../../server/toast.js';
|
|
19
|
+
const SEARCH_MODES = {
|
|
20
|
+
rapid: {
|
|
21
|
+
model: 'perplexity-fast',
|
|
22
|
+
maxTokens: 1500,
|
|
23
|
+
systemPrompt: 'You are a quick web search assistant. Provide concise, accurate answers with key sources. Be efficient and direct.',
|
|
24
|
+
label: 'Recherche Rapide',
|
|
25
|
+
emoji: 'β‘',
|
|
26
|
+
},
|
|
27
|
+
medium: {
|
|
28
|
+
model: 'perplexity-fast',
|
|
29
|
+
maxTokens: 3000,
|
|
30
|
+
systemPrompt: 'You are a web search assistant. Provide comprehensive research with analysis, sources, and reasoning steps. Always include source URLs.',
|
|
31
|
+
label: 'Recherche Standard',
|
|
32
|
+
emoji: 'π',
|
|
33
|
+
},
|
|
34
|
+
deep: {
|
|
35
|
+
model: 'perplexity-reasoning',
|
|
36
|
+
maxTokens: 8000,
|
|
37
|
+
systemPrompt: 'You are a deep research assistant. Provide exhaustive research with multiple perspectives, detailed analysis, all relevant sources, and thorough fact-checking. Consider edge cases and alternative viewpoints. Always include source URLs.',
|
|
38
|
+
label: 'Recherche Profonde',
|
|
39
|
+
emoji: 'π¬',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
// βββ Cost Estimation ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
43
|
+
function estimateSearchCost(mode) {
|
|
44
|
+
switch (mode) {
|
|
45
|
+
case 'rapid': return 0.001;
|
|
46
|
+
case 'medium': return 0.003;
|
|
47
|
+
case 'deep': return 0.008;
|
|
48
|
+
default: return 0.003;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// βββ Tool Definition ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
52
|
+
export const polliWebSearchTool = tool({
|
|
53
|
+
description: `Search the web using Pollinations AI with three depth levels.
|
|
54
|
+
|
|
55
|
+
**Modes:**
|
|
56
|
+
|
|
57
|
+
| Mode | Modèle | Usage | Coût estimé |
|
|
58
|
+
|------|--------|-------|-------------|
|
|
59
|
+
| β‘ rapid | perplexity-fast | Quick facts, current events | ~0.001 π» |
|
|
60
|
+
| π medium | perplexity-fast | Standard research with sources | ~0.003 π» |
|
|
61
|
+
| π¬ deep | perplexity-reasoning | In-depth analysis, multi-perspective | ~0.008 π» |
|
|
62
|
+
|
|
63
|
+
**π‘ Tips:**
|
|
64
|
+
- Use \`rapid\` for quick lookups and current news
|
|
65
|
+
- Use \`medium\` for documentation search and general queries
|
|
66
|
+
- Use \`deep\` for complex research, fact-checking, and analysis
|
|
67
|
+
- Add \`recency\` filter for time-sensitive queries`,
|
|
68
|
+
args: {
|
|
69
|
+
query: tool.schema.string().describe('Search query or research question'),
|
|
70
|
+
mode: tool.schema.enum(['rapid', 'medium', 'deep']).optional()
|
|
71
|
+
.describe('Search depth (default: medium)'),
|
|
72
|
+
include_sources: tool.schema.boolean().optional()
|
|
73
|
+
.describe('Include source URLs in response (default: true)'),
|
|
74
|
+
recency: tool.schema.enum(['any', 'day', 'week', 'month']).optional()
|
|
75
|
+
.describe('Filter by recency (default: any)'),
|
|
76
|
+
},
|
|
77
|
+
async execute(args, context) {
|
|
78
|
+
const apiKey = getApiKey();
|
|
79
|
+
if (!apiKey) {
|
|
80
|
+
return `β Web Search nΓ©cessite une clΓ© API Pollinations.
|
|
81
|
+
π§ Connectez votre clΓ© avec /pollinations connect`;
|
|
82
|
+
}
|
|
83
|
+
const mode = args.mode || 'medium';
|
|
84
|
+
const modeConfig = SEARCH_MODES[mode];
|
|
85
|
+
const includeSources = args.include_sources !== false;
|
|
86
|
+
// Cost Guard
|
|
87
|
+
const estimatedCost = estimateSearchCost(mode);
|
|
88
|
+
const costCheck = checkCostControl('polli_web_search', args, modeConfig.model, estimatedCost, 'audio'); // text models use audio category
|
|
89
|
+
if (!costCheck.allowed) {
|
|
90
|
+
return costCheck.message || 'β OpΓ©ration bloquΓ©e par le Cost Guard.';
|
|
91
|
+
}
|
|
92
|
+
// Emit start toast
|
|
93
|
+
const config = loadConfig();
|
|
94
|
+
const argsStr = config.gui?.logs === 'verbose' ? `\nParameters: ${JSON.stringify(args)}` : '';
|
|
95
|
+
emitStatusToast('info', `${modeConfig.emoji} ${modeConfig.label}: ${args.query.substring(0, 40)}...${argsStr}`, 'π polli_web_search');
|
|
96
|
+
// Metadata
|
|
97
|
+
context.metadata({ title: `${modeConfig.emoji} Search: ${args.query.substring(0, 50)}...` });
|
|
98
|
+
try {
|
|
99
|
+
// Build recency hint
|
|
100
|
+
const recencyHints = {
|
|
101
|
+
any: '',
|
|
102
|
+
day: 'Focus on information from the last 24 hours. ',
|
|
103
|
+
week: 'Focus on information from the last week. ',
|
|
104
|
+
month: 'Focus on information from the last month. ',
|
|
105
|
+
};
|
|
106
|
+
const systemPrompt = `${modeConfig.systemPrompt}
|
|
107
|
+
${recencyHints[args.recency || 'any']}
|
|
108
|
+
${includeSources ? 'Always include source URLs at the end of your response.' : ''}`;
|
|
109
|
+
const { data: responseData, headers: responseHeaders } = await httpsPost('https://gen.pollinations.ai/v1/chat/completions', {
|
|
110
|
+
model: modeConfig.model,
|
|
111
|
+
messages: [
|
|
112
|
+
{ role: 'system', content: systemPrompt },
|
|
113
|
+
{ role: 'user', content: args.query },
|
|
114
|
+
],
|
|
115
|
+
max_tokens: modeConfig.maxTokens,
|
|
116
|
+
}, {
|
|
117
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
118
|
+
});
|
|
119
|
+
const jsonData = JSON.parse(responseData.toString());
|
|
120
|
+
const content = jsonData.choices?.[0]?.message?.content || 'No results found';
|
|
121
|
+
// Extract actual cost from headers
|
|
122
|
+
let actualCost = estimatedCost;
|
|
123
|
+
if (responseHeaders) {
|
|
124
|
+
const costTracking = extractCostFromHeaders(responseHeaders);
|
|
125
|
+
if (costTracking.costUsd !== undefined) {
|
|
126
|
+
actualCost = costTracking.costUsd;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Build result
|
|
130
|
+
const lines = [];
|
|
131
|
+
// Inject costWarning at top if present
|
|
132
|
+
if (costCheck.message && !costCheck.allowed) {
|
|
133
|
+
lines.push(costCheck.message);
|
|
134
|
+
lines.push('');
|
|
135
|
+
}
|
|
136
|
+
lines.push(`${modeConfig.emoji} ${modeConfig.label}`);
|
|
137
|
+
lines.push(`ββββββββββββββββββ`);
|
|
138
|
+
lines.push(`Query: ${args.query}`);
|
|
139
|
+
lines.push(`Mode: ${mode} | Modèle: ${modeConfig.model}`);
|
|
140
|
+
if (args.recency && args.recency !== 'any') {
|
|
141
|
+
lines.push(`RΓ©cence: ${args.recency}`);
|
|
142
|
+
}
|
|
143
|
+
if (isTokenBased('audio', modeConfig.model)) {
|
|
144
|
+
const maxCost = estimatedCost * 3;
|
|
145
|
+
lines.push(`CoΓ»t estimΓ©: ${formatCost(actualCost)} (Max thΓ©orique: ${formatCost(maxCost)})`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
lines.push(`CoΓ»t estimΓ©: ${formatCost(actualCost)}`);
|
|
149
|
+
}
|
|
150
|
+
lines.push('');
|
|
151
|
+
lines.push(content);
|
|
152
|
+
// Emit success toast
|
|
153
|
+
emitStatusToast('success', `Recherche terminΓ©e β (${mode})`, 'π polli_web_search');
|
|
154
|
+
return lines.join('\n');
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
emitStatusToast('error', `Erreur: ${err.message?.substring(0, 60)}`, 'π polli_web_search');
|
|
158
|
+
if (err.message?.includes('402') || err.message?.includes('Payment')) {
|
|
159
|
+
return `β CrΓ©dits pollen insuffisants.`;
|
|
160
|
+
}
|
|
161
|
+
return `β Erreur Web Search: ${err.message}`;
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
});
|