opencode-pollinations-plugin 6.1.0-beta.9 → 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.
- package/README.de.md +130 -0
- package/README.es.md +130 -0
- package/README.fr.md +130 -0
- package/README.it.md +130 -0
- package/README.md +87 -73
- package/dist/index.js +52 -161
- package/dist/locales/de.json +374 -0
- package/dist/locales/en.json +373 -0
- package/dist/locales/es.json +374 -0
- package/dist/locales/fr.json +373 -0
- package/dist/locales/index.d.ts +1 -0
- package/dist/locales/index.js +37 -0
- package/dist/locales/it.json +374 -0
- package/dist/server/commands.d.ts +6 -0
- package/dist/server/commands.js +394 -125
- package/dist/server/config.d.ts +34 -23
- package/dist/server/config.js +200 -108
- package/dist/server/connect-response.d.ts +2 -0
- package/dist/server/connect-response.js +59 -0
- package/dist/server/generate-config.d.ts +3 -30
- package/dist/server/generate-config.js +164 -106
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +124 -149
- package/dist/server/logger.d.ts +8 -0
- package/dist/server/logger.js +38 -0
- package/dist/server/models/cache.d.ts +35 -0
- package/dist/server/models/cache.js +160 -0
- package/dist/server/models/fetcher.d.ts +18 -0
- package/dist/server/models/fetcher.js +194 -0
- package/dist/server/models/index.d.ts +6 -0
- package/dist/server/models/index.js +5 -0
- package/dist/server/models/manual.d.ts +15 -0
- package/dist/server/models/manual.js +92 -0
- package/dist/server/models/types.d.ts +55 -0
- package/dist/server/models/types.js +7 -0
- package/dist/server/models/worker.d.ts +22 -0
- package/dist/server/models/worker.js +174 -0
- package/dist/server/pollinations-api.d.ts +11 -0
- package/dist/server/pollinations-api.js +21 -8
- package/dist/server/proxy.js +222 -307
- package/dist/server/quota.d.ts +2 -0
- package/dist/server/quota.js +89 -86
- package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
- package/dist/server/scripts/pollinations_pricing.js +246 -0
- package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
- package/dist/server/scripts/test_cost_endpoints.js +61 -0
- package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
- package/dist/server/scripts/test_dynamic_pricing.js +39 -0
- package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
- package/dist/server/scripts/test_freetier_audit.js +215 -0
- package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
- package/dist/server/scripts/test_parallel_cost.js +104 -0
- package/dist/server/toast.d.ts +7 -1
- package/dist/server/toast.js +43 -10
- package/dist/tools/design/gen_diagram.d.ts +2 -0
- package/dist/tools/design/gen_diagram.js +94 -0
- package/dist/tools/design/gen_palette.d.ts +2 -0
- package/dist/tools/design/gen_palette.js +182 -0
- package/dist/tools/design/gen_qrcode.d.ts +2 -0
- package/dist/tools/design/gen_qrcode.js +50 -0
- package/dist/tools/ffmpeg.d.ts +24 -0
- package/dist/tools/ffmpeg.js +54 -0
- package/dist/tools/index.d.ts +25 -0
- package/dist/tools/index.js +86 -0
- package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
- package/dist/tools/pollinations/beta_discovery.js +201 -0
- package/dist/tools/pollinations/cost-guard.d.ts +38 -0
- package/dist/tools/pollinations/cost-guard.js +136 -0
- package/dist/tools/pollinations/deepsearch.d.ts +7 -0
- package/dist/tools/pollinations/deepsearch.js +80 -0
- package/dist/tools/pollinations/gen_audio.d.ts +18 -0
- package/dist/tools/pollinations/gen_audio.js +220 -0
- package/dist/tools/pollinations/gen_image.d.ts +11 -0
- package/dist/tools/pollinations/gen_image.js +211 -0
- package/dist/tools/pollinations/gen_music.d.ts +14 -0
- package/dist/tools/pollinations/gen_music.js +157 -0
- package/dist/tools/pollinations/gen_video.d.ts +16 -0
- package/dist/tools/pollinations/gen_video.js +249 -0
- package/dist/tools/pollinations/polli_config.d.ts +2 -0
- package/dist/tools/pollinations/polli_config.js +95 -0
- package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
- package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
- package/dist/tools/pollinations/polli_status.d.ts +2 -0
- package/dist/tools/pollinations/polli_status.js +31 -0
- package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
- package/dist/tools/pollinations/polli_web_search.js +126 -0
- package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
- package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
- package/dist/tools/pollinations/shared.d.ts +181 -0
- package/dist/tools/pollinations/shared.js +758 -0
- package/dist/tools/pollinations/test_estimators.d.ts +1 -0
- package/dist/tools/pollinations/test_estimators.js +22 -0
- package/dist/tools/pollinations/transcribe_audio.d.ts +13 -0
- package/dist/tools/pollinations/transcribe_audio.js +171 -0
- package/dist/tools/power/extract_audio.d.ts +2 -0
- package/dist/tools/power/extract_audio.js +179 -0
- package/dist/tools/power/extract_frames.d.ts +2 -0
- package/dist/tools/power/extract_frames.js +237 -0
- package/dist/tools/power/file_to_url.d.ts +2 -0
- package/dist/tools/power/file_to_url.js +217 -0
- package/dist/tools/power/remove_background.d.ts +2 -0
- package/dist/tools/power/remove_background.js +404 -0
- package/dist/tools/power/rmbg_keys.d.ts +2 -0
- package/dist/tools/power/rmbg_keys.js +79 -0
- package/dist/tools/shared.d.ts +30 -0
- package/dist/tools/shared.js +80 -0
- package/package.json +10 -4
- package/dist/server/models-seed.d.ts +0 -18
- 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,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;
|