opencode-pollinations-plugin 6.0.0-beta.18 → 6.0.0-beta.2
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 +7 -7
- package/dist/index.js +15 -85
- package/dist/server/commands.js +19 -9
- package/dist/server/generate-config.d.ts +3 -30
- package/dist/server/generate-config.js +164 -100
- package/dist/server/proxy.js +109 -65
- package/dist/tools/design/gen_diagram.d.ts +2 -0
- package/dist/tools/design/gen_diagram.js +97 -0
- package/dist/tools/design/gen_palette.d.ts +2 -0
- package/dist/tools/design/gen_palette.js +185 -0
- package/dist/tools/design/gen_qrcode.d.ts +2 -0
- package/dist/tools/design/gen_qrcode.js +60 -0
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.js +75 -0
- package/dist/tools/power/extract_frames.d.ts +2 -0
- package/dist/tools/power/extract_frames.js +215 -0
- package/dist/tools/power/file_to_url.d.ts +2 -0
- package/dist/tools/power/file_to_url.js +217 -0
- package/dist/tools/power/remove_background.d.ts +2 -0
- package/dist/tools/power/remove_background.js +115 -0
- package/package.json +6 -4
- package/dist/server/models-seed.d.ts +0 -18
- package/dist/server/models-seed.js +0 -55
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 🌸 Pollinations AI Plugin for OpenCode (v5.
|
|
1
|
+
# 🌸 Pollinations AI Plugin for OpenCode (v5.9.0)
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
<img src="https://avatars.githubusercontent.com/u/88394740?s=400&v=4" alt="Pollinations.ai Logo" width="200">
|
|
@@ -10,11 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
<div align="center">
|
|
12
12
|
|
|
13
|
-

|
|
14
14
|

|
|
15
|
-
 | [🛣️ Roadmap](./ROADMAP.md)
|
|
15
|
+

|
|
18
16
|
|
|
19
17
|
</div>
|
|
20
18
|
|
|
@@ -136,8 +134,9 @@ OpenCode uses NPM as its registry. To publish:
|
|
|
136
134
|
|
|
137
135
|
### 1. The Basics (Free Mode)
|
|
138
136
|
Just type in the chat. You are in **Manual Mode** by default.
|
|
139
|
-
- Model: `openai` (GPT-
|
|
140
|
-
- Model: `mistral` (Mistral
|
|
137
|
+
- Model: `openai-fast` (GPT-OSS 20b)
|
|
138
|
+
- Model: `mistral` (Mistral Small 3.1)
|
|
139
|
+
- ...
|
|
141
140
|
|
|
142
141
|
### 🔑 Configuration (API Key)
|
|
143
142
|
|
|
@@ -157,6 +156,7 @@ Just type in the chat. You are in **Manual Mode** by default.
|
|
|
157
156
|
|
|
158
157
|
## 🔗 Links
|
|
159
158
|
|
|
159
|
+
- **Sign up Pollinations Beta (more and best free tiers access and paids models)**: [pollinations.ai](https://enter.pollinations.ai)
|
|
160
160
|
- **Pollinations Website**: [pollinations.ai](https://pollinations.ai)
|
|
161
161
|
- **Discord Community**: [Join us!](https://discord.gg/pollinations-ai-885844321461485618)
|
|
162
162
|
- **OpenCode Ecosystem**: [opencode.ai](https://opencode.ai/docs/ecosystem#plugins)
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
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
|
|
4
|
+
import { loadConfig } 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';
|
|
8
8
|
import { createCommandHooks } from './server/commands.js';
|
|
9
|
+
import { createToolRegistry } from './tools/index.js';
|
|
9
10
|
import { createRequire } from 'module';
|
|
10
11
|
const require = createRequire(import.meta.url);
|
|
11
12
|
const LOG_FILE = '/tmp/opencode_pollinations_v4.log';
|
|
@@ -15,10 +16,12 @@ function log(msg) {
|
|
|
15
16
|
}
|
|
16
17
|
catch (e) { }
|
|
17
18
|
}
|
|
18
|
-
//
|
|
19
|
+
// Port killing removed: Using dynamic ports.
|
|
19
20
|
const startProxy = () => {
|
|
20
21
|
return new Promise((resolve) => {
|
|
21
22
|
const server = http.createServer(async (req, res) => {
|
|
23
|
+
// ... (Request Handling) ...
|
|
24
|
+
// We reuse the existing logic structure but simplified startup
|
|
22
25
|
log(`[Proxy] Request: ${req.method} ${req.url}`);
|
|
23
26
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
24
27
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
@@ -60,6 +63,7 @@ const startProxy = () => {
|
|
|
60
63
|
res.writeHead(404);
|
|
61
64
|
res.end("Not Found");
|
|
62
65
|
});
|
|
66
|
+
// Listen on random port (0) to avoid conflicts (CLI/IDE)
|
|
63
67
|
server.listen(0, '127.0.0.1', () => {
|
|
64
68
|
// @ts-ignore
|
|
65
69
|
const assignedPort = server.address().port;
|
|
@@ -72,81 +76,6 @@ const startProxy = () => {
|
|
|
72
76
|
});
|
|
73
77
|
});
|
|
74
78
|
};
|
|
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
|
-
});
|
|
150
79
|
// === PLUGIN EXPORT ===
|
|
151
80
|
export const PollinationsPlugin = async (ctx) => {
|
|
152
81
|
log(`Plugin Initializing v${require('../package.json').version}...`);
|
|
@@ -156,12 +85,15 @@ export const PollinationsPlugin = async (ctx) => {
|
|
|
156
85
|
setGlobalClient(ctx.client);
|
|
157
86
|
const toastHooks = createToastHooks(ctx.client);
|
|
158
87
|
const commandHooks = createCommandHooks();
|
|
88
|
+
// Build tool registry (conditional on API key presence)
|
|
89
|
+
const toolRegistry = createToolRegistry();
|
|
90
|
+
log(`[Tools] ${Object.keys(toolRegistry).length} tools registered`);
|
|
159
91
|
return {
|
|
160
|
-
|
|
161
|
-
auth: createAuthHook(),
|
|
92
|
+
tool: toolRegistry,
|
|
162
93
|
async config(config) {
|
|
163
94
|
log("[Hook] config() called");
|
|
164
|
-
//
|
|
95
|
+
// STARTUP only - No complex hot reload logic
|
|
96
|
+
// The user must restart OpenCode to refresh this list if they change keys.
|
|
165
97
|
const modelsArray = await generatePollinationsConfig();
|
|
166
98
|
const modelsObj = {};
|
|
167
99
|
for (const m of modelsArray) {
|
|
@@ -169,14 +101,12 @@ export const PollinationsPlugin = async (ctx) => {
|
|
|
169
101
|
}
|
|
170
102
|
if (!config.provider)
|
|
171
103
|
config.provider = {};
|
|
104
|
+
// Dynamic Provider Name
|
|
172
105
|
const version = require('../package.json').version;
|
|
173
106
|
config.provider['pollinations'] = {
|
|
174
|
-
id: '
|
|
107
|
+
id: 'pollinations',
|
|
175
108
|
name: `Pollinations AI (v${version})`,
|
|
176
|
-
options: {
|
|
177
|
-
baseURL: localBaseUrl,
|
|
178
|
-
apiKey: 'plugin-managed', // Key is managed by auth hook
|
|
179
|
-
},
|
|
109
|
+
options: { baseURL: localBaseUrl },
|
|
180
110
|
models: modelsObj
|
|
181
111
|
};
|
|
182
112
|
log(`[Hook] Registered ${Object.keys(modelsObj).length} models.`);
|
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, true);
|
|
307
|
+
// 2. Check if we got Enterprise models
|
|
308
|
+
const enterpriseModels = models.filter(m => m.id.startsWith('enter/'));
|
|
309
|
+
if (enterpriseModels.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 = enterpriseModels.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! (${enterpriseModels.length} modèles Pro 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: ${enterpriseModels.length} (dont ${diamondCount} 💎 Paid)${forcedModeMsg}`
|
|
343
343
|
};
|
|
344
344
|
}
|
|
345
345
|
else {
|
|
@@ -351,8 +351,18 @@ 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
|
-
|
|
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é.");
|
|
356
366
|
}
|
|
357
367
|
}
|
|
358
368
|
catch (e) {
|
|
@@ -1,34 +1,8 @@
|
|
|
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
|
-
}
|
|
29
1
|
interface OpenCodeModel {
|
|
30
2
|
id: string;
|
|
31
3
|
name: string;
|
|
4
|
+
object: string;
|
|
5
|
+
variants?: any;
|
|
32
6
|
options?: any;
|
|
33
7
|
limit?: {
|
|
34
8
|
context?: number;
|
|
@@ -38,7 +12,6 @@ interface OpenCodeModel {
|
|
|
38
12
|
input?: string[];
|
|
39
13
|
output?: string[];
|
|
40
14
|
};
|
|
41
|
-
tool_call?: boolean;
|
|
42
15
|
}
|
|
43
|
-
export declare function generatePollinationsConfig(forceApiKey?: string): Promise<OpenCodeModel[]>;
|
|
16
|
+
export declare function generatePollinationsConfig(forceApiKey?: string, forceStrict?: boolean): Promise<OpenCodeModel[]>;
|
|
44
17
|
export {};
|
|
@@ -1,12 +1,11 @@
|
|
|
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
1
|
import * as https from 'https';
|
|
8
2
|
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
9
5
|
import { loadConfig } from './config.js';
|
|
6
|
+
const HOMEDIR = os.homedir();
|
|
7
|
+
const CONFIG_DIR_POLLI = path.join(HOMEDIR, '.pollinations');
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR_POLLI, 'config.json');
|
|
10
9
|
// --- LOGGING ---
|
|
11
10
|
const LOG_FILE = '/tmp/opencode_pollinations_config.log';
|
|
12
11
|
function log(msg) {
|
|
@@ -14,18 +13,15 @@ function log(msg) {
|
|
|
14
13
|
const ts = new Date().toISOString();
|
|
15
14
|
if (!fs.existsSync(LOG_FILE))
|
|
16
15
|
fs.writeFileSync(LOG_FILE, '');
|
|
17
|
-
fs.appendFileSync(LOG_FILE, `[ConfigGen
|
|
16
|
+
fs.appendFileSync(LOG_FILE, `[ConfigGen] ${ts} ${msg}\n`);
|
|
18
17
|
}
|
|
19
18
|
catch (e) { }
|
|
19
|
+
// Force output to stderr for CLI visibility if needed, but clean.
|
|
20
20
|
}
|
|
21
|
-
//
|
|
21
|
+
// Fetch Helper
|
|
22
22
|
function fetchJson(url, headers = {}) {
|
|
23
23
|
return new Promise((resolve, reject) => {
|
|
24
|
-
const
|
|
25
|
-
...headers,
|
|
26
|
-
'User-Agent': 'Mozilla/5.0 (compatible; OpenCode-Pollinations/6.0; +https://opencode.ai)'
|
|
27
|
-
};
|
|
28
|
-
const req = https.get(url, { headers: finalHeaders }, (res) => {
|
|
24
|
+
const req = https.get(url, { headers }, (res) => {
|
|
29
25
|
let data = '';
|
|
30
26
|
res.on('data', chunk => data += chunk);
|
|
31
27
|
res.on('end', () => {
|
|
@@ -35,7 +31,7 @@ function fetchJson(url, headers = {}) {
|
|
|
35
31
|
}
|
|
36
32
|
catch (e) {
|
|
37
33
|
log(`JSON Parse Error for ${url}: ${e}`);
|
|
38
|
-
resolve([]);
|
|
34
|
+
resolve([]); // Fail safe -> empty list
|
|
39
35
|
}
|
|
40
36
|
});
|
|
41
37
|
});
|
|
@@ -43,132 +39,200 @@ function fetchJson(url, headers = {}) {
|
|
|
43
39
|
log(`Network Error for ${url}: ${e.message}`);
|
|
44
40
|
reject(e);
|
|
45
41
|
});
|
|
46
|
-
req.setTimeout(
|
|
42
|
+
req.setTimeout(5000, () => {
|
|
47
43
|
req.destroy();
|
|
48
44
|
reject(new Error('Timeout'));
|
|
49
45
|
});
|
|
50
46
|
});
|
|
51
47
|
}
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
function formatName(id, censored = true) {
|
|
49
|
+
let clean = id.replace(/^pollinations\//, '').replace(/-/g, ' ');
|
|
50
|
+
clean = clean.replace(/\b\w/g, l => l.toUpperCase());
|
|
51
|
+
if (!censored)
|
|
52
|
+
clean += " (Uncensored)";
|
|
53
|
+
return clean;
|
|
54
|
+
}
|
|
55
|
+
// --- MAIN GENERATOR logic ---
|
|
56
|
+
// --- MAIN GENERATOR logic ---
|
|
57
|
+
export async function generatePollinationsConfig(forceApiKey, forceStrict = false) {
|
|
54
58
|
const config = loadConfig();
|
|
55
59
|
const modelsOutput = [];
|
|
60
|
+
log(`Starting Configuration (V5.1.22 Hot-Reload)...`);
|
|
61
|
+
// Use forced key (from Hook) or cached key
|
|
56
62
|
const effectiveKey = forceApiKey || config.apiKey;
|
|
57
|
-
|
|
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;
|
|
70
|
-
}
|
|
71
|
-
// 3. Fetch models from Enterprise endpoint
|
|
63
|
+
// 1. FREE UNIVERSE
|
|
72
64
|
try {
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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;
|
|
84
|
-
}
|
|
85
|
-
const mapped = mapModel(m);
|
|
65
|
+
// Switch to main models endpoint (User provided curl confirms it has 'description')
|
|
66
|
+
const freeList = await fetchJson('https://text.pollinations.ai/models');
|
|
67
|
+
const list = Array.isArray(freeList) ? freeList : (freeList.data || []);
|
|
68
|
+
list.forEach((m) => {
|
|
69
|
+
const mapped = mapModel(m, 'free/', '[Free] ');
|
|
86
70
|
modelsOutput.push(mapped);
|
|
87
71
|
});
|
|
88
|
-
log(`
|
|
89
|
-
log(`Model IDs: ${modelsOutput.map(m => m.id).join(', ')}`);
|
|
72
|
+
log(`Fetched ${modelsOutput.length} Free models.`);
|
|
90
73
|
}
|
|
91
74
|
catch (e) {
|
|
92
|
-
log(`Error fetching models: ${e
|
|
93
|
-
//
|
|
75
|
+
log(`Error fetching Free models: ${e}`);
|
|
76
|
+
// Fallback Robust (Offline support)
|
|
77
|
+
modelsOutput.push({ id: "free/mistral", name: "[Free] Mistral Nemo (Fallback)", object: "model", variants: {} });
|
|
78
|
+
modelsOutput.push({ id: "free/openai", name: "[Free] OpenAI (Fallback)", object: "model", variants: {} });
|
|
79
|
+
modelsOutput.push({ id: "free/gemini", name: "[Free] Gemini Flash (Fallback)", object: "model", variants: {} });
|
|
80
|
+
}
|
|
81
|
+
// 1.5 FORCE ENSURE CRITICAL MODELS
|
|
82
|
+
// Sometimes the API list changes or is cached weirdly. We force vital models.
|
|
83
|
+
const hasGemini = modelsOutput.find(m => m.id === 'free/gemini');
|
|
84
|
+
if (!hasGemini) {
|
|
85
|
+
log(`[ConfigGen] Force-injecting free/gemini.`);
|
|
86
|
+
modelsOutput.push({ id: "free/gemini", name: "[Free] Gemini Flash (Force)", object: "model", variants: {} });
|
|
87
|
+
}
|
|
88
|
+
// ALIAS Removed for Clean Config
|
|
89
|
+
// const hasGeminiAlias = modelsOutput.find(m => m.id === 'pollinations/free/gemini');
|
|
90
|
+
// if (!hasGeminiAlias) {
|
|
91
|
+
// modelsOutput.push({ id: "pollinations/free/gemini", name: "[Free] Gemini Flash (Alias)", object: "model", variants: {} });
|
|
92
|
+
// }
|
|
93
|
+
// 2. ENTERPRISE UNIVERSE
|
|
94
|
+
if (effectiveKey && effectiveKey.length > 5 && effectiveKey !== 'dummy') {
|
|
95
|
+
try {
|
|
96
|
+
// Use /text/models for full metadata (input_modalities, tools, reasoning, pricing)
|
|
97
|
+
const enterListRaw = await fetchJson('https://gen.pollinations.ai/text/models', {
|
|
98
|
+
'Authorization': `Bearer ${effectiveKey}`
|
|
99
|
+
});
|
|
100
|
+
const enterList = Array.isArray(enterListRaw) ? enterListRaw : (enterListRaw.data || []);
|
|
101
|
+
const paidModels = [];
|
|
102
|
+
enterList.forEach((m) => {
|
|
103
|
+
if (m.tools === false)
|
|
104
|
+
return;
|
|
105
|
+
const mapped = mapModel(m, 'enter/', '[Enter] ');
|
|
106
|
+
modelsOutput.push(mapped);
|
|
107
|
+
if (m.paid_only) {
|
|
108
|
+
paidModels.push(mapped.id.replace('enter/', '')); // Store bare ID "gemini-large"
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
log(`Total models (Free+Pro): ${modelsOutput.length}`);
|
|
112
|
+
// Save Paid Models List for Proxy
|
|
113
|
+
try {
|
|
114
|
+
const paidListPath = path.join(config.gui ? path.dirname(CONFIG_FILE) : '/tmp', 'pollinations-paid-models.json');
|
|
115
|
+
// Ensure dir exists (re-use config dir logic from config.ts if possible, or just assume it exists since config loaded)
|
|
116
|
+
if (fs.existsSync(path.dirname(paidListPath))) {
|
|
117
|
+
fs.writeFileSync(paidListPath, JSON.stringify(paidModels));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
log(`Error saving paid models list: ${e}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
log(`Error fetching Enterprise models: ${e}`);
|
|
126
|
+
// STRICT MODE (Validation): Do not return fake fallback models.
|
|
127
|
+
if (forceStrict)
|
|
128
|
+
throw e;
|
|
129
|
+
// Fallback Robust for Enterprise (User has Key but discovery failed)
|
|
130
|
+
modelsOutput.push({ id: "enter/gpt-4o", name: "[Enter] GPT-4o (Fallback)", object: "model", variants: {} });
|
|
131
|
+
// ...
|
|
132
|
+
modelsOutput.push({ id: "enter/claude-3-5-sonnet", name: "[Enter] Claude 3.5 Sonnet (Fallback)", object: "model", variants: {} });
|
|
133
|
+
modelsOutput.push({ id: "enter/deepseek-reasoner", name: "[Enter] DeepSeek R1 (Fallback)", object: "model", variants: {} });
|
|
134
|
+
}
|
|
94
135
|
}
|
|
95
136
|
return modelsOutput;
|
|
96
137
|
}
|
|
97
|
-
// ---
|
|
138
|
+
// --- CAPABILITY ICONS ---
|
|
98
139
|
function getCapabilityIcons(raw) {
|
|
99
140
|
const icons = [];
|
|
100
|
-
// Vision:
|
|
101
|
-
if (raw.input_modalities?.includes('image')
|
|
141
|
+
// Vision: accepts images
|
|
142
|
+
if (raw.input_modalities?.includes('image'))
|
|
102
143
|
icons.push('👁️');
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (raw.input_modalities?.includes('audio') || raw.audio === true) {
|
|
144
|
+
// Audio Input
|
|
145
|
+
if (raw.input_modalities?.includes('audio'))
|
|
106
146
|
icons.push('🎙️');
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (raw.output_modalities?.includes('audio')) {
|
|
147
|
+
// Audio Output
|
|
148
|
+
if (raw.output_modalities?.includes('audio'))
|
|
110
149
|
icons.push('🔊');
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (raw.reasoning === true) {
|
|
150
|
+
// Reasoning capability
|
|
151
|
+
if (raw.reasoning === true)
|
|
114
152
|
icons.push('🧠');
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
153
|
+
// Web Search (from description)
|
|
154
|
+
if (raw.description?.toLowerCase().includes('search') ||
|
|
155
|
+
raw.name?.includes('search') ||
|
|
156
|
+
raw.name?.includes('perplexity')) {
|
|
118
157
|
icons.push('🔍');
|
|
119
158
|
}
|
|
120
|
-
//
|
|
121
|
-
if (raw.tools === true)
|
|
122
|
-
icons.push('
|
|
123
|
-
}
|
|
124
|
-
// Paid only
|
|
125
|
-
if (raw.paid_only === true) {
|
|
126
|
-
icons.push('💎');
|
|
127
|
-
}
|
|
159
|
+
// Tool/Function calling
|
|
160
|
+
if (raw.tools === true)
|
|
161
|
+
icons.push('💻');
|
|
128
162
|
return icons.length > 0 ? ` ${icons.join('')}` : '';
|
|
129
163
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
function mapModel(raw) {
|
|
136
|
-
const rawId = raw.name;
|
|
137
|
-
// Build display name
|
|
164
|
+
// --- MAPPING ENGINE ---
|
|
165
|
+
function mapModel(raw, prefix, namePrefix) {
|
|
166
|
+
const rawId = raw.id || raw.name;
|
|
167
|
+
const fullId = prefix + rawId; // ex: "free/gemini" or "enter/nomnom" (prefix passed is "enter/")
|
|
138
168
|
let baseName = raw.description;
|
|
139
169
|
if (!baseName || baseName === rawId) {
|
|
140
|
-
baseName = formatName(rawId);
|
|
170
|
+
baseName = formatName(rawId, raw.censored !== false);
|
|
141
171
|
}
|
|
142
|
-
//
|
|
172
|
+
// CLEANUP: Simple Truncation Rule (Requested by User)
|
|
173
|
+
// "Start from left, find ' - ', delete everything after."
|
|
143
174
|
if (baseName && baseName.includes(' - ')) {
|
|
144
175
|
baseName = baseName.split(' - ')[0].trim();
|
|
145
176
|
}
|
|
177
|
+
let namePrefixFinal = namePrefix;
|
|
178
|
+
if (raw.paid_only) {
|
|
179
|
+
namePrefixFinal = namePrefix.replace('[Enter]', '[💎 Paid]');
|
|
180
|
+
}
|
|
181
|
+
// Get capability icons from API metadata
|
|
146
182
|
const capabilityIcons = getCapabilityIcons(raw);
|
|
147
|
-
const finalName = `${baseName}${capabilityIcons}`;
|
|
148
|
-
// Determine modalities for OpenCode
|
|
149
|
-
const inputMods = raw.input_modalities || ['text'];
|
|
150
|
-
const outputMods = raw.output_modalities || ['text'];
|
|
183
|
+
const finalName = `${namePrefixFinal}${baseName}${capabilityIcons}`;
|
|
151
184
|
const modelObj = {
|
|
152
|
-
id:
|
|
185
|
+
id: fullId,
|
|
153
186
|
name: finalName,
|
|
187
|
+
object: 'model',
|
|
188
|
+
variants: {},
|
|
189
|
+
// Declare modalities for OpenCode vision support
|
|
154
190
|
modalities: {
|
|
155
|
-
input:
|
|
156
|
-
output:
|
|
157
|
-
}
|
|
158
|
-
tool_call: raw.tools === true && rawId !== 'nomnom' // NomNom: no tools
|
|
191
|
+
input: raw.input_modalities || ['text'],
|
|
192
|
+
output: raw.output_modalities || ['text']
|
|
193
|
+
}
|
|
159
194
|
};
|
|
160
|
-
//
|
|
195
|
+
// --- ENRICHISSEMENT ---
|
|
196
|
+
if (raw.reasoning === true || rawId.includes('thinking') || rawId.includes('reasoning')) {
|
|
197
|
+
modelObj.variants = { ...modelObj.variants, high_reasoning: { options: { reasoningEffort: "high", budgetTokens: 16000 } } };
|
|
198
|
+
}
|
|
199
|
+
if (rawId.includes('gemini') && !rawId.includes('fast')) {
|
|
200
|
+
if (!modelObj.variants.high_reasoning && (rawId === 'gemini' || rawId === 'gemini-large')) {
|
|
201
|
+
modelObj.variants.high_reasoning = { options: { reasoningEffort: "high", budgetTokens: 16000 } };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (rawId.includes('claude') || rawId.includes('mistral') || rawId.includes('llama')) {
|
|
205
|
+
modelObj.variants.safe_tokens = { options: { maxTokens: 8000 } };
|
|
206
|
+
}
|
|
207
|
+
// NOVA FIX: Bedrock limit ~10k (User reported error > 10000)
|
|
208
|
+
// We MUST set the limit on the model object itself so OpenCode respects it by default.
|
|
161
209
|
if (rawId.includes('nova')) {
|
|
162
|
-
modelObj.limit = {
|
|
210
|
+
modelObj.limit = {
|
|
211
|
+
output: 8000,
|
|
212
|
+
context: 128000 // Nova Micro/Lite/Pro usually 128k
|
|
213
|
+
};
|
|
214
|
+
// Also keep variant just in case
|
|
215
|
+
modelObj.variants.bedrock_safe = { options: { maxTokens: 8000 } };
|
|
216
|
+
}
|
|
217
|
+
// BEDROCK/ENTERPRISE LIMITS (Chickytutor only)
|
|
218
|
+
if (rawId.includes('chickytutor')) {
|
|
219
|
+
modelObj.limit = {
|
|
220
|
+
output: 8192,
|
|
221
|
+
context: 128000
|
|
222
|
+
};
|
|
163
223
|
}
|
|
164
|
-
if
|
|
165
|
-
|
|
166
|
-
|
|
224
|
+
// NOMNOM FIX: User reported error if max_tokens is missing.
|
|
225
|
+
// Also it is a 'Gemini-scrape' model, so we treat it similar to Gemini but with strict limit.
|
|
226
|
+
if (rawId.includes('nomnom') || rawId.includes('scrape')) {
|
|
227
|
+
modelObj.limit = {
|
|
228
|
+
output: 2048, // User used 1500 successfully
|
|
229
|
+
context: 32768
|
|
230
|
+
};
|
|
167
231
|
}
|
|
168
|
-
if (rawId.includes('
|
|
169
|
-
|
|
170
|
-
|
|
232
|
+
if (rawId.includes('fast') || rawId.includes('flash') || rawId.includes('lite')) {
|
|
233
|
+
if (!rawId.includes('gemini')) {
|
|
234
|
+
modelObj.variants.speed = { options: { thinking: { disabled: true } } };
|
|
235
|
+
}
|
|
171
236
|
}
|
|
172
|
-
log(`[Mapped] ${modelObj.id} → ${modelObj.name} | tools=${modelObj.tool_call} | modalities=${JSON.stringify(modelObj.modalities)}`);
|
|
173
237
|
return modelObj;
|
|
174
238
|
}
|