opencode-pollinations-plugin 5.8.2 → 5.8.4-beta.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.
@@ -5,7 +5,17 @@ import * as path from 'path';
5
5
  import { loadConfig } from './config.js';
6
6
  const HOMEDIR = os.homedir();
7
7
  const CONFIG_DIR_POLLI = path.join(HOMEDIR, '.pollinations');
8
- const CONFIG_FILE = path.join(CONFIG_DIR_POLLI, 'config.json');
8
+ const CACHE_FILE = path.join(CONFIG_DIR_POLLI, 'models-cache.json');
9
+ // --- CONSTANTS ---
10
+ // Seed from _archs/debug_free.json
11
+ const DEFAULT_FREE_MODELS = [
12
+ { "name": "gemini", "description": "Gemini 2.5 Flash Lite", "tier": "anonymous", "tools": true, "input_modalities": ["text", "image"], "output_modalities": ["text"], "vision": true },
13
+ { "name": "mistral", "description": "Mistral Small 3.2 24B", "tier": "anonymous", "tools": true, "input_modalities": ["text"], "output_modalities": ["text"], "vision": false },
14
+ { "name": "openai-fast", "description": "GPT-OSS 20B Reasoning LLM (OVH)", "tier": "anonymous", "tools": true, "input_modalities": ["text"], "output_modalities": ["text"], "vision": false, "reasoning": true },
15
+ { "name": "bidara", "description": "BIDARA (Biomimetic Designer)", "tier": "anonymous", "community": true, "input_modalities": ["text", "image"], "output_modalities": ["text"], "vision": true },
16
+ { "name": "chickytutor", "description": "ChickyTutor AI Language Tutor", "tier": "anonymous", "community": true, "input_modalities": ["text"], "output_modalities": ["text"] },
17
+ { "name": "midijourney", "description": "MIDIjourney", "tier": "anonymous", "community": true, "input_modalities": ["text"], "output_modalities": ["text"] }
18
+ ];
9
19
  // --- LOGGING ---
10
20
  const LOG_FILE = '/tmp/opencode_pollinations_config.log';
11
21
  function log(msg) {
@@ -16,27 +26,41 @@ function log(msg) {
16
26
  fs.appendFileSync(LOG_FILE, `[ConfigGen] ${ts} ${msg}\n`);
17
27
  }
18
28
  catch (e) { }
19
- // Force output to stderr for CLI visibility if needed, but clean.
20
29
  }
21
- // Fetch Helper
22
- // Fetch Helper
30
+ // --- NETWORK HELPER ---
31
+ function fetchHead(url) {
32
+ return new Promise((resolve) => {
33
+ // Use Node.js native https check for minimal overhead
34
+ const req = https.request(url, { method: 'HEAD', timeout: 5000 }, (res) => {
35
+ resolve(res.headers['etag'] || null);
36
+ });
37
+ req.on('error', () => resolve(null));
38
+ req.on('timeout', () => { req.destroy(); resolve(null); });
39
+ req.end();
40
+ });
41
+ }
23
42
  function fetchJson(url, headers = {}) {
24
43
  return new Promise((resolve, reject) => {
25
44
  const finalHeaders = {
26
45
  ...headers,
27
- 'User-Agent': 'Mozilla/5.0 (compatible; OpenCode/5.8.1; +https://opencode.ai)'
46
+ 'User-Agent': 'Mozilla/5.0 (compatible; OpenCode/5.8.4; +https://opencode.ai)'
28
47
  };
29
48
  const req = https.get(url, { headers: finalHeaders }, (res) => {
49
+ const etag = res.headers['etag'];
30
50
  let data = '';
31
51
  res.on('data', chunk => data += chunk);
32
52
  res.on('end', () => {
33
53
  try {
34
54
  const json = JSON.parse(data);
55
+ // HACK: Attach ETag to the object to pass it up
56
+ if (etag && typeof json === 'object') {
57
+ Object.defineProperty(json, '_etag', { value: etag, enumerable: false, writable: true });
58
+ }
35
59
  resolve(json);
36
60
  }
37
61
  catch (e) {
38
62
  log(`JSON Parse Error for ${url}: ${e}`);
39
- resolve([]); // Fail safe -> empty list
63
+ resolve([]); // Fail safe
40
64
  }
41
65
  });
42
66
  });
@@ -50,167 +74,160 @@ function fetchJson(url, headers = {}) {
50
74
  });
51
75
  });
