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.
- package/dist/server/generate-config.js +142 -134
- package/package.json +2 -2
|
@@ -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
|
|
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
|
-
//
|
|
22
|
-
|
|
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.
|
|
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
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
131
|
+
catch (e) {
|
|
132
|
+
log(`Smart Refresh check failed: ${e}. Ignoring.`);
|
|
81
133
|
}
|
|
82
134
|
}
|
|
83
|
-
|
|
84
|
-
log(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
// ---
|
|
200
|
+
// --- UTILS ---
|
|
179
201
|
function getCapabilityIcons(raw) {
|
|
180
202
|
const icons = [];
|
|
181
|
-
|
|
182
|
-
if (raw.input_modalities?.includes('image'))
|
|
203
|
+
if (raw.input_modalities?.includes('image') || raw.vision === true)
|
|
183
204
|
icons.push('👁️');
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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;
|
|
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
|
-
//
|
|
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')
|
|
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.
|
|
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
|
+
}
|