opencode-pollinations-plugin 6.1.0-beta.8 → 6.2.0

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 -293
  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 +10 -4
  108. package/dist/server/models-seed.d.ts +0 -18
  109. package/dist/server/models-seed.js +0 -55
@@ -0,0 +1,38 @@
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
+ export interface CostCheckResult {
10
+ allowed: boolean;
11
+ reason?: string;
12
+ confirmationRequired?: boolean;
13
+ pendingRequestId?: string;
14
+ message?: string;
15
+ }
16
+ export interface PendingRequest {
17
+ id: string;
18
+ toolName: string;
19
+ args: any;
20
+ estimatedCost: number;
21
+ model: string;
22
+ timestamp: number;
23
+ }
24
+ export declare function savePendingRequest(req: PendingRequest): void;
25
+ export declare function getPendingRequest(id: string): PendingRequest | null;
26
+ export declare function removePendingRequest(id: string): void;
27
+ export declare function isTokenBased(category: 'image' | 'video' | 'audio' | 'text', modelName: string): boolean;
28
+ /**
29
+ * Check if a generation should proceed based on cost control settings.
30
+ *
31
+ * @param toolName - Name of the tool calling the check (e.g. 'polli_gen_video')
32
+ * @param args - Original arguments passed to the tool
33
+ * @param modelName - The model being used
34
+ * @param estimatedCost - Estimated cost in Pollen
35
+ * @param category - The model category ('image' | 'video' | 'audio')
36
+ * @returns CostCheckResult
37
+ */
38
+ export declare function checkCostControl(toolName: string, args: any, modelName: string, estimatedCost: number, category?: 'image' | 'video' | 'audio'): CostCheckResult;
@@ -0,0 +1,136 @@
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 { t } from '../../locales/index.js';
13
+ import * as fs from 'fs';
14
+ import * as path from 'path';
15
+ import * as os from 'os';
16
+ // ─── Pending Requests Store ────────────────────────────────────────────────
17
+ const PENDING_STORE_PATH = path.join(os.homedir(), '.config', 'opencode', 'pollinations_pending_requests.json');
18
+ export function savePendingRequest(req) {
19
+ const dir = path.dirname(PENDING_STORE_PATH);
20
+ if (!fs.existsSync(dir))
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ let pending = {};
23
+ if (fs.existsSync(PENDING_STORE_PATH)) {
24
+ try {
25
+ pending = JSON.parse(fs.readFileSync(PENDING_STORE_PATH, 'utf-8'));
26
+ }
27
+ catch (e) { }
28
+ }
29
+ pending[req.id] = req;
30
+ fs.writeFileSync(PENDING_STORE_PATH, JSON.stringify(pending, null, 2));
31
+ }
32
+ export function getPendingRequest(id) {
33
+ if (!fs.existsSync(PENDING_STORE_PATH))
34
+ return null;
35
+ try {
36
+ const pending = JSON.parse(fs.readFileSync(PENDING_STORE_PATH, 'utf-8'));
37
+ return pending[id] || null;
38
+ }
39
+ catch (e) {
40
+ return null; // File corrupted or unreadable
41
+ }
42
+ }
43
+ export function removePendingRequest(id) {
44
+ if (!fs.existsSync(PENDING_STORE_PATH))
45
+ return;
46
+ try {
47
+ const pending = JSON.parse(fs.readFileSync(PENDING_STORE_PATH, 'utf-8'));
48
+ if (pending[id]) {
49
+ delete pending[id];
50
+ fs.writeFileSync(PENDING_STORE_PATH, JSON.stringify(pending, null, 2));
51
+ }
52
+ }
53
+ catch (e) { }
54
+ }
55
+ // ─── Main Function ───────────────────────────────────────────────────────
56
+ export function isTokenBased(category, modelName) {
57
+ const m = ModelRegistry.getByNameOrAlias(category, modelName);
58
+ return !!(m?.pricing && (m.pricing.completionImageTokens !== undefined ||
59
+ m.pricing.completionVideoTokens !== undefined ||
60
+ m.pricing.completionAudioTokens !== undefined ||
61
+ m.pricing.completionTextTokens !== undefined ||
62
+ m.pricing.promptTextTokens !== undefined ||
63
+ m.pricing.promptImageTokens !== undefined ||
64
+ m.pricing.promptAudioTokens !== undefined) && (m.pricing.completionVideoSeconds === undefined && m.pricing.completionAudioSeconds === undefined));
65
+ }
66
+ /**
67
+ * Check if a generation should proceed based on cost control settings.
68
+ *
69
+ * @param toolName - Name of the tool calling the check (e.g. 'polli_gen_video')
70
+ * @param args - Original arguments passed to the tool
71
+ * @param modelName - The model being used
72
+ * @param estimatedCost - Estimated cost in Pollen
73
+ * @param category - The model category ('image' | 'video' | 'audio')
74
+ * @returns CostCheckResult
75
+ */
76
+ export function checkCostControl(toolName, args, modelName, estimatedCost, category = 'image') {
77
+ const config = loadConfig();
78
+ const enablePaid = config.enablePaidTools !== false; // default true
79
+ const askConfirm = config.costConfirmationRequired === true; // default true
80
+ const costLimit = config.costThreshold ?? 0.0;
81
+ const m = ModelRegistry.getByNameOrAlias(category, modelName);
82
+ // Détection token-based (si le modèle a une de ces propriétés de tarification, il est variable)
83
+ const _isTokenBased = isTokenBased(category, modelName);
84
+ const maxCost = _isTokenBased ? estimatedCost * 3 : estimatedCost;
85
+ // ─── Bypass Check (For polli_gen_confirm) ────────────────
86
+ if (args && args[Symbol.for('polli_confirmed')]) {
87
+ return {
88
+ allowed: true,
89
+ message: _isTokenBased
90
+ ? t('cost_guard.cost_validated_max', { maxCost: formatCost(maxCost) })
91
+ : t('cost_guard.cost_validated', { cost: formatCost(estimatedCost) })
92
+ };
93
+ }
94
+ // ─── Rule 1: Wallet Protection (Hard Block) ────
95
+ if (!enablePaid) {
96
+ if (m?.paid_only) {
97
+ return {
98
+ allowed: false,
99
+ reason: 'paid_model_disabled',
100
+ message: t('cost_guard.wallet_protected', { modelName }),
101
+ };
102
+ }
103
+ // TODO: (Future) Add Check against FreeTier Quota empty API
104
+ }
105
+ // ─── Rule 2: Cost Confirmation (Suspend & Ticket) ─
106
+ if (askConfirm && maxCost > costLimit) {
107
+ const reqId = `req_${Math.random().toString(16).substring(2, 10)}`;
108
+ savePendingRequest({
109
+ id: reqId,
110
+ toolName,
111
+ args,
112
+ estimatedCost: maxCost,
113
+ model: modelName,
114
+ timestamp: Date.now()
115
+ });
116
+ const reasonText = _isTokenBased
117
+ ? t('cost_guard.reason_token', { cost: formatCost(estimatedCost), maxCost: formatCost(maxCost), limit: formatCost(costLimit) })
118
+ : t('cost_guard.reason_fixed', { cost: formatCost(estimatedCost), limit: formatCost(costLimit) });
119
+ return {
120
+ allowed: false, // NOT allowed to proceed automatically
121
+ confirmationRequired: true,
122
+ pendingRequestId: reqId,
123
+ reason: 'cost_exceeds_limit',
124
+ message: t('cost_guard.confirmation_req', { reasonText, reqId }),
125
+ };
126
+ }
127
+ // ─── All checks passed ────────────────────────────────────────────────
128
+ return {
129
+ allowed: true,
130
+ message: estimatedCost > 0
131
+ ? (_isTokenBased
132
+ ? t('cost_guard.estimated_max', { cost: formatCost(estimatedCost), maxCost: formatCost(maxCost) })
133
+ : t('cost_guard.estimated', { cost: formatCost(estimatedCost) }))
134
+ : undefined,
135
+ };
136
+ }
@@ -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,220 @@
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, sanitizeFilename, } 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
+ import { t } from '../../locales/index.js';
25
+ // ─── TTS Configuration ────────────────────────────────────────────────────
26
+ const OPENAI_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
27
+ const ELEVENLABS_VOICES = [
28
+ 'rachel', 'domi', 'bella', 'elli', 'charlotte', 'dorothy',
29
+ 'sarah', 'emily', 'lily', 'matilda',
30
+ 'adam', 'antoni', 'arnold', 'josh', 'sam', 'daniel',
31
+ 'charlie', 'james', 'fin', 'callum', 'liam', 'george', 'brian', 'bill',
32
+ 'ash', 'ballad', 'coral', 'sage', 'verse',
33
+ ];
34
+ const DEFAULT_VOICE = 'alloy';
35
+ const DEFAULT_MODEL = 'openai-audio'; // Changed: openai-audio is now default (least expensive)
36
+ const DEFAULT_FORMAT = 'mp3';
37
+ // ─── Tool Definition ──────────────────────────────────────────────────────
38
+ export const polliGenAudioTool = tool({
39
+ description: t('tools.polli_gen_audio.desc'),
40
+ args: {
41
+ text: tool.schema.string().describe(t('tools.polli_gen_audio.arg_text')),
42
+ voice: tool.schema.string().optional().describe(t('tools.polli_gen_audio.arg_voice', { voice: DEFAULT_VOICE })),
43
+ model: tool.schema.string().describe(t('tools.polli_gen_audio.arg_model', { model: DEFAULT_MODEL })),
44
+ format: tool.schema.enum(['mp3', 'wav', 'pcm16']).optional().describe(t('tools.polli_gen_audio.arg_format')),
45
+ save_to: tool.schema.string().optional().describe(t('tools.polli_gen_audio.arg_save_to')),
46
+ filename: tool.schema.string().optional().describe(t('tools.polli_gen_audio.arg_filename')),
47
+ },
48
+ async execute(args, context) {
49
+ const apiKey = getApiKey();
50
+ if (!apiKey) {
51
+ return t('tools.polli_gen_audio.req_key');
52
+ }
53
+ const text = args.text;
54
+ const model = args.model;
55
+ const voice = args.voice || DEFAULT_VOICE;
56
+ const format = args.format || DEFAULT_FORMAT;
57
+ // Validate model (unknown models accepted as beta)
58
+ const audioModels = getAudioModels();
59
+ const modelInfo = audioModels[model];
60
+ const isBetaModel = !modelInfo;
61
+ if (isBetaModel) {
62
+ emitStatusToast('warning', t('tools.polli_gen_audio.warn_beta', { model }), '🔊 gen_audio');
63
+ }
64
+ // Validate voice for selected model
65
+ if (model === 'openai-audio' && !OPENAI_VOICES.includes(voice)) {
66
+ return t('tools.polli_gen_audio.unsupported_openai', { voice, voices: OPENAI_VOICES.join(', ') });
67
+ }
68
+ if (model === 'elevenlabs' && !ELEVENLABS_VOICES.includes(voice)) {
69
+ return t('tools.polli_gen_audio.unsupported_elevenlabs', { voice, count: ELEVENLABS_VOICES.length });
70
+ }
71
+ // Estimate cost
72
+ const estimatedCost = estimateTtsCost(text.length);
73
+ // Cost Guard check V2
74
+ const costCheck = checkCostControl('polli_gen_audio', args, model, estimatedCost, 'audio');
75
+ if (!costCheck.allowed) {
76
+ return costCheck.message || t('tools.polli_gen_audio.blocked');
77
+ }
78
+ // Emit start toast
79
+ const config = loadConfig();
80
+ const argsStr = config.gui?.logs === 'verbose' ? `\nParameters: ${JSON.stringify(args)}` : '';
81
+ emitStatusToast('info', t('tools.polli_gen_audio.toast_start', { model, length: text.length }) + argsStr, '🔊 polli_gen_audio');
82
+ // Metadata
83
+ context.metadata({ title: `🔊 TTS: ${voice}${isBetaModel ? ' (beta)' : ''} (${text.length} chars)` });
84
+ try {
85
+ let audioData;
86
+ let responseHeaders = {};
87
+ let actualFormat = format;
88
+ if (model === 'openai-audio') {
89
+ // === OpenAI Audio: Use modalities endpoint ===
90
+ // POST /v1/chat/completions with audio modalities
91
+ const response = await httpsPost('https://gen.pollinations.ai/v1/chat/completions', {
92
+ model: 'openai-audio',
93
+ modalities: ['text', 'audio'],
94
+ audio: {
95
+ voice: voice,
96
+ format: format,
97
+ },
98
+ messages: [
99
+ {
100
+ role: 'user',
101
+ content: text
102
+ }
103
+ ],
104
+ }, {
105
+ 'Authorization': `Bearer ${apiKey}`,
106
+ });
107
+ const data = JSON.parse(response.data.toString());
108
+ // Extract audio from response
109
+ const audioBase64 = data.choices?.[0]?.message?.audio?.data;
110
+ if (!audioBase64) {
111
+ throw new Error('No audio data in response');
112
+ }
113
+ audioData = Buffer.from(audioBase64, 'base64');
114
+ responseHeaders = response.headers;
115
+ }
116
+ else if (model === 'elevenlabs') {
117
+ // === ElevenLabs: Use audio endpoint ===
118
+ // GET/POST /audio/{text}
119
+ const promptEncoded = encodeURIComponent(text);
120
+ const url = `https://gen.pollinations.ai/audio/${promptEncoded}?model=elevenlabs&voice=${voice}`;
121
+ // For elevenlabs, we might need a different approach
122
+ // Let's use POST with JSON body
123
+ const response = await httpsPost('https://gen.pollinations.ai/v1/audio/speech', {
124
+ model: 'elevenlabs',
125
+ input: text,
126
+ voice: voice,
127
+ }, {
128
+ 'Authorization': `Bearer ${apiKey}`,
129
+ });
130
+ // Check if response is JSON (error) or binary (audio)
131
+ const contentType = response.headers['content-type'] || '';
132
+ if (contentType.includes('application/json')) {
133
+ const data = JSON.parse(response.data.toString());
134
+ throw new Error(data.error?.message || 'Unknown error');
135
+ }
136
+ audioData = response.data;
137
+ responseHeaders = response.headers;
138
+ }
139
+ else {
140
+ // Fallback to OpenAI-compatible endpoint
141
+ const response = await httpsPost('https://gen.pollinations.ai/v1/audio/speech', {
142
+ model: model,
143
+ input: text,
144
+ voice: voice,
145
+ }, {
146
+ 'Authorization': `Bearer ${apiKey}`,
147
+ });
148
+ audioData = response.data;
149
+ responseHeaders = response.headers;
150
+ }
151
+ // Save audio
152
+ let outputDir = getDefaultOutputDir('audio');
153
+ let filename = args.filename ? sanitizeFilename(args.filename) : undefined;
154
+ if (args.save_to) {
155
+ if (args.save_to.match(/\.(mp3|wav|ogg|m4a)$/i)) {
156
+ outputDir = path.dirname(args.save_to);
157
+ filename = path.basename(args.save_to);
158
+ }
159
+ else {
160
+ outputDir = args.save_to;
161
+ }
162
+ }
163
+ ensureDir(outputDir);
164
+ filename = filename || generateFilename('tts', `${model}_${voice}`, actualFormat);
165
+ const filePath = path.join(outputDir, filename.endsWith(`.${actualFormat}`) ? filename : `${filename}.${actualFormat}`);
166
+ fs.writeFileSync(filePath, audioData);
167
+ const fileSize = fs.statSync(filePath).size;
168
+ // Estimate duration (approx 15 chars per second for speech)
169
+ const estimatedDuration = Math.ceil(text.length / 15);
170
+ let actualCost = estimatedCost;
171
+ if (responseHeaders) {
172
+ const costTracking = extractCostFromHeaders(responseHeaders);
173
+ if (costTracking.costUsd !== undefined)
174
+ actualCost = costTracking.costUsd;
175
+ }
176
+ // Build result
177
+ const lines = [];
178
+ // Inject costWarning at top if present
179
+ if (costCheck.message && !costCheck.allowed) {
180
+ lines.push(costCheck.message);
181
+ lines.push('');
182
+ }
183
+ lines.push(t('tools.polli_gen_audio.res_title'));
184
+ lines.push(`━━━━━━━━━━━━━━━━━━`);
185
+ lines.push(t('tools.polli_gen_audio.res_text', { text: `${text.substring(0, 60)}${text.length > 60 ? '...' : ''}` }));
186
+ lines.push(t('tools.polli_gen_audio.res_model', { model: `${model}${isBetaModel ? ' (beta)' : model === 'openai-audio' ? ' (recommandé)' : ''}` }));
187
+ lines.push(t('tools.polli_gen_audio.res_voice', { voice }));
188
+ lines.push(t('tools.polli_gen_audio.res_format', { format: actualFormat }));
189
+ lines.push(t('tools.polli_gen_audio.res_est_duration', { duration: estimatedDuration }));
190
+ lines.push(t('tools.polli_gen_audio.res_file', { path: filePath }));
191
+ lines.push(t('tools.polli_gen_audio.res_size', { size: formatFileSize(fileSize) }));
192
+ // Cost info
193
+ if (isCostEstimatorEnabled()) {
194
+ if (isTokenBased('audio', model)) {
195
+ const maxCost = estimatedCost * 3;
196
+ lines.push(t('tools.polli_gen_audio.res_cost_tok', { cost: formatCost(actualCost), maxCost: formatCost(maxCost) }));
197
+ }
198
+ else {
199
+ lines.push(t('tools.polli_gen_audio.res_cost', { cost: formatCost(actualCost) }));
200
+ }
201
+ }
202
+ if (responseHeaders['x-request-id']) {
203
+ lines.push(t('tools.polli_gen_audio.res_request_id', { id: responseHeaders['x-request-id'] }));
204
+ }
205
+ // Emit success toast
206
+ emitStatusToast('success', t('tools.polli_gen_audio.toast_success', { model, voice }), '🔊 gen_audio');
207
+ return lines.join('\n');
208
+ }
209
+ catch (err) {
210
+ emitStatusToast('error', t('tools.polli_gen_audio.toast_err', { error: err.message?.substring(0, 60) }), '🔊 gen_audio');
211
+ if (err.message?.includes('402') || err.message?.includes('Payment')) {
212
+ return t('tools.polli_gen_audio.err_pollen');
213
+ }
214
+ if (err.message?.includes('401') || err.message?.includes('403')) {
215
+ return t('tools.polli_gen_audio.err_auth');
216
+ }
217
+ return t('tools.polli_gen_audio.err_tts', { error: err.message });
218
+ }
219
+ },
220
+ });
@@ -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;