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.
Files changed (73) hide show
  1. package/README.md +11 -6
  2. package/dist/index.js +40 -10
  3. package/dist/server/commands.d.ts +4 -0
  4. package/dist/server/commands.js +296 -12
  5. package/dist/server/config.d.ts +5 -0
  6. package/dist/server/config.js +163 -35
  7. package/dist/server/connect-response.d.ts +2 -0
  8. package/dist/server/connect-response.js +141 -0
  9. package/dist/server/generate-config.js +10 -24
  10. package/dist/server/logger.d.ts +8 -0
  11. package/dist/server/logger.js +36 -0
  12. package/dist/server/models/cache.d.ts +35 -0
  13. package/dist/server/models/cache.js +160 -0
  14. package/dist/server/models/fetcher.d.ts +18 -0
  15. package/dist/server/models/fetcher.js +150 -0
  16. package/dist/server/models/index.d.ts +6 -0
  17. package/dist/server/models/index.js +5 -0
  18. package/dist/server/models/manual.d.ts +15 -0
  19. package/dist/server/models/manual.js +92 -0
  20. package/dist/server/models/types.d.ts +55 -0
  21. package/dist/server/models/types.js +7 -0
  22. package/dist/server/models/worker.d.ts +21 -0
  23. package/dist/server/models/worker.js +97 -0
  24. package/dist/server/pollinations-api.js +1 -8
  25. package/dist/server/proxy.js +52 -27
  26. package/dist/server/quota.d.ts +2 -8
  27. package/dist/server/quota.js +47 -89
  28. package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
  29. package/dist/server/scripts/pollinations_pricing.js +246 -0
  30. package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
  31. package/dist/server/scripts/test_cost_endpoints.js +61 -0
  32. package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
  33. package/dist/server/scripts/test_dynamic_pricing.js +39 -0
  34. package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
  35. package/dist/server/scripts/test_freetier_audit.js +215 -0
  36. package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
  37. package/dist/server/scripts/test_parallel_cost.js +104 -0
  38. package/dist/server/toast.d.ts +4 -1
  39. package/dist/server/toast.js +27 -10
  40. package/dist/tools/ffmpeg.d.ts +24 -0
  41. package/dist/tools/ffmpeg.js +54 -0
  42. package/dist/tools/index.d.ts +10 -8
  43. package/dist/tools/index.js +27 -25
  44. package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
  45. package/dist/tools/pollinations/beta_discovery.js +197 -0
  46. package/dist/tools/pollinations/cost-guard.d.ts +38 -0
  47. package/dist/tools/pollinations/cost-guard.js +141 -0
  48. package/dist/tools/pollinations/gen_audio.d.ts +1 -1
  49. package/dist/tools/pollinations/gen_audio.js +65 -23
  50. package/dist/tools/pollinations/gen_image.d.ts +5 -7
  51. package/dist/tools/pollinations/gen_image.js +146 -160
  52. package/dist/tools/pollinations/gen_music.d.ts +1 -1
  53. package/dist/tools/pollinations/gen_music.js +57 -16
  54. package/dist/tools/pollinations/gen_video.d.ts +1 -1
  55. package/dist/tools/pollinations/gen_video.js +99 -65
  56. package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
  57. package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
  58. package/dist/tools/pollinations/polli_status.d.ts +2 -0
  59. package/dist/tools/pollinations/polli_status.js +31 -0
  60. package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
  61. package/dist/tools/pollinations/polli_web_search.js +164 -0
  62. package/dist/tools/pollinations/shared.d.ts +34 -39
  63. package/dist/tools/pollinations/shared.js +300 -89
  64. package/dist/tools/pollinations/test_estimators.d.ts +1 -0
  65. package/dist/tools/pollinations/test_estimators.js +22 -0
  66. package/dist/tools/pollinations/transcribe_audio.d.ts +5 -9
  67. package/dist/tools/pollinations/transcribe_audio.js +31 -72
  68. package/dist/tools/power/extract_audio.js +26 -27
  69. package/dist/tools/power/extract_frames.js +24 -27
  70. package/dist/tools/power/remove_background.js +2 -1
  71. package/dist/tools/power/rmbg_keys.js +2 -1
  72. package/dist/tools/shared.js +9 -3
  73. 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-12 - Verified API Reference
4
+ * Updated: 2026-02-19 - Dynamic ModelRegistry + Cost Guard + Toasts
5
5
  *
