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.
- package/LICENSE.md +21 -0
- package/README.md +140 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +128 -0
- package/dist/provider.d.ts +1 -0
- package/dist/provider.js +135 -0
- package/dist/provider_v1.d.ts +1 -0
- package/dist/provider_v1.js +135 -0
- package/dist/server/commands.d.ts +10 -0
- package/dist/server/commands.js +302 -0
- package/dist/server/config.d.ts +49 -0
- package/dist/server/config.js +159 -0
- package/dist/server/generate-config.d.ts +13 -0
- package/dist/server/generate-config.js +154 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +120 -0
- package/dist/server/pollinations-api.d.ts +48 -0
- package/dist/server/pollinations-api.js +147 -0
- package/dist/server/proxy.d.ts +2 -0
- package/dist/server/proxy.js +588 -0
- package/dist/server/quota.d.ts +15 -0
- package/dist/server/quota.js +210 -0
- package/dist/server/router.d.ts +8 -0
- package/dist/server/router.js +122 -0
- package/dist/server/status.d.ts +3 -0
- package/dist/server/status.js +31 -0
- package/dist/server/toast.d.ts +6 -0
- package/dist/server/toast.js +78 -0
- package/package.json +53 -0
|
@@ -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 {};
|