opencode-pollinations-plugin 6.0.0-beta.25 → 6.0.0-beta.5

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.md CHANGED
@@ -1,4 +1,4 @@
1
- # 🌸 Pollinations AI Plugin for OpenCode (v5.6.0)
1
+ # 🌸 Pollinations AI Plugin for OpenCode (v5.9.0)
2
2
 
3
3
  <div align="center">
4
4
  <img src="https://avatars.githubusercontent.com/u/88394740?s=400&v=4" alt="Pollinations.ai Logo" width="200">
@@ -10,11 +10,9 @@
10
10
 
11
11
  <div align="center">
12
12
 
13
- ![Version](https://img.shields.io/badge/version-5.8.4--beta.15-orange.svg)
13
+ ![Version](https://img.shields.io/badge/version-5.6.0-blue.svg)
14
14
  ![License](https://img.shields.io/badge/license-MIT-green.svg)
15
- ![Status](https://img.shields.io/badge/status-Beta-yellow.svg)
16
-
17
- [📜 View Changelog](./CHANGELOG.md) | [🛣️ Roadmap](./ROADMAP.md)
15
+ ![Status](https://img.shields.io/badge/status-Stable-success.svg)
18
16
 
19
17
  </div>
20
18
 
@@ -136,8 +134,9 @@ OpenCode uses NPM as its registry. To publish:
136
134
 
137
135
  ### 1. The Basics (Free Mode)
138
136
  Just type in the chat. You are in **Manual Mode** by default.
139
- - Model: `openai` (GPT-4o Mini equivalent)
140
- - Model: `mistral` (Mistral Nemo)
137
+ - Model: `openai-fast` (GPT-OSS 20b)
138
+ - Model: `mistral` (Mistral Small 3.1)
139
+ - ...
141
140
 
142
141
  ### 🔑 Configuration (API Key)
143
142
 
@@ -157,6 +156,7 @@ Just type in the chat. You are in **Manual Mode** by default.
157
156
 
158
157
  ## 🔗 Links
159
158
 
159
+ - **Sign up Pollinations Beta (more and best free tiers access and paids models)**: [pollinations.ai](https://enter.pollinations.ai)
160
160
  - **Pollinations Website**: [pollinations.ai](https://pollinations.ai)
161
161
  - **Discord Community**: [Join us!](https://discord.gg/pollinations-ai-885844321461485618)
162
162
  - **OpenCode Ecosystem**: [opencode.ai](https://opencode.ai/docs/ecosystem#plugins)
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import * as http from 'http';
2
2
  import * as fs from 'fs';
3
3
  import { generatePollinationsConfig } from './server/generate-config.js';
4
- import { loadConfig, saveConfig } from './server/config.js';
4
+ import { loadConfig } from './server/config.js';
5
5
  import { handleChatCompletion } from './server/proxy.js';
6
- import { createToastHooks, setGlobalClient } from './server/toast.js';
6
+ import { createToastHooks, createToolHooks, setGlobalClient } from './server/toast.js';
7
7
  import { createStatusHooks } from './server/status.js';
8
- import { createCommandHooks } from './server/commands.js';
8
+ import { createCommandHooks, setClientForCommands } from './server/commands.js';
9
+ import { createToolRegistry } from './tools/index.js';
9
10
  import { createRequire } from 'module';
10
11
  const require = createRequire(import.meta.url);
11
12
  const LOG_FILE = '/tmp/opencode_pollinations_v4.log';
@@ -15,17 +16,12 @@ function log(msg) {
15
16
  }
16
17
  catch (e) { }
17
18
  }
18
- // === PROXY SERVER (Singleton with Fixed Port) ===
19
- const DEFAULT_PORT = 18888;
20
- const GLOBAL_SERVER_KEY = '__POLLINATIONS_PROXY_SERVER__';
19
+ // Port killing removed: Using dynamic ports.
21
20
  const startProxy = () => {
22
- // Check if server exists in global scope (survives module reloads)
23
- if (global[GLOBAL_SERVER_KEY]) {
24
- log(`[Proxy] Reusing existing global server on port ${DEFAULT_PORT}`);
25
- return Promise.resolve(DEFAULT_PORT);
26
- }
27
21
  return new Promise((resolve) => {
28
22
  const server = http.createServer(async (req, res) => {
23
+ // ... (Request Handling) ...
24
+ // We reuse the existing logic structure but simplified startup
29
25
  log(`[Proxy] Request: ${req.method} ${req.url}`);
30
26
  res.setHeader('Access-Control-Allow-Origin', '*');
31
27
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
@@ -67,156 +63,38 @@ const startProxy = () => {
67
63
  res.writeHead(404);
68
64
  res.end("Not Found");
69
65
  });
70
- // Try fixed port first, fallback to dynamic if occupied
71
- const tryListen = (port, fallbackToDynamic) => {
72
- server.listen(port, '127.0.0.1', () => {
73
- // @ts-ignore
74
- const assignedPort = server.address().port;
75
- global[GLOBAL_SERVER_KEY] = server;
76
- log(`[Proxy] Started v${require('../package.json').version} on port ${assignedPort}${port === 0 ? ' (dynamic fallback)' : ''}`);
77
- resolve(assignedPort);
78
- });
79
- server.on('error', (e) => {
80
- if (e.code === 'EADDRINUSE' && fallbackToDynamic) {
81
- log(`[Proxy] Port ${port} in use, falling back to dynamic port`);
82
- server.removeAllListeners('error');
83
- tryListen(0, false); // Try dynamic port
84
- }
85
- else {
86
- log(`[Proxy] Fatal Error: ${e}`);
87
- resolve(0);
88
- }
89
- });
90
- };
91
- tryListen(DEFAULT_PORT, true);
66
+ // Listen on random port (0) to avoid conflicts (CLI/IDE)
67
+ server.listen(0, '127.0.0.1', () => {
68
+ // @ts-ignore
69
+ const assignedPort = server.address().port;
70
+ log(`[Proxy] Started v${require('../package.json').version} (Dynamic Port) on port ${assignedPort}`);
71
+ resolve(assignedPort);
72
+ });
73
+ server.on('error', (e) => {
74
+ log(`[Proxy] Fatal Error: ${e}`);
75
+ resolve(0);
76
+ });
92
77
  });
93
78
  };
94
- // === AUTH HOOK: Native /connect Integration ===
95
- // Auth Hook moved inside plugin to access context
96
79
  // === PLUGIN EXPORT ===
97
80
  export const PollinationsPlugin = async (ctx) => {
98
81
  log(`Plugin Initializing v${require('../package.json').version}...`);
99
- // START PROXY on fixed port
82
+ // START PROXY
100
83
  const port = await startProxy();
101
84
  const localBaseUrl = `http://127.0.0.1:${port}/v1`;
102
85
  setGlobalClient(ctx.client);
86
+ setClientForCommands(ctx.client);
103
87
  const toastHooks = createToastHooks(ctx.client);
104
88
  const commandHooks = createCommandHooks();
105
- // Helper: Refresh provider config (for hot-reload after /connect)
106
- let isRefreshing = false;
107
- const refreshProviderConfig = async () => {
108
- if (isRefreshing)
109
- return;
110
- isRefreshing = true;
111
- try {
112
- log('[Event] Refreshing provider config after auth update...');
113
- const modelsArray = await generatePollinationsConfig();
114
- const modelsObj = {};
115
- for (const m of modelsArray) {
116
- modelsObj[m.id] = m;
117
- }
118
- const version = require('../package.json').version;
119
- // CRITICAL: Fetch current config first to avoid overwriting other providers
120
- let currentConfig = {};
121
- try {
122
- // Try to fetch existing config to preserve other providers
123
- const response = await ctx.client.fetch('/config');
124
- if (response.ok) {
125
- currentConfig = await response.json();
126
- }
127
- }
128
- catch (err) {
129
- log(`[Event] Warning: Could not fetch current config: ${err}`);
130
- }
131
- // Safe Merge
132
- if (!currentConfig.provider)
133
- currentConfig.provider = {};
134
- currentConfig.provider.pollinations = {
135
- id: 'openai',
136
- name: `Pollinations AI (v${version})`,
137
- options: {
138
- baseURL: localBaseUrl,
139
- apiKey: 'plugin-managed',
140
- },
141
- models: modelsObj
142
- };
143
- // Use Server API to update config with the MERGED object
144
- await ctx.client.fetch('/config', {
145
- method: 'PATCH',
146
- headers: { 'Content-Type': 'application/json' },
147
- body: JSON.stringify({
148
- provider: currentConfig.provider
149
- })
150
- });
151
- log(`[Event] Provider config refreshed with ${Object.keys(modelsObj).length} models.`);
152
- }
153
- catch (e) {
154
- log(`[Event] Failed to refresh provider config: ${e}`);
155
- }
156
- finally {
157
- // Debounce: prevent another refresh for 5 seconds
158
- setTimeout(() => { isRefreshing = false; }, 5000);
159
- }
160
- };
89
+ // Build tool registry (conditional on API key presence)
90
+ const toolRegistry = createToolRegistry();
91
+ log(`[Tools] ${Object.keys(toolRegistry).length} tools registered`);
161
92
  return {
162
- // AUTH HOOK: Native /connect integration (INLINED)
163
- auth: {
164
- provider: 'pollinations',
165
- loader: async (auth, provider) => {
166
- log('[AuthHook] loader() called');
167
- try {
168
- const authData = await auth();
169
- if (authData && 'key' in authData) {
170
- const k = authData.key;
171
- saveConfig({ apiKey: k });
172
- return { apiKey: k };
173
- }
174
- }
175
- catch (e) {
176
- log(`[AuthHook] loader error: ${e}`);
177
- }
178
- const config = loadConfig();
179
- if (config.apiKey)
180
- return { apiKey: config.apiKey };
181
- return {};
182
- },
183
- methods: [{
184
- type: 'api',
185
- label: 'Pollinations API Key',
186
- prompts: [{
187
- type: 'text',
188
- key: 'apiKey',
189
- message: 'Enter Pollinations API Key (starts with sk_)',
190
- validate: (v) => (!v || v.length < 10) ? 'Invalid key' : undefined
191
- }],
192
- authorize: async (inputs) => {
193
- log(`[AuthHook] authorize() called`);
194
- if (!inputs?.apiKey)
195
- return { type: 'failed' };
196
- try {
197
- const r = await fetch('https://gen.pollinations.ai/text/models', {
198
- headers: { 'Authorization': `Bearer ${inputs.apiKey}` }
199
- });
200
- if (r.ok) {
201
- log('[AuthHook] Success. Saving & Refreshing Config...');
202
- saveConfig({ apiKey: inputs.apiKey });
203
- // CRITICAL: Refresh config IMMEDIATELY after successful auth
204
- await refreshProviderConfig();
205
- return { type: 'success', key: inputs.apiKey };
206
- }
207
- }
208
- catch (e) {
209
- log(`[AuthHook] Auth error: ${e}`);
210
- }
211
- return { type: 'failed' };
212
- }
213
- }]
214
- },
215
- // Event hook removed (logic moved to authorize)
216
- event: async ({ event }) => { },
93
+ tool: toolRegistry,
217
94
  async config(config) {
218
95
  log("[Hook] config() called");
219
- // Generate models based on current auth state
96
+ // STARTUP only - No complex hot reload logic
97
+ // The user must restart OpenCode to refresh this list if they change keys.
220
98
  const modelsArray = await generatePollinationsConfig();
221
99
  const modelsObj = {};
222
100
  for (const m of modelsArray) {
@@ -224,19 +102,18 @@ export const PollinationsPlugin = async (ctx) => {
224
102
  }
225
103
  if (!config.provider)
226
104
  config.provider = {};
105
+ // Dynamic Provider Name
227
106
  const version = require('../package.json').version;
228
107
  config.provider['pollinations'] = {
229
- id: 'openai',
108
+ id: 'pollinations',
230
109
  name: `Pollinations AI (v${version})`,
231
- options: {
232
- baseURL: localBaseUrl,
233
- apiKey: 'plugin-managed', // Key is managed by auth hook
234
- },
110
+ options: { baseURL: localBaseUrl },
235
111
  models: modelsObj
236
112
  };
237
113
  log(`[Hook] Registered ${Object.keys(modelsObj).length} models.`);
238
114
  },
239
115
  ...toastHooks,
116
+ ...createToolHooks(ctx.client),
240
117
  ...createStatusHooks(ctx.client),
241
118
  ...commandHooks
242
119
  };
@@ -9,8 +9,10 @@ interface CommandResult {
9
9
  response?: string;
10
10
  error?: string;
11
11
  }
12
+ export declare function setClientForCommands(client: any): void;
12
13
  export declare function handleCommand(command: string): Promise<CommandResult>;
13
14
  export declare function createCommandHooks(): {
14
15
  'tui.command.execute': (input: any, output: any) => Promise<void>;
16
+ 'command.execute.before': (input: any, output: any) => Promise<void>;
15
17
  };
16
18
  export {};
@@ -130,6 +130,10 @@ function calculateCurrentPeriodStats(usage, lastReset, tierLimit) {
130
130
  };
131
131
  }
132
132
  // === COMMAND HANDLER ===
133
+ let globalClient = null;
134
+ export function setClientForCommands(client) {
135
+ globalClient = client;
136
+ }
133
137
  export async function handleCommand(command) {
134
138
  const parts = command.trim().split(/\s+/);
135
139
  if (!parts[0].startsWith('/poll')) {
@@ -150,6 +154,22 @@ export async function handleCommand(command) {
150
154
  return handleConfigCommand(args);
151
155
  case 'help':
152
156
  return handleHelpCommand();
157
+ case 'addKey': // Internal command for UI
158
+ if (globalClient) {
159
+ globalClient.tui.appendPrompt({ value: "/pollinations rmbg_key_add " }); // Using the old command? No, use tool?
160
+ // Wait, tools are not commands.
161
+ // But we removed rmbg_key_add command!
162
+ // We should instruct user to use the tool?
163
+ // Or user types: rmbg_keys { action: "add", key: "..." }
164
+ // That's painful to type.
165
+ // Can we alias a command to a tool call?
166
+ // /pollinations rmbg_key_add is GONE.
167
+ // Let's re-introduce a helper command just for this UI flow?
168
+ // Or better: appendPrompt({ value: 'rmbg_keys { action: "add", key: "' });
169
+ globalClient.tui.appendPrompt({ value: 'rmbg_keys { action: "add", key: "' });
170
+ return { handled: true };
171
+ }
172
+ return { handled: true, error: "TUI not available" };
153
173
  default:
154
174
  return {
155
175
  handled: true,
@@ -303,15 +323,15 @@ async function handleConnectCommand(args) {
303
323
  // 1. Universal Validation (No Syntax Check) - Functional Check
304
324
  emitStatusToast('info', 'Vérification de la clé...', 'Pollinations Config');
305
325
  try {
306
- const models = await generatePollinationsConfig(key);
307
- // 2. Check if we got real models (not just connect placeholder)
308
- const realModels = models.filter(m => m.id !== 'connect');
309
- if (realModels.length > 0) {
326
+ const models = await generatePollinationsConfig(key, true);
327
+ // 2. Check if we got Enterprise models
328
+ const enterpriseModels = models.filter(m => m.id.startsWith('enter/'));
329
+ if (enterpriseModels.length > 0) {
310
330
  // SUCCESS
311
331
  saveConfig({ apiKey: key }); // Don't force mode 'pro'. Let user decide.
312
332
  const masked = key.substring(0, 6) + '...';
313
333
  // Count Paid Only models found
314
- const diamondCount = realModels.filter(m => m.name.includes('💎')).length;
334
+ const diamondCount = enterpriseModels.filter(m => m.name.includes('💎')).length;
315
335
  // CHECK RESTRICTIONS: Strict Check (Usage + Profile + Balance)
316
336
  let forcedModeMsg = "";
317
337
  let isLimited = false;
@@ -336,10 +356,10 @@ async function handleConnectCommand(args) {
336
356
  else {
337
357
  saveConfig({ apiKey: key, keyHasAccessToProfile: true }); // Let user keep current mode or default
338
358
  }
339
- emitStatusToast('success', `Clé Valide! (${realModels.length} modèles débloqués)`, 'Pollinations Config');
359
+ emitStatusToast('success', `Clé Valide! (${enterpriseModels.length} modèles Pro débloqués)`, 'Pollinations Config');
340
360
  return {
341
361
  handled: true,
342
- response: `✅ **Connexion Réussie!**\n- Clé: \`${masked}\`\n- Modèles Débloqués: ${realModels.length} (dont ${diamondCount} 💎 Paid)${forcedModeMsg}`
362
+ response: `✅ **Connexion Réussie!**\n- Clé: \`${masked}\`\n- Modèles Débloqués: ${enterpriseModels.length} (dont ${diamondCount} 💎 Paid)${forcedModeMsg}`
343
363
  };
344
364
  }
345
365
  else {
@@ -351,8 +371,18 @@ async function handleConnectCommand(args) {
351
371
  // Wait, generate-config falls back to providing a list containing "[Enter] GPT-4o (Fallback)" if fetch failed.
352
372
  // So we need to detect if it's a "REAL" fetch or a "FALLBACK" fetch.
353
373
  // The fallback models have `variants: {}` usually, but real ones might too.
354
- // v6.0: No fallback prefix check needed. If models is empty (only connect), key is invalid.
355
- throw new Error("Aucun modèle détecté pour cette clé. Clé invalide ou expirée.");
374
+ // A better check: The fallback list is hardcoded in generate-config.ts catch block.
375
+ // Let's modify generate-config to return EMPTY list on error?
376
+ // Or just check if the returned models work?
377
+ // Simplest: If `generatePollinationsConfig` returns any model starting with `enter/` that includes "(Fallback)" in name, we assume failure?
378
+ // "GPT-4o (Fallback)" is the name.
379
+ const isFallback = models.some(m => m.name.includes('(Fallback)') && m.id.startsWith('enter/'));
380
+ if (isFallback) {
381
+ throw new Error("Clé rejetée par l'API (Accès refusé ou invalide).");
382
+ }
383
+ // If we are here, we got no enter models, or empty list?
384
+ // If key is valid but has no access?
385
+ throw new Error("Aucun modèle Enterprise détecté pour cette clé.");
356
386
  }
357
387
  }
358
388
  catch (e) {
@@ -432,17 +462,21 @@ function handleConfigCommand(args) {
432
462
  }
433
463
  function handleHelpCommand() {
434
464
  const help = `
435
- ### 🌸 Pollinations Plugin - Commandes V5
465
+ ### 🌸 Pollinations Plugin - Commandes V6
436
466
 
467
+ **Mode & Usage**
437
468
  - **\`/pollinations mode [mode]\`**: Change le mode (manual, alwaysfree, pro).
438
469
  - **\`/pollinations usage [full]\`**: Affiche le dashboard (full = détail).
439
- - **\`/pollinations fallback <main> [agent]\`**: Configure le Safety Net (Free).
470
+ - **\`/pollinations fallback <main> [agent]\`**: Configure le Safety Net.
471
+
472
+ **Configuration**
440
473
  - **\`/pollinations config [key] [value]\`**:
441
- - \`status_gui\`: none, alert, all (Status Dashboard).
442
- - \`logs_gui\`: none, error, verbose (Logs Techniques).
443
- - \`threshold_tier\`: 0-100 (Alerte %).
444
- - \`threshold_wallet\`: 0-100 (Safety Net %).
445
- - \`status_bar\`: true/false (Widget).
474
+ - \`status_gui\`: none, alert, all
475
+ - \`logs_gui\`: none, error, verbose
476
+ - \`threshold_tier\` / \`threshold_wallet\`: 0-100
477
+ - \`status_bar\`: true/false
478
+
479
+ > 💡 **RMBG keys**: Use the \`rmbg_keys\` tool (works with any model).
446
480
  `.trim();
447
481
  return { handled: true, response: help };
448
482
  }
@@ -450,16 +484,44 @@ function handleHelpCommand() {
450
484
  export function createCommandHooks() {
451
485
  return {
452
486
  'tui.command.execute': async (input, output) => {
453
- const result = await handleCommand(input.command);
454
- if (result.handled) {
455
- output.handled = true;
456
- if (result.response) {
457
- output.response = result.response;
458
- }
459
- if (result.error) {
460
- output.error = result.error;
487
+ if (!input.command.startsWith('/pollinations')) {
488
+ return;
489
+ }
490
+ try {
491
+ // Parse command
492
+ const rawArgs = input.command.replace('/pollinations', '').trim();
493
+ const result = await handleCommand(rawArgs);
494
+ if (result.handled) {
495
+ if (result.error) {
496
+ output.error = `❌ **Erreur:** ${result.error}`;
497
+ }
498
+ else if (result.response) {
499
+ output.response = result.response;
500
+ }
501
+ // If no response and no error, assume handled silently (like appendPrompt)
461
502
  }
462
503
  }
504
+ catch (err) {
505
+ output.error = `❌ **Erreur Critique:** ${err.message}`;
506
+ }
507
+ },
508
+ // Hook for UI Commands (Palette / Buttons)
509
+ 'command.execute.before': async (input, output) => {
510
+ const cmd = input.command;
511
+ if (cmd === 'pollinations.addKey') {
512
+ handleCommand('addKey'); // Trigger UI helper
513
+ }
514
+ else if (cmd === 'pollinations.usage') {
515
+ const res = await handleCommand('usage');
516
+ // For native commands, we might need a different way to show output if not in chat context
517
+ // But here we are likely in a context where we can't easily hijack output unless we use a toast or open a file?
518
+ // Let's use toast for simple feedback or appendPrompt for complex interaction.
519
+ if (res.response)
520
+ globalClient?.tui.showToast({ title: "Pollinations Usage", metadata: { type: 'info', message: "Usage info sent to chat/logs" } });
521
+ }
522
+ else if (cmd === 'pollinations.mode') {
523
+ globalClient?.tui.appendPrompt({ value: "/pollinations mode " });
524
+ }
463
525
  }
464
526
  };
465
527
  }
@@ -1,34 +1,8 @@
1
- /**
2
- * generate-config.ts - v6.0 Simplified
3
- *
4
- * Single endpoint: gen.pollinations.ai/text/models
5
- * No more Free tier, no cache ETag, no prefixes
6
- */
7
- export interface PollinationsModel {
8
- name: string;
9
- description?: string;
10
- type?: string;
11
- tools?: boolean;
12
- reasoning?: boolean;
13
- context?: number;
14
- context_window?: number;
15
- input_modalities?: string[];
16
- output_modalities?: string[];
17
- paid_only?: boolean;
18
- vision?: boolean;
19
- audio?: boolean;
20
- pricing?: {
21
- promptTextTokens?: number;
22
- completionTextTokens?: number;
23
- promptImageTokens?: number;
24
- promptAudioTokens?: number;
25
- completionAudioTokens?: number;
26
- };
27
- [key: string]: any;
28
- }
29
1
  interface OpenCodeModel {
30
2
  id: string;
31
3
  name: string;
4
+ object: string;
5
+ variants?: any;
32
6
  options?: any;
33
7
  limit?: {
34
8
  context?: number;
@@ -38,7 +12,6 @@ interface OpenCodeModel {
38
12
  input?: string[];
39
13
  output?: string[];
40
14
  };
41
- tool_call?: boolean;
42
15
  }
43
- export declare function generatePollinationsConfig(forceApiKey?: string): Promise<OpenCodeModel[]>;
16
+ export declare function generatePollinationsConfig(forceApiKey?: string, forceStrict?: boolean): Promise<OpenCodeModel[]>;
44
17
  export {};