6
- * Supports:
7
- * - FREE models: sana, zimage (flux REMOVED, turbo BROKEN)
8
- * - ENTER models: flux, kontext, seedream, klein, gptimage, imagen-4, etc.
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, FREE_IMAGE_MODELS, PAID_IMAGE_MODELS, } from './shared.js';
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 genImageTool = tool({
20
+ export const polliGenImageTool = tool({
26
21
  description: `Generate an image from a text prompt using Pollinations AI.
27
22
 
28
- **🆓 FREE Models** (no API key, no cost):
29
- - \`sana\`: Default free model (~60KB, reliable)
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
- **⚙️ Per-Model Parameters**:
57
- - \`width/height\`: All models (default: 1024x1024)
58
- - \`quality\`: gptimage only (low/med/high)
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 or sana if no key)'),
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 || (hasKey ? DEFAULT_MODEL : FREE_DEFAULT_MODEL);
45
+ let model = args.model || DEFAULT_MODEL;
78
46
  const width = args.width || 1024;
79
47
  const height = args.height || 1024;
80
- // Check if it's a free model
81
- const isFreeModel = Object.keys(FREE_IMAGE_MODELS).includes(model);
82
- const isReliableFree = RELIABLE_FREE_MODELS.includes(model);
83
- // Check if it's a paid model
84
- const paidModelInfo = PAID_IMAGE_MODELS[model];
85
- const isPaidModel = !!paidModelInfo;
86
- // Validate model selection
87
- if (isFreeModel && !isReliableFree) {
88
- return `⚠️ Le modèle "${model}" n'est plus fiable.
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
- if (isPaidModel && !hasKey) {
94
- return `❌ Le modèle "${model}" nécessite une clé API.
95
- 💡 Utilisez un modèle gratuit: ${RELIABLE_FREE_MODELS.join(', ')}
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 && isPaidModel && !supportsI2I(model)) {
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(PAID_IMAGE_MODELS)
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 = isPaidModel ? estimateImageCost(model) : 0;
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
- if (isFreeModel && !hasKey) {
121
- // === FREE endpoint (image.pollinations.ai) ===
122
- const params = new URLSearchParams({
123
- nologo: 'true',
124
- private: 'true',
125
- });
126
- if (model !== 'sana') {
127
- params.set('model', model);
128
- }
129
- if (args.seed !== undefined) {
130
- params.set('seed', String(args.seed));
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
- else {
138
- // === ENTER endpoint (gen.pollinations.ai) ===
139
- const params = new URLSearchParams({
140
- nologo: 'true',
141
- private: 'true',
142
- width: String(width),
143
- height: String(height),
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
- // Transparent (gptimage only)
168
- if (args.transparent !== undefined && model.startsWith('gptimage')) {
169
- params.set('transparent', String(args.transparent));
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
- const promptEncoded = encodeURIComponent(args.prompt);
172
- const url = `https://gen.pollinations.ai/image/${promptEncoded}?${params}`;
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
- const filename = args.filename || generateFilename('image', usedModel, 'png');
188
- const filePath = path.join(outputDir, filename.endsWith('.png') ? filename : `${filename}.png`);
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
- const fileSize = fs.statSync(filePath).size;
191
- // Extract actual cost from headers if available
192
- let actualCost = estimatedCost;
193
- if (isCostEstimatorEnabled() && responseHeaders['x-usage-completion-image-tokens']) {
194
- const tokens = parseFloat(responseHeaders['x-usage-completion-image-tokens']);
195
- // Token-based cost calculation would go here
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 lines = [
199
- `🎨 Image Générée`,
200
- `━━━━━━━━━━━━━━━━━━`,
201
- `Prompt: ${args.prompt.substring(0, 100)}${args.prompt.length > 100 ? '...' : ''}`,
202
- `Modèle: ${usedModel}${isFreeModel ? ' (GRATUIT)' : ''}`,
203
- `Résolution: ${width}×${height}`,
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
- // Cost info
212
- if (isFreeModel) {
213
- lines.push(`Coût: GRATUIT`);
214
- }
215
- else {
216
- lines.push(`Coût estimé: ${formatCost(actualCost)}`);
217
- if (responseHeaders['x-request-id']) {
218
- lines.push(`Request ID: ${responseHeaders['x-request-id']}`);
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
- 💡 Essayez un modèle gratuit: ${RELIABLE_FREE_MODELS.join(', ')}`;
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.
@@ -11,4 +11,4 @@
11
11
  * - instrumental: boolean (vocals or instrumental only)
12
12
  */
13
13
  import { type ToolDefinition } from '@opencode-ai/plugin/tool';
14
- export declare const genMusicTool: ToolDefinition;
14
+ export declare const polliGenMusicTool: ToolDefinition;
@@ -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 genMusicTool = tool({
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); // ~1.2s per second of audio
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
- const outputDir = args.save_to || getDefaultOutputDir('music');
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
- const filename = args.filename || generateFilename('music', MODEL_NAME, 'mp3');
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
- `🎵 Musique Générée`,
106
- `━━━━━━━━━━━━━━━━━━`,
107
- `Prompt: ${args.prompt}`,
108
- `Durée: ~${duration}s`,
109
- `Mode: ${instrumental ? 'Instrumental (sans voix)' : 'Avec voix possible'}`,
110
- `Fichier: ${filePath}`,
111
- `Taille: ${formatFileSize(fileSize)}`,
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
- lines.push(`Coût estimé: ${formatCost(estimatedCost)}`);
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.`;
@@ -13,4 +13,4 @@
13
13
  * - x-usage-completion-video-tokens (seedance, seedance-pro)
14
14
  */
15
15
  import { type ToolDefinition } from '@opencode-ai/plugin/tool';
16
- export declare const genVideoTool: ToolDefinition;
16
+ export declare const polliGenVideoTool: ToolDefinition;