opencode-pollinations-plugin 6.1.0-beta.2 → 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 (96) hide show
  1. package/README.md +242 -62
  2. package/dist/index.js +68 -159
  3. package/dist/server/commands.d.ts +6 -0
  4. package/dist/server/commands.js +400 -71
  5. package/dist/server/config.d.ts +32 -23
  6. package/dist/server/config.js +183 -99
  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.d.ts +3 -30
  10. package/dist/server/generate-config.js +164 -106
  11. package/dist/server/index.d.ts +2 -1
  12. package/dist/server/index.js +124 -149
  13. package/dist/server/logger.d.ts +8 -0
  14. package/dist/server/logger.js +36 -0
  15. package/dist/server/models/cache.d.ts +35 -0
  16. package/dist/server/models/cache.js +160 -0
  17. package/dist/server/models/fetcher.d.ts +18 -0
  18. package/dist/server/models/fetcher.js +150 -0
  19. package/dist/server/models/index.d.ts +6 -0
  20. package/dist/server/models/index.js +5 -0
  21. package/dist/server/models/manual.d.ts +15 -0
  22. package/dist/server/models/manual.js +92 -0
  23. package/dist/server/models/types.d.ts +55 -0
  24. package/dist/server/models/types.js +7 -0
  25. package/dist/server/models/worker.d.ts +21 -0
  26. package/dist/server/models/worker.js +97 -0
  27. package/dist/server/pollinations-api.d.ts +11 -0
  28. package/dist/server/pollinations-api.js +21 -8
  29. package/dist/server/proxy.js +223 -160
  30. package/dist/server/quota.d.ts +2 -0
  31. package/dist/server/quota.js +89 -86
  32. package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
  33. package/dist/server/scripts/pollinations_pricing.js +246 -0
  34. package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
  35. package/dist/server/scripts/test_cost_endpoints.js +61 -0
  36. package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
  37. package/dist/server/scripts/test_dynamic_pricing.js +39 -0
  38. package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
  39. package/dist/server/scripts/test_freetier_audit.js +215 -0
  40. package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
  41. package/dist/server/scripts/test_parallel_cost.js +104 -0
  42. package/dist/server/toast.d.ts +7 -1
  43. package/dist/server/toast.js +43 -10
  44. package/dist/tools/design/gen_diagram.d.ts +2 -0
  45. package/dist/tools/design/gen_diagram.js +94 -0
  46. package/dist/tools/design/gen_palette.d.ts +2 -0
  47. package/dist/tools/design/gen_palette.js +182 -0
  48. package/dist/tools/design/gen_qrcode.d.ts +2 -0
  49. package/dist/tools/design/gen_qrcode.js +50 -0
  50. package/dist/tools/ffmpeg.d.ts +24 -0
  51. package/dist/tools/ffmpeg.js +54 -0
  52. package/dist/tools/index.d.ts +24 -0
  53. package/dist/tools/index.js +83 -0
  54. package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
  55. package/dist/tools/pollinations/beta_discovery.js +197 -0
  56. package/dist/tools/pollinations/cost-guard.d.ts +38 -0
  57. package/dist/tools/pollinations/cost-guard.js +141 -0
  58. package/dist/tools/pollinations/deepsearch.d.ts +7 -0
  59. package/dist/tools/pollinations/deepsearch.js +80 -0
  60. package/dist/tools/pollinations/gen_audio.d.ts +18 -0
  61. package/dist/tools/pollinations/gen_audio.js +246 -0
  62. package/dist/tools/pollinations/gen_image.d.ts +11 -0
  63. package/dist/tools/pollinations/gen_image.js +225 -0
  64. package/dist/tools/pollinations/gen_music.d.ts +14 -0
  65. package/dist/tools/pollinations/gen_music.js +180 -0
  66. package/dist/tools/pollinations/gen_video.d.ts +16 -0
  67. package/dist/tools/pollinations/gen_video.js +256 -0
  68. package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
  69. package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
  70. package/dist/tools/pollinations/polli_status.d.ts +2 -0
  71. package/dist/tools/pollinations/polli_status.js +31 -0
  72. package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
  73. package/dist/tools/pollinations/polli_web_search.js +164 -0
  74. package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
  75. package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
  76. package/dist/tools/pollinations/shared.d.ts +165 -0
  77. package/dist/tools/pollinations/shared.js +665 -0
  78. package/dist/tools/pollinations/test_estimators.d.ts +1 -0
  79. package/dist/tools/pollinations/test_estimators.js +22 -0
  80. package/dist/tools/pollinations/transcribe_audio.d.ts +13 -0
  81. package/dist/tools/pollinations/transcribe_audio.js +194 -0
  82. package/dist/tools/power/extract_audio.d.ts +2 -0
  83. package/dist/tools/power/extract_audio.js +179 -0
  84. package/dist/tools/power/extract_frames.d.ts +2 -0
  85. package/dist/tools/power/extract_frames.js +237 -0
  86. package/dist/tools/power/file_to_url.d.ts +2 -0
  87. package/dist/tools/power/file_to_url.js +217 -0
  88. package/dist/tools/power/remove_background.d.ts +2 -0
  89. package/dist/tools/power/remove_background.js +366 -0
  90. package/dist/tools/power/rmbg_keys.d.ts +2 -0
  91. package/dist/tools/power/rmbg_keys.js +79 -0
  92. package/dist/tools/shared.d.ts +30 -0
  93. package/dist/tools/shared.js +80 -0
  94. package/package.json +10 -4
  95. package/dist/server/models-seed.d.ts +0 -18
  96. package/dist/server/models-seed.js +0 -55
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Cost Guard V2 — Wallet protection and Cost Confirmation System
3
+ *
4
+ * Sprint 6: Refactored Cost Control based on user directives.
5
+ *
6
+ * Rule 1 (enablePaidTools): Hard block for paid models if disabled.
7
+ * Rule 2 (costConfirmationRequired): Suspends execution if cost > threshold, returns an ID.
8
+ */
9
+ import { loadConfig } from '../../server/config.js';
10
+ import { ModelRegistry } from '../../server/models/index.js';
11
+ import { formatCost } from './shared.js';
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import * as os from 'os';
15
+ // ─── Pending Requests Store ────────────────────────────────────────────────
16
+ const PENDING_STORE_PATH = path.join(os.homedir(), '.config', 'opencode', 'pollinations_pending_requests.json');
17
+ export function savePendingRequest(req) {
18
+ const dir = path.dirname(PENDING_STORE_PATH);
19
+ if (!fs.existsSync(dir))
20
+ fs.mkdirSync(dir, { recursive: true });
21
+ let pending = {};
22
+ if (fs.existsSync(PENDING_STORE_PATH)) {
23
+ try {
24
+ pending = JSON.parse(fs.readFileSync(PENDING_STORE_PATH, 'utf-8'));
25
+ }
26
+ catch (e) { }
27
+ }
28
+ pending[req.id] = req;
29
+ fs.writeFileSync(PENDING_STORE_PATH, JSON.stringify(pending, null, 2));
30
+ }
31
+ export function getPendingRequest(id) {
32
+ if (!fs.existsSync(PENDING_STORE_PATH))
33
+ return null;
34
+ try {
35
+ const pending = JSON.parse(fs.readFileSync(PENDING_STORE_PATH, 'utf-8'));
36
+ return pending[id] || null;
37
+ }
38
+ catch (e) {
39
+ return null; // File corrupted or unreadable
40
+ }
41
+ }
42
+ export function removePendingRequest(id) {
43
+ if (!fs.existsSync(PENDING_STORE_PATH))
44
+ return;
45
+ try {
46
+ const pending = JSON.parse(fs.readFileSync(PENDING_STORE_PATH, 'utf-8'));
47
+ if (pending[id]) {
48
+ delete pending[id];
49
+ fs.writeFileSync(PENDING_STORE_PATH, JSON.stringify(pending, null, 2));
50
+ }
51
+ }
52
+ catch (e) { }
53
+ }
54
+ // ─── Main Function ───────────────────────────────────────────────────────
55
+ export function isTokenBased(category, modelName) {
56
+ const m = ModelRegistry.getByNameOrAlias(category, modelName);
57
+ return !!(m?.pricing && (m.pricing.completionImageTokens !== undefined ||
58
+ m.pricing.completionVideoTokens !== undefined ||
59
+ m.pricing.completionAudioTokens !== undefined ||
60
+ m.pricing.completionTextTokens !== undefined ||
61
+ m.pricing.promptTextTokens !== undefined ||
62
+ m.pricing.promptImageTokens !== undefined ||
63
+ m.pricing.promptAudioTokens !== undefined) && (m.pricing.completionVideoSeconds === undefined && m.pricing.completionAudioSeconds === undefined));
64
+ }
65
+ /**
66
+ * Check if a generation should proceed based on cost control settings.
67
+ *
68
+ * @param toolName - Name of the tool calling the check (e.g. 'polli_gen_video')
69
+ * @param args - Original arguments passed to the tool
70
+ * @param modelName - The model being used
71
+ * @param estimatedCost - Estimated cost in Pollen
72
+ * @param category - The model category ('image' | 'video' | 'audio')
73
+ * @returns CostCheckResult
74
+ */
75
+ export function checkCostControl(toolName, args, modelName, estimatedCost, category = 'image') {
76
+ const config = loadConfig();
77
+ const enablePaid = config.enablePaidTools !== false; // default true
78
+ const askConfirm = config.costConfirmationRequired === true; // default true
79
+ const costLimit = config.costThreshold ?? 0.0;
80
+ const m = ModelRegistry.getByNameOrAlias(category, modelName);
81
+ // Détection token-based (si le modèle a une de ces propriétés de tarification, il est variable)
82
+ const _isTokenBased = isTokenBased(category, modelName);
83
+ const maxCost = _isTokenBased ? estimatedCost * 3 : estimatedCost;
84
+ // ─── Bypass Check (For polli_gen_confirm) ────────────────
85
+ if (args && args[Symbol.for('polli_confirmed')]) {
86
+ return {
87
+ allowed: true,
88
+ message: _isTokenBased
89
+ ? `💰 Coût validé (Max théorique: ${formatCost(maxCost)})`
90
+ : `💰 Coût validé: ${formatCost(estimatedCost)}`
91
+ };
92
+ }
93
+ // ─── Rule 1: Wallet Protection (Hard Block) ────
94
+ if (!enablePaid) {
95
+ if (m?.paid_only) {
96
+ return {
97
+ allowed: false,
98
+ reason: 'paid_model_disabled',
99
+ message: `❌ **Wallet Protégé**
100
+ Modèle: ${modelName} (payant)
101
+ enablePaidTools: désactivé
102
+ Résultat: REJETÉ, demandez à l'utilisateur d'activer le mode enablePaidTools via la commande pollinations appropriée si vous voulez utiliser ce modèle.`,
103
+ };
104
+ }
105
+ // TODO: (Future) Add Check against FreeTier Quota empty API
106
+ }
107
+ // ─── Rule 2: Cost Confirmation (Suspend & Ticket) ─
108
+ if (askConfirm && maxCost > costLimit) {
109
+ const reqId = `req_${Math.random().toString(16).substring(2, 10)}`;
110
+ savePendingRequest({
111
+ id: reqId,
112
+ toolName,
113
+ args,
114
+ estimatedCost: maxCost,
115
+ model: modelName,
116
+ timestamp: Date.now()
117
+ });
118
+ return {
119
+ allowed: false, // NOT allowed to proceed automatically
120
+ confirmationRequired: true,
121
+ pendingRequestId: reqId,
122
+ reason: 'cost_exceeds_limit',
123
+ message: `⚠️ **Confirmation de Coût Requise**
124
+ ${_isTokenBased
125
+ ? `Le coût estimé moyen est de ${formatCost(estimatedCost)} cependant ce modèle token-based peut vous coûter jusqu'à ${formatCost(maxCost)} ce qui dépasse le seuil défini (${formatCost(costLimit)}).`
126
+ : `Le coût estimé de cette action (${formatCost(estimatedCost)}) dépasse le seuil défini (${formatCost(costLimit)}).`}
127
+ 💳 **Pour valider cette transaction et exécuter la requête**,
128
+ Présentez le cout à l'utilisateur et demandez explicitement sa validation !!!
129
+ (S'il valide, appelez l'outil \`polli_gen_confirm\` avec l'ID : \`${reqId}\` et l'action \`confirm\`. S'il refuse, appelez l'outil avec l'action \`cancel\` pour purger la requête).`,
130
+ };
131
+ }
132
+ // ─── All checks passed ────────────────────────────────────────────────
133
+ return {
134
+ allowed: true,
135
+ message: estimatedCost > 0
136
+ ? (_isTokenBased
137
+ ? `💰 Coût estimé moyen: ${formatCost(estimatedCost)} (Max: ${formatCost(maxCost)})`
138
+ : `💰 Coût estimé: ${formatCost(estimatedCost)}`)
139
+ : undefined,
140
+ };
141
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * deepsearch Tool - Deep Research with AI
3
+ *
4
+ * Uses perplexity-reasoning for in-depth research and analysis
5
+ */
6
+ import { type ToolDefinition } from '@opencode-ai/plugin/tool';
7
+ export declare const deepsearchTool: ToolDefinition;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * deepsearch Tool - Deep Research with AI
3
+ *
4
+ * Uses perplexity-reasoning for in-depth research and analysis
5
+ */
6
+ import { tool } from '@opencode-ai/plugin/tool';
7
+ import { getApiKey, httpsPost, } from './shared.js';
8
+ // ─── Tool Definition ──────────────────────────────────────────────────────
9
+ export const deepsearchTool = tool({
10
+ description: `Perform deep research and analysis on a topic using AI reasoning.
11
+
12
+ **Model:** perplexity-reasoning
13
+
14
+ This tool provides comprehensive research with:
15
+ - Multi-step reasoning
16
+ - Source citations
17
+ - In-depth analysis
18
+ - Fact verification
19
+
20
+ **Use for:**
21
+ - Complex research questions
22
+ - Technical analysis
23
+ - Fact-checking
24
+ - Comparative studies
25
+
26
+ **Cost:** ~0.000002-0.000008 🌻 per token (very affordable)`,
27
+ args: {
28
+ query: tool.schema.string().describe('Research query or question to investigate'),
29
+ depth: tool.schema.enum(['quick', 'standard', 'thorough']).optional()
30
+ .describe('Research depth (default: standard)'),
31
+ },
32
+ async execute(args, context) {
33
+ const apiKey = getApiKey();
34
+ if (!apiKey) {
35
+ return `❌ Deep Search nécessite une clé API Pollinations.
36
+ 🔧 Connectez votre clé avec /pollinations connect`;
37
+ }
38
+ const model = 'perplexity-reasoning';
39
+ const depth = args.depth || 'standard';
40
+ // Metadata
41
+ context.metadata({ title: `🔍 Deep Search: ${args.query.substring(0, 50)}...` });
42
+ try {
43
+ // Build system prompt based on depth
44
+ const systemPrompts = {
45
+ quick: 'Provide a concise but thorough answer with key sources. Be efficient.',
46
+ standard: 'Provide comprehensive research with analysis, sources, and reasoning steps.',
47
+ thorough: 'Provide exhaustive research with multiple perspectives, detailed analysis, all relevant sources, and thorough fact-checking. Consider edge cases and alternative viewpoints.',
48
+ };
49
+ const { data } = await httpsPost('https://gen.pollinations.ai/v1/chat/completions', {
50
+ model: model,
51
+ messages: [
52
+ { role: 'system', content: systemPrompts[depth] },
53
+ { role: 'user', content: args.query },
54
+ ],
55
+ max_tokens: depth === 'thorough' ? 8000 : depth === 'standard' ? 4000 : 2000,
56
+ }, {
57
+ 'Authorization': `Bearer ${apiKey}`,
58
+ });
59
+ const jsonData = JSON.parse(data.toString());
60
+ const content = jsonData.choices?.[0]?.message?.content || 'No response';
61
+ // Format result
62
+ const lines = [
63
+ `🔍 Deep Search Results`,
64
+ `━━━━━━━━━━━━━━━━━━`,
65
+ `Query: ${args.query}`,
66
+ `Depth: ${depth}`,
67
+ `Model: ${model}`,
68
+ ``,
69
+ content,
70
+ ];
71
+ return lines.join('\n');
72
+ }
73
+ catch (err) {
74
+ if (err.message?.includes('402') || err.message?.includes('Payment')) {
75
+ return `❌ Crédits insuffisants.`;
76
+ }
77
+ return `❌ Erreur Deep Search: ${err.message}`;
78
+ }
79
+ },
80
+ });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * gen_audio Tool - Pollinations Text-to-Speech
3
+ *
4
+ * Updated: 2026-02-12 - Verified API Reference
5
+ *
6
+ * Two TTS options:
7
+ * 1. openai-audio (DEFAULT): GPT-4o Audio Preview - uses /v1/chat/completions with modalities
8
+ * - Supports both TTS and STT (Speech-to-Text)
9
+ * - Least expensive option
10
+ * - Voices: alloy, echo, fable, onyx, nova, shimmer
11
+ * - Formats: mp3, wav, pcm16
12
+ *
13
+ * 2. elevenlabs: ElevenLabs v3 TTS - uses /audio/{text}
14
+ * - 34 expressive voices
15
+ * - Higher quality but more expensive
16
+ */
17
+ import { type ToolDefinition } from '@opencode-ai/plugin/tool';
18
+ export declare const polliGenAudioTool: ToolDefinition;
@@ -0,0 +1,246 @@
1
+ /**
2
+ * gen_audio Tool - Pollinations Text-to-Speech
3
+ *
4
+ * Updated: 2026-02-12 - Verified API Reference
5
+ *
6
+ * Two TTS options:
7
+ * 1. openai-audio (DEFAULT): GPT-4o Audio Preview - uses /v1/chat/completions with modalities
8
+ * - Supports both TTS and STT (Speech-to-Text)
9
+ * - Least expensive option
10
+ * - Voices: alloy, echo, fable, onyx, nova, shimmer
11
+ * - Formats: mp3, wav, pcm16
12
+ *
13
+ * 2. elevenlabs: ElevenLabs v3 TTS - uses /audio/{text}
14
+ * - 34 expressive voices
15
+ * - Higher quality but more expensive
16
+ */
17
+ import { tool } from '@opencode-ai/plugin/tool';
18
+ import * as fs from 'fs';
19
+ import * as path from 'path';
20
+ import { getApiKey, httpsPost, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, estimateTtsCost, extractCostFromHeaders, isCostEstimatorEnabled, getAudioModels, } from './shared.js';
21
+ import { loadConfig } from '../../server/config.js';
22
+ import { checkCostControl, isTokenBased } from './cost-guard.js';
23
+ import { emitStatusToast } from '../../server/toast.js';
24
+ // ─── TTS Configuration ────────────────────────────────────────────────────
25
+ const OPENAI_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
26
+ const ELEVENLABS_VOICES = [
27
+ 'rachel', 'domi', 'bella', 'elli', 'charlotte', 'dorothy',
28
+ 'sarah', 'emily', 'lily', 'matilda',
29
+ 'adam', 'antoni', 'arnold', 'josh', 'sam', 'daniel',
30
+ 'charlie', 'james', 'fin', 'callum', 'liam', 'george', 'brian', 'bill',
31
+ 'ash', 'ballad', 'coral', 'sage', 'verse',
32
+ ];
33
+ const DEFAULT_VOICE = 'alloy';
34
+ const DEFAULT_MODEL = 'openai-audio'; // Changed: openai-audio is now default (least expensive)
35
+ const DEFAULT_FORMAT = 'mp3';
36
+ // ─── Tool Definition ──────────────────────────────────────────────────────
37
+ export const polliGenAudioTool = tool({
38
+ description: `Convert text to speech using Pollinations AI.
39
+
40
+ **🔊 Models:**
41
+
42
+ | Model | Type | Voices | Format | Cost | Notes |
43
+ |-------|------|--------|--------|------|-------|
44
+ | openai-audio | TTS + STT | 6 | mp3, wav, pcm16 | Lowest | **DEFAULT** - GPT-4o Audio |
45
+ | elevenlabs | TTS | 34 | mp3, wav, etc. | Higher | Expressive voices |
46
+
47
+ **🎵 OpenAI Audio (Default, Recommended):**
48
+ - Voices: \`alloy\`, \`echo\`, \`fable\`, \`onyx\`, \`nova\`, \`shimmer\`
49
+ - Formats: \`mp3\` (default), \`wav\`, \`pcm16\`
50
+ - Uses GPT-4o Audio Preview modalities endpoint
51
+ - Lowest cost option
52
+
53
+ **🎤 ElevenLabs:**
54
+ - 34 expressive voices including: rachel, domi, bella, adam, etc.
55
+ - Higher quality natural-sounding speech
56
+ - More expensive but more expressive
57
+
58
+ **💡 Tips:**
59
+ - Use \`openai-audio\` for cost-effective TTS
60
+ - Use \`elevenlabs\` for more expressive/character voices
61
+ - For STT (transcription), use the \`transcribe_audio\` tool`,
62
+ args: {
63
+ text: tool.schema.string().describe('Text to convert to speech'),
64
+ voice: tool.schema.string().optional().describe(`Voice to use (default: ${DEFAULT_VOICE})`),
65
+ model: tool.schema.string().optional().describe(`TTS model (default: ${DEFAULT_MODEL})`),
66
+ format: tool.schema.enum(['mp3', 'wav', 'pcm16']).optional().describe('Audio format (default: mp3, openai-audio only)'),
67
+ save_to: tool.schema.string().optional().describe('Custom output directory'),
68
+ filename: tool.schema.string().optional().describe('Custom filename (without extension)'),
69
+ },
70
+ async execute(args, context) {
71
+ const apiKey = getApiKey();
72
+ if (!apiKey) {
73
+ return `❌ Le TTS nécessite une clé API Pollinations.
74
+ 🔧 Connectez votre clé avec /pollinations connect`;
75
+ }
76
+ const text = args.text;
77
+ const model = args.model || DEFAULT_MODEL;
78
+ const voice = args.voice || DEFAULT_VOICE;
79
+ const format = args.format || DEFAULT_FORMAT;
80
+ // Validate model (unknown models accepted as beta)
81
+ const audioModels = getAudioModels();
82
+ const modelInfo = audioModels[model];
83
+ const isBetaModel = !modelInfo;
84
+ if (isBetaModel) {
85
+ emitStatusToast('warning', `Modèle "${model}" non référencé — mode (beta)`, '🔊 gen_audio');
86
+ }
87
+ // Validate voice for selected model
88
+ if (model === 'openai-audio' && !OPENAI_VOICES.includes(voice)) {
89
+ return `⚠️ Voix "${voice}" non supportée par openai-audio.
90
+ 💡 Voix OpenAI: ${OPENAI_VOICES.join(', ')}`;
91
+ }
92
+ if (model === 'elevenlabs' && !ELEVENLABS_VOICES.includes(voice)) {
93
+ return `⚠️ Voix "${voice}" non reconnue pour elevenlabs.
94
+ 💡 Voix ElevenLabs populaires: rachel, domi, bella, adam, josh...
95
+ 📋 Total: ${ELEVENLABS_VOICES.length} voix disponibles`;
96
+ }
97
+ // Estimate cost
98
+ const estimatedCost = estimateTtsCost(text.length);
99
+ // Cost Guard check V2
100
+ const costCheck = checkCostControl('polli_gen_audio', args, model, estimatedCost, 'audio');
101
+ if (!costCheck.allowed) {
102
+ return costCheck.message || '❌ Opération bloquée par le Cost Guard.';
103
+ }
104
+ // Emit start toast
105
+ const config = loadConfig();
106
+ const argsStr = config.gui?.logs === 'verbose' ? `\nParameters: ${JSON.stringify(args)}` : '';
107
+ emitStatusToast('info', `Génération audio: ${model} (${text.length} chars)${argsStr}`, '🔊 polli_gen_audio');
108
+ // Metadata
109
+ context.metadata({ title: `🔊 TTS: ${voice}${isBetaModel ? ' (beta)' : ''} (${text.length} chars)` });
110
+ try {
111
+ let audioData;
112
+ let responseHeaders = {};
113
+ let actualFormat = format;
114
+ if (model === 'openai-audio') {
115
+ // === OpenAI Audio: Use modalities endpoint ===
116
+ // POST /v1/chat/completions with audio modalities
117
+ const response = await httpsPost('https://gen.pollinations.ai/v1/chat/completions', {
118
+ model: 'openai-audio',
119
+ modalities: ['text', 'audio'],
120
+ audio: {
121
+ voice: voice,
122
+ format: format,
123
+ },
124
+ messages: [
125
+ {
126
+ role: 'user',
127
+ content: text
128
+ }
129
+ ],
130
+ }, {
131
+ 'Authorization': `Bearer ${apiKey}`,
132
+ });
133
+ const data = JSON.parse(response.data.toString());
134
+ // Extract audio from response
135
+ const audioBase64 = data.choices?.[0]?.message?.audio?.data;
136
+ if (!audioBase64) {
137
+ throw new Error('No audio data in response');
138
+ }
139
+ audioData = Buffer.from(audioBase64, 'base64');
140
+ responseHeaders = response.headers;
141
+ }
142
+ else if (model === 'elevenlabs') {
143
+ // === ElevenLabs: Use audio endpoint ===
144
+ // GET/POST /audio/{text}
145
+ const promptEncoded = encodeURIComponent(text);
146
+ const url = `https://gen.pollinations.ai/audio/${promptEncoded}?model=elevenlabs&voice=${voice}`;
147
+ // For elevenlabs, we might need a different approach
148
+ // Let's use POST with JSON body
149
+ const response = await httpsPost('https://gen.pollinations.ai/v1/audio/speech', {
150
+ model: 'elevenlabs',
151
+ input: text,
152
+ voice: voice,
153
+ }, {
154
+ 'Authorization': `Bearer ${apiKey}`,
155
+ });
156
+ // Check if response is JSON (error) or binary (audio)
157
+ const contentType = response.headers['content-type'] || '';
158
+ if (contentType.includes('application/json')) {
159
+ const data = JSON.parse(response.data.toString());
160
+ throw new Error(data.error?.message || 'Unknown error');
161
+ }
162
+ audioData = response.data;
163
+ responseHeaders = response.headers;
164
+ }
165
+ else {
166
+ // Fallback to OpenAI-compatible endpoint
167
+ const response = await httpsPost('https://gen.pollinations.ai/v1/audio/speech', {
168
+ model: model,
169
+ input: text,
170
+ voice: voice,
171
+ }, {
172
+ 'Authorization': `Bearer ${apiKey}`,
173
+ });
174
+ audioData = response.data;
175
+ responseHeaders = response.headers;
176
+ }
177
+ // Save audio
178
+ let outputDir = getDefaultOutputDir('audio');
179
+ let filename = args.filename;
180
+ if (args.save_to) {
181
+ if (args.save_to.match(/\.(mp3|wav|ogg|m4a)$/i)) {
182
+ outputDir = path.dirname(args.save_to);
183
+ filename = path.basename(args.save_to);
184
+ }
185
+ else {
186
+ outputDir = args.save_to;
187
+ }
188
+ }
189
+ ensureDir(outputDir);
190
+ filename = filename || generateFilename('tts', `${model}_${voice}`, actualFormat);
191
+ const filePath = path.join(outputDir, filename.endsWith(`.${actualFormat}`) ? filename : `${filename}.${actualFormat}`);
192
+ fs.writeFileSync(filePath, audioData);
193
+ const fileSize = fs.statSync(filePath).size;
194
+ // Estimate duration (approx 15 chars per second for speech)
195
+ const estimatedDuration = Math.ceil(text.length / 15);
196
+ let actualCost = estimatedCost;
197
+ if (responseHeaders) {
198
+ const costTracking = extractCostFromHeaders(responseHeaders);
199
+ if (costTracking.costUsd !== undefined)
200
+ actualCost = costTracking.costUsd;
201
+ }
202
+ // Build result
203
+ const lines = [];
204
+ // Inject costWarning at top if present
205
+ if (costCheck.message && !costCheck.allowed) {
206
+ lines.push(costCheck.message);
207
+ lines.push('');
208
+ }
209
+ lines.push(`🔊 Audio Généré (TTS)`);
210
+ lines.push(`━━━━━━━━━━━━━━━━━━`);
211
+ lines.push(`Texte: "${text.substring(0, 60)}${text.length > 60 ? '...' : ''}"`);
212
+ lines.push(`Modèle: ${model}${isBetaModel ? ' (beta)' : model === 'openai-audio' ? ' (recommandé)' : ''}`);
213
+ lines.push(`Voix: ${voice}`);
214
+ lines.push(`Format: ${actualFormat}`);
215
+ lines.push(`Durée estimée: ~${estimatedDuration}s`);
216
+ lines.push(`Fichier: ${filePath}`);
217
+ lines.push(`Taille: ${formatFileSize(fileSize)}`);
218
+ // Cost info
219
+ if (isCostEstimatorEnabled()) {
220
+ if (isTokenBased('audio', model)) {
221
+ const maxCost = estimatedCost * 3;
222
+ lines.push(`Coût: ${formatCost(actualCost)} (Max théorique: ${formatCost(maxCost)})`);
223
+ }
224
+ else {
225
+ lines.push(`Coût: ${formatCost(actualCost)}`);
226
+ }
227
+ }
228
+ if (responseHeaders['x-request-id']) {
229
+ lines.push(`Request ID: ${responseHeaders['x-request-id']}`);
230
+ }
231
+ // Emit success toast
232
+ emitStatusToast('success', `Audio généré ✓ (${model}, ${voice})`, '🔊 gen_audio');
233
+ return lines.join('\n');
234
+ }
235
+ catch (err) {
236
+ emitStatusToast('error', `Erreur: ${err.message?.substring(0, 60)}`, '🔊 gen_audio');
237
+ if (err.message?.includes('402') || err.message?.includes('Payment')) {
238
+ return `❌ Crédits pollen insuffisants.`;
239
+ }
240
+ if (err.message?.includes('401') || err.message?.includes('403')) {
241
+ return `❌ Clé API invalide ou non autorisée.`;
242
+ }
243
+ return `❌ Erreur TTS: ${err.message}`;
244
+ }
245
+ },
246
+ });
@@ -0,0 +1,11 @@
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 { type ToolDefinition } from '@opencode-ai/plugin/tool';
11
+ export declare const polliGenImageTool: ToolDefinition;