opencode-pollinations-plugin 5.8.4-beta.9 → 6.0.0-beta.18

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
 
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
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 } from './server/config.js';
4
+ import { loadConfig, saveConfig } from './server/config.js';
5
5
  import { handleChatCompletion } from './server/proxy.js';
6
6
  import { createToastHooks, setGlobalClient } from './server/toast.js';
7
7
  import { createStatusHooks } from './server/status.js';
@@ -15,12 +15,10 @@ function log(msg) {
15
15
  }
16
16
  catch (e) { }
17
17
  }
18
- // Port killing removed: Using dynamic ports.
18
+ // === PROXY SERVER ===
19
19
  const startProxy = () => {
20
20
  return new Promise((resolve) => {
21
21
  const server = http.createServer(async (req, res) => {
22
- // ... (Request Handling) ...
23
- // We reuse the existing logic structure but simplified startup
24
22
  log(`[Proxy] Request: ${req.method} ${req.url}`);
25
23
  res.setHeader('Access-Control-Allow-Origin', '*');
26
24
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
@@ -62,7 +60,6 @@ const startProxy = () => {
62
60
  res.writeHead(404);
63
61
  res.end("Not Found");
64
62
  });
65
- // Listen on random port (0) to avoid conflicts (CLI/IDE)
66
63
  server.listen(0, '127.0.0.1', () => {
67
64
  // @ts-ignore
68
65
  const assignedPort = server.address().port;
@@ -75,6 +72,81 @@ const startProxy = () => {
75
72
  });
76
73
  });
77
74
  };
75
+ // === AUTH HOOK: Native /connect Integration ===
76
+ const createAuthHook = () => ({
77
+ provider: 'pollinations',
78
+ // LOADER: Called by OpenCode when it needs credentials
79
+ // This enables HOT RELOAD - called before each request that needs auth
80
+ loader: async (auth, provider) => {
81
+ log('[AuthHook] loader() called - fetching credentials');
82
+ try {
83
+ const authData = await auth();
84
+ if (authData && 'key' in authData && authData.key) {
85
+ log(`[AuthHook] Got key from OpenCode auth: ${authData.key.substring(0, 8)}...`);
86
+ // Sync to our config for other parts of the plugin
87
+ saveConfig({ apiKey: authData.key });
88
+ return { apiKey: authData.key };
89
+ }
90
+ }
91
+ catch (e) {
92
+ log(`[AuthHook] loader() error: ${e}`);
93
+ }
94
+ // Fallback to our own config
95
+ const config = loadConfig();
96
+ if (config.apiKey) {
97
+ log(`[AuthHook] Using key from plugin config: ${config.apiKey.substring(0, 8)}...`);
98
+ return { apiKey: config.apiKey };
99
+ }
100
+ log('[AuthHook] No API key available');
101
+ return {};
102
+ },
103
+ // METHODS: Define how user can authenticate
104
+ methods: [{
105
+ type: 'api',
106
+ label: 'API Key',
107
+ prompts: [{
108
+ type: 'text',
109
+ key: 'apiKey',
110
+ message: 'Enter your Pollinations API Key',
111
+ placeholder: 'sk_...',
112
+ validate: (value) => {
113
+ if (!value || value.length < 10) {
114
+ return 'API key must be at least 10 characters';
115
+ }
116
+ if (!value.startsWith('sk_') && !value.startsWith('sk-')) {
117
+ return 'API key should start with sk_ or sk-';
118
+ }
119
+ return undefined; // Valid
120
+ }
121
+ }],
122
+ authorize: async (inputs) => {
123
+ log(`[AuthHook] authorize() called with key: ${inputs?.apiKey?.substring(0, 8)}...`);
124
+ if (!inputs?.apiKey) {
125
+ return { type: 'failed' };
126
+ }
127
+ // Validate key by testing API
128
+ try {
129
+ const response = await fetch('https://gen.pollinations.ai/text/models', {
130
+ headers: { 'Authorization': `Bearer ${inputs.apiKey}` }
131
+ });
132
+ if (response.ok) {
133
+ log('[AuthHook] Key validated successfully');
134
+ // Save to our config for immediate use
135
+ saveConfig({ apiKey: inputs.apiKey });
136
+ return { type: 'success', key: inputs.apiKey };
137
+ }
138
+ else {
139
+ log(`[AuthHook] Key validation failed: ${response.status}`);
140
+ return { type: 'failed' };
141
+ }
142
+ }
143
+ catch (e) {
144
+ log(`[AuthHook] Key validation error: ${e}`);
145
+ return { type: 'failed' };
146
+ }
147
+ }
148
+ }]
149
+ });
78
150
  // === PLUGIN EXPORT ===
79
151
  export const PollinationsPlugin = async (ctx) => {
80
152
  log(`Plugin Initializing v${require('../package.json').version}...`);
@@ -85,10 +157,11 @@ export const PollinationsPlugin = async (ctx) => {
85
157
  const toastHooks = createToastHooks(ctx.client);
86
158
  const commandHooks = createCommandHooks();
87
159
  return {
160
+ // AUTH HOOK: Native /connect integration
161
+ auth: createAuthHook(),
88
162
  async config(config) {
89
163
  log("[Hook] config() called");
90
- // STARTUP only - No complex hot reload logic
91
- // The user must restart OpenCode to refresh this list if they change keys.
164
+ // Generate models based on current auth state
92
165
  const modelsArray = await generatePollinationsConfig();
93
166
  const modelsObj = {};
94
167
  for (const m of modelsArray) {
@@ -96,17 +169,17 @@ export const PollinationsPlugin = async (ctx) => {
96
169
  }
97
170
  if (!config.provider)
98
171
  config.provider = {};
99
- // Dynamic Provider Name
100
172
  const version = require('../package.json').version;
101
173
  config.provider['pollinations'] = {
102
- id: 'pollinations',
103
- npm: require('../package.json').name,
174
+ id: 'openai',
104
175
  name: `Pollinations AI (v${version})`,
105
- options: { baseURL: localBaseUrl },
176
+ options: {
177
+ baseURL: localBaseUrl,
178
+ apiKey: 'plugin-managed', // Key is managed by auth hook
179
+ },
106
180
  models: modelsObj
107
181
  };
108
182
  log(`[Hook] Registered ${Object.keys(modelsObj).length} models.`);
109
- log(`[Hook] Keys: ${Object.keys(modelsObj).join(', ')}`);
110
183
  },
111
184
  ...toastHooks,
112
185
  ...createStatusHooks(ctx.client),
@@ -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,210 +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
162
  modelObj.limit = { output: 8000, context: 128000 };
261
163
  }
262
- if (rawId.includes('nomnom') || rawId.includes('scrape')) {
164
+ if (rawId === 'nomnom') {
263
165
  modelObj.limit = { output: 2048, context: 32768 };
166
+ modelObj.tool_call = false; // NomNom is a router, no external tools
264
167
  }
265
- if (rawId.includes('chicky')) {
266
- modelObj.limit = { output: 8192, context: 8192 };
168
+ if (rawId.includes('chicky') || rawId.includes('mistral')) {
169
+ modelObj.limit = { output: 4096, context: 8192 };
170
+ modelObj.options = { maxTokens: 4096 };
267
171
  }
268
- /*
269
- if (rawId.includes('fast') || rawId.includes('flash')) {
270
- if (!rawId.includes('gemini')) {
271
- modelObj.variants.speed = { options: { thinking: { disabled: true } } };
272
- }
273
- }
274
- */
172
+ log(`[Mapped] ${modelObj.id} → ${modelObj.name} | tools=${modelObj.tool_call} | modalities=${JSON.stringify(modelObj.modalities)}`);
275
173
  return modelObj;
276
174
  }
@@ -216,15 +216,16 @@ export async function handleChatCompletion(req, res, bodyRaw) {
216
216
  // LOAD QUOTA FOR SAFETY CHECKS
217
217
  const { getQuotaStatus, formatQuotaForToast } = await import('./quota.js');
218
218
  const quota = await getQuotaStatus(false);
219
- // A. Resolve Base Target
220
- if (actualModel.startsWith('enter/')) {
221
- isEnterprise = true;
222
- actualModel = actualModel.replace('enter/', '');
219
+ // A. Resolve Base Target - v6.0: All models go to gen.pollinations.ai
220
+ // Strip any legacy prefixes for backwards compatibility
221
+ if (actualModel.startsWith('enter/') || actualModel.startsWith('enter-')) {
222
+ actualModel = actualModel.replace(/^enter[-/]/, '');
223
223
  }
224
- else if (actualModel.startsWith('free/')) {
225
- isEnterprise = false;
226
- actualModel = actualModel.replace('free/', '');
224
+ else if (actualModel.startsWith('free/') || actualModel.startsWith('free-')) {
225
+ actualModel = actualModel.replace(/^free[-/]/, '');
227
226
  }
227
+ // v6.0: Everything is enterprise now (requires API key)
228
+ isEnterprise = true;
228
229
  // A.1 PAID MODEL ENFORCEMENT (V5.5 Strategy)
229
230
  // Check dynamic list saved by generate-config.ts
230
231
  if (isEnterprise) {
@@ -336,24 +337,16 @@ export async function handleChatCompletion(req, res, bodyRaw) {
336
337
  }
337
338
  }
338
339
  }
339
- // C. Construct URL & Headers
340
- if (isEnterprise) {
341
- if (!config.apiKey) {
342
- emitLogToast('error', "Missing API Key for Enterprise Model", 'Proxy Error');
343
- res.writeHead(401, { 'Content-Type': 'application/json' });
344
- res.end(JSON.stringify({ error: { message: "API Key required for Enterprise models." } }));
345
- return;
346
- }
347
- targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
348
- authHeader = `Bearer ${config.apiKey}`;
349
- log(`Routing to ENTERPRISE: ${actualModel}`);
350
- }
351
- else {
352
- targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
353
- authHeader = undefined;
354
- log(`Routing to FREE: ${actualModel} ${isFallbackActive ? '(FALLBACK)' : ''}`);
355
- // emitLogToast('info', `Routing to: FREE UNIVERSE (${actualModel})`, 'Pollinations Routing'); // Too noisy
340
+ // C. Construct URL & Headers - v6.0: Always gen.pollinations.ai
341
+ if (!config.apiKey) {
342
+ emitLogToast('error', "Missing API Key - Use /pollinations connect <key>", 'Proxy Error');
343
+ res.writeHead(401, { 'Content-Type': 'application/json' });
344
+ res.end(JSON.stringify({ error: { message: "API Key required. Use /pollinations connect <your_key> to connect." } }));
345
+ return;
356
346
  }
347
+ targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
348
+ authHeader = `Bearer ${config.apiKey}`;
349
+ log(`Routing to gen.pollinations.ai: ${actualModel}`);
357
350
  // NOTIFY SWITCH
358
351
  if (isFallbackActive) {
359
352
  emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
@@ -477,6 +470,14 @@ export async function handleChatCompletion(req, res, bodyRaw) {
477
470
  log(`[Proxy] Gemini Logic: Tools=${proxyBody.tools ? proxyBody.tools.length : 'REMOVED'}, Stops NOT Injected.`);
478
471
  }
479
472
  }
473
+ // B5. BEDROCK TOKEN LIMIT FIX
474
+ if (actualModel.includes("chicky") || actualModel.includes("mistral")) {
475
+ // Force max_tokens if not present or too high (Bedrock outputs usually max 4k, context 8k+ but strict check)
476
+ if (!proxyBody.max_tokens || proxyBody.max_tokens > 4096) {
477
+ proxyBody.max_tokens = 4096;
478
+ log(`[Proxy] Enforcing max_tokens=4096 for ${actualModel} (Bedrock Limit)`);
479
+ }
480
+ }
480
481
  // C. GEMINI ID BACKTRACKING & SIGNATURE
481
482
  if ((actualModel.includes("gemini") || actualModel === "nomnom") && proxyBody.messages) {
482
483
  const lastMsg = proxyBody.messages[proxyBody.messages.length - 1];
@@ -585,10 +586,10 @@ export async function handleChatCompletion(req, res, bodyRaw) {
585
586
  // 2. Notify
586
587
  emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
587
588
  emitLogToast('warning', `Recovering from ${fetchRes.status} -> Switching to ${actualModel}`, 'Safety Net');
588
- // 3. Re-Prepare Request
589
- targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
589
+ // 3. Re-Prepare Request - v6.0: Stay on gen.pollinations.ai
590
+ targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
590
591
  const retryHeaders = { ...headers };
591
- delete retryHeaders['Authorization']; // Free = No Auth
592
+ // Keep Authorization for gen.pollinations.ai
592
593
  const retryBody = { ...proxyBody, model: actualModel };
593
594
  // 4. Retry Fetch
594
595
  const retryRes = await fetchWithRetry(targetUrl, {
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.9",
4
+ "version": "6.0.0-beta.18",
5
5
  "description": "Native Pollinations.ai Provider Plugin for OpenCode",
6
6
  "publisher": "pollinations",
7
7
  "repository": {