opencode-pollinations-plugin 5.1.23 → 5.2.1

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
@@ -116,17 +116,16 @@ Just type in the chat. You are in **Manual Mode** by default.
116
116
  - Model: `openai` (GPT-4o Mini equivalent)
117
117
  - Model: `mistral` (Mistral Nemo)
118
118
 
119
- ### 2. Going Pro
120
- 1. Get your API Key from [pollinations.ai](https://pollinations.ai).
121
- 2. Run command:
122
- ```bash
123
- /pollinations config apiKey sk_YourSecretKey
124
- ```
125
- 3. Set mode to Pro:
126
- ```bash
127
- /pollinations mode pro
128
- ```
129
- 4. **Reload Window**.
119
+ ### 🔑 Configuration (API Key)
120
+
121
+ 1. Run the setup command:
122
+ ```bash
123
+ /connect
124
+ ```
125
+ 2. Choose "pollinations" and enter your key if you have one (or leave blank for free tier).
126
+ 3. **IMPORTANT**: You must **restart OpenCode** for the model list to update with your new tier (e.g. to see Paid models).
127
+
128
+ ### 🤖 Models
130
129
 
131
130
  ## 🔗 Links
132
131
 
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import * as http from 'http';
2
2
  import * as fs from 'fs';
3
3
  import { execSync } from 'child_process';
4
4
  import { generatePollinationsConfig } from './server/generate-config.js';
5
- import { loadConfig, updateCache } from './server/config.js';
5
+ import { loadConfig } from './server/config.js';
6
6
  import { handleChatCompletion } from './server/proxy.js';
7
7
  import { createToastHooks, setGlobalClient } from './server/toast.js';
8
8
  import { createStatusHooks } from './server/status.js';
@@ -42,13 +42,11 @@ const startProxy = () => {
42
42
  res.writeHead(200, { 'Content-Type': 'application/json' });
43
43
  res.end(JSON.stringify({
44
44
  status: "ok",
45
- version: "v4.0.5",
45
+ version: "v5.2.0",
46
46
  mode: config.mode
47
47
  }));
48
48
  return;
49
49
  }
50
- // SUPPORT FLEXIBLE DES PATHS
51
- // Le SDK peut envoyer /v1/chat/completions ou juste /chat/completions
52
50
  if (req.method === 'POST' && (req.url === '/v1/chat/completions' || req.url === '/chat/completions')) {
53
51
  const chunks = [];
54
52
  req.on('data', chunk => chunks.push(chunk));
@@ -72,7 +70,7 @@ const startProxy = () => {
72
70
  res.end("Not Found");
73
71
  });
74
72
  server.listen(TRACKING_PORT, '127.0.0.1', () => {
75
- log(`[Proxy] Started V4 on port ${TRACKING_PORT}`);
73
+ log(`[Proxy] Started V5.2 on port ${TRACKING_PORT}`);
76
74
  resolve(TRACKING_PORT);
77
75
  });
78
76
  server.on('error', (e) => {
@@ -83,52 +81,35 @@ const startProxy = () => {
83
81
  };
84
82
  // === PLUGIN EXPORT ===
85
83
  export const PollinationsPlugin = async (ctx) => {
86
- log("Plugin Initializing V4.0.5 (Path Fix)...");
87
- try {
88
- log(`[DEBUG] CTX Keys: ${Object.keys(ctx).join(', ')}`);
89
- }
90
- catch (e) {
91
- log(`[DEBUG] Error inspecting ctx: ${e}`);
92
- }
84
+ log("Plugin Initializing V5.2.0 (Stable)...");
85
+ // START PROXY
93
86
  const port = await startProxy();
94
- // IMPORTANT: On ajoute /v1 à la base URL pour guider le SDK,
95
- // mais le proxy accepte aussi sans.
96
87
  const localBaseUrl = `http://127.0.0.1:${port}/v1`;
97
- setGlobalClient(ctx.client); // REGISTER CLIENT FOR IMMEDIATE TOASTS
88
+ setGlobalClient(ctx.client);
98
89
  const toastHooks = createToastHooks(ctx.client);
99
90
  const commandHooks = createCommandHooks();
100
91
  return {
101
92
  async config(config) {
102
93
  log("[Hook] config() called");
103
- // Extract API Key from incoming config to ensure Hot Reload
104
- const incomingKey = config.provider?.pollinations?.options?.apiKey ||
105
- config.provider?.pollinations_enter?.options?.apiKey;
106
- if (incomingKey) {
107
- log(`[Hook] Detected API Key update.`);
108
- updateCache({ apiKey: incomingKey, mode: 'pro' });
109
- }
110
- const modelsArray = await generatePollinationsConfig(incomingKey);
94
+ // STARTUP only - No complex hot reload logic
95
+ // The user must restart OpenCode to refresh this list if they change keys.
96
+ const modelsArray = await generatePollinationsConfig();
111
97
  const modelsObj = {};
112
98
  for (const m of modelsArray) {
113
- // Ensure ID is relative for mapping ("free/gemini")
114
- // BUT Provider needs full ID ? No, the object key is the relative ID
115
- // OpenCode: provider.models[id]
116
- // id comes from generatePollinationsConfig which returns "free/gemini"
117
- // So modelsObj["free/gemini"] = ... matches.
118
99
  modelsObj[m.id] = m;
119
100
  }
120
101
  if (!config.provider)
121
102
  config.provider = {};
122
103
  config.provider['pollinations'] = {
123
104
  id: 'pollinations',
124
- name: 'Pollinations V5.1 (Native)',
105
+ name: 'Pollinations V5.2 (Native)',
125
106
  options: { baseURL: localBaseUrl },
126
107
  models: modelsObj
127
108
  };
128
109
  log(`[Hook] Registered ${Object.keys(modelsObj).length} models.`);
129
110
  },
130
111
  ...toastHooks,
131
- ...createStatusHooks(ctx.client), // New Status Bar Logic
112
+ ...createStatusHooks(ctx.client),
132
113
  ...commandHooks
133
114
  };
134
115
  };
@@ -21,11 +21,8 @@ export interface PollinationsConfigV5 {
21
21
  };
22
22
  enablePaidTools: boolean;
23
23
  statusBar: boolean;
24
- _loadedAt?: number;
25
24
  }
26
- export declare function subscribeToConfigChange(callback: () => void): void;
27
25
  export declare function loadConfig(): PollinationsConfigV5;
28
- export declare function updateCache(newConfig: Partial<PollinationsConfigV5>): void;
29
26
  export declare function saveConfig(updates: Partial<PollinationsConfigV5>): {
30
27
  version: string;
31
28
  mode: "manual" | "alwaysfree" | "pro";
@@ -49,5 +46,4 @@ export declare function saveConfig(updates: Partial<PollinationsConfigV5>): {
49
46
  };
50
47
  enablePaidTools: boolean;
51
48
  statusBar: boolean;
52
- _loadedAt?: number;
53
49
  };
@@ -9,7 +9,7 @@ const CONFIG_DIR_OPENCODE = path.join(HOMEDIR, '.config', 'opencode');
9
9
  const OPENCODE_CONFIG_FILE = path.join(CONFIG_DIR_OPENCODE, 'opencode.json');
10
10
  const AUTH_FILE = path.join(HOMEDIR, '.local', 'share', 'opencode', 'auth.json');
11
11
  // LOAD PACKAGE VERSION
12
- let PKG_VERSION = '5.0.0';
12
+ let PKG_VERSION = '5.2.0';
13
13
  try {
14
14
  const pkgPath = path.join(__dirname, '../../package.json');
15
15
  if (fs.existsSync(pkgPath)) {
@@ -21,27 +21,15 @@ catch (e) { }
21
21
  const DEFAULT_CONFIG_V5 = {
22
22
  version: PKG_VERSION,
23
23
  mode: 'manual',
24
- gui: {
25
- status: 'alert',
26
- logs: 'none'
27
- },
28
- thresholds: {
29
- tier: 10, // Alert if < 10%
30
- wallet: 5 // Switch if < 5% (Wallet Protection)
31
- },
24
+ gui: { status: 'alert', logs: 'none' },
25
+ thresholds: { tier: 10, wallet: 5 },
32
26
  fallbacks: {
33
- free: {
34
- main: 'free/mistral', // Fallback gratuit solide
35
- agent: 'free/openai-fast' // Agent gratuit rapide
36
- },
37
- enter: {
38
- agent: 'free/gemini' // Agent de secours (Free Gemini)
39
- }
27
+ free: { main: 'free/mistral', agent: 'free/openai-fast' },
28
+ enter: { agent: 'free/gemini' }
40
29
  },
41
30
  enablePaidTools: false,
42
31
  statusBar: true
43
32
  };
44
- // Debug Helper
45
33
  function logConfig(msg) {
46
34
  try {
47
35
  if (!fs.existsSync('/tmp/opencode_pollinations_config_debug.log')) {
@@ -51,95 +39,20 @@ function logConfig(msg) {
51
39
  }
52
40
  catch (e) { }
53
41
  }
54
- // CACHE & WATCHER
55
- let cachedConfig = null;
56
- const listeners = [];
57
- export function subscribeToConfigChange(callback) {
58
- listeners.push(callback);
59
- }
60
- function notifyListeners() {
61
- listeners.forEach(cb => {
62
- try {
63
- cb();
64
- }
65
- catch (e) {
66
- logConfig(`Listener Error: ${e}`);
67
- }
68
- });
69
- }
70
- // Watchers (Debounced)
71
- const watchedFiles = new Set();
72
- function watchFileSafe(filePath) {
73
- if (watchedFiles.has(filePath))
74
- return;
75
- try {
76
- if (!fs.existsSync(filePath))
77
- return;
78
- fs.watchFile(filePath, { interval: 2000 }, (curr, prev) => {
79
- if (curr.mtime !== prev.mtime) {
80
- logConfig(`File Changed: ${path.basename(filePath)}. Reloading Config...`);
81
- cachedConfig = readConfigFromDisk();
82
- notifyListeners();
83
- }
84
- });
85
- watchedFiles.add(filePath);
86
- }
87
- catch (e) {
88
- logConfig(`Watch Error for ${filePath}: ${e}`);
89
- }
90
- }
42
+ // SIMPLE LOAD (Direct Disk Read - No Caching, No Watchers)
43
+ // This ensures the Proxy ALWAYS sees the latest state from auth.json
91
44
  export function loadConfig() {
92
- // 1. Return Cache if available
93
- if (cachedConfig)
94
- return cachedConfig;
95
- // 2. Or Read Fresh
96
- cachedConfig = readConfigFromDisk();
97
- // 3. Initiate Watchers (Lazy)
98
- watchFileSafe(CONFIG_FILE);
99
- watchFileSafe(AUTH_FILE);
100
- watchFileSafe(OPENCODE_CONFIG_FILE);
101
- // SMART CACHE: Check mtime to ensure freshness (Hot Reload Fix)
102
- try {
103
- if (fs.existsSync(AUTH_FILE)) {
104
- const stats = fs.statSync(AUTH_FILE);
105
- // If cache is null or file is newer than our last load
106
- if (!cachedConfig || !cachedConfig._loadedAt || stats.mtimeMs > cachedConfig._loadedAt) {
107
- logConfig(`[SmartCache] Auth file changed (mtime). Reloading...`);
108
- cachedConfig = readConfigFromDisk();
109
- }
110
- }
111
- }
112
- catch (e) {
113
- logConfig(`[SmartCache] Error checking mtime: ${e}`);
114
- }
115
- return cachedConfig;
45
+ return readConfigFromDisk();
116
46
  }
117
- export function updateCache(newConfig) {
118
- const current = loadConfig();
119
- cachedConfig = { ...current, ...newConfig, _loadedAt: Date.now() }; // Update timestamp
120
- logConfig(`Cache Force-Updated via Hook. Mode: ${cachedConfig.mode}`);
121
- }
122
- // INTERNAL READER (The old loadConfig logic)
123
47
  function readConfigFromDisk() {
124
48
  let config = { ...DEFAULT_CONFIG_V5 };
125
49
  let keyFound = false;
126
- // 1. Try Custom Config
50
+ // 1. Custom Config
127
51
  try {
128
52
  if (fs.existsSync(CONFIG_FILE)) {
129
53
  const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
130
54
  const custom = JSON.parse(raw);
131
- // ... (Migration Logic is handled on Save, we trust Disk content here mostly)
132
- if (!custom.version || custom.version < 5) {
133
- // If old, we don't migrate inside read to avoid write-loops.
134
- // We just read what we can.
135
- if (custom.apiKey)
136
- config.apiKey = custom.apiKey;
137
- if (custom.mode)
138
- config.mode = custom.mode;
139
- }
140
- else {
141
- config = { ...config, ...custom };
142
- }
55
+ config = { ...config, ...custom };
143
56
  if (config.apiKey)
144
57
  keyFound = true;
145
58
  }
@@ -147,13 +60,12 @@ function readConfigFromDisk() {
147
60
  catch (e) {
148
61
  logConfig(`Error loading config: ${e}`);
149
62
  }
150
- // 2. Try Native Auth Storage (Recovery)
63
+ // 2. Auth Store (Priority)
151
64
  if (!keyFound) {
152
65
  try {
153
66
  if (fs.existsSync(AUTH_FILE)) {
154
67
  const raw = fs.readFileSync(AUTH_FILE, 'utf-8');
155
68
  const authData = JSON.parse(raw);
156
- // Supports flat key or nested object
157
69
  const entry = authData['pollinations'] || authData['pollinations_enter'] || authData['pollinations_api_key'];
158
70
  if (entry) {
159
71
  const key = (typeof entry === 'object' && entry.key) ? entry.key : entry;
@@ -161,7 +73,6 @@ function readConfigFromDisk() {
161
73
  config.apiKey = key;
162
74
  config.mode = 'pro';
163
75
  keyFound = true;
164
- logConfig(`Hot-Loaded API Key from Auth Store`);
165
76
  }
166
77
  }
167
78
  }
@@ -170,7 +81,7 @@ function readConfigFromDisk() {
170
81
  logConfig(`Error reading auth.json: ${e}`);
171
82
  }
172
83
  }
173
- // 3. Try OpenCode Config (Fallback)
84
+ // 3. OpenCode Config (Fallback)
174
85
  if (!keyFound) {
175
86
  try {
176
87
  if (fs.existsSync(OPENCODE_CONFIG_FILE)) {
@@ -187,22 +98,19 @@ function readConfigFromDisk() {
187
98
  }
188
99
  catch (e) { }
189
100
  }
190
- // Default mode correction
191
101
  if (!keyFound && config.mode === 'pro') {
192
102
  config.mode = 'manual';
193
103
  }
194
- return { ...config, version: PKG_VERSION, _loadedAt: Date.now() };
104
+ return { ...config, version: PKG_VERSION };
195
105
  }
196
106
  export function saveConfig(updates) {
197
107
  try {
198
- // We must base updates on current state (even if cached is slightly old, we refresh first?)
199
- const current = readConfigFromDisk(); // Read disk for safety before write
108
+ const current = readConfigFromDisk();
200
109
  const updated = { ...current, ...updates, version: PKG_VERSION };
201
110
  if (!fs.existsSync(CONFIG_DIR_POLLI)) {
202
111
  fs.mkdirSync(CONFIG_DIR_POLLI, { recursive: true });
203
112
  }
204
113
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2));
205
- cachedConfig = updated; // Update Cache immediately
206
114
  return updated;
207
115
  }
208
116
  catch (e) {
@@ -2,18 +2,9 @@ import * as http from 'http';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { getAggregatedModels } from './pollinations-api.js';
5
- import { loadConfig, saveConfig, subscribeToConfigChange } from './config.js';
5
+ import { loadConfig, saveConfig } from './config.js';
6
6
  import { handleChatCompletion } from './proxy.js';
7
- import { emitStatusToast } from './toast.js';
8
7
  const LOG_FILE = path.join(process.env.HOME || '/tmp', '.config/opencode/plugins/pollinations-v3.log');
9
- // Hot Reload Listener
10
- subscribeToConfigChange(() => {
11
- const config = loadConfig();
12
- const mode = config.mode.toUpperCase();
13
- const hasKey = !!config.apiKey;
14
- log(`[HOT RELOAD] Config Updated. Mode: ${mode}, HasKey: ${hasKey}`);
15
- emitStatusToast('info', `Configuration Updated (Hot Reload) | Mode: ${mode}`, 'Pollinations System');
16
- });
17
8
  // Simple file logger
18
9
  function log(msg) {
19
10
  const ts = new Date().toISOString();
@@ -228,12 +228,12 @@ export async function handleChatCompletion(req, res, bodyRaw) {
228
228
  isFallbackActive = true;
229
229
  fallbackReason = "Quota Unreachable (Safety)";
230
230
  }
231
- else if (quota.tierRemaining <= 0.1) {
232
- log(`[SafetyNet] AlwaysFree Mode: Daily Tier Empty. Switching to Free Fallback.`);
231
+ else if (quota.tierRemaining <= (config.thresholds.tier / 100)) {
232
+ log(`[SafetyNet] AlwaysFree Mode: Tier threshold (${config.thresholds.tier}%) reached. Switching to Free Fallback.`);
233
233
  actualModel = config.fallbacks.free.main.replace('free/', '');
234
234
  isEnterprise = false;
235
235
  isFallbackActive = true;
236
- fallbackReason = "Daily Tier Empty (Wallet Protected)";
236
+ fallbackReason = `Daily Tier < ${config.thresholds.tier}% (Wallet Protected)`;
237
237
  }
238
238
  }
239
239
  }
@@ -246,12 +246,12 @@ export async function handleChatCompletion(req, res, bodyRaw) {
246
246
  isFallbackActive = true;
247
247
  fallbackReason = "Quota Unreachable (Safety)";
248
248
  }
249
- else if (quota.walletBalance < 0.10 && quota.tierRemaining <= 0.1) {
250
- log(`[SafetyNet] Pro Mode: Wallet Critical (<$0.10). Switching to Free Fallback.`);
249
+ else if (quota.walletBalance < config.thresholds.wallet && quota.tierRemaining <= 0.1) { // Tier is loose here, wallet is primary
250
+ log(`[SafetyNet] Pro Mode: Wallet Critical (<$${config.thresholds.wallet}). Switching to Free Fallback.`);
251
251
  actualModel = config.fallbacks.free.main.replace('free/', '');
252
252
  isEnterprise = false;
253
253
  isFallbackActive = true;
254
- fallbackReason = "Wallet Critical";
254
+ fallbackReason = `Wallet < $${config.thresholds.wallet}`;
255
255
  }
256
256
  }
257
257
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-pollinations-plugin",
3
3
  "displayName": "Pollinations AI (V5.1)",
4
- "version": "5.1.23",
4
+ "version": "5.2.1",
5
5
  "description": "Native Pollinations.ai Provider Plugin for OpenCode",
6
6
  "publisher": "pollinations",
7
7
  "repository": {