opencode-pollinations-plugin 5.1.3

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.
@@ -0,0 +1,302 @@
1
+ import { loadConfig, saveConfig } from './config.js';
2
+ import { getQuotaStatus } from './quota.js';
3
+ import { emitStatusToast } from './toast.js';
4
+ import { getDetailedUsage } from './pollinations-api.js';
5
+ // === CONSTANTS & PRICING ===
6
+ const TIER_LIMITS = {
7
+ spore: { pollen: 1, emoji: '🦠' },
8
+ seed: { pollen: 3, emoji: '🌱' },
9
+ flower: { pollen: 10, emoji: '🌸' },
10
+ nectar: { pollen: 20, emoji: '🍯' },
11
+ };
12
+ // === MARKDOWN HELPERS ===
13
+ function formatPollen(amount) {
14
+ return `${amount.toFixed(2)} 🌼`;
15
+ }
16
+ function formatTokens(tokens) {
17
+ if (tokens >= 1_000_000)
18
+ return `${(tokens / 1_000_000).toFixed(2)}M`;
19
+ if (tokens >= 1_000)
20
+ return `${(tokens / 1_000).toFixed(1)}K`;
21
+ return tokens.toString();
22
+ }
23
+ function formatDuration(ms) {
24
+ const hours = Math.floor(ms / (1000 * 60 * 60));
25
+ const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
26
+ return `${hours}h ${minutes}m`;
27
+ }
28
+ function progressBar(value, max) {
29
+ const percentage = max > 0 ? Math.round((value / max) * 10) : 0;
30
+ const filled = '█'.repeat(percentage);
31
+ const empty = '░'.repeat(10 - percentage);
32
+ return `\`${filled}${empty}\` (${(value / max * 100).toFixed(0)}%)`;
33
+ }
34
+ function parseUsageTimestamp(timestamp) {
35
+ return new Date(timestamp.replace(' ', 'T') + 'Z');
36
+ }
37
+ function calculateResetDate(nextResetAt) {
38
+ const now = new Date();
39
+ const lastReset = new Date(nextResetAt.getTime() - 24 * 60 * 60 * 1000);
40
+ return lastReset;
41
+ }
42
+ function calculateCurrentPeriodStats(usage, lastReset, tierLimit) {
43
+ let tierUsed = 0;
44
+ let packUsed = 0;
45
+ let totalRequests = 0;
46
+ let inputTokens = 0;
47
+ let outputTokens = 0;
48
+ const models = new Map();
49
+ const entries = usage.filter(entry => {
50
+ const t = parseUsageTimestamp(entry.timestamp);
51
+ return t >= lastReset;
52
+ });
53
+ for (const entry of entries) {
54
+ totalRequests++;
55
+ inputTokens += (entry.input_text_tokens || 0);
56
+ outputTokens += (entry.output_text_tokens || 0);
57
+ if (entry.meter_source === 'tier')
58
+ tierUsed += entry.cost_usd;
59
+ else
60
+ packUsed += entry.cost_usd;
61
+ const modelName = entry.model || 'unknown';
62
+ const existing = models.get(modelName) || { requests: 0, cost: 0, source: entry.meter_source, inputTokens: 0, outputTokens: 0 };
63
+ existing.requests++;
64
+ existing.cost += entry.cost_usd;
65
+ existing.inputTokens += (entry.input_text_tokens || 0);
66
+ existing.outputTokens += (entry.output_text_tokens || 0);
67
+ models.set(modelName, existing);
68
+ }
69
+ return {
70
+ tierUsed,
71
+ tierRemaining: Math.max(0, tierLimit - tierUsed),
72
+ packUsed,
73
+ totalRequests,
74
+ inputTokens,
75
+ outputTokens,
76
+ models
77
+ };
78
+ }
79
+ // === COMMAND HANDLER ===
80
+ export async function handleCommand(command) {
81
+ const parts = command.trim().split(/\s+/);
82
+ if (!parts[0].startsWith('/poll')) {
83
+ return { handled: false };
84
+ }
85
+ const subCommand = parts[1];
86
+ const args = parts.slice(2);
87
+ switch (subCommand) {
88
+ case 'mode':
89
+ return handleModeCommand(args);
90
+ case 'usage':
91
+ return await handleUsageCommand(args);
92
+ case 'fallback':
93
+ return handleFallbackCommand(args);
94
+ case 'config':
95
+ return handleConfigCommand(args);
96
+ case 'help':
97
+ return handleHelpCommand();
98
+ default:
99
+ return {
100
+ handled: true,
101
+ error: `Commande inconnue: ${subCommand}. Utilisez /pollinations help`
102
+ };
103
+ }
104
+ }
105
+ // === SUB-COMMANDS ===
106
+ function handleModeCommand(args) {
107
+ const mode = args[0];
108
+ if (!mode) {
109
+ const config = loadConfig();
110
+ return {
111
+ handled: true,
112
+ response: `Mode actuel: ${config.mode}`
113
+ };
114
+ }
115
+ if (!['manual', 'alwaysfree', 'pro'].includes(mode)) {
116
+ return {
117
+ handled: true,
118
+ error: `Mode invalide: ${mode}. Valeurs: manual, alwaysfree, pro`
119
+ };
120
+ }
121
+ saveConfig({ mode: mode });
122
+ const config = loadConfig();
123
+ if (config.gui.status !== 'none') {
124
+ emitStatusToast('success', `Mode changé vers: ${mode}`, 'Pollinations Config');
125
+ }
126
+ return {
127
+ handled: true,
128
+ response: `✅ Mode changé: ${mode}`
129
+ };
130
+ }
131
+ async function handleUsageCommand(args) {
132
+ const isFull = args[0] === 'full';
133
+ try {
134
+ const quota = await getQuotaStatus(true);
135
+ const config = loadConfig();
136
+ const resetDate = quota.nextResetAt.toLocaleString('fr-FR', { hour: '2-digit', minute: '2-digit' });
137
+ const timeUntilReset = quota.nextResetAt.getTime() - Date.now();
138
+ const durationStr = formatDuration(Math.max(0, timeUntilReset));
139
+ let response = `### 🌸 Dashboard Pollinations (${config.mode.toUpperCase()})\n\n`;
140
+ response += `**Ressources**\n`;
141
+ response += `- **Tier**: ${quota.tierEmoji} ${quota.tier.toUpperCase()} (${quota.tierLimit} pollen/jour)\n`;
142
+ response += `- **Quota**: ${formatPollen(quota.tierLimit - quota.tierRemaining)} / ${formatPollen(quota.tierLimit)}\n`;
143
+ response += `- **Usage**: ${progressBar(quota.tierLimit - quota.tierRemaining, quota.tierLimit)}\n`;
144
+ response += `- **Wallet**: $${quota.walletBalance.toFixed(2)}\n`;
145
+ response += `- **Reset**: ${resetDate} (dans ${durationStr})\n`;
146
+ if (isFull && config.apiKey) {
147
+ const usageData = await getDetailedUsage(config.apiKey);
148
+ if (usageData && usageData.usage) {
149
+ const lastReset = calculateResetDate(quota.nextResetAt);
150
+ const stats = calculateCurrentPeriodStats(usageData.usage, lastReset, quota.tierLimit);
151
+ response += `\n### 📊 Détail Période (depuis ${lastReset.toLocaleTimeString()})\n`;
152
+ response += `**Total Requêtes**: ${stats.totalRequests} | **Tokens**: In ${formatTokens(stats.inputTokens)} / Out ${formatTokens(stats.outputTokens)}\n\n`;
153
+ response += `| Modèle | Reqs | Coût | Tokens |\n`;
154
+ response += `| :--- | :---: | :---: | :---: |\n`;
155
+ const sorted = Array.from(stats.models.entries()).sort((a, b) => b[1].cost - a[1].cost);
156
+ for (const [model, data] of sorted) {
157
+ response += `| \`${model}\` | ${data.requests} | ${formatPollen(data.cost)} | ${formatTokens(data.inputTokens + data.outputTokens)} |\n`;
158
+ }
159
+ }
160
+ else {
161
+ response += `\n> ⚠️ *Impossible de récupérer l'historique détaillé.*\n`;
162
+ }
163
+ }
164
+ else if (isFull) {
165
+ response += `\n> ⚠️ *Mode Full nécessite une API Key.*\n`;
166
+ }
167
+ else {
168
+ response += `\n_Tapez_ \`/pollinations usage full\` _pour le détail._\n`;
169
+ }
170
+ return { handled: true, response: response.trim() };
171
+ }
172
+ catch (e) {
173
+ return { handled: true, error: `Erreur: ${e}` };
174
+ }
175
+ }
176
+ function handleFallbackCommand(args) {
177
+ const [main, agent] = args;
178
+ if (!main) {
179
+ const config = loadConfig();
180
+ const freeConfig = `Free: main=${config.fallbacks.free.main}, agent=${config.fallbacks.free.agent}`;
181
+ const enterConfig = `Enter: agent=${config.fallbacks.enter.agent}`;
182
+ return {
183
+ handled: true,
184
+ response: `Fallbacks actuels:\n${freeConfig}\n${enterConfig}`
185
+ };
186
+ }
187
+ // Default behavior for "/poll fallback <model> <agent>" is setting FREE fallbacks
188
+ // User needs to use commands (maybe add /poll fallback enter ...) later
189
+ // For now, map to Free Fallback as it's the primary Safety Net
190
+ const config = loadConfig();
191
+ saveConfig({
192
+ fallbacks: {
193
+ ...config.fallbacks,
194
+ free: {
195
+ main: main,
196
+ agent: agent || config.fallbacks.free.agent
197
+ }
198
+ }
199
+ });
200
+ return {
201
+ handled: true,
202
+ response: `✅ Fallback (Free) configuré: main=${main}, agent=${agent || config.fallbacks.free.agent}`
203
+ };
204
+ }
205
+ function handleConfigCommand(args) {
206
+ const [key, value] = args;
207
+ if (!key) {
208
+ const config = loadConfig();
209
+ return {
210
+ handled: true,
211
+ response: JSON.stringify(config, null, 2)
212
+ };
213
+ }
214
+ if (key === 'toast_verbosity' && value) {
215
+ // BACKWARD COMPAT (Maps to Status GUI)
216
+ if (!['none', 'alert', 'all'].includes(value)) {
217
+ return { handled: true, error: 'Valeurs: none, alert, all' };
218
+ }
219
+ const config = loadConfig();
220
+ saveConfig({
221
+ gui: {
222
+ ...config.gui,
223
+ status: value
224
+ }
225
+ });
226
+ return { handled: true, response: `✅ status_gui = ${value} (Legacy Mapping)` };
227
+ }
228
+ if (key === 'status_gui' && value) {
229
+ if (!['none', 'alert', 'all'].includes(value))
230
+ return { handled: true, error: 'Valeurs: none, alert, all' };
231
+ const config = loadConfig();
232
+ saveConfig({ gui: { ...config.gui, status: value } });
233
+ return { handled: true, response: `✅ status_gui = ${value}` };
234
+ }
235
+ if (key === 'logs_gui' && value) {
236
+ if (!['none', 'error', 'verbose'].includes(value))
237
+ return { handled: true, error: 'Valeurs: none, error, verbose' };
238
+ const config = loadConfig();
239
+ saveConfig({ gui: { ...config.gui, logs: value } });
240
+ return { handled: true, response: `✅ logs_gui = ${value}` };
241
+ }
242
+ if (key === 'threshold_tier' && value) {
243
+ const threshold = parseInt(value);
244
+ if (isNaN(threshold) || threshold < 0 || threshold > 100) {
245
+ return { handled: true, error: 'Valeur entre 0 et 100 requise' };
246
+ }
247
+ const config = loadConfig();
248
+ saveConfig({ thresholds: { ...config.thresholds, tier: threshold } });
249
+ return { handled: true, response: `✅ threshold_tier = ${threshold}%` };
250
+ }
251
+ if (key === 'threshold_wallet' && value) {
252
+ const threshold = parseInt(value);
253
+ if (isNaN(threshold) || threshold < 0 || threshold > 100) {
254
+ return { handled: true, error: 'Valeur entre 0 et 100 requise' };
255
+ }
256
+ const config = loadConfig();
257
+ saveConfig({ thresholds: { ...config.thresholds, wallet: threshold } });
258
+ return { handled: true, response: `✅ threshold_wallet = ${threshold}%` };
259
+ }
260
+ if (key === 'status_bar' && value) {
261
+ const enabled = value === 'true';
262
+ saveConfig({ statusBar: enabled });
263
+ return { handled: true, response: `✅ status_bar = ${enabled}` };
264
+ }
265
+ return {
266
+ handled: true,
267
+ error: `Clé inconnue: ${key}. Clés: status_gui, logs_gui, threshold_tier, threshold_wallet, status_bar`
268
+ };
269
+ }
270
+ function handleHelpCommand() {
271
+ const help = `
272
+ ### 🌸 Pollinations Plugin - Commandes V5
273
+
274
+ - **\`/pollinations mode [mode]\`**: Change le mode (manual, alwaysfree, pro).
275
+ - **\`/pollinations usage [full]\`**: Affiche le dashboard (full = détail).
276
+ - **\`/pollinations fallback <main> [agent]\`**: Configure le Safety Net (Free).
277
+ - **\`/pollinations config [key] [value]\`**:
278
+ - \`status_gui\`: none, alert, all (Status Dashboard).
279
+ - \`logs_gui\`: none, error, verbose (Logs Techniques).
280
+ - \`threshold_tier\`: 0-100 (Alerte %).
281
+ - \`threshold_wallet\`: 0-100 (Safety Net %).
282
+ - \`status_bar\`: true/false (Widget).
283
+ `.trim();
284
+ return { handled: true, response: help };
285
+ }
286
+ // === INTEGRATION OPENCODE ===
287
+ export function createCommandHooks() {
288
+ return {
289
+ 'tui.command.execute': async (input, output) => {
290
+ const result = await handleCommand(input.command);
291
+ if (result.handled) {
292
+ output.handled = true;
293
+ if (result.response) {
294
+ output.response = result.response;
295
+ }
296
+ if (result.error) {
297
+ output.error = result.error;
298
+ }
299
+ }
300
+ }
301
+ };
302
+ }
@@ -0,0 +1,49 @@
1
+ export interface PollinationsConfigV5 {
2
+ version: number;
3
+ mode: 'manual' | 'alwaysfree' | 'pro';
4
+ apiKey?: string;
5
+ gui: {
6
+ status: 'none' | 'alert' | 'all';
7
+ logs: 'none' | 'error' | 'verbose';
8
+ };
9
+ thresholds: {
10
+ tier: number;
11
+ wallet: number;
12
+ };
13
+ fallbacks: {
14
+ free: {
15
+ main: string;
16
+ agent: string;
17
+ };
18
+ enter: {
19
+ agent: string;
20
+ };
21
+ };
22
+ enablePaidTools: boolean;
23
+ statusBar: boolean;
24
+ }
25
+ export declare function loadConfig(): PollinationsConfigV5;
26
+ export declare function saveConfig(updates: Partial<PollinationsConfigV5>): {
27
+ version: number;
28
+ mode: "manual" | "alwaysfree" | "pro";
29
+ apiKey?: string;
30
+ gui: {
31
+ status: "none" | "alert" | "all";
32
+ logs: "none" | "error" | "verbose";
33
+ };
34
+ thresholds: {
35
+ tier: number;
36
+ wallet: number;
37
+ };
38
+ fallbacks: {
39
+ free: {
40
+ main: string;
41
+ agent: string;
42
+ };
43
+ enter: {
44
+ agent: string;
45
+ };
46
+ };
47
+ enablePaidTools: boolean;
48
+ statusBar: boolean;
49
+ };
@@ -0,0 +1,159 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ // PATHS
5
+ const HOMEDIR = os.homedir();
6
+ const CONFIG_DIR_POLLI = path.join(HOMEDIR, '.pollinations');
7
+ const CONFIG_FILE = path.join(CONFIG_DIR_POLLI, 'config.json');
8
+ const CONFIG_DIR_OPENCODE = path.join(HOMEDIR, '.config', 'opencode');
9
+ const OPENCODE_CONFIG_FILE = path.join(CONFIG_DIR_OPENCODE, 'opencode.json');
10
+ const AUTH_FILE = path.join(HOMEDIR, '.local', 'share', 'opencode', 'auth.json');
11
+ const DEFAULT_CONFIG_V5 = {
12
+ version: 5,
13
+ mode: 'manual',
14
+ gui: {
15
+ status: 'alert',
16
+ logs: 'none'
17
+ },
18
+ thresholds: {
19
+ tier: 10, // Alert if < 10%
20
+ wallet: 5 // Switch if < 5% (Wallet Protection)
21
+ },
22
+ fallbacks: {
23
+ free: {
24
+ main: 'free/mistral', // Fallback gratuit solide
25
+ agent: 'free/openai-fast' // Agent gratuit rapide
26
+ },
27
+ enter: {
28
+ agent: 'free/gemini' // Agent de secours (Free Gemini)
29
+ }
30
+ },
31
+ enablePaidTools: false,
32
+ statusBar: true
33
+ };
34
+ // Debug Helper
35
+ function logConfig(msg) {
36
+ try {
37
+ if (!fs.existsSync('/tmp/opencode_pollinations_config_debug.log')) {
38
+ fs.writeFileSync('/tmp/opencode_pollinations_config_debug.log', '');
39
+ }
40
+ fs.appendFileSync('/tmp/opencode_pollinations_config_debug.log', `[${new Date().toISOString()}] ${msg}\n`);
41
+ }
42
+ catch (e) { }
43
+ }
44
+ export function loadConfig() {
45
+ let config = { ...DEFAULT_CONFIG_V5 };
46
+ let keyFound = false;
47
+ // 1. Try Custom Config
48
+ try {
49
+ if (fs.existsSync(CONFIG_FILE)) {
50
+ const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
51
+ const custom = JSON.parse(raw);
52
+ // === MIGRATION LOGIC V4 -> V5 ===
53
+ if (!custom.version || custom.version < 5) {
54
+ logConfig(`Migrating Config V${custom.version || 0} -> V5`);
55
+ // Migrate GUI
56
+ if (custom.toastVerbosity) {
57
+ if (custom.toastVerbosity === 'none') {
58
+ config.gui.status = 'none';
59
+ config.gui.logs = 'none';
60
+ }
61
+ if (custom.toastVerbosity === 'alert') {
62
+ config.gui.status = 'alert';
63
+ config.gui.logs = 'error';
64
+ }
65
+ if (custom.toastVerbosity === 'all') {
66
+ config.gui.status = 'all';
67
+ config.gui.logs = 'verbose';
68
+ }
69
+ }
70
+ // Migrate Fallbacks
71
+ if (custom.fallbackModels) {
72
+ if (custom.fallbackModels.main)
73
+ config.fallbacks.free.main = custom.fallbackModels.main;
74
+ if (custom.fallbackModels.agent) {
75
+ config.fallbacks.free.agent = custom.fallbackModels.agent;
76
+ config.fallbacks.enter.agent = custom.fallbackModels.agent;
77
+ }
78
+ }
79
+ // Preserve others
80
+ if (custom.apiKey)
81
+ config.apiKey = custom.apiKey;
82
+ if (custom.mode)
83
+ config.mode = custom.mode;
84
+ if (custom.statusBar !== undefined)
85
+ config.statusBar = custom.statusBar;
86
+ // Save Migrated
87
+ saveConfig(config);
88
+ }
89
+ else {
90
+ // Already V5
91
+ config = { ...config, ...custom };
92
+ }
93
+ if (config.apiKey)
94
+ keyFound = true;
95
+ }
96
+ }
97
+ catch (e) {
98
+ logConfig(`Error loading config: ${e}`);
99
+ }
100
+ // 2. Try Native Auth Storage (Recovery)
101
+ if (!keyFound) {
102
+ try {
103
+ if (fs.existsSync(AUTH_FILE)) {
104
+ const raw = fs.readFileSync(AUTH_FILE, 'utf-8');
105
+ const authData = JSON.parse(raw);
106
+ const entry = authData['pollinations'] || authData['pollinations_enter'];
107
+ if (entry) {
108
+ const key = (typeof entry === 'object' && entry.key) ? entry.key : entry;
109
+ if (key && typeof key === 'string' && key.length > 10) {
110
+ config.apiKey = key;
111
+ config.mode = 'pro';
112
+ keyFound = true;
113
+ logConfig(`Recovered API Key from Auth Store`);
114
+ }
115
+ }
116
+ }
117
+ }
118
+ catch (e) {
119
+ logConfig(`Error reading auth.json: ${e}`);
120
+ }
121
+ }
122
+ // 3. Try OpenCode Config
123
+ if (!keyFound) {
124
+ try {
125
+ if (fs.existsSync(OPENCODE_CONFIG_FILE)) {
126
+ const raw = fs.readFileSync(OPENCODE_CONFIG_FILE, 'utf-8');
127
+ const data = JSON.parse(raw);
128
+ const nativeKey = data?.provider?.pollinations?.options?.apiKey ||
129
+ data?.provider?.pollinations_enter?.options?.apiKey;
130
+ if (nativeKey && nativeKey.length > 5 && nativeKey !== 'dummy') {
131
+ config.apiKey = nativeKey;
132
+ config.mode = 'pro';
133
+ keyFound = true;
134
+ }
135
+ }
136
+ }
137
+ catch (e) { }
138
+ }
139
+ // Default mode logic
140
+ if (!keyFound && config.mode === 'pro') {
141
+ config.mode = 'manual';
142
+ }
143
+ return config;
144
+ }
145
+ export function saveConfig(updates) {
146
+ try {
147
+ const current = loadConfig();
148
+ const updated = { ...current, ...updates, version: 5 };
149
+ if (!fs.existsSync(CONFIG_DIR_POLLI)) {
150
+ fs.mkdirSync(CONFIG_DIR_POLLI, { recursive: true });
151
+ }
152
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2));
153
+ return updated;
154
+ }
155
+ catch (e) {
156
+ logConfig(`Error saving config: ${e}`);
157
+ throw e;
158
+ }
159
+ }
@@ -0,0 +1,13 @@
1
+ interface OpenCodeModel {
2
+ id: string;
3
+ name: string;
4
+ object: string;
5
+ variants?: any;
6
+ options?: any;
7
+ limit?: {
8
+ context?: number;
9
+ output?: number;
10
+ };
11
+ }
12
+ export declare function generatePollinationsConfig(): Promise<OpenCodeModel[]>;
13
+ export {};
@@ -0,0 +1,154 @@
1
+ import * as https from 'https';
2
+ import * as fs from 'fs';
3
+ import { loadConfig } from './config.js';
4
+ // --- LOGGING ---
5
+ const LOG_FILE = '/tmp/opencode_pollinations_config.log';
6
+ function log(msg) {
7
+ try {
8
+ const ts = new Date().toISOString();
9
+ if (!fs.existsSync(LOG_FILE))
10
+ fs.writeFileSync(LOG_FILE, '');
11
+ fs.appendFileSync(LOG_FILE, `[ConfigGen] ${ts} ${msg}\n`);
12
+ }
13
+ catch (e) { }
14
+ // Force output to stderr for CLI visibility if needed, but clean.
15
+ }
16
+ // Fetch Helper
17
+ function fetchJson(url, headers = {}) {
18
+ return new Promise((resolve, reject) => {
19
+ const req = https.get(url, { headers }, (res) => {
20
+ let data = '';
21
+ res.on('data', chunk => data += chunk);
22
+ res.on('end', () => {
23
+ try {
24
+ const json = JSON.parse(data);
25
+ resolve(json);
26
+ }
27
+ catch (e) {
28
+ log(`JSON Parse Error for ${url}: ${e}`);
29
+ resolve([]); // Fail safe -> empty list
30
+ }
31
+ });
32
+ });
33
+ req.on('error', (e) => {
34
+ log(`Network Error for ${url}: ${e.message}`);
35
+ reject(e);
36
+ });
37
+ req.setTimeout(5000, () => {
38
+ req.destroy();
39
+ reject(new Error('Timeout'));
40
+ });
41
+ });
42
+ }
43
+ function formatName(id, censored = true) {
44
+ let clean = id.replace(/^pollinations\//, '').replace(/-/g, ' ');
45
+ clean = clean.replace(/\b\w/g, l => l.toUpperCase());
46
+ if (!censored)
47
+ clean += " (Uncensored)";
48
+ return clean;
49
+ }
50
+ // --- MAIN GENERATOR logic ---
51
+ export async function generatePollinationsConfig() {
52
+ const config = loadConfig();
53
+ const modelsOutput = [];
54
+ log(`Starting Configuration (V4.5 Clean Dynamic)...`);
55
+ // 1. FREE UNIVERSE
56
+ try {
57
+ // Switch to main models endpoint (User provided curl confirms it has 'description')
58
+ const freeList = await fetchJson('https://text.pollinations.ai/models');
59
+ const list = Array.isArray(freeList) ? freeList : (freeList.data || []);
60
+ list.forEach((m) => {
61
+ const mapped = mapModel(m, 'free/', '[Free] ');
62
+ modelsOutput.push(mapped);
63
+ });
64
+ log(`Fetched ${modelsOutput.length} Free models.`);
65
+ }
66
+ catch (e) {
67
+ log(`Error fetching Free models: ${e}`);
68
+ // Fallback Robust (Offline support)
69
+ modelsOutput.push({ id: "free/mistral", name: "[Free] Mistral Nemo (Fallback)", object: "model", variants: {} });
70
+ modelsOutput.push({ id: "free/openai", name: "[Free] OpenAI (Fallback)", object: "model", variants: {} });
71
+ modelsOutput.push({ id: "free/gemini", name: "[Free] Gemini Flash (Fallback)", object: "model", variants: {} });
72
+ }
73
+ // 2. ENTERPRISE UNIVERSE
74
+ if (config.apiKey && config.apiKey.length > 5 && config.apiKey !== 'dummy') {
75
+ try {
76
+ const enterListRaw = await fetchJson('https://gen.pollinations.ai/text/models', {
77
+ 'Authorization': `Bearer ${config.apiKey}`
78
+ });
79
+ const enterList = Array.isArray(enterListRaw) ? enterListRaw : (enterListRaw.data || []);
80
+ enterList.forEach((m) => {
81
+ if (m.tools === false)
82
+ return;
83
+ const mapped = mapModel(m, 'enter/', '[Enter] ');
84
+ modelsOutput.push(mapped);
85
+ });
86
+ log(`Total models (Free+Pro): ${modelsOutput.length}`);
87
+ }
88
+ catch (e) {
89
+ log(`Error fetching Enterprise models: ${e}`);
90
+ // Fallback Robust for Enterprise (User has Key but discovery failed)
91
+ modelsOutput.push({ id: "enter/gpt-4o", name: "[Enter] GPT-4o (Fallback)", object: "model", variants: {} });
92
+ modelsOutput.push({ id: "enter/claude-3-5-sonnet", name: "[Enter] Claude 3.5 Sonnet (Fallback)", object: "model", variants: {} });
93
+ modelsOutput.push({ id: "enter/deepseek-reasoner", name: "[Enter] DeepSeek R1 (Fallback)", object: "model", variants: {} });
94
+ }
95
+ }
96
+ return modelsOutput;
97
+ }
98
+ // --- MAPPING ENGINE ---
99
+ function mapModel(raw, prefix, namePrefix) {
100
+ const rawId = raw.id || raw.name;
101
+ const fullId = prefix + rawId; // ex: "free/gemini" or "enter/nomnom" (prefix passed is "enter/")
102
+ let baseName = raw.description;
103
+ if (!baseName || baseName === rawId) {
104
+ baseName = formatName(rawId, raw.censored !== false);
105
+ }
106
+ // CLEANUP: Simple Truncation Rule (Requested by User)
107
+ // "Start from left, find ' - ', delete everything after."
108
+ if (baseName && baseName.includes(' - ')) {
109
+ baseName = baseName.split(' - ')[0].trim();
110
+ }
111
+ const finalName = `${namePrefix}${baseName}`;
112
+ const modelObj = {
113
+ id: fullId,
114
+ name: finalName,
115
+ object: 'model',
116
+ variants: {}
117
+ };
118
+ // --- ENRICHISSEMENT ---
119
+ if (raw.reasoning === true || rawId.includes('thinking') || rawId.includes('reasoning')) {
120
+ modelObj.variants = { ...modelObj.variants, high_reasoning: { options: { reasoningEffort: "high", budgetTokens: 16000 } } };
121
+ }
122
+ if (rawId.includes('gemini') && !rawId.includes('fast')) {
123
+ if (!modelObj.variants.high_reasoning && (rawId === 'gemini' || rawId === 'gemini-large')) {
124
+ modelObj.variants.high_reasoning = { options: { reasoningEffort: "high", budgetTokens: 16000 } };
125
+ }
126
+ }
127
+ if (rawId.includes('claude') || rawId.includes('mistral') || rawId.includes('llama')) {
128
+ modelObj.variants.safe_tokens = { options: { maxTokens: 8000 } };
129
+ }
130
+ // NOVA FIX: Bedrock limit ~10k (User reported error > 10000)
131
+ // We MUST set the limit on the model object itself so OpenCode respects it by default.
132
+ if (rawId.includes('nova')) {
133
+ modelObj.limit = {
134
+ output: 8000,
135
+ context: 128000 // Nova Micro/Lite/Pro usually 128k
136
+ };
137
+ // Also keep variant just in case
138
+ modelObj.variants.bedrock_safe = { options: { maxTokens: 8000 } };
139
+ }
140
+ // NOMNOM FIX: User reported error if max_tokens is missing.
141
+ // Also it is a 'Gemini-scrape' model, so we treat it similar to Gemini but with strict limit.
142
+ if (rawId.includes('nomnom') || rawId.includes('scrape')) {
143
+ modelObj.limit = {
144
+ output: 2048, // User used 1500 successfully
145
+ context: 32768
146
+ };
147
+ }
148
+ if (rawId.includes('fast') || rawId.includes('flash') || rawId.includes('lite')) {
149
+ if (!rawId.includes('gemini')) {
150
+ modelObj.variants.speed = { options: { thinking: { disabled: true } } };
151
+ }
152
+ }
153
+ return modelObj;
154
+ }
@@ -0,0 +1 @@
1
+ export {};