52
76
  }
53
- function formatName(id, censored = true) {
54
- let clean = id.replace(/^pollinations\//, '').replace(/-/g, ' ');
55
- clean = clean.replace(/\b\w/g, l => l.toUpperCase());
56
- if (!censored)
57
- clean += " (Uncensored)";
58
- return clean;
77
+ function loadCache() {
78
+ try {
79
+ if (fs.existsSync(CACHE_FILE)) {
80
+ const content = fs.readFileSync(CACHE_FILE, 'utf-8');
81
+ return JSON.parse(content);
82
+ }
83
+ }
84
+ catch (e) {
85
+ log(`Error loading cache: ${e}`);
86
+ }
87
+ return null;
59
88
  }
60
- // --- MAIN GENERATOR logic ---
89
+ function saveCache(models, etag) {
90
+ try {
91
+ const data = {
92
+ timestamp: Date.now(),
93
+ etag: etag,
94
+ models: models
95
+ };
96
+ if (!fs.existsSync(CONFIG_DIR_POLLI))
97
+ fs.mkdirSync(CONFIG_DIR_POLLI, { recursive: true });
98
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
99
+ }
100
+ catch (e) {
101
+ log(`Error saving cache: ${e}`);
102
+ }
103
+ }
104
+ // --- GENERATOR LOGIC ---
61
105
  export async function generatePollinationsConfig(forceApiKey, forceStrict = false) {
62
106
  const config = loadConfig();
63
107
  const modelsOutput = [];
64
- log(`Starting Configuration (v5.8.2)...`);
65
- // Use forced key (from Hook) or cached key
108
+ log(`Starting Configuration (v5.8.4-Beta1)...`);
66
109
  const effectiveKey = forceApiKey || config.apiKey;
67
- // 1. FREE UNIVERSE
68
- try {
69
- // Switch to main models endpoint
70
- const freeList = await fetchJson('https://text.pollinations.ai/models');
71
- const list = Array.isArray(freeList) ? freeList : (freeList.data || []);
72
- if (list.length > 0) {
73
- list.forEach((m) => {
74
- const mapped = mapModel(m, 'free/', '[Free] ');
75
- modelsOutput.push(mapped);
76
- });
77
- log(`Fetched ${modelsOutput.length} Free models.`);
110
+ // 1. FREE UNIVERSE (Smart Cache System)
111
+ let freeModelsList = [];
112
+ let isOffline = false;
113
+ let cache = loadCache();
114
+ const CACHE_TTL = 7 * 24 * 3600 * 1000; // 7 days
115
+ // Decision Logic
116
+ const now = Date.now();
117
+ let shouldFetch = !cache || (now - cache.timestamp > CACHE_TTL);
118
+ // ETag Check: If cache is valid but we want to be proactive
119
+ if (!shouldFetch && cache && cache.etag) {
120
+ try {
121
+ log('Smart Refresh: Checking for updates (HEAD)...');
122
+ const remoteEtag = await fetchHead('https://text.pollinations.ai/models');
123
+ if (remoteEtag && remoteEtag !== cache.etag) {
124
+ log(`Update Detected! (Remote: ${remoteEtag} != Local: ${cache.etag}). Forcing refresh.`);
125
+ shouldFetch = true;
126
+ }
127
+ else {
128
+ log('Cache is clean (ETag match). No refresh needed.');
129
+ }
78
130
  }
79
- else {
80
- throw new Error('Empty list returned from Free API');
131
+ catch (e) {
132
+ log(`Smart Refresh check failed: ${e}. Ignoring.`);
81
133
  }
82
134
  }
83
- catch (e) {
84
- log(`Error fetching Free models: ${e}`);
85
- // Fallback Robust (Offline support) - NOW WITH ICONS
86
- modelsOutput.push({
87
- id: "free/mistral",
88
- name: "[Free] Mistral Nemo (Fallback) 💻",
89
- object: "model",
90
- variants: { safe_tokens: { options: { maxTokens: 8000 } } },
91
- modalities: { input: ['text'], output: ['text'] }
92
- });
93
- modelsOutput.push({
94
- id: "free/openai",
95
- name: "[Free] OpenAI (Fallback) 👁️💻",
96
- object: "model",
97
- variants: {},
98
- modalities: { input: ['text', 'image'], output: ['text'] }
99
- });
100
- modelsOutput.push({
101
- id: "free/gemini",
102
- name: "[Free] Gemini Flash (Fallback) 👁️💻",
103
- object: "model",
104
- variants: { high_reasoning: { options: { reasoningEffort: "high", budgetTokens: 16000 } } },
105
- modalities: { input: ['text', 'image'], output: ['text'] }
106
- });
107
- modelsOutput.push({
108
- id: "free/searchgpt",
109
- name: "[Free] SearchGPT (Fallback) 🔍",
110
- object: "model",
111
- variants: {},
112
- modalities: { input: ['text'], output: ['text'] }
113
- });
135
+ if (shouldFetch) {
136
+ log('Fetching fresh Free models...');
137
+ try {
138
+ const raw = await fetchJson('https://text.pollinations.ai/models');
139
+ const list = Array.isArray(raw) ? raw : (raw.data || []);
140
+ const newEtag = raw._etag; // Get hidden ETag
141
+ if (list.length > 0) {
142
+ freeModelsList = list;
143
+ saveCache(list, newEtag);
144
+ log(`Fetched and cached ${list.length} models (ETag: ${newEtag || 'N/A'}).`);
145
+ }
146
+ else {
147
+ throw new Error('API returned empty list');
148
+ }
149
+ }
150
+ catch (e) {
151
+ log(`Fetch failed: ${e}.`);
152
+ isOffline = true;
153
+ // Fallback Logic
154
+ if (cache && cache.models.length > 0) {
155
+ log('Using cached models (Offline).');
156
+ freeModelsList = cache.models;
157
+ }
158
+ else {
159
+ log('Using DEFAULT SEED models (Offline + No Cache).');
160
+ freeModelsList = DEFAULT_FREE_MODELS;
161
+ }
162
+ }
114
163
  }
115
- // 1.5 FORCE ENSURE CRITICAL MODELS
116
- // Sometimes the API list changes or is cached weirdly. We force vital models.
117
- const hasGemini = modelsOutput.find(m => m.id === 'free/gemini');
118
- if (!hasGemini) {
119
- log(`[ConfigGen] Force-injecting free/gemini.`);
120
- modelsOutput.push({
121
- id: "free/gemini",
122
- name: "[Free] Gemini Flash (Force) 👁️💻",
123
- object: "model",
124
- variants: {},
125
- modalities: { input: ['text', 'image'], output: ['text'] }
126
- });
164
+ else {
165
+ log('Using cached models (Skipped fetch).');
166
+ freeModelsList = cache.models;
127
167
  }
128
- // ALIAS Removed for Clean Config
129
- // const hasGeminiAlias = modelsOutput.find(m => m.id === 'pollinations/free/gemini');
130
- // if (!hasGeminiAlias) {
131
- // modelsOutput.push({ id: "pollinations/free/gemini", name: "[Free] Gemini Flash (Alias)", object: "model", variants: {} });
132
- // }
168
+ // Map Free Models
169
+ freeModelsList.forEach((m) => {
170
+ // Tag (Offline) only if we explicitly failed a fetch attempt or are using Fallback SEED when fetch failed.
171
+ // If we use cache because it's valid (Skipped fetch), we don't tag (Offline).
172
+ const suffix = isOffline ? ' (Offline)' : '';
173
+ const mapped = mapModel(m, 'free/', `[Free] `, suffix);
174
+ modelsOutput.push(mapped);
175
+ });
133
176
  // 2. ENTERPRISE UNIVERSE
134
177
  if (effectiveKey && effectiveKey.length > 5 && effectiveKey !== 'dummy') {
135
178
  try {
136
- // Use /text/models for full metadata (input_modalities, tools, reasoning, pricing)
137
179
  const enterListRaw = await fetchJson('https://gen.pollinations.ai/text/models', {
138
180
  'Authorization': `Bearer ${effectiveKey}`
139
181
  });
140
182
  const enterList = Array.isArray(enterListRaw) ? enterListRaw : (enterListRaw.data || []);
141
- const paidModels = [];
142
183
  enterList.forEach((m) => {
143
184
  if (m.tools === false)
144
185
  return;
145
186
  const mapped = mapModel(m, 'enter/', '[Enter] ');
146
187
  modelsOutput.push(mapped);
147
- if (m.paid_only) {
148
- paidModels.push(mapped.id.replace('enter/', '')); // Store bare ID "gemini-large"
149
- }
150
188
  });
151
189
  log(`Total models (Free+Pro): ${modelsOutput.length}`);
152
- // Save Paid Models List for Proxy
153
- try {
154
- const paidListPath = path.join(config.gui ? path.dirname(CONFIG_FILE) : '/tmp', 'pollinations-paid-models.json');
155
- // Ensure dir exists (re-use config dir logic from config.ts if possible, or just assume it exists since config loaded)
156
- if (fs.existsSync(path.dirname(paidListPath))) {
157
- fs.writeFileSync(paidListPath, JSON.stringify(paidModels));
158
- }
159
- }
160
- catch (e) {
161
- log(`Error saving paid models list: ${e}`);
162
- }
163
190
  }
164
191
  catch (e) {
165
192
  log(`Error fetching Enterprise models: ${e}`);
166
- // STRICT MODE (Validation): Do not return fake fallback models.
167
193
  if (forceStrict)
168
194
  throw e;
169
- // Fallback Robust for Enterprise (User has Key but discovery failed)
170
195
  modelsOutput.push({ id: "enter/gpt-4o", name: "[Enter] GPT-4o (Fallback)", object: "model", variants: {} });
171
- // ...
172
- modelsOutput.push({ id: "enter/claude-3-5-sonnet", name: "[Enter] Claude 3.5 Sonnet (Fallback)", object: "model", variants: {} });
173
- modelsOutput.push({ id: "enter/deepseek-reasoner", name: "[Enter] DeepSeek R1 (Fallback)", object: "model", variants: {} });
174
196
  }
175
197
  }
176
198
  return modelsOutput;
177
199
  }
178
- // --- CAPABILITY ICONS ---
200
+ // --- UTILS ---
179
201
  function getCapabilityIcons(raw) {
180
202
  const icons = [];
181
- // Vision: accepts images
182
- if (raw.input_modalities?.includes('image'))
203
+ if (raw.input_modalities?.includes('image') || raw.vision === true)
183
204
  icons.push('👁️');
184
- // Audio Input
185
- if (raw.input_modalities?.includes('audio'))
205
+ if (raw.input_modalities?.includes('audio') || raw.audio === true)
186
206
  icons.push('🎙️');
187
- // Audio Output
188
207
  if (raw.output_modalities?.includes('audio'))
189
208
  icons.push('🔊');
190
- // Reasoning capability
191
209
  if (raw.reasoning === true)
192
210
  icons.push('🧠');
193
- // Web Search (from description)
194
- if (raw.description?.toLowerCase().includes('search') ||
195
- raw.name?.includes('search') ||
196
- raw.name?.includes('perplexity')) {
211
+ if (raw.description?.toLowerCase().includes('search') || raw.name?.includes('search'))
197
212
  icons.push('🔍');
198
- }
199
- // Tool/Function calling
200
213
  if (raw.tools === true)
201
214
  icons.push('💻');
202
215
  return icons.length > 0 ? ` ${icons.join('')}` : '';
203
216
  }
204
- // --- MAPPING ENGINE ---
205
- function mapModel(raw, prefix, namePrefix) {
217
+ function formatName(id, censored = true) {
218
+ let clean = id.replace(/^pollinations\//, '').replace(/-/g, ' ');
219
+ clean = clean.replace(/\b\w/g, l => l.toUpperCase());
220
+ if (!censored)
221
+ clean += " (Uncensored)";
222
+ return clean;
223
+ }
224
+ function mapModel(raw, prefix, namePrefix, nameSuffix = '') {
206
225
  const rawId = raw.id || raw.name;
207
- const fullId = prefix + rawId; // ex: "free/gemini" or "enter/nomnom" (prefix passed is "enter/")
226
+ const fullId = prefix + rawId;
208
227
  let baseName = raw.description;
209
228
  if (!baseName || baseName === rawId) {
210
229
  baseName = formatName(rawId, raw.censored !== false);
211
230
  }
212
- // CLEANUP: Simple Truncation Rule (Requested by User)
213
- // "Start from left, find ' - ', delete everything after."
214
231
  if (baseName && baseName.includes(' - ')) {
215
232
  baseName = baseName.split(' - ')[0].trim();
216
233
  }
@@ -218,16 +235,19 @@ function mapModel(raw, prefix, namePrefix) {
218
235
  if (raw.paid_only) {
219
236
  namePrefixFinal = namePrefix.replace('[Enter]', '[💎 Paid]');
220
237
  }
221
- // Get capability icons from API metadata
222
238
  const capabilityIcons = getCapabilityIcons(raw);
223
- const finalName = `${namePrefixFinal}${baseName}${capabilityIcons}`;
239
+ const finalName = `${namePrefixFinal}${baseName}${nameSuffix}${capabilityIcons}`;
224
240
  const modelObj = {
225
241
  id: fullId,
226
242
  name: finalName,
227
243
  object: 'model',
228
- variants: {}
244
+ variants: {},
245
+ modalities: {
246
+ input: raw.input_modalities || ['text'],
247
+ output: raw.output_modalities || ['text']
248
+ }
229
249
  };
230
- // --- ENRICHISSEMENT ---
250
+ // Enrichissements
231
251
  if (raw.reasoning === true || rawId.includes('thinking') || rawId.includes('reasoning')) {
232
252
  modelObj.variants = { ...modelObj.variants, high_reasoning: { options: { reasoningEffort: "high", budgetTokens: 16000 } } };
233
253
  }
@@ -239,25 +259,13 @@ function mapModel(raw, prefix, namePrefix) {
239
259
  if (rawId.includes('claude') || rawId.includes('mistral') || rawId.includes('llama')) {
240
260
  modelObj.variants.safe_tokens = { options: { maxTokens: 8000 } };
241
261
  }
242
- // NOVA FIX: Bedrock limit ~10k (User reported error > 10000)
243
- // We MUST set the limit on the model object itself so OpenCode respects it by default.
244
262
  if (rawId.includes('nova')) {
245
- modelObj.limit = {
246
- output: 8000,
247
- context: 128000 // Nova Micro/Lite/Pro usually 128k
248
- };
249
- // Also keep variant just in case
250
- modelObj.variants.bedrock_safe = { options: { maxTokens: 8000 } };
263
+ modelObj.limit = { output: 8000, context: 128000 };
251
264
  }
252
- // NOMNOM FIX: User reported error if max_tokens is missing.
253
- // Also it is a 'Gemini-scrape' model, so we treat it similar to Gemini but with strict limit.
254
265
  if (rawId.includes('nomnom') || rawId.includes('scrape')) {
255
- modelObj.limit = {
256
- output: 2048, // User used 1500 successfully
257
- context: 32768
258
- };
266
+ modelObj.limit = { output: 2048, context: 32768 };
259
267
  }
260
- if (rawId.includes('fast') || rawId.includes('flash') || rawId.includes('lite')) {
268
+ if (rawId.includes('fast') || rawId.includes('flash')) {
261
269
  if (!rawId.includes('gemini')) {
262
270
  modelObj.variants.speed = { options: { thinking: { disabled: true } } };
263
271
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-pollinations-plugin",
3
3
  "displayName": "Pollinations AI (V5.6)",
4
- "version": "5.8.2",
4
+ "version": "5.8.4-beta.1",
5
5
  "description": "Native Pollinations.ai Provider Plugin for OpenCode",
6
6
  "publisher": "pollinations",
7
7
  "repository": {
@@ -55,4 +55,4 @@
55
55
  "@types/node": "^20.0.0",
56
56
  "typescript": "^5.0.0"
57
57
  }
58
- }
58
+ }