opencode-pollinations-plugin 5.8.4-beta.14 → 5.8.4-beta.16

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
@@ -10,9 +10,11 @@
10
10
 
11
11
  <div align="center">
12
12
 
13
- ![Version](https://img.shields.io/badge/version-5.6.0-blue.svg)
13
+ ![Version](https://img.shields.io/badge/version-5.8.4--beta.15-orange.svg)
14
14
  ![License](https://img.shields.io/badge/license-MIT-green.svg)
15
- ![Status](https://img.shields.io/badge/status-Stable-success.svg)
15
+ ![Status](https://img.shields.io/badge/status-Beta-yellow.svg)
16
+
17
+ [📜 View Changelog](./CHANGELOG.md) | [🛣️ Roadmap](./ROADMAP.md)
16
18
 
17
19
  </div>
18
20
 
@@ -303,15 +303,15 @@ async function handleConnectCommand(args) {
303
303
  // 1. Universal Validation (No Syntax Check) - Functional Check
304
304
  emitStatusToast('info', 'Vérification de la clé...', 'Pollinations Config');
305
305
  try {
306
- const models = await generatePollinationsConfig(key, true);
307
- // 2. Check if we got Enterprise models
308
- const enterpriseModels = models.filter(m => m.id.startsWith('enter/'));
309
- if (enterpriseModels.length > 0) {
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) {
310
310
  // SUCCESS
311
311
  saveConfig({ apiKey: key }); // Don't force mode 'pro'. Let user decide.
312
312
  const masked = key.substring(0, 6) + '...';
313
313
  // Count Paid Only models found
314
- const diamondCount = enterpriseModels.filter(m => m.name.includes('💎')).length;
314
+ const diamondCount = realModels.filter(m => m.name.includes('💎')).length;
315
315
  // CHECK RESTRICTIONS: Strict Check (Usage + Profile + Balance)
316
316
  let forcedModeMsg = "";
317
317
  let isLimited = false;
@@ -336,10 +336,10 @@ async function handleConnectCommand(args) {
336
336
  else {
337
337
  saveConfig({ apiKey: key, keyHasAccessToProfile: true }); // Let user keep current mode or default
338
338
  }
339
- emitStatusToast('success', `Clé Valide! (${enterpriseModels.length} modèles Pro débloqués)`, 'Pollinations Config');
339
+ emitStatusToast('success', `Clé Valide! (${realModels.length} modèles débloqués)`, 'Pollinations Config');
340
340
  return {
341
341
  handled: true,
342
- response: `✅ **Connexion Réussie!**\n- Clé: \`${masked}\`\n- Modèles Débloqués: ${enterpriseModels.length} (dont ${diamondCount} 💎 Paid)${forcedModeMsg}`
342
+ response: `✅ **Connexion Réussie!**\n- Clé: \`${masked}\`\n- Modèles Débloqués: ${realModels.length} (dont ${diamondCount} 💎 Paid)${forcedModeMsg}`
343
343
  };
344
344
  }
345
345
  else {
@@ -351,18 +351,8 @@ async function handleConnectCommand(args) {
351
351
  // Wait, generate-config falls back to providing a list containing "[Enter] GPT-4o (Fallback)" if fetch failed.
352
352
  // So we need to detect if it's a "REAL" fetch or a "FALLBACK" fetch.
353
353
  // The fallback models have `variants: {}` usually, but real ones might too.
354
- // A better check: The fallback list is hardcoded in generate-config.ts catch block.
355
- // Let's modify generate-config to return EMPTY list on error?
356
- // Or just check if the returned models work?
357
- // Simplest: If `generatePollinationsConfig` returns any model starting with `enter/` that includes "(Fallback)" in name, we assume failure?
358
- // "GPT-4o (Fallback)" is the name.
359
- const isFallback = models.some(m => m.name.includes('(Fallback)') && m.id.startsWith('enter/'));
360
- if (isFallback) {
361
- throw new Error("Clé rejetée par l'API (Accès refusé ou invalide).");
362
- }
363
- // If we are here, we got no enter models, or empty list?
364
- // If key is valid but has no access?
365
- throw new Error("Aucun modèle Enterprise détecté pour cette clé.");
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.");
366
356
  }
367
357
  }
368
358
  catch (e) {
@@ -1,3 +1,31 @@
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
+ }
1
29
  interface OpenCodeModel {
2
30
  id: string;
3
31
  name: string;
@@ -12,5 +40,5 @@ interface OpenCodeModel {
12
40
  };
13
41
  tool_call?: boolean;
14
42
  }
15
- export declare function generatePollinationsConfig(forceApiKey?: string, forceStrict?: boolean): Promise<OpenCodeModel[]>;
43
+ export declare function generatePollinationsConfig(forceApiKey?: string): Promise<OpenCodeModel[]>;
16
44
  export {};
@@ -1,14 +1,12 @@
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
+ */
1
7
  import * as https from 'https';
2
8
  import * as fs from 'fs';
3
- import * as os from 'os';
4
- import * as path from 'path';
5
9
  import { loadConfig } from './config.js';
6
- const HOMEDIR = os.homedir();
7
- const CONFIG_DIR_POLLI = path.join(HOMEDIR, '.pollinations');
8
- const CACHE_FILE = path.join(CONFIG_DIR_POLLI, 'models-cache.json');
9
- // --- CONSTANTS ---
10
- // Seed from models-seed.ts
11
- import { FREE_MODELS_SEED } from './models-seed.js';
12
10
  // --- LOGGING ---
13
11
  const LOG_FILE = '/tmp/opencode_pollinations_config.log';
14
12
  function log(msg) {
@@ -16,44 +14,28 @@ function log(msg) {
16
14
  const ts = new Date().toISOString();
17
15
  if (!fs.existsSync(LOG_FILE))
18
16
  fs.writeFileSync(LOG_FILE, '');
19
- fs.appendFileSync(LOG_FILE, `[ConfigGen] ${ts} ${msg}\n`);
17
+ fs.appendFileSync(LOG_FILE, `[ConfigGen v6.0] ${ts} ${msg}\n`);
20
18
  }
21
19
  catch (e) { }
22
20
  }
23
21
  // --- NETWORK HELPER ---
24
- function fetchHead(url) {
25
- return new Promise((resolve) => {
26
- // Use Node.js native https check for minimal overhead
27
- const req = https.request(url, { method: 'HEAD', timeout: 5000 }, (res) => {
28
- resolve(res.headers['etag'] || null);
29
- });
30
- req.on('error', () => resolve(null));
31
- req.on('timeout', () => { req.destroy(); resolve(null); });
32
- req.end();
33
- });
34
- }
35
22
  function fetchJson(url, headers = {}) {
36
23
  return new Promise((resolve, reject) => {
37
24
  const finalHeaders = {
38
25
  ...headers,
39
- 'User-Agent': 'Mozilla/5.0 (compatible; OpenCode/5.8.4; +https://opencode.ai)'
26
+ 'User-Agent': 'Mozilla/5.0 (compatible; OpenCode-Pollinations/6.0; +https://opencode.ai)'
40
27
  };
41
28
  const req = https.get(url, { headers: finalHeaders }, (res) => {
42
- const etag = res.headers['etag'];
43
29
  let data = '';
44
30
  res.on('data', chunk => data += chunk);
45
31
  res.on('end', () => {
46
32
  try {
47
33
  const json = JSON.parse(data);
48
- // HACK: Attach ETag to the object to pass it up
49
- if (etag && typeof json === 'object') {
50
- Object.defineProperty(json, '_etag', { value: etag, enumerable: false, writable: true });
51
- }
52
34
  resolve(json);
53
35
  }
54
36
  catch (e) {
55
37
  log(`JSON Parse Error for ${url}: ${e}`);
56
- resolve([]); // Fail safe
38
+ resolve([]);
57
39
  }
58
40
  });
59
41
  });
@@ -67,218 +49,126 @@ function fetchJson(url, headers = {}) {
67
49
  });
68
50
  });
69
51
  }
70
- function loadCache() {
71
- try {
72
- if (fs.existsSync(CACHE_FILE)) {
73
- const content = fs.readFileSync(CACHE_FILE, 'utf-8');
74
- return JSON.parse(content);
75
- }
76
- }
77
- catch (e) {
78
- log(`Error loading cache: ${e}`);
79
- }
80
- return null;
81
- }
82
- function saveCache(models, etag) {
83
- try {
84
- const data = {
85
- timestamp: Date.now(),
86
- etag: etag,
87
- models: models
88
- };
89
- if (!fs.existsSync(CONFIG_DIR_POLLI))
90
- fs.mkdirSync(CONFIG_DIR_POLLI, { recursive: true });
91
- fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
92
- }
93
- catch (e) {
94
- log(`Error saving cache: ${e}`);
95
- }
96
- }
97
52
  // --- GENERATOR LOGIC ---
98
- export async function generatePollinationsConfig(forceApiKey, forceStrict = false) {
53
+ export async function generatePollinationsConfig(forceApiKey) {
99
54
  const config = loadConfig();
100
55
  const modelsOutput = [];
101
- log(`Starting Configuration (v5.8.4-Debug-Tools)...`);
102
56
  const effectiveKey = forceApiKey || config.apiKey;
103
- // 1. FREE UNIVERSE (Smart Cache System)
104
- let freeModelsList = [];
105
- let isOffline = false;
106
- let cache = loadCache();
107
- const CACHE_TTL = 7 * 24 * 3600 * 1000; // 7 days
108
- // Decision Logic
109
- const now = Date.now();
110
- let shouldFetch = !cache || (now - cache.timestamp > CACHE_TTL);
111
- // ETag Check: If cache is valid but we want to be proactive
112
- if (!shouldFetch && cache && cache.etag) {
113
- try {
114
- log('Smart Refresh: Checking for updates (HEAD)...');
115
- const remoteEtag = await fetchHead('https://text.pollinations.ai/models');
116
- if (remoteEtag && remoteEtag !== cache.etag) {
117
- log(`Update Detected! (Remote: ${remoteEtag} != Local: ${cache.etag}). Forcing refresh.`);
118
- shouldFetch = true;
119
- }
120
- else {
121
- log('Cache is clean (ETag match). No refresh needed.');
122
- }
123
- }
124
- catch (e) {
125
- log(`Smart Refresh check failed: ${e}. Ignoring.`);
126
- }
57
+ log(`Starting Configuration v6.0...`);
58
+ log(`API Key present: ${!!effectiveKey}`);
59
+ // 1. ALWAYS add "connect" placeholder model
60
+ modelsOutput.push({
61
+ id: 'connect',
62
+ name: '🔑 Connect your Pollinations Account',
63
+ modalities: { input: ['text'], output: ['text'] },
64
+ tool_call: false
65
+ });
66
+ // 2. If no API key, return only connect placeholder
67
+ if (!effectiveKey || effectiveKey.length < 5 || effectiveKey === 'dummy') {
68
+ log('No API key configured. Returning connect placeholder only.');
69
+ return modelsOutput;
127
70
  }
128
- if (shouldFetch) {
129
- log('Fetching fresh Free models...');
130
- try {
131
- const raw = await fetchJson('https://text.pollinations.ai/models');
132
- const list = Array.isArray(raw) ? raw : (raw.data || []);
133
- const newEtag = raw._etag; // Get hidden ETag
134
- if (list.length > 0) {
135
- freeModelsList = list;
136
- saveCache(list, newEtag);
137
- log(`Fetched and cached ${list.length} models (ETag: ${newEtag || 'N/A'}).`);
138
- }
139
- else {
140
- throw new Error('API returned empty list');
141
- }
142
- }
143
- catch (e) {
144
- log(`Fetch failed: ${e}.`);
145
- isOffline = true;
146
- // Fallback Logic
147
- if (cache && cache.models.length > 0) {
148
- log('Using cached models (Offline).');
149
- freeModelsList = cache.models;
150
- }
151
- else {
152
- log('Using DEFAULT SEED models (Offline + No Cache).');
153
- freeModelsList = FREE_MODELS_SEED;
71
+ // 3. Fetch models from Enterprise endpoint
72
+ try {
73
+ log('Fetching models from gen.pollinations.ai/text/models...');
74
+ const rawList = await fetchJson('https://gen.pollinations.ai/text/models', {
75
+ 'Authorization': `Bearer ${effectiveKey}`
76
+ });
77
+ const modelsList = Array.isArray(rawList) ? rawList : (rawList.data || []);
78
+ log(`Received ${modelsList.length} models from API`);
79
+ modelsList.forEach((m) => {
80
+ // Skip models without tools support (except nomnom - special handling)
81
+ if (m.tools === false && m.name !== 'nomnom') {
82
+ log(`Skipping ${m.name} (no tools)`);
83
+ return;
154
84
  }
155
- }
156
- }
157
- else {
158
- log('Using cached models (Skipped fetch).');
159
- freeModelsList = cache.models;
85
+ const mapped = mapModel(m);
86
+ modelsOutput.push(mapped);
87
+ });
88
+ log(`Total models registered: ${modelsOutput.length}`);
89
+ log(`Model IDs: ${modelsOutput.map(m => m.id).join(', ')}`);
160
90
  }
161
- // Map Free Models
162
- freeModelsList.forEach((m) => {
163
- // Tag (Offline) only if we explicitly failed a fetch attempt or are using Fallback SEED when fetch failed.
164
- // If we use cache because it's valid (Skipped fetch), we don't tag (Offline).
165
- const suffix = isOffline ? ' (Offline)' : '';
166
- const mapped = mapModel(m, 'free-', `[Free] `, suffix);
167
- modelsOutput.push(mapped);
168
- });
169
- // 2. ENTERPRISE UNIVERSE
170
- if (effectiveKey && effectiveKey.length > 5 && effectiveKey !== 'dummy') {
171
- try {
172
- const enterListRaw = await fetchJson('https://gen.pollinations.ai/text/models', {
173
- 'Authorization': `Bearer ${effectiveKey}`
174
- });
175
- const enterList = Array.isArray(enterListRaw) ? enterListRaw : (enterListRaw.data || []);
176
- enterList.forEach((m) => {
177
- if (m.tools === false)
178
- return;
179
- const mapped = mapModel(m, 'enter-', '[Enter] ');
180
- modelsOutput.push(mapped);
181
- });
182
- log(`Total models (Free+Pro): ${modelsOutput.length}`);
183
- log(`Generated IDs: ${modelsOutput.map(m => m.id).join(', ')}`);
184
- }
185
- catch (e) {
186
- log(`Error fetching Enterprise models: ${e}`);
187
- if (forceStrict)
188
- throw e;
189
- // STRICT: No Fallback for Enterprise. If API is down, we have 0 Enter models.
190
- }
91
+ catch (e) {
92
+ log(`Error fetching models: ${e.message}`);
93
+ // Return connect placeholder only on error
191
94
  }
192
95
  return modelsOutput;
193
96
  }
194
97
  // --- UTILS ---
195
98
  function getCapabilityIcons(raw) {
196
99
  const icons = [];
197
- if (raw.input_modalities?.includes('image') || raw.vision === true)
100
+ // Vision: check both input_modalities and legacy vision flag
101
+ if (raw.input_modalities?.includes('image') || raw.vision === true) {
198
102
  icons.push('👁️');
199
- if (raw.input_modalities?.includes('audio') || raw.audio === true)
103
+ }
104
+ // Audio input
105
+ if (raw.input_modalities?.includes('audio') || raw.audio === true) {
200
106
  icons.push('🎙️');
201
- if (raw.output_modalities?.includes('audio'))
107
+ }
108
+ // Audio output
109
+ if (raw.output_modalities?.includes('audio')) {
202
110
  icons.push('🔊');
203
- if (raw.reasoning === true)
111
+ }
112
+ // Reasoning
113
+ if (raw.reasoning === true) {
204
114
  icons.push('🧠');
205
- if (raw.description?.toLowerCase().includes('search') || raw.name?.includes('search'))
115
+ }
116
+ // Search capability
117
+ if (raw.description?.toLowerCase().includes('search') || raw.name?.includes('search')) {
206
118
  icons.push('🔍');
207
- if (raw.tools === true)
208
- icons.push('💻');
119
+ }
120
+ // Tools
121
+ if (raw.tools === true) {
122
+ icons.push('🛠️');
123
+ }
124
+ // Paid only
125
+ if (raw.paid_only === true) {
126
+ icons.push('💎');
127
+ }
209
128
  return icons.length > 0 ? ` ${icons.join('')}` : '';
210
129
  }
211
- function formatName(id, censored = true) {
212
- let clean = id.replace(/^pollinations\//, '').replace(/-/g, ' ');
130
+ function formatName(id) {
131
+ let clean = id.replace(/-/g, ' ');
213
132
  clean = clean.replace(/\b\w/g, l => l.toUpperCase());
214
- if (!censored)
215
- clean += " (Uncensored)";
216
133
  return clean;
217
134
  }
218
- function mapModel(raw, prefix, namePrefix, nameSuffix = '') {
219
- const rawId = raw.id || raw.name;
220
- const fullId = prefix + rawId;
135
+ function mapModel(raw) {
136
+ const rawId = raw.name;
137
+ // Build display name
221
138
  let baseName = raw.description;
222
139
  if (!baseName || baseName === rawId) {
223
- baseName = formatName(rawId, raw.censored !== false);
140
+ baseName = formatName(rawId);
224
141
  }
142
+ // Truncate after first " - "
225
143
  if (baseName && baseName.includes(' - ')) {
226
144
  baseName = baseName.split(' - ')[0].trim();
227
145
  }
228
- let namePrefixFinal = namePrefix;
229
- if (raw.paid_only) {
230
- namePrefixFinal = namePrefix.replace('[Enter]', '[💎 Paid]');
231
- }
232
146
  const capabilityIcons = getCapabilityIcons(raw);
233
- const finalName = `${namePrefixFinal}${baseName}${nameSuffix}${capabilityIcons}`;
147
+ const finalName = `${baseName}${capabilityIcons}`;
148
+ // Determine modalities for OpenCode
149
+ const inputMods = raw.input_modalities || ['text'];
150
+ const outputMods = raw.output_modalities || ['text'];
234
151
  const modelObj = {
235
- id: fullId,
152
+ id: rawId, // No prefix! Direct model ID
236
153
  name: finalName,
237
- // object: 'model',
238
- // variants: {}, // POTENTIAL SCHEMA VIOLATION
239
154
  modalities: {
240
- input: raw.input_modalities || ['text'],
241
- output: raw.output_modalities || ['text']
155
+ input: inputMods,
156
+ output: outputMods
242
157
  },
243
- tool_call: false // FORCE DEBUG DISABLE
158
+ tool_call: raw.tools === true && rawId !== 'nomnom' // NomNom: no tools
244
159
  };
245
- // Enrichissements
246
- /*
247
- if (raw.reasoning === true || rawId.includes('thinking') || rawId.includes('reasoning')) {
248
- modelObj.variants = { ...modelObj.variants, high_reasoning: { options: { reasoningEffort: "high", budgetTokens: 16000 } } };
249
- }
250
- if (rawId.includes('gemini') && !rawId.includes('fast')) {
251
- if (!modelObj.variants.high_reasoning && (rawId === 'gemini' || rawId === 'gemini-large')) {
252
- modelObj.variants.high_reasoning = { options: { reasoningEffort: "high", budgetTokens: 16000 } };
253
- }
254
- }
255
- if (rawId.includes('claude') || rawId.includes('mistral') || rawId.includes('llama')) {
256
- modelObj.variants.safe_tokens = { options: { maxTokens: 8000 } };
257
- }
258
- */
160
+ // Model-specific limits
259
161
  if (rawId.includes('nova')) {
260
- if (rawId.includes('nova')) {
261
- modelObj.limit = { output: 8000, context: 128000 };
262
- }
263
- if (rawId.includes('nomnom') || rawId.includes('scrape')) {
264
- modelObj.limit = { output: 2048, context: 32768 };
265
- }
266
- if (rawId.includes('chicky') || rawId.includes('mistral')) {
267
- modelObj.limit = { output: 4096, context: 8192 };
268
- modelObj.options = { ...modelObj.options, maxTokens: 4096 };
269
- log(`[LimitConfig] Applied strict limit to ${fullId}: output=4096, context=8192`);
270
- }
271
- /*
272
- if (rawId.includes('fast') || rawId.includes('flash')) {
273
- if (!rawId.includes('gemini')) {
274
- modelObj.variants.speed = { options: { thinking: { disabled: true } } };
275
- }
162
+ modelObj.limit = { output: 8000, context: 128000 };
276
163
  }
277
- */
164
+ if (rawId === 'nomnom') {
165
+ modelObj.limit = { output: 2048, context: 32768 };
166
+ modelObj.tool_call = false; // NomNom is a router, no external tools
278
167
  }
279
- // DEBUG LIMITS
280
- if (modelObj.limit) {
281
- log(`[Model] ${modelObj.id} Limit: ${JSON.stringify(modelObj.limit)}`);
168
+ if (rawId.includes('chicky') || rawId.includes('mistral')) {
169
+ modelObj.limit = { output: 4096, context: 8192 };
170
+ modelObj.options = { maxTokens: 4096 };
282
171
  }
172
+ log(`[Mapped] ${modelObj.id} → ${modelObj.name} | tools=${modelObj.tool_call} | modalities=${JSON.stringify(modelObj.modalities)}`);
283
173
  return modelObj;
284
174
  }
@@ -478,6 +478,14 @@ export async function handleChatCompletion(req, res, bodyRaw) {
478
478
  log(`[Proxy] Gemini Logic: Tools=${proxyBody.tools ? proxyBody.tools.length : 'REMOVED'}, Stops NOT Injected.`);
479
479
  }
480
480
  }
481
+ // B5. BEDROCK TOKEN LIMIT FIX
482
+ if (actualModel.includes("chicky") || actualModel.includes("mistral")) {
483
+ // Force max_tokens if not present or too high (Bedrock outputs usually max 4k, context 8k+ but strict check)
484
+ if (!proxyBody.max_tokens || proxyBody.max_tokens > 4096) {
485
+ proxyBody.max_tokens = 4096;
486
+ log(`[Proxy] Enforcing max_tokens=4096 for ${actualModel} (Bedrock Limit)`);
487
+ }
488
+ }
481
489
  // C. GEMINI ID BACKTRACKING & SIGNATURE
482
490
  if ((actualModel.includes("gemini") || actualModel === "nomnom") && proxyBody.messages) {
483
491
  const lastMsg = proxyBody.messages[proxyBody.messages.length - 1];
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-beta.14",
4
+ "version": "5.8.4-beta.16",
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
+ }