opencode-pollinations-plugin 6.1.0-beta.9 → 6.2.1

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 (109) hide show
  1. package/README.de.md +130 -0
  2. package/README.es.md +130 -0
  3. package/README.fr.md +130 -0
  4. package/README.it.md +130 -0
  5. package/README.md +87 -73
  6. package/dist/index.js +52 -161
  7. package/dist/locales/de.json +374 -0
  8. package/dist/locales/en.json +373 -0
  9. package/dist/locales/es.json +374 -0
  10. package/dist/locales/fr.json +373 -0
  11. package/dist/locales/index.d.ts +1 -0
  12. package/dist/locales/index.js +37 -0
  13. package/dist/locales/it.json +374 -0
  14. package/dist/server/commands.d.ts +6 -0
  15. package/dist/server/commands.js +394 -125
  16. package/dist/server/config.d.ts +34 -23
  17. package/dist/server/config.js +200 -108
  18. package/dist/server/connect-response.d.ts +2 -0
  19. package/dist/server/connect-response.js +59 -0
  20. package/dist/server/generate-config.d.ts +3 -30
  21. package/dist/server/generate-config.js +164 -106
  22. package/dist/server/index.d.ts +2 -1
  23. package/dist/server/index.js +124 -149
  24. package/dist/server/logger.d.ts +8 -0
  25. package/dist/server/logger.js +38 -0
  26. package/dist/server/models/cache.d.ts +35 -0
  27. package/dist/server/models/cache.js +160 -0
  28. package/dist/server/models/fetcher.d.ts +18 -0
  29. package/dist/server/models/fetcher.js +194 -0
  30. package/dist/server/models/index.d.ts +6 -0
  31. package/dist/server/models/index.js +5 -0
  32. package/dist/server/models/manual.d.ts +15 -0
  33. package/dist/server/models/manual.js +92 -0
  34. package/dist/server/models/types.d.ts +55 -0
  35. package/dist/server/models/types.js +7 -0
  36. package/dist/server/models/worker.d.ts +22 -0
  37. package/dist/server/models/worker.js +174 -0
  38. package/dist/server/pollinations-api.d.ts +11 -0
  39. package/dist/server/pollinations-api.js +21 -8
  40. package/dist/server/proxy.js +222 -307
  41. package/dist/server/quota.d.ts +2 -0
  42. package/dist/server/quota.js +89 -86
  43. package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
  44. package/dist/server/scripts/pollinations_pricing.js +246 -0
  45. package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
  46. package/dist/server/scripts/test_cost_endpoints.js +61 -0
  47. package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
  48. package/dist/server/scripts/test_dynamic_pricing.js +39 -0
  49. package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
  50. package/dist/server/scripts/test_freetier_audit.js +215 -0
  51. package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
  52. package/dist/server/scripts/test_parallel_cost.js +104 -0
  53. package/dist/server/toast.d.ts +7 -1
  54. package/dist/server/toast.js +43 -10
  55. package/dist/tools/design/gen_diagram.d.ts +2 -0
  56. package/dist/tools/design/gen_diagram.js +94 -0
  57. package/dist/tools/design/gen_palette.d.ts +2 -0
  58. package/dist/tools/design/gen_palette.js +182 -0
  59. package/dist/tools/design/gen_qrcode.d.ts +2 -0
  60. package/dist/tools/design/gen_qrcode.js +50 -0
  61. package/dist/tools/ffmpeg.d.ts +24 -0
  62. package/dist/tools/ffmpeg.js +54 -0
  63. package/dist/tools/index.d.ts +25 -0
  64. package/dist/tools/index.js +86 -0
  65. package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
  66. package/dist/tools/pollinations/beta_discovery.js +201 -0
  67. package/dist/tools/pollinations/cost-guard.d.ts +38 -0
  68. package/dist/tools/pollinations/cost-guard.js +136 -0
  69. package/dist/tools/pollinations/deepsearch.d.ts +7 -0
  70. package/dist/tools/pollinations/deepsearch.js +80 -0
  71. package/dist/tools/pollinations/gen_audio.d.ts +18 -0
  72. package/dist/tools/pollinations/gen_audio.js +220 -0
  73. package/dist/tools/pollinations/gen_image.d.ts +11 -0
  74. package/dist/tools/pollinations/gen_image.js +211 -0
  75. package/dist/tools/pollinations/gen_music.d.ts +14 -0
  76. package/dist/tools/pollinations/gen_music.js +157 -0
  77. package/dist/tools/pollinations/gen_video.d.ts +16 -0
  78. package/dist/tools/pollinations/gen_video.js +249 -0
  79. package/dist/tools/pollinations/polli_config.d.ts +2 -0
  80. package/dist/tools/pollinations/polli_config.js +95 -0
  81. package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
  82. package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
  83. package/dist/tools/pollinations/polli_status.d.ts +2 -0
  84. package/dist/tools/pollinations/polli_status.js +31 -0
  85. package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
  86. package/dist/tools/pollinations/polli_web_search.js +126 -0
  87. package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
  88. package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
  89. package/dist/tools/pollinations/shared.d.ts +181 -0
  90. package/dist/tools/pollinations/shared.js +758 -0
  91. package/dist/tools/pollinations/test_estimators.d.ts +1 -0
  92. package/dist/tools/pollinations/test_estimators.js +22 -0
  93. package/dist/tools/pollinations/transcribe_audio.d.ts +13 -0
  94. package/dist/tools/pollinations/transcribe_audio.js +171 -0
  95. package/dist/tools/power/extract_audio.d.ts +2 -0
  96. package/dist/tools/power/extract_audio.js +179 -0
  97. package/dist/tools/power/extract_frames.d.ts +2 -0
  98. package/dist/tools/power/extract_frames.js +237 -0
  99. package/dist/tools/power/file_to_url.d.ts +2 -0
  100. package/dist/tools/power/file_to_url.js +217 -0
  101. package/dist/tools/power/remove_background.d.ts +2 -0
  102. package/dist/tools/power/remove_background.js +404 -0
  103. package/dist/tools/power/rmbg_keys.d.ts +2 -0
  104. package/dist/tools/power/rmbg_keys.js +79 -0
  105. package/dist/tools/shared.d.ts +30 -0
  106. package/dist/tools/shared.js +80 -0
  107. package/package.json +9 -3
  108. package/dist/server/models-seed.d.ts +0 -18
  109. package/dist/server/models-seed.js +0 -55
