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 +4 -2
- package/dist/index.js +85 -12
- package/dist/server/commands.js +9 -19
- package/dist/server/generate-config.d.ts +29 -1
- package/dist/server/generate-config.js +89 -191
- package/dist/server/proxy.js +28 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
|
|
11
11
|
<div align="center">
|
|
12
12
|
|
|
13
|
-

|
|
14
14
|

|
|
15
|
-

|
|
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
|
-
//
|
|
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
|
-
//
|
|
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: '
|
|
103
|
-
npm: require('../package.json').name,
|
|
174
|
+
id: 'openai',
|
|
104
175
|
name: `Pollinations AI (v${version})`,
|
|
105
|
-
options: {
|
|
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),
|
package/dist/server/commands.js
CHANGED
|
@@ -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
|
|
307
|
-
// 2. Check if we got
|
|
308
|
-
const
|
|
309
|
-
if (
|
|
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 =
|
|
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! (${
|
|
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: ${
|
|
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
|
-
//
|
|
355
|
-
|
|
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
|
|
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/
|
|
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([]);
|
|
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
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
158
|
-
log(
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
103
|
+
}
|
|
104
|
+
// Audio input
|
|
105
|
+
if (raw.input_modalities?.includes('audio') || raw.audio === true) {
|
|
200
106
|
icons.push('🎙️');
|
|
201
|
-
|
|
107
|
+
}
|
|
108
|
+
// Audio output
|
|
109
|
+
if (raw.output_modalities?.includes('audio')) {
|
|
202
110
|
icons.push('🔊');
|
|
203
|
-
|
|
111
|
+
}
|
|
112
|
+
// Reasoning
|
|
113
|
+
if (raw.reasoning === true) {
|
|
204
114
|
icons.push('🧠');
|
|
205
|
-
|
|
115
|
+
}
|
|
116
|
+
// Search capability
|
|
117
|
+
if (raw.description?.toLowerCase().includes('search') || raw.name?.includes('search')) {
|
|
206
118
|
icons.push('🔍');
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
212
|
-
let clean = id.replace(
|
|
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
|
|
219
|
-
const rawId = raw.
|
|
220
|
-
|
|
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
|
|
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 = `${
|
|
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:
|
|
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:
|
|
241
|
-
output:
|
|
155
|
+
input: inputMods,
|
|
156
|
+
output: outputMods
|
|
242
157
|
},
|
|
243
|
-
tool_call:
|
|
158
|
+
tool_call: raw.tools === true && rawId !== 'nomnom' // NomNom: no tools
|
|
244
159
|
};
|
|
245
|
-
//
|
|
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
|
|
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:
|
|
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
|
}
|
package/dist/server/proxy.js
CHANGED
|
@@ -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
|
-
|
|
221
|
-
|
|
222
|
-
actualModel = actualModel.replace(
|
|
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
|
-
|
|
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 (
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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://
|
|
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
|
-
|
|
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": "
|
|
4
|
+
"version": "6.0.0-beta.18",
|
|
5
5
|
"description": "Native Pollinations.ai Provider Plugin for OpenCode",
|
|
6
6
|
"publisher": "pollinations",
|
|
7
7
|
"repository": {
|