opencode-pollinations-plugin 6.1.0-beta.12 → 6.1.0-beta.22
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 +11 -6
- package/dist/index.js +40 -10
- package/dist/server/commands.d.ts +4 -0
- package/dist/server/commands.js +296 -12
- package/dist/server/config.d.ts +5 -0
- package/dist/server/config.js +163 -35
- package/dist/server/connect-response.d.ts +2 -0
- package/dist/server/connect-response.js +141 -0
- package/dist/server/generate-config.js +10 -24
- 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.js +1 -8
- package/dist/server/proxy.js +52 -27
- package/dist/server/quota.d.ts +2 -8
- package/dist/server/quota.js +47 -89
- 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 +4 -1
- package/dist/server/toast.js +27 -10
- package/dist/tools/ffmpeg.d.ts +24 -0
- package/dist/tools/ffmpeg.js +54 -0
- package/dist/tools/index.d.ts +10 -8
- package/dist/tools/index.js +27 -25
- 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/gen_audio.d.ts +1 -1
- package/dist/tools/pollinations/gen_audio.js +65 -23
- package/dist/tools/pollinations/gen_image.d.ts +5 -7
- package/dist/tools/pollinations/gen_image.js +146 -160
- package/dist/tools/pollinations/gen_music.d.ts +1 -1
- package/dist/tools/pollinations/gen_music.js +57 -16
- package/dist/tools/pollinations/gen_video.d.ts +1 -1
- package/dist/tools/pollinations/gen_video.js +99 -65
- 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/shared.d.ts +34 -39
- package/dist/tools/pollinations/shared.js +300 -89
- 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 +5 -9
- package/dist/tools/pollinations/transcribe_audio.js +31 -72
- package/dist/tools/power/extract_audio.js +26 -27
- package/dist/tools/power/extract_frames.js +24 -27
- package/dist/tools/power/remove_background.js +2 -1
- package/dist/tools/power/rmbg_keys.js +2 -1
- package/dist/tools/shared.js +9 -3
- package/package.json +2 -2
|
@@ -1,66 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* gen_image Tool - Pollinations Image Generation
|
|
3
3
|
*
|
|
4
|
-
* Updated: 2026-02-
|
|
4
|
+
* Updated: 2026-02-19 - Dynamic ModelRegistry + Cost Guard + Toasts
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - Image-to-Image (I2I): klein, klein-large, kontext, seedream, seedream-pro, nanobanana, nanobanana-pro
|
|
10
|
-
* - Multi-image I2I: seedream-pro (comma-separated URLs)
|
|
6
|
+
* All models are dynamic from the Pollinations API.
|
|
7
|
+
* Unknown models are accepted as (beta) and passed through to the API.
|
|
8
|
+
* Cost Guard reads enablePaidTools, costConfirmationRequired, costThreshold.
|
|
11
9
|
*/
|
|
12
10
|
import { tool } from '@opencode-ai/plugin/tool';
|
|
13
11
|
import * as fs from 'fs';
|
|
14
12
|
import * as path from 'path';
|
|
15
|
-
import { getApiKey, hasApiKey, httpsGet, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, estimateImageCost, isCostEstimatorEnabled, supportsI2I,
|
|
13
|
+
import { getApiKey, hasApiKey, httpsGet, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, estimateImageCost, extractCostFromHeaders, isCostEstimatorEnabled, supportsI2I, getPaidImageModels, fetchEnterBalance, } from './shared.js';
|
|
14
|
+
import { loadConfig } from '../../server/config.js';
|
|
15
|
+
import { checkCostControl, isTokenBased } from './cost-guard.js';
|
|
16
|
+
import { emitStatusToast } from '../../server/toast.js';
|
|
16
17
|
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
17
|
-
/**
|
|
18
|
-
* FREE models that work reliably (2026-02-12)
|
|
19
|
-
* WARNING: flux removed from free, turbo shows deprecated notice
|
|
20
|
-
*/
|
|
21
|
-
const RELIABLE_FREE_MODELS = ['sana', 'zimage'];
|
|
22
18
|
const DEFAULT_MODEL = 'flux';
|
|
23
|
-
const FREE_DEFAULT_MODEL = 'sana';
|
|
24
19
|
// ─── Tool Definition ──────────────────────────────────────────────────────
|
|
25
|
-
export const
|
|
20
|
+
export const polliGenImageTool = tool({
|
|
26
21
|
description: `Generate an image from a text prompt using Pollinations AI.
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- \`zimage\`: Low quality alias (~35KB)
|
|
31
|
-
- ⚠️ \`turbo\`: BROKEN - shows deprecated notice
|
|
32
|
-
- ⚠️ \`flux\`: REMOVED from free tier!
|
|
33
|
-
|
|
34
|
-
**💎 ENTER Models** (requires API key):
|
|
35
|
-
| Model | Cost | T2I | I2I | Notes |
|
|
36
|
-
|-------|------|-----|-----|-------|
|
|
37
|
-
| flux | 0.0002 🌻 | ✅ | ❌ | Fast high-quality |
|
|
38
|
-
| zimage | 0.0002 🌻 | ✅ | ❌ | 6B Flux with 2x upscaling |
|
|
39
|
-
| imagen-4 | 0.0025 🌻 | ✅ | ❌ | Google high fidelity |
|
|
40
|
-
| klein | 0.008 🌻 | ✅ | ✅ | FLUX.2 Klein 4B |
|
|
41
|
-
| klein-large | 0.012 🌻 | ✅ | ✅ | FLUX.2 Klein 9B |
|
|
42
|
-
| kontext | 0.04 🌻 | ✅ | ✅ | In-Context Editing |
|
|
43
|
-
| seedream | 0.03 🌻 | ✅ | ✅ | ByteDance ARK quality |
|
|
44
|
-
| seedream-pro | 0.04 🌻 | ✅ | ✅ | 4K, Multi-Image support |
|
|
45
|
-
| gptimage | tokens | ✅ | ❌ | OpenAI GPT Image Mini |
|
|
46
|
-
| gptimage-large | tokens | ✅ | ❌ | OpenAI GPT Image 1.5 |
|
|
47
|
-
| nanobanana | tokens | ✅ | ✅ | Gemini 2.5 Flash |
|
|
48
|
-
| nanobanana-pro | tokens | ✅ | ✅ | Gemini 3 Pro Thinking |
|
|
49
|
-
|
|
50
|
-
**🖼️ Image-to-Image (I2I)**:
|
|
51
|
-
Models with I2I support can transform existing images.
|
|
52
|
-
- Use \`reference_image\` parameter with URL or local path
|
|
53
|
-
- \`seedream-pro\` supports multiple images (comma-separated URLs)
|
|
54
|
-
- \`kontext\` specializes in in-context editing
|
|
23
|
+
💡 **Modèles Image Dynamiques** :
|
|
24
|
+
L'API Pollinations (Enter) possède une quantité importante de modèles (Flux, Midjourney, Seedream, etc.) et ils changent fréquemment. Le catalogue à jour est listé ci-dessous.
|
|
55
25
|
|
|
56
|
-
|
|
57
|
-
- \`
|
|
58
|
-
-
|
|
59
|
-
- \`transparent\`: gptimage only (true/false)
|
|
60
|
-
- \`seed\`: Reproducibility (-1 for random)`,
|
|
26
|
+
**Exemples d'utilisation Optionnelle** :
|
|
27
|
+
- **I2I (Image-to-Image)** : Utilisez le paramètre \`reference_image\` avec une URL ou chemin local si le modèle le supporte.
|
|
28
|
+
- L'outil embarque un "costGuard" automatique gérant la confirmation des coûts.`,
|
|
61
29
|
args: {
|
|
62
30
|
prompt: tool.schema.string().describe('Description of the image to generate'),
|
|
63
|
-
model: tool.schema.string().optional().describe('Model to use (default: flux
|
|
31
|
+
model: tool.schema.string().optional().describe('Model to use (default: flux). Unknown models accepted as (beta).'),
|
|
64
32
|
width: tool.schema.number().min(256).max(4096).optional().describe('Image width (default: 1024)'),
|
|
65
33
|
height: tool.schema.number().min(256).max(4096).optional().describe('Image height (default: 1024)'),
|
|
66
34
|
reference_image: tool.schema.string().optional().describe('URL(s) for image-to-image editing (comma-separated for multi-image models)'),
|
|
@@ -74,156 +42,174 @@ Models with I2I support can transform existing images.
|
|
|
74
42
|
const apiKey = getApiKey();
|
|
75
43
|
const hasKey = hasApiKey();
|
|
76
44
|
// Determine model based on key presence
|
|
77
|
-
let model = args.model ||
|
|
45
|
+
let model = args.model || DEFAULT_MODEL;
|
|
78
46
|
const width = args.width || 1024;
|
|
79
47
|
const height = args.height || 1024;
|
|
80
|
-
//
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
${model === 'turbo' ? '`turbo` affiche une notice de dépréciation.' : ''}
|
|
90
|
-
💡 Modèles gratuits recommandés: ${RELIABLE_FREE_MODELS.join(', ')}
|
|
91
|
-
${hasKey ? `💎 Ou utilisez un modèle payant: flux, kontext, seedream...` : ''}`;
|
|
48
|
+
// Fetch known models from registry
|
|
49
|
+
const imageModels = getPaidImageModels();
|
|
50
|
+
const knownModel = !!imageModels[model];
|
|
51
|
+
const isBetaModel = !knownModel;
|
|
52
|
+
// Force Auth Check for ALL Image Generations
|
|
53
|
+
if (!hasKey) {
|
|
54
|
+
return `❌ **Clé API Requise** pour la génération d'images.
|
|
55
|
+
💡 Utilisez \`/pollinations connect <clé>\` pour activer le service.
|
|
56
|
+
💎 Modèles disponibles: ${Object.keys(imageModels).slice(0, 5).join(', ')}...`;
|
|
92
57
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
🔧 Ou connectez votre clé avec /pollinations connect`;
|
|
58
|
+
// Unknown model → beta passthrough (don't reject)
|
|
59
|
+
if (isBetaModel) {
|
|
60
|
+
emitStatusToast('warning', `Modèle "${model}" non référencé — mode (beta)`, '🎨 gen_image');
|
|
97
61
|
}
|
|
98
|
-
// Validate I2I support
|
|
99
|
-
if (args.reference_image &&
|
|
62
|
+
// Validate I2I support (for known models only; beta models get default behavior)
|
|
63
|
+
if (args.reference_image && knownModel && !supportsI2I(model)) {
|
|
100
64
|
return `⚠️ Le modèle "${model}" ne supporte pas l'Image-to-Image.
|
|
101
|
-
💡 Modèles I2I supportés: ${Object.entries(
|
|
65
|
+
💡 Modèles I2I supportés: ${Object.entries(imageModels)
|
|
102
66
|
.filter(([, info]) => info.i2i)
|
|
103
67
|
.map(([name]) => name)
|
|
104
68
|
.join(', ')}`;
|
|
105
69
|
}
|
|
106
|
-
// Check if model exists
|
|
107
|
-
if (!isFreeModel && !isPaidModel) {
|
|
108
|
-
return `❌ Modèle inconnu: ${model}
|
|
109
|
-
💡 Modèles gratuits: ${RELIABLE_FREE_MODELS.join(', ')}
|
|
110
|
-
💎 Modèles payants: ${Object.keys(PAID_IMAGE_MODELS).slice(0, 5).join(', ')}...`;
|
|
111
|
-
}
|
|
112
70
|
// Estimate cost
|
|
113
|
-
const estimatedCost =
|
|
71
|
+
const estimatedCost = estimateImageCost(model);
|
|
72
|
+
// Cost Guard check V2
|
|
73
|
+
const costCheck = checkCostControl('polli_gen_image', args, model, estimatedCost, 'image');
|
|
74
|
+
if (!costCheck.allowed) {
|
|
75
|
+
return costCheck.message || '❌ Opération bloquée par le Cost Guard.';
|
|
76
|
+
}
|
|
77
|
+
// Emit start toast
|
|
78
|
+
const config = loadConfig();
|
|
79
|
+
const argsStr = config.gui?.logs === 'verbose' ? `\nParameters: ${JSON.stringify(args)}` : '';
|
|
80
|
+
emitStatusToast('info', `Génération image: ${model} (${width}×${height})${argsStr}`, '🎨 polli_gen_image');
|
|
114
81
|
// Set metadata
|
|
115
|
-
context.metadata({ title: `🎨 Image: ${model}` });
|
|
82
|
+
context.metadata({ title: `🎨 Image: ${model}${isBetaModel ? ' (beta)' : ''}` });
|
|
116
83
|
try {
|
|
117
84
|
let imageData;
|
|
118
85
|
let responseHeaders = {};
|
|
119
86
|
let usedModel = model;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const promptEncoded = encodeURIComponent(args.prompt);
|
|
133
|
-
const url = `https://image.pollinations.ai/${promptEncoded}?${params}`;
|
|
134
|
-
const result = await httpsGet(url);
|
|
135
|
-
imageData = result.data;
|
|
87
|
+
// === ENTER endpoint ONLY (gen.pollinations.ai) ===
|
|
88
|
+
const params = new URLSearchParams({
|
|
89
|
+
nologo: 'true',
|
|
90
|
+
private: 'true',
|
|
91
|
+
width: String(width),
|
|
92
|
+
height: String(height),
|
|
93
|
+
});
|
|
94
|
+
// Model parameter
|
|
95
|
+
params.set('model', model);
|
|
96
|
+
// Seed
|
|
97
|
+
if (args.seed !== undefined) {
|
|
98
|
+
params.set('seed', String(args.seed));
|
|
136
99
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// Model parameter
|
|
146
|
-
params.set('model', model);
|
|
147
|
-
// Seed
|
|
148
|
-
if (args.seed !== undefined) {
|
|
149
|
-
params.set('seed', String(args.seed));
|
|
150
|
-
}
|
|
151
|
-
// I2I: reference image(s)
|
|
152
|
-
if (args.reference_image) {
|
|
153
|
-
// Check if it's a local file path
|
|
154
|
-
let imageUrl = args.reference_image;
|
|
155
|
-
if (!args.reference_image.startsWith('http')) {
|
|
156
|
-
// For local files, we'd need to upload first
|
|
157
|
-
// For now, require URL
|
|
158
|
-
return `❌ Les fichiers locaux nécessitent d'être uploadés d'abord.
|
|
100
|
+
// I2I: reference image(s)
|
|
101
|
+
if (args.reference_image) {
|
|
102
|
+
// Check if it's a local file path
|
|
103
|
+
let imageUrl = args.reference_image;
|
|
104
|
+
if (!args.reference_image.startsWith('http')) {
|
|
105
|
+
// For local files, we'd need to upload first
|
|
106
|
+
// For now, require URL
|
|
107
|
+
return `❌ Les fichiers locaux nécessitent d'être uploadés d'abord.
|
|
159
108
|
💡 Utilisez l'outil \`file_to_url\` pour obtenir une URL publique.`;
|
|
160
|
-
}
|
|
161
|
-
params.set('image', imageUrl);
|
|
162
|
-
}
|
|
163
|
-
// Quality (gptimage only)
|
|
164
|
-
if (args.quality && model.startsWith('gptimage')) {
|
|
165
|
-
params.set('quality', args.quality);
|
|
166
109
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
110
|
+
params.set('image', imageUrl);
|
|
111
|
+
}
|
|
112
|
+
// Quality (gptimage only)
|
|
113
|
+
if (args.quality && model.startsWith('gptimage')) {
|
|
114
|
+
params.set('quality', args.quality);
|
|
115
|
+
}
|
|
116
|
+
// Transparent (gptimage only)
|
|
117
|
+
if (args.transparent !== undefined && model.startsWith('gptimage')) {
|
|
118
|
+
params.set('transparent', String(args.transparent));
|
|
119
|
+
}
|
|
120
|
+
const promptEncoded = encodeURIComponent(args.prompt);
|
|
121
|
+
const url = `https://gen.pollinations.ai/image/${promptEncoded}?${params}`;
|
|
122
|
+
const headers = {};
|
|
123
|
+
if (apiKey)
|
|
124
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
125
|
+
// 1. Fetch balance avant génération
|
|
126
|
+
const balBefore = await fetchEnterBalance();
|
|
127
|
+
const result = await httpsGet(url, headers);
|
|
128
|
+
imageData = result.data;
|
|
129
|
+
responseHeaders = result.headers;
|
|
130
|
+
// Update used model from response if available
|
|
131
|
+
if (responseHeaders['x-model-used']) {
|
|
132
|
+
usedModel = responseHeaders['x-model-used'];
|
|
133
|
+
}
|
|
134
|
+
// Save the image
|
|
135
|
+
let outputDir = getDefaultOutputDir('images');
|
|
136
|
+
let filename = args.filename;
|
|
137
|
+
if (args.save_to) {
|
|
138
|
+
if (args.save_to.match(/\.(png|jpe?g|webp|gif)$/i)) {
|
|
139
|
+
outputDir = path.dirname(args.save_to);
|
|
140
|
+
filename = path.basename(args.save_to);
|
|
170
141
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const headers = {};
|
|
174
|
-
if (apiKey)
|
|
175
|
-
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
176
|
-
const result = await httpsGet(url, headers);
|
|
177
|
-
imageData = result.data;
|
|
178
|
-
responseHeaders = result.headers;
|
|
179
|
-
// Update used model from response if available
|
|
180
|
-
if (responseHeaders['x-model-used']) {
|
|
181
|
-
usedModel = responseHeaders['x-model-used'];
|
|
142
|
+
else {
|
|
143
|
+
outputDir = args.save_to;
|
|
182
144
|
}
|
|
183
145
|
}
|
|
184
|
-
// Save the image
|
|
185
|
-
const outputDir = args.save_to || getDefaultOutputDir('images');
|
|
186
146
|
ensureDir(outputDir);
|
|
187
|
-
|
|
188
|
-
const filePath = path.join(outputDir, filename.
|
|
147
|
+
filename = filename || generateFilename('image', usedModel, 'png');
|
|
148
|
+
const filePath = path.join(outputDir, filename.includes('.') ? filename : `${filename}.png`);
|
|
189
149
|
fs.writeFileSync(filePath, imageData);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
let
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
150
|
+
// 2. Fetch balance après génération (delay for API sync)
|
|
151
|
+
let balAfter = null;
|
|
152
|
+
let realCost;
|
|
153
|
+
if (balBefore !== null) {
|
|
154
|
+
await new Promise(r => setTimeout(r, 1000)); // Laisse le temps au ledger
|
|
155
|
+
balAfter = await fetchEnterBalance();
|
|
156
|
+
if (balAfter !== null) {
|
|
157
|
+
realCost = Math.round((balBefore - balAfter) * 10000) / 10000;
|
|
158
|
+
}
|
|
196
159
|
}
|
|
160
|
+
// Extract cost from headers as fallback/info
|
|
161
|
+
const costTracking = extractCostFromHeaders(responseHeaders);
|
|
197
162
|
// Build result
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
163
|
+
const fileSize = fs.statSync(filePath).size;
|
|
164
|
+
const lines = [];
|
|
165
|
+
// Inject costWarning at top if present
|
|
166
|
+
if (costCheck.message && !costCheck.allowed) { // Assuming costWarning should come from costCheck if not allowed
|
|
167
|
+
lines.push(costCheck.message);
|
|
168
|
+
lines.push('');
|
|
169
|
+
}
|
|
170
|
+
lines.push(`🎨 Image Générée`);
|
|
171
|
+
lines.push(`━━━━━━━━━━━━━━━━━━`);
|
|
172
|
+
lines.push(`Prompt: ${args.prompt.substring(0, 100)}${args.prompt.length > 100 ? '...' : ''}`);
|
|
173
|
+
lines.push(`Modèle: ${usedModel}${isBetaModel ? ' (beta)' : ''}`);
|
|
174
|
+
lines.push(`Résolution: ${width}×${height}`);
|
|
205
175
|
// Add I2I info if used
|
|
206
176
|
if (args.reference_image) {
|
|
207
177
|
lines.push(`I2I Source: ${args.reference_image.substring(0, 50)}...`);
|
|
208
178
|
}
|
|
209
179
|
lines.push(`Fichier: ${filePath}`);
|
|
210
180
|
lines.push(`Taille: ${formatFileSize(fileSize)}`);
|
|
211
|
-
//
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
181
|
+
// Pricing details (Estimé vs Réel)
|
|
182
|
+
if (isCostEstimatorEnabled()) {
|
|
183
|
+
const maxCost = estimatedCost * 3;
|
|
184
|
+
lines.push(`\n💰 **Rapport Financier :**`);
|
|
185
|
+
if (isTokenBased('image', usedModel)) {
|
|
186
|
+
lines.push(`- Coût Estimé : ${formatCost(estimatedCost)} (Max théorique: ${formatCost(maxCost)})`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
lines.push(`- Coût Estimé : ${formatCost(estimatedCost)}`);
|
|
190
|
+
}
|
|
191
|
+
if (realCost !== undefined) {
|
|
192
|
+
lines.push(`- Coût Réel : **${formatCost(realCost)}** (via Solde Wallet)`);
|
|
219
193
|
}
|
|
194
|
+
else if (costTracking.costUsd !== undefined) {
|
|
195
|
+
lines.push(`- Coût Réel : **${formatCost(costTracking.costUsd)}** (via Headers API)`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
lines.push(`- Coût Réel : Inconnu (API injoignable)`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (responseHeaders['x-request-id']) {
|
|
202
|
+
lines.push(`Request ID: ${responseHeaders['x-request-id']}`);
|
|
220
203
|
}
|
|
204
|
+
// Emit success toast
|
|
205
|
+
emitStatusToast('success', `Image générée ✓ (${usedModel})`, '🎨 gen_image', { filePath: filePath });
|
|
221
206
|
return lines.join('\n');
|
|
222
207
|
}
|
|
223
208
|
catch (err) {
|
|
209
|
+
emitStatusToast('error', `Erreur: ${err.message?.substring(0, 60)}`, '🎨 gen_image');
|
|
224
210
|
if (err.message?.includes('402') || err.message?.includes('Payment')) {
|
|
225
|
-
return `❌ Crédits insuffisants pour le modèle "${model}".
|
|
226
|
-
💡
|
|
211
|
+
return `❌ Crédits pollen insuffisants pour le modèle "${model}".
|
|
212
|
+
💡 Vérifiez votre solde avec /pollinations usage`;
|
|
227
213
|
}
|
|
228
214
|
if (err.message?.includes('401') || err.message?.includes('403')) {
|
|
229
215
|
return `❌ Clé API invalide ou non autorisée.
|
|
@@ -13,14 +13,17 @@
|
|
|
13
13
|
import { tool } from '@opencode-ai/plugin/tool';
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
|
-
import { getApiKey, httpsGet, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, estimateMusicCost, isCostEstimatorEnabled, } from './shared.js';
|
|
16
|
+
import { getApiKey, httpsGet, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, estimateMusicCost, extractCostFromHeaders, isCostEstimatorEnabled, } from './shared.js';
|
|
17
|
+
import { loadConfig } from '../../server/config.js';
|
|
18
|
+
import { checkCostControl, isTokenBased } from './cost-guard.js';
|
|
19
|
+
import { emitStatusToast } from '../../server/toast.js';
|
|
17
20
|
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
18
21
|
const MIN_DURATION = 3;
|
|
19
22
|
const MAX_DURATION = 300; // 5 minutes
|
|
20
23
|
const DEFAULT_DURATION = 10;
|
|
21
24
|
const MODEL_NAME = 'elevenmusic';
|
|
22
25
|
// ─── Tool Definition ──────────────────────────────────────────────────────
|
|
23
|
-
export const
|
|
26
|
+
export const polliGenMusicTool = tool({
|
|
24
27
|
description: `Generate music from a text description using Pollinations AI.
|
|
25
28
|
|
|
26
29
|
**🎵 Model:** elevenmusic (ElevenLabs Music)
|
|
@@ -65,8 +68,17 @@ export const genMusicTool = tool({
|
|
|
65
68
|
const instrumental = args.instrumental || false;
|
|
66
69
|
// Estimate cost
|
|
67
70
|
const estimatedCost = estimateMusicCost(duration);
|
|
71
|
+
// Cost Guard check V2
|
|
72
|
+
const costCheck = checkCostControl('polli_gen_music', args, MODEL_NAME, estimatedCost, 'audio');
|
|
73
|
+
if (!costCheck.allowed) {
|
|
74
|
+
return costCheck.message || '❌ Opération bloquée par le Cost Guard.';
|
|
75
|
+
}
|
|
68
76
|
// Estimate generation time
|
|
69
|
-
const genTimeSeconds = Math.ceil(duration * 1.2);
|
|
77
|
+
const genTimeSeconds = Math.ceil(duration * 1.2);
|
|
78
|
+
// Emit start toast
|
|
79
|
+
const config = loadConfig();
|
|
80
|
+
const argsStr = config.gui?.logs === 'verbose' ? `\nParameters: ${JSON.stringify(args)}` : '';
|
|
81
|
+
emitStatusToast('info', `Génération musique: ${duration}s (~${genTimeSeconds}s gen)${argsStr}`, '🎵 polli_gen_music');
|
|
70
82
|
// Metadata
|
|
71
83
|
context.metadata({ title: `🎵 Music: ${duration}s (~${genTimeSeconds}s gen time)` });
|
|
72
84
|
try {
|
|
@@ -94,25 +106,51 @@ export const genMusicTool = tool({
|
|
|
94
106
|
const audioData = result.data;
|
|
95
107
|
const responseHeaders = result.headers;
|
|
96
108
|
// Save audio
|
|
97
|
-
|
|
109
|
+
let outputDir = getDefaultOutputDir('music');
|
|
110
|
+
let filename = args.filename;
|
|
111
|
+
if (args.save_to) {
|
|
112
|
+
if (args.save_to.match(/\.(mp3|wav|ogg|m4a)$/i)) {
|
|
113
|
+
outputDir = path.dirname(args.save_to);
|
|
114
|
+
filename = path.basename(args.save_to);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
outputDir = args.save_to;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
98
120
|
ensureDir(outputDir);
|
|
99
|
-
|
|
121
|
+
filename = filename || generateFilename('music', MODEL_NAME, 'mp3');
|
|
100
122
|
const filePath = path.join(outputDir, filename.endsWith('.mp3') ? filename : `${filename}.mp3`);
|
|
101
123
|
fs.writeFileSync(filePath, audioData);
|
|
102
124
|
const fileSize = fs.statSync(filePath).size;
|
|
125
|
+
let actualCost = estimatedCost;
|
|
126
|
+
if (responseHeaders) {
|
|
127
|
+
const costTracking = extractCostFromHeaders(responseHeaders);
|
|
128
|
+
if (costTracking.costUsd !== undefined)
|
|
129
|
+
actualCost = costTracking.costUsd;
|
|
130
|
+
}
|
|
103
131
|
// Build result
|
|
104
|
-
const lines = [
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
132
|
+
const lines = [];
|
|
133
|
+
// Inject costWarning at top if present
|
|
134
|
+
if (costCheck.message && !costCheck.allowed) {
|
|
135
|
+
lines.push(costCheck.message);
|
|
136
|
+
lines.push('');
|
|
137
|
+
}
|
|
138
|
+
lines.push(`🎵 Musique Générée`);
|
|
139
|
+
lines.push(`━━━━━━━━━━━━━━━━━━`);
|
|
140
|
+
lines.push(`Prompt: ${args.prompt}`);
|
|
141
|
+
lines.push(`Durée: ~${duration}s`);
|
|
142
|
+
lines.push(`Mode: ${instrumental ? 'Instrumental (sans voix)' : 'Avec voix possible'}`);
|
|
143
|
+
lines.push(`Fichier: ${filePath}`);
|
|
144
|
+
lines.push(`Taille: ${formatFileSize(fileSize)}`);
|
|
113
145
|
// Cost info
|
|
114
146
|
if (isCostEstimatorEnabled()) {
|
|
115
|
-
|
|
147
|
+
if (isTokenBased('audio', MODEL_NAME)) {
|
|
148
|
+
const maxCost = estimatedCost * 3;
|
|
149
|
+
lines.push(`Coût: ${formatCost(actualCost)} (Max théorique: ${formatCost(maxCost)})`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
lines.push(`Coût: ${formatCost(actualCost)}`);
|
|
153
|
+
}
|
|
116
154
|
}
|
|
117
155
|
if (responseHeaders['x-model-used']) {
|
|
118
156
|
lines.push(`Modèle utilisé: ${responseHeaders['x-model-used']}`);
|
|
@@ -120,11 +158,14 @@ export const genMusicTool = tool({
|
|
|
120
158
|
if (responseHeaders['x-request-id']) {
|
|
121
159
|
lines.push(`Request ID: ${responseHeaders['x-request-id']}`);
|
|
122
160
|
}
|
|
161
|
+
// Emit success toast
|
|
162
|
+
emitStatusToast('success', `Musique générée ✓ (${duration}s)`, '🎵 gen_music');
|
|
123
163
|
return lines.join('\n');
|
|
124
164
|
}
|
|
125
165
|
catch (err) {
|
|
166
|
+
emitStatusToast('error', `Erreur: ${err.message?.substring(0, 60)}`, '🎵 gen_music');
|
|
126
167
|
if (err.message?.includes('402') || err.message?.includes('Payment')) {
|
|
127
|
-
return `❌ Crédits insuffisants.`;
|
|
168
|
+
return `❌ Crédits pollen insuffisants.`;
|
|
128
169
|
}
|
|
129
170
|
if (err.message?.includes('401') || err.message?.includes('403')) {
|
|
130
171
|
return `❌ Clé API invalide ou non autorisée.`;
|