@@ -0,0 +1,211 @@
1
+ /**
2
+ * gen_image Tool - Pollinations Image Generation
3
+ *
4
+ * Updated: 2026-02-19 - Dynamic ModelRegistry + Cost Guard + Toasts
5
+ *
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.
9
+ */
10
+ import { tool } from '@opencode-ai/plugin/tool';
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { getApiKey, hasApiKey, httpsGet, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, estimateImageCost, extractCostFromHeaders, isCostEstimatorEnabled, supportsI2I, getPaidImageModels, fetchEnterBalance, sanitizeFilename, validateHttpUrl, } 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';
17
+ import { t } from '../../locales/index.js';
18
+ // ─── Constants ─────────────────────────────────────────────────────────────
19
+ const DEFAULT_MODEL = 'flux';
20
+ // ─── Tool Definition ──────────────────────────────────────────────────────
21
+ export const polliGenImageTool = tool({
22
+ description: t('tools.image.desc'),
23
+ args: {
24
+ prompt: tool.schema.string().describe(t('tools.image.arg_prompt')),
25
+ model: tool.schema.string().describe(t('tools.image.arg_model')),
26
+ width: tool.schema.number().min(256).max(4096).optional().describe(t('tools.image.arg_width')),
27
+ height: tool.schema.number().min(256).max(4096).optional().describe(t('tools.image.arg_height')),
28
+ reference_image: tool.schema.string().optional().describe(t('tools.image.arg_ref')),
29
+ seed: tool.schema.number().optional().describe(t('tools.image.arg_seed')),
30
+ quality: tool.schema.enum(['low', 'med', 'high']).optional().describe(t('tools.image.arg_quality')),
31
+ transparent: tool.schema.boolean().optional().describe(t('tools.image.arg_trans')),
32
+ save_to: tool.schema.string().optional().describe(t('tools.image.arg_save_to')),
33
+ filename: tool.schema.string().optional().describe(t('tools.image.arg_filename')),
34
+ },
35
+ async execute(args, context) {
36
+ const apiKey = getApiKey();
37
+ const hasKey = hasApiKey();
38
+ // Determine model based on key presence
39
+ let model = args.model;
40
+ const width = args.width || 1024;
41
+ const height = args.height || 1024;
42
+ // Fetch known models from registry
43
+ const imageModels = getPaidImageModels();
44
+ const knownModel = !!imageModels[model];
45
+ const isBetaModel = !knownModel;
46
+ // Force Auth Check for ALL Image Generations
47
+ if (!hasKey) {
48
+ return t('tools.image.req_key', { models: Object.keys(imageModels).slice(0, 5).join(', ') });
49
+ }
50
+ // Unknown model → beta passthrough (don't reject)
51
+ if (isBetaModel) {
52
+ emitStatusToast('warning', t('tools.image.unreferenced_model', { model }), '🎨 gen_image');
53
+ }
54
+ // Validate I2I support (for known models only; beta models get default behavior)
55
+ if (args.reference_image) {
56
+ if (!validateHttpUrl(args.reference_image)) {
57
+ return t('tools.image.req_url_i2i') || '❌ URL invalide. Utilisez http:// ou https://';
58
+ }
59
+ if (knownModel && !supportsI2I(model)) {
60
+ const models = Object.entries(imageModels)
61
+ .filter(([, info]) => info.i2i)
62
+ .map(([name]) => name)
63
+ .join(', ');
64
+ return t('tools.image.no_i2i', { model, models });
65
+ }
66
+ }
67
+ // Estimate cost
68
+ const estimatedCost = estimateImageCost(model);
69
+ // Cost Guard check V2
70
+ const costCheck = checkCostControl('polli_gen_image', args, model, estimatedCost, 'image');
71
+ if (!costCheck.allowed) {
72
+ return costCheck.message || t('tools.image.blocked');
73
+ }
74
+ // Emit start toast
75
+ const config = loadConfig();
76
+ const argsStr = config.gui?.logs === 'verbose' ? `\nParameters: ${JSON.stringify(args)}` : '';
77
+ emitStatusToast('info', t('tools.image.generating', { model, width, height }) + argsStr, '🎨 polli_gen_image');
78
+ // Set metadata
79
+ context.metadata({ title: `🎨 Image: ${model}${isBetaModel ? ' (beta)' : ''}` });
80
+ try {
81
+ let imageData;
82
+ let responseHeaders = {};
83
+ let usedModel = model;
84
+ // === ENTER endpoint ONLY (gen.pollinations.ai) ===
85
+ const params = new URLSearchParams({
86
+ nologo: 'true',
87
+ private: 'true',
88
+ width: String(width),
89
+ height: String(height),
90
+ });
91
+ // Model parameter
92
+ params.set('model', model);
93
+ // Seed
94
+ if (args.seed !== undefined) {
95
+ params.set('seed', String(args.seed));
96
+ }
97
+ // I2I: reference image(s)
98
+ if (args.reference_image) {
99
+ params.set('image', args.reference_image);
100
+ }
101
+ // Quality (gptimage only)
102
+ if (args.quality && model.startsWith('gptimage')) {
103
+ params.set('quality', args.quality);
104
+ }
105
+ // Transparent (gptimage only)
106
+ if (args.transparent !== undefined && model.startsWith('gptimage')) {
107
+ params.set('transparent', String(args.transparent));
108
+ }
109
+ const promptEncoded = encodeURIComponent(args.prompt);
110
+ const url = `https://gen.pollinations.ai/image/${promptEncoded}?${params}`;
111
+ const headers = {};
112
+ if (apiKey)
113
+ headers['Authorization'] = `Bearer ${apiKey}`;
114
+ // 1. Fetch balance avant génération
115
+ const balBefore = await fetchEnterBalance();
116
+ const result = await httpsGet(url, headers);
117
+ imageData = result.data;
118
+ responseHeaders = result.headers;
119
+ // Update used model from response if available
120
+ if (responseHeaders['x-model-used']) {
121
+ usedModel = responseHeaders['x-model-used'];
122
+ }
123
+ // Save the image
124
+ let outputDir = getDefaultOutputDir('images');
125
+ let filename = args.filename ? sanitizeFilename(args.filename) : undefined;
126
+ if (args.save_to) {
127
+ if (args.save_to.match(/\.(png|jpe?g|webp|gif)$/i)) {
128
+ outputDir = path.dirname(args.save_to);
129
+ filename = path.basename(args.save_to);
130
+ }
131
+ else {
132
+ outputDir = args.save_to;
133
+ }
134
+ }
135
+ ensureDir(outputDir);
136
+ filename = filename || generateFilename('image', usedModel, 'png');
137
+ const filePath = path.join(outputDir, filename.includes('.') ? filename : `${filename}.png`);
138
+ fs.writeFileSync(filePath, imageData);
139
+ // 2. Fetch balance après génération (delay for API sync)
140
+ let balAfter = null;
141
+ let realCost;
142
+ if (balBefore !== null) {
143
+ await new Promise(r => setTimeout(r, 1000)); // Laisse le temps au ledger
144
+ balAfter = await fetchEnterBalance();
145
+ if (balAfter !== null) {
146
+ realCost = Math.round((balBefore - balAfter) * 10000) / 10000;
147
+ }
148
+ }
149
+ // Extract cost from headers as fallback/info
150
+ const costTracking = extractCostFromHeaders(responseHeaders);
151
+ // Build result
152
+ const fileSize = fs.statSync(filePath).size;
153
+ const lines = [];
154
+ // Inject costWarning at top if present
155
+ if (costCheck.message && !costCheck.allowed) { // Assuming costWarning should come from costCheck if not allowed
156
+ lines.push(costCheck.message);
157
+ lines.push('');
158
+ }
159
+ lines.push(t('tools.image.res_title'));
160
+ lines.push(`━━━━━━━━━━━━━━━━━━`);
161
+ lines.push(t('tools.image.res_prompt', { prompt: args.prompt.substring(0, 100) + (args.prompt.length > 100 ? '...' : '') }));
162
+ lines.push(t('tools.image.res_model', { model: `${usedModel}${isBetaModel ? ' (beta)' : ''}` }));
163
+ lines.push(t('tools.image.res_res', { width, height }));
164
+ // Add I2I info if used
165
+ if (args.reference_image) {
166
+ lines.push(t('tools.image.res_i2i_src', { src: args.reference_image.substring(0, 50) + '...' }));
167
+ }
168
+ lines.push(t('tools.image.res_file', { path: filePath }));
169
+ lines.push(t('tools.image.res_size', { size: formatFileSize(fileSize) }));
170
+ // Pricing details (Estimé vs Réel)
171
+ if (isCostEstimatorEnabled()) {
172
+ const maxCost = estimatedCost * 3;
173
+ lines.push(t('tools.image.cost_title'));
174
+ if (isTokenBased('image', usedModel)) {
175
+ lines.push(`- Cost : ${formatCost(estimatedCost)} (Max théorique: ${formatCost(maxCost)})`);
176
+ }
177
+ else {
178
+ lines.push(t('tools.image.cost_estimated', { cost: formatCost(estimatedCost) }));
179
+ }
180
+ if (realCost !== undefined) {
181
+ lines.push(t('tools.image.cost_real_wallet', { cost: formatCost(realCost) }));
182
+ }
183
+ else if (costTracking.costUsd !== undefined) {
184
+ lines.push(t('tools.image.cost_real_api', { cost: formatCost(costTracking.costUsd) }));
185
+ }
186
+ else {
187
+ lines.push(t('tools.image.cost_unknown'));
188
+ }
189
+ }
190
+ if (responseHeaders['x-request-id']) {
191
+ lines.push(`Request ID: ${responseHeaders['x-request-id']}`);
192
+ }
193
+ // Emit success toast
194
+ emitStatusToast('success', t('tools.image.success', { model: usedModel }), '🎨 gen_image', { filePath: filePath });
195
+ return lines.join('\n');
196
+ }
197
+ catch (err) {
198
+ emitStatusToast('error', t('tools.image.error', { error: err.message?.substring(0, 60) }), '🎨 gen_image');
199
+ if (err.message?.includes('402') || err.message?.includes('Payment')) {
200
+ return t('tools.image.insufficient_funds', { model });
201
+ }
202
+ if (err.message?.includes('401') || err.message?.includes('403')) {
203
+ return t('tools.image.invalid_key');
204
+ }
205
+ if (err.message?.includes('400')) {
206
+ return t('tools.image.invalid_params', { error: err.message });
207
+ }
208
+ return t('tools.image.gen_error_msg', { error: err.message });
209
+ }
210
+ },
211
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * gen_music Tool - Pollinations Music Generation
3
+ *
4
+ * Updated: 2026-02-12 - Verified API Reference
5
+ *
6
+ * Model: elevenmusic (ElevenLabs Music)
7
+ * Endpoint: gen.pollinations.ai/audio/{text}
8
+ *
9
+ * Parameters:
10
+ * - duration: 3-300 seconds
11
+ * - instrumental: boolean (vocals or instrumental only)
12
+ */
13
+ import { type ToolDefinition } from '@opencode-ai/plugin/tool';
14
+ export declare const polliGenMusicTool: ToolDefinition;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * gen_music Tool - Pollinations Music Generation
3
+ *
4
+ * Updated: 2026-02-12 - Verified API Reference
5
+ *
6
+ * Model: elevenmusic (ElevenLabs Music)
7
+ * Endpoint: gen.pollinations.ai/audio/{text}
8
+ *
9
+ * Parameters:
10
+ * - duration: 3-300 seconds
11
+ * - instrumental: boolean (vocals or instrumental only)
12
+ */
13
+ import { tool } from '@opencode-ai/plugin/tool';
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import { getApiKey, httpsGet, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, estimateMusicCost, extractCostFromHeaders, isCostEstimatorEnabled, sanitizeFilename, } 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';
20
+ import { t } from '../../locales/index.js';
21
+ // ─── Constants ─────────────────────────────────────────────────────────────
22
+ const DEFAULT_DURATION = 10;
23
+ const MODEL_NAME = 'elevenmusic';
24
+ // ─── Tool Definition ──────────────────────────────────────────────────────
25
+ export const polliGenMusicTool = tool({
26
+ description: t('tools.polli_gen_music.desc'),
27
+ args: {
28
+ prompt: tool.schema.string().describe(t('tools.polli_gen_music.arg_prompt')),
29
+ duration: tool.schema.number().optional()
30
+ .describe(t('tools.polli_gen_music.arg_duration', { default: DEFAULT_DURATION, max: 300 })),
31
+ instrumental: tool.schema.boolean().optional().describe(t('tools.polli_gen_music.arg_instrumental')),
32
+ seed: tool.schema.number().optional().describe(t('tools.polli_gen_music.arg_seed')),
33
+ save_to: tool.schema.string().optional().describe(t('tools.polli_gen_music.arg_save_to')),
34
+ filename: tool.schema.string().optional().describe(t('tools.polli_gen_music.arg_filename')),
35
+ },
36
+ async execute(args, context) {
37
+ const apiKey = getApiKey();
38
+ if (!apiKey) {
39
+ return t('tools.polli_gen_music.req_key');
40
+ }
41
+ // Get dynamic range from ModelRegistry (populated via OpenAPI)
42
+ const { getMusicModel } = await import('./shared.js');
43
+ const modelConfig = getMusicModel()[MODEL_NAME];
44
+ const [minDuration, maxDuration] = modelConfig?.duration || [3, 300];
45
+ const duration = Math.min(Math.max(args.duration || DEFAULT_DURATION, minDuration), maxDuration);
46
+ const instrumental = args.instrumental || false;
47
+ // Estimate cost
48
+ const estimatedCost = estimateMusicCost(duration);
49
+ // Cost Guard check V2
50
+ const costCheck = checkCostControl('polli_gen_music', args, MODEL_NAME, estimatedCost, 'audio');
51
+ if (!costCheck.allowed) {
52
+ return costCheck.message || t('tools.polli_gen_music.blocked');
53
+ }
54
+ // Estimate generation time
55
+ const genTimeSeconds = Math.ceil(duration * 1.2);
56
+ // Emit start toast
57
+ const config = loadConfig();
58
+ const argsStr = config.gui?.logs === 'verbose' ? `\nParameters: ${JSON.stringify(args)}` : '';
59
+ emitStatusToast('info', t('tools.polli_gen_music.toast_start', { duration, time: genTimeSeconds }) + argsStr, '🎵 polli_gen_music');
60
+ // Metadata
61
+ context.metadata({ title: `🎵 Music: ${duration}s (~${genTimeSeconds}s gen time)` });
62
+ try {
63
+ // Build URL
64
+ const params = new URLSearchParams({
65
+ model: MODEL_NAME,
66
+ nologo: 'true',
67
+ private: 'true',
68
+ duration: String(duration),
69
+ });
70
+ if (instrumental) {
71
+ params.set('instrumental', 'true');
72
+ }
73
+ // Seed for reproducibility
74
+ if (args.seed !== undefined) {
75
+ params.set('seed', String(args.seed));
76
+ }
77
+ const promptEncoded = encodeURIComponent(args.prompt);
78
+ const url = `https://gen.pollinations.ai/audio/${promptEncoded}?${params}`;
79
+ const headers = {
80
+ 'Authorization': `Bearer ${apiKey}`,
81
+ };
82
+ // Music generation takes time
83
+ const result = await httpsGet(url, headers);
84
+ const audioData = result.data;
85
+ const responseHeaders = result.headers;
86
+ // Save audio
87
+ let outputDir = getDefaultOutputDir('music');
88
+ let filename = args.filename ? sanitizeFilename(args.filename) : undefined;
89
+ if (args.save_to) {
90
+ if (args.save_to.match(/\.(mp3|wav|ogg|m4a)$/i)) {
91
+ outputDir = path.dirname(args.save_to);
92
+ filename = path.basename(args.save_to);
93
+ }
94
+ else {
95
+ outputDir = args.save_to;
96
+ }
97
+ }
98
+ ensureDir(outputDir);
99
+ filename = filename || generateFilename('music', MODEL_NAME, 'mp3');
100
+ const filePath = path.join(outputDir, filename.endsWith('.mp3') ? filename : `${filename}.mp3`);
101
+ fs.writeFileSync(filePath, audioData);
102
+ const fileSize = fs.statSync(filePath).size;
103
+ let actualCost = estimatedCost;
104
+ if (responseHeaders) {
105
+ const costTracking = extractCostFromHeaders(responseHeaders);
106
+ if (costTracking.costUsd !== undefined)
107
+ actualCost = costTracking.costUsd;
108
+ }
109
+ // Build result
110
+ const lines = [];
111
+ // Inject costWarning at top if present
112
+ if (costCheck.message && !costCheck.allowed) {
113
+ lines.push(costCheck.message);
114
+ lines.push('');
115
+ }
116
+ lines.push(t('tools.polli_gen_music.res_title'));
117
+ lines.push(`━━━━━━━━━━━━━━━━━━`);
118
+ lines.push(t('tools.polli_gen_music.res_prompt', { prompt: args.prompt }));
119
+ lines.push(t('tools.polli_gen_music.res_duration', { duration }));
120
+ lines.push(t('tools.polli_gen_music.res_mode', { mode: instrumental ? t('tools.polli_gen_music.res_mode_inst') : t('tools.polli_gen_music.res_mode_vocal') }));
121
+ lines.push(t('tools.polli_gen_music.res_file', { path: filePath }));
122
+ lines.push(t('tools.polli_gen_music.res_size', { size: formatFileSize(fileSize) }));
123
+ // Cost info
124
+ if (isCostEstimatorEnabled()) {
125
+ if (isTokenBased('audio', MODEL_NAME)) {
126
+ const maxCost = estimatedCost * 3;
127
+ lines.push(t('tools.polli_gen_music.res_cost_tok', { cost: formatCost(actualCost), maxCost: formatCost(maxCost) }));
128
+ }
129
+ else {
130
+ lines.push(t('tools.polli_gen_music.res_cost', { cost: formatCost(actualCost) }));
131
+ }
132
+ }
133
+ if (responseHeaders['x-model-used']) {
134
+ lines.push(t('tools.polli_gen_music.res_model_used', { model: responseHeaders['x-model-used'] }));
135
+ }
136
+ if (responseHeaders['x-request-id']) {
137
+ lines.push(t('tools.polli_gen_music.res_request_id', { id: responseHeaders['x-request-id'] }));
138
+ }
139
+ // Emit success toast
140
+ emitStatusToast('success', t('tools.polli_gen_music.toast_success', { duration }), '🎵 gen_music');
141
+ return lines.join('\n');
142
+ }
143
+ catch (err) {
144
+ emitStatusToast('error', t('tools.polli_gen_music.toast_err', { error: err.message?.substring(0, 60) }), '🎵 gen_music');
145
+ if (err.message?.includes('402') || err.message?.includes('Payment')) {
146
+ return t('tools.polli_gen_music.err_pollen');
147
+ }
148
+ if (err.message?.includes('401') || err.message?.includes('403')) {
149
+ return t('tools.polli_gen_music.err_auth');
150
+ }
151
+ if (err.message?.includes('Timeout')) {
152
+ return t('tools.polli_gen_music.err_timeout');
153
+ }
154
+ return t('tools.polli_gen_music.err_gen', { error: err.message });
155
+ }
156
+ },
157
+ });
@@ -0,0 +1,16 @@
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 { type ToolDefinition } from '@opencode-ai/plugin/tool';
16
+ export declare const polliGenVideoTool: ToolDefinition;
@@ -0,0 +1,249 @@
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, sanitizeFilename, validateHttpUrl, } 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
+ import { t } from '../../locales/index.js';
23
+ // ─── Constants ─────────────────────────────────────────────────────────────
24
+ const CHEAPEST_MODEL = 'grok-video';
25
+ const DEFAULT_DURATION = 3;
26
+ const DEFAULT_ASPECT_RATIO = '16:9';
27
+ // ─── Tool Definition ──────────────────────────────────────────────────────
28
+ export const polliGenVideoTool = tool({
29
+ description: t('tools.polli_gen_video.desc'),
30
+ args: {
31
+ prompt: tool.schema.string().describe(t('tools.polli_gen_video.arg_prompt')),
32
+ model: tool.schema.string().describe(t('tools.polli_gen_video.arg_model', { model: CHEAPEST_MODEL })),
33
+ duration: tool.schema.number().optional().describe(t('tools.polli_gen_video.arg_duration')),
34
+ aspect_ratio: tool.schema.enum(['16:9', '9:16', '1:1', '4:3']).optional().describe(t('tools.polli_gen_video.arg_aspect')),
35
+ reference_image: tool.schema.string().optional().describe(t('tools.polli_gen_video.arg_ref')),
36
+ seed: tool.schema.number().optional().describe(t('tools.polli_gen_video.arg_seed')),
37
+ save_to: tool.schema.string().optional().describe(t('tools.polli_gen_video.arg_save_to')),
38
+ filename: tool.schema.string().optional().describe(t('tools.polli_gen_video.arg_filename')),
39
+ },
40
+ async execute(args, context) {
41
+ const apiKey = getApiKey();
42
+ if (!apiKey) {
43
+ return t('tools.polli_gen_video.req_key');
44
+ }
45
+ const model = args.model;
46
+ const aspectRatio = args.aspect_ratio || DEFAULT_ASPECT_RATIO;
47
+ // Get model config from dynamic registry
48
+ const videoModels = getVideoModels();
49
+ const modelConfig = videoModels[model];
50
+ const isBetaModel = !modelConfig;
51
+ if (isBetaModel) {
52
+ emitStatusToast('warning', t('tools.polli_gen_video.warn_beta', { model }), '🎬 gen_video');
53
+ }
54
+ // Validate duration (for known models; beta models use defaults)
55
+ const [minDuration, maxDuration] = isBetaModel ? [1, 20] : getDurationRange(model);
56
+ const duration = args.duration || Math.min(DEFAULT_DURATION, maxDuration);
57
+ if (duration < minDuration || duration > maxDuration) {
58
+ return t('tools.polli_gen_video.invalid_duration', { model, duration, min: minDuration, max: maxDuration });
59
+ }
60
+ // Validate aspect ratio (for known models; beta models accept any)
61
+ if (!isBetaModel && !validateAspectRatio(model, aspectRatio)) {
62
+ return t('tools.polli_gen_video.invalid_aspect', { model, aspect: aspectRatio, supported: modelConfig.aspectRatios.join(', ') });
63
+ }
64
+ // Check I2V requirements & validation
65
+ if (args.reference_image) {
66
+ const urls = args.reference_image.split(',').map(u => u.trim());
67
+ for (const u of urls) {
68
+ if (!validateHttpUrl(u)) {
69
+ return '❌ URL invalide. Utilisez http:// ou https://';
70
+ }
71
+ }
72
+ }
73
+ const requiresReferenceImage = !isBetaModel && requiresI2V(model);
74
+ const supportsReferenceImage = isBetaModel || supportsI2V(model);
75
+ if (requiresReferenceImage && !args.reference_image) {
76
+ return t('tools.polli_gen_video.req_i2v', { model });
77
+ }
78
+ if (args.reference_image && !supportsReferenceImage) {
79
+ const models = Object.entries(videoModels)
80
+ .filter(([, info]) => info.i2v)
81
+ .map(([name]) => name)
82
+ .join(', ');
83
+ return t('tools.polli_gen_video.no_i2v', { model, models });
84
+ }
85
+ // Estimate cost
86
+ const estimatedCost = estimateVideoCost(model, duration);
87
+ // Cost Guard check V2
88
+ const costCheck = checkCostControl('polli_gen_video', args, model, estimatedCost, 'video');
89
+ if (!costCheck.allowed) {
90
+ return costCheck.message || t('tools.polli_gen_video.blocked');
91
+ }
92
+ // Emit start toast
93
+ const config = loadConfig();
94
+ const argsStr = config.gui?.logs === 'verbose' ? `\nParameters: ${JSON.stringify(args)}` : '';
95
+ emitStatusToast('info', t('tools.polli_gen_video.toast_start', { model, duration }) + argsStr, '🎬 polli_gen_video');
96
+ // Metadata
97
+ context.metadata({ title: `🎬 Video: ${model}${isBetaModel ? ' (beta)' : ''} (${duration}s)` });
98
+ try {
99
+ // Build URL
100
+ const params = new URLSearchParams({
101
+ model: model,
102
+ nologo: 'true',
103
+ private: 'true',
104
+ });
105
+ // Duration parameter
106
+ params.set('duration', String(duration));
107
+ // Aspect ratio - convert to width/height for API
108
+ const aspectToSize = {
109
+ '16:9': { w: 1920, h: 1080 },
110
+ '9:16': { w: 1080, h: 1920 },
111
+ '1:1': { w: 1024, h: 1024 },
112
+ '4:3': { w: 1440, h: 1080 },
113
+ };
114
+ const size = aspectToSize[aspectRatio] || aspectToSize['16:9'];
115
+ params.set('width', String(size.w));
116
+ params.set('height', String(size.h));
117
+ // I2V: reference image(s)
118
+ if (args.reference_image) {
119
+ // Veo interpolation: comma-separated URLs
120
+ // Other I2V models: single URL
121
+ params.set('image', args.reference_image);
122
+ }
123
+ // Seed for reproducibility
124
+ if (args.seed !== undefined) {
125
+ params.set('seed', String(args.seed));
126
+ }
127
+ const promptEncoded = encodeURIComponent(args.prompt);
128
+ const url = `https://gen.pollinations.ai/image/${promptEncoded}?${params}`;
129
+ const headers = {
130
+ 'Authorization': `Bearer ${apiKey}`,
131
+ };
132
+ // 1. Fetch balance avant génération
133
+ const balBefore = await fetchEnterBalance();
134
+ // Video generation takes time (30-70 seconds depending on model)
135
+ const result = await httpsGet(url, headers);
136
+ const videoData = result.data;
137
+ const responseHeaders = result.headers;
138
+ // Save video
139
+ let outputDir = getDefaultOutputDir('videos');
140
+ let filename = args.filename ? sanitizeFilename(args.filename) : undefined;
141
+ if (args.save_to) {
142
+ if (args.save_to.match(/\.(mp4|webm|mov|avi)$/i)) {
143
+ outputDir = path.dirname(args.save_to);
144
+ filename = path.basename(args.save_to);
145
+ }
146
+ else {
147
+ outputDir = args.save_to;
148
+ }
149
+ }
150
+ ensureDir(outputDir);
151
+ filename = filename || generateFilename('video', model, 'mp4');
152
+ const filePath = path.join(outputDir, filename.endsWith('.mp4') ? filename : `${filename}.mp4`);
153
+ fs.writeFileSync(filePath, videoData);
154
+ const fileSize = fs.statSync(filePath).size;
155
+ // Extract actual cost from headers as fallback
156
+ const costTracking = extractCostFromHeaders(responseHeaders);
157
+ // 2. Fetch balance après génération (delay for API sync)
158
+ let balAfter = null;
159
+ let realCost;
160
+ if (balBefore !== null) {
161
+ await new Promise(r => setTimeout(r, 1000)); // Laisse le temps au ledger
162
+ balAfter = await fetchEnterBalance();
163
+ if (balAfter !== null) {
164
+ realCost = Math.round((balBefore - balAfter) * 10000) / 10000;
165
+ }
166
+ }
167
+ // Build result
168
+ const lines = [];
169
+ // Inject costWarning at top if present
170
+ if (costCheck.message && !costCheck.allowed) { // Assuming costWarning should come from costCheck if not allowed
171
+ lines.push(costCheck.message);
172
+ lines.push('');
173
+ }
174
+ lines.push(t('tools.polli_gen_video.res_title'));
175
+ lines.push(`━━━━━━━━━━━━━━━━━━`);
176
+ lines.push(t('tools.polli_gen_video.res_prompt', { prompt: args.prompt.substring(0, 80) + (args.prompt.length > 80 ? '...' : '') }));
177
+ lines.push(t('tools.polli_gen_video.res_model', { model: `${model}${isBetaModel ? ' (beta)' : ''}${modelConfig?.cost?.includes('💎') ? ' 💎' : ''}` }));
178
+ lines.push(t('tools.polli_gen_video.res_duration', { duration }));
179
+ lines.push(t('tools.polli_gen_video.res_aspect', { aspect: aspectRatio }));
180
+ // Add I2V info if used
181
+ if (args.reference_image) {
182
+ const isInterpolation = model === 'veo' && args.reference_image.includes(',');
183
+ lines.push(t('tools.polli_gen_video.res_i2v_mode', { mode: isInterpolation ? t('tools.polli_gen_video.res_i2v_interp') : t('tools.polli_gen_video.res_i2v_single') }));
184
+ lines.push(t('tools.polli_gen_video.res_source', { url: args.reference_image.substring(0, 50) + '...' }));
185
+ }
186
+ // Audio info (known models only)
187
+ if (modelConfig?.audio) {
188
+ lines.push(t('tools.polli_gen_video.res_audio_ok'));
189
+ }
190
+ else if (!isBetaModel) {
191
+ lines.push(t('tools.polli_gen_video.res_audio_no'));
192
+ }
193
+ lines.push(t('tools.polli_gen_video.res_file', { path: filePath }));
194
+ lines.push(t('tools.polli_gen_video.res_size', { size: formatFileSize(fileSize) }));
195
+ // Pricing details (Estimé vs Réel)
196
+ if (isCostEstimatorEnabled()) {
197
+ const maxCost = estimatedCost * 3;
198
+ lines.push(t('tools.polli_gen_video.res_cost_title'));
199
+ if (isTokenBased('video', model)) {
200
+ lines.push(t('tools.polli_gen_video.res_cost_est_tok', { cost: formatCost(estimatedCost), maxCost: formatCost(maxCost) }));
201
+ }
202
+ else {
203
+ lines.push(t('tools.polli_gen_video.res_cost_est', { cost: formatCost(estimatedCost) }));
204
+ }
205
+ if (realCost !== undefined) {
206
+ lines.push(t('tools.polli_gen_video.res_cost_real_wallet', { cost: formatCost(realCost) }));
207
+ }
208
+ else if (costTracking.costUsd !== undefined) {
209
+ lines.push(t('tools.polli_gen_video.res_cost_real_headers', { cost: formatCost(costTracking.costUsd) }));
210
+ }
211
+ else {
212
+ lines.push(t('tools.polli_gen_video.res_cost_real_unknown'));
213
+ }
214
+ }
215
+ if (responseHeaders['x-model-used']) {
216
+ lines.push(t('tools.polli_gen_video.res_model_used', { model: responseHeaders['x-model-used'] }));
217
+ }
218
+ if (responseHeaders['x-request-id']) {
219
+ lines.push(t('tools.polli_gen_video.res_request_id', { id: responseHeaders['x-request-id'] }));
220
+ }
221
+ // Gen time estimate (known models only)
222
+ if (modelConfig?.genTime) {
223
+ lines.push(t('tools.polli_gen_video.res_time', { time: modelConfig.genTime }));
224
+ }
225
+ // Emit success toast
226
+ emitStatusToast('success', t('tools.polli_gen_video.toast_success', { model, duration }), '🎬 gen_video');
227
+ return lines.join('\n');
228
+ }
229
+ catch (err) {
230
+ emitStatusToast('error', t('tools.polli_gen_video.toast_err', { error: err.message?.substring(0, 60) }), '🎬 gen_video');
231
+ if (err.message?.includes('402') || err.message?.includes('Payment')) {
232
+ return t('tools.polli_gen_video.err_pollen');
233
+ }
234
+ if (err.message?.includes('400')) {
235
+ if (requiresI2V(model) && !args.reference_image) {
236
+ return t('tools.polli_gen_video.err_i2v_req', { model });
237
+ }
238
+ return t('tools.polli_gen_video.err_invalid', { msg: err.message });
239
+ }
240
+ if (err.message?.includes('520') && model === 'ltx-2') {
241
+ return t('tools.polli_gen_video.err_520');
242
+ }
243
+ if (err.message?.includes('Timeout')) {
244
+ return t('tools.polli_gen_video.err_timeout');
245
+ }
246
+ return t('tools.polli_gen_video.err_gen', { error: err.message });
247
+ }
248
+ },
249
+ });
@@ -0,0 +1,2 @@
1
+ import { type ToolDefinition } from '@opencode-ai/plugin/tool';
2
+ export declare const polliConfigTool: ToolDefinition;