opencode-pollinations-plugin 5.9.0 → 6.0.0-beta.19
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 +116 -20
- package/dist/server/commands.js +9 -19
- package/dist/server/generate-config.d.ts +30 -3
- package/dist/server/generate-config.js +101 -158
- package/dist/server/models-seed.d.ts +18 -0
- package/dist/server/models-seed.js +55 -0
- package/dist/server/proxy.js +53 -28
- package/package.json +2 -2
- package/dist/debug_check.js +0 -36
- package/dist/provider.d.ts +0 -1
- package/dist/provider.js +0 -135
- package/dist/provider_v1.d.ts +0 -1
- package/dist/provider_v1.js +0 -135
- package/dist/test-require.js +0 -9
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,18 @@ function log(msg) {
|
|
|
15
15
|
}
|
|
16
16
|
catch (e) { }
|
|
17
17
|
}
|
|
18
|
-
//
|
|
18
|
+
// === PROXY SERVER (Singleton with Fixed Port) ===
|
|
19
|
+
const DEFAULT_PORT = 18888;
|
|
20
|
+
let existingServer = null;
|
|
21
|
+
let existingPort = 0;
|
|
19
22
|
const startProxy = () => {
|
|
23
|
+
// Singleton: reuse existing server if already running
|
|
24
|
+
if (existingServer && existingPort > 0) {
|
|
25
|
+
log(`[Proxy] Reusing existing server on port ${existingPort}`);
|
|
26
|
+
return Promise.resolve(existingPort);
|
|
27
|
+
}
|
|
20
28
|
return new Promise((resolve) => {
|
|
21
29
|
const server = http.createServer(async (req, res) => {
|
|
22
|
-
// ... (Request Handling) ...
|
|
23
|
-
// We reuse the existing logic structure but simplified startup
|
|
24
30
|
log(`[Proxy] Request: ${req.method} ${req.url}`);
|
|
25
31
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
26
32
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
@@ -62,19 +68,106 @@ const startProxy = () => {
|
|
|
62
68
|
res.writeHead(404);
|
|
63
69
|
res.end("Not Found");
|
|
64
70
|
});
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
// Try fixed port first, fallback to dynamic if occupied
|
|
72
|
+
const tryListen = (port, fallbackToDynamic) => {
|
|
73
|
+
server.listen(port, '127.0.0.1', () => {
|
|
74
|
+
// @ts-ignore
|
|
75
|
+
const assignedPort = server.address().port;
|
|
76
|
+
existingServer = server;
|
|
77
|
+
existingPort = assignedPort;
|
|
78
|
+
log(`[Proxy] Started v${require('../package.json').version} on port ${assignedPort}${port === 0 ? ' (dynamic fallback)' : ''}`);
|
|
79
|
+
resolve(assignedPort);
|
|
80
|
+
});
|
|
81
|
+
server.on('error', (e) => {
|
|
82
|
+
if (e.code === 'EADDRINUSE' && fallbackToDynamic) {
|
|
83
|
+
log(`[Proxy] Port ${port} in use, falling back to dynamic port`);
|
|
84
|
+
server.removeAllListeners('error');
|
|
85
|
+
tryListen(0, false); // Try dynamic port
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
log(`[Proxy] Fatal Error: ${e}`);
|
|
89
|
+
resolve(0);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
tryListen(DEFAULT_PORT, true);
|
|
76
94
|
});
|
|
77
95
|
};
|
|
96
|
+
// === AUTH HOOK: Native /connect Integration ===
|
|
97
|
+
const createAuthHook = () => ({
|
|
98
|
+
provider: 'pollinations',
|
|
99
|
+
// LOADER: Called by OpenCode when it needs credentials
|
|
100
|
+
// This enables HOT RELOAD - called before each request that needs auth
|
|
101
|
+
loader: async (auth, provider) => {
|
|
102
|
+
log('[AuthHook] loader() called - fetching credentials');
|
|
103
|
+
try {
|
|
104
|
+
const authData = await auth();
|
|
105
|
+
if (authData && 'key' in authData && authData.key) {
|
|
106
|
+
log(`[AuthHook] Got key from OpenCode auth: ${authData.key.substring(0, 8)}...`);
|
|
107
|
+
// Sync to our config for other parts of the plugin
|
|
108
|
+
saveConfig({ apiKey: authData.key });
|
|
109
|
+
return { apiKey: authData.key };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
log(`[AuthHook] loader() error: ${e}`);
|
|
114
|
+
}
|
|
115
|
+
// Fallback to our own config
|
|
116
|
+
const config = loadConfig();
|
|
117
|
+
if (config.apiKey) {
|
|
118
|
+
log(`[AuthHook] Using key from plugin config: ${config.apiKey.substring(0, 8)}...`);
|
|
119
|
+
return { apiKey: config.apiKey };
|
|
120
|
+
}
|
|
121
|
+
log('[AuthHook] No API key available');
|
|
122
|
+
return {};
|
|
123
|
+
},
|
|
124
|
+
// METHODS: Define how user can authenticate
|
|
125
|
+
methods: [{
|
|
126
|
+
type: 'api',
|
|
127
|
+
label: 'API Key',
|
|
128
|
+
prompts: [{
|
|
129
|
+
type: 'text',
|
|
130
|
+
key: 'apiKey',
|
|
131
|
+
message: 'Enter your Pollinations API Key',
|
|
132
|
+
placeholder: 'sk_...',
|
|
133
|
+
validate: (value) => {
|
|
134
|
+
if (!value || value.length < 10) {
|
|
135
|
+
return 'API key must be at least 10 characters';
|
|
136
|
+
}
|
|
137
|
+
if (!value.startsWith('sk_') && !value.startsWith('sk-')) {
|
|
138
|
+
return 'API key should start with sk_ or sk-';
|
|
139
|
+
}
|
|
140
|
+
return undefined; // Valid
|
|
141
|
+
}
|
|
142
|
+
}],
|
|
143
|
+
authorize: async (inputs) => {
|
|
144
|
+
log(`[AuthHook] authorize() called with key: ${inputs?.apiKey?.substring(0, 8)}...`);
|
|
145
|
+
if (!inputs?.apiKey) {
|
|
146
|
+
return { type: 'failed' };
|
|
147
|
+
}
|
|
148
|
+
// Validate key by testing API
|
|
149
|
+
try {
|
|
150
|
+
const response = await fetch('https://gen.pollinations.ai/text/models', {
|
|
151
|
+
headers: { 'Authorization': `Bearer ${inputs.apiKey}` }
|
|
152
|
+
});
|
|
153
|
+
if (response.ok) {
|
|
154
|
+
log('[AuthHook] Key validated successfully');
|
|
155
|
+
// Save to our config for immediate use
|
|
156
|
+
saveConfig({ apiKey: inputs.apiKey });
|
|
157
|
+
return { type: 'success', key: inputs.apiKey };
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
log(`[AuthHook] Key validation failed: ${response.status}`);
|
|
161
|
+
return { type: 'failed' };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
log(`[AuthHook] Key validation error: ${e}`);
|
|
166
|
+
return { type: 'failed' };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}]
|
|
170
|
+
});
|
|
78
171
|
// === PLUGIN EXPORT ===
|
|
79
172
|
export const PollinationsPlugin = async (ctx) => {
|
|
80
173
|
log(`Plugin Initializing v${require('../package.json').version}...`);
|
|
@@ -85,10 +178,11 @@ export const PollinationsPlugin = async (ctx) => {
|
|
|
85
178
|
const toastHooks = createToastHooks(ctx.client);
|
|
86
179
|
const commandHooks = createCommandHooks();
|
|
87
180
|
return {
|
|
181
|
+
// AUTH HOOK: Native /connect integration
|
|
182
|
+
auth: createAuthHook(),
|
|
88
183
|
async config(config) {
|
|
89
184
|
log("[Hook] config() called");
|
|
90
|
-
//
|
|
91
|
-
// The user must restart OpenCode to refresh this list if they change keys.
|
|
185
|
+
// Generate models based on current auth state
|
|
92
186
|
const modelsArray = await generatePollinationsConfig();
|
|
93
187
|
const modelsObj = {};
|
|
94
188
|
for (const m of modelsArray) {
|
|
@@ -96,12 +190,14 @@ export const PollinationsPlugin = async (ctx) => {
|
|
|
96
190
|
}
|
|
97
191
|
if (!config.provider)
|
|
98
192
|
config.provider = {};
|
|
99
|
-
// Dynamic Provider Name
|
|
100
193
|
const version = require('../package.json').version;
|
|
101
194
|
config.provider['pollinations'] = {
|
|
102
|
-
id: '
|
|
195
|
+
id: 'openai',
|
|
103
196
|
name: `Pollinations AI (v${version})`,
|
|
104
|
-
options: {
|
|
197
|
+
options: {
|
|
198
|
+
baseURL: localBaseUrl,
|
|
199
|
+
apiKey: 'plugin-managed', // Key is managed by auth hook
|
|
200
|
+
},
|
|
105
201
|
models: modelsObj
|
|
106
202
|
};
|
|
107
203
|
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);
|
|
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,8 +1,34 @@
|
|
|
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;
|
|
4
|
-
object: string;
|
|
5
|
-
variants?: any;
|
|
6
32
|
options?: any;
|
|
7
33
|
limit?: {
|
|
8
34
|
context?: number;
|
|
@@ -12,6 +38,7 @@ interface OpenCodeModel {
|
|
|
12
38
|
input?: string[];
|
|
13
39
|
output?: string[];
|
|
14
40
|
};
|
|
41
|
+
tool_call?: boolean;
|
|
15
42
|
}
|
|
16
|
-
export declare function generatePollinationsConfig(forceApiKey?: string
|
|
43
|
+
export declare function generatePollinationsConfig(forceApiKey?: string): Promise<OpenCodeModel[]>;
|
|
17
44
|
export {};
|
|
@@ -1,11 +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 CONFIG_FILE = path.join(CONFIG_DIR_POLLI, 'config.json');
|
|
9
10
|
// --- LOGGING ---
|
|
10
11
|
const LOG_FILE = '/tmp/opencode_pollinations_config.log';
|
|
11
12
|
function log(msg) {
|
|
@@ -13,15 +14,18 @@ function log(msg) {
|
|
|
13
14
|
const ts = new Date().toISOString();
|
|
14
15
|
if (!fs.existsSync(LOG_FILE))
|
|
15
16
|
fs.writeFileSync(LOG_FILE, '');
|
|
16
|
-
fs.appendFileSync(LOG_FILE, `[ConfigGen] ${ts} ${msg}\n`);
|
|
17
|
+
fs.appendFileSync(LOG_FILE, `[ConfigGen v6.0] ${ts} ${msg}\n`);
|
|
17
18
|
}
|
|
18
19
|
catch (e) { }
|
|
19
|
-
// Force output to stderr for CLI visibility if needed, but clean.
|
|
20
20
|
}
|
|
21
|
-
//
|
|
21
|
+
// --- NETWORK HELPER ---
|
|
22
22
|
function fetchJson(url, headers = {}) {
|
|
23
23
|
return new Promise((resolve, reject) => {
|
|
24
|
-
const
|
|
24
|
+
const finalHeaders = {
|
|
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) => {
|
|
25
29
|
let data = '';
|
|
26
30
|
res.on('data', chunk => data += chunk);
|
|
27
31
|
res.on('end', () => {
|
|
@@ -31,7 +35,7 @@ function fetchJson(url, headers = {}) {
|
|
|
31
35
|
}
|
|
32
36
|
catch (e) {
|
|
33
37
|
log(`JSON Parse Error for ${url}: ${e}`);
|
|
34
|
-
resolve([]);
|
|
38
|
+
resolve([]);
|
|
35
39
|
}
|
|
36
40
|
});
|
|
37
41
|
});
|
|
@@ -39,193 +43,132 @@ function fetchJson(url, headers = {}) {
|
|
|
39
43
|
log(`Network Error for ${url}: ${e.message}`);
|
|
40
44
|
reject(e);
|
|
41
45
|
});
|
|
42
|
-
req.setTimeout(
|
|
46
|
+
req.setTimeout(15000, () => {
|
|
43
47
|
req.destroy();
|
|
44
48
|
reject(new Error('Timeout'));
|
|
45
49
|
});
|
|
46
50
|
});
|
|
47
51
|
}
|
|
48
|
-
|
|
49
|
-
|
|
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) {
|
|
52
|
+
// --- GENERATOR LOGIC ---
|
|
53
|
+
export async function generatePollinationsConfig(forceApiKey) {
|
|
58
54
|
const config = loadConfig();
|
|
59
55
|
const modelsOutput = [];
|
|
60
|
-
log(`Starting Configuration (V5.1.22 Hot-Reload)...`);
|
|
61
|
-
// Use forced key (from Hook) or cached key
|
|
62
56
|
const effectiveKey = forceApiKey || config.apiKey;
|
|
63
|
-
|
|
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;
|
|
70
|
+
}
|
|
71
|
+
// 3. Fetch models from Enterprise endpoint
|
|
64
72
|
try {
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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;
|
|
84
|
+
}
|
|
85
|
+
const mapped = mapModel(m);
|
|
70
86
|
modelsOutput.push(mapped);
|
|
71
87
|
});
|
|
72
|
-
log(`
|
|
88
|
+
log(`Total models registered: ${modelsOutput.length}`);
|
|
89
|
+
log(`Model IDs: ${modelsOutput.map(m => m.id).join(', ')}`);
|
|
73
90
|
}
|
|
74
91
|
catch (e) {
|
|
75
|
-
log(`Error fetching
|
|
76
|
-
//
|
|
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
|
-
}
|
|
92
|
+
log(`Error fetching models: ${e.message}`);
|
|
93
|
+
// Return connect placeholder only on error
|
|
135
94
|
}
|
|
136
95
|
return modelsOutput;
|
|
137
96
|
}
|
|
138
|
-
// ---
|
|
97
|
+
// --- UTILS ---
|
|
139
98
|
function getCapabilityIcons(raw) {
|
|
140
99
|
const icons = [];
|
|
141
|
-
// Vision:
|
|
142
|
-
if (raw.input_modalities?.includes('image'))
|
|
100
|
+
// Vision: check both input_modalities and legacy vision flag
|
|
101
|
+
if (raw.input_modalities?.includes('image') || raw.vision === true) {
|
|
143
102
|
icons.push('👁️');
|
|
144
|
-
|
|
145
|
-
|
|
103
|
+
}
|
|
104
|
+
// Audio input
|
|
105
|
+
if (raw.input_modalities?.includes('audio') || raw.audio === true) {
|
|
146
106
|
icons.push('🎙️');
|
|
147
|
-
|
|
148
|
-
|
|
107
|
+
}
|
|
108
|
+
// Audio output
|
|
109
|
+
if (raw.output_modalities?.includes('audio')) {
|
|
149
110
|
icons.push('🔊');
|
|
150
|
-
|
|
151
|
-
|
|
111
|
+
}
|
|
112
|
+
// Reasoning
|
|
113
|
+
if (raw.reasoning === true) {
|
|
152
114
|
icons.push('🧠');
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
raw.name?.includes('perplexity')) {
|
|
115
|
+
}
|
|
116
|
+
// Search capability
|
|
117
|
+
if (raw.description?.toLowerCase().includes('search') || raw.name?.includes('search')) {
|
|
157
118
|
icons.push('🔍');
|
|
158
119
|
}
|
|
159
|
-
//
|
|
160
|
-
if (raw.tools === true)
|
|
161
|
-
icons.push('
|
|
120
|
+
// Tools
|
|
121
|
+
if (raw.tools === true) {
|
|
122
|
+
icons.push('🛠️');
|
|
123
|
+
}
|
|
124
|
+
// Paid only
|
|
125
|
+
if (raw.paid_only === true) {
|
|
126
|
+
icons.push('💎');
|
|
127
|
+
}
|
|
162
128
|
return icons.length > 0 ? ` ${icons.join('')}` : '';
|
|
163
129
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
130
|
+
function formatName(id) {
|
|
131
|
+
let clean = id.replace(/-/g, ' ');
|
|
132
|
+
clean = clean.replace(/\b\w/g, l => l.toUpperCase());
|
|
133
|
+
return clean;
|
|
134
|
+
}
|
|
135
|
+
function mapModel(raw) {
|
|
136
|
+
const rawId = raw.name;
|
|
137
|
+
// Build display name
|
|
168
138
|
let baseName = raw.description;
|
|
169
139
|
if (!baseName || baseName === rawId) {
|
|
170
|
-
baseName = formatName(rawId
|
|
140
|
+
baseName = formatName(rawId);
|
|
171
141
|
}
|
|
172
|
-
//
|
|
173
|
-
// "Start from left, find ' - ', delete everything after."
|
|
142
|
+
// Truncate after first " - "
|
|
174
143
|
if (baseName && baseName.includes(' - ')) {
|
|
175
144
|
baseName = baseName.split(' - ')[0].trim();
|
|
176
145
|
}
|
|
177
|
-
let namePrefixFinal = namePrefix;
|
|
178
|
-
if (raw.paid_only) {
|
|
179
|
-
namePrefixFinal = namePrefix.replace('[Enter]', '[💎 Paid]');
|
|
180
|
-
}
|
|
181
|
-
// Get capability icons from API metadata
|
|
182
146
|
const capabilityIcons = getCapabilityIcons(raw);
|
|
183
|
-
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'];
|
|
184
151
|
const modelObj = {
|
|
185
|
-
id:
|
|
152
|
+
id: rawId, // No prefix! Direct model ID
|
|
186
153
|
name: finalName,
|
|
187
|
-
object: 'model',
|
|
188
|
-
variants: {},
|
|
189
|
-
// Declare modalities for OpenCode vision support
|
|
190
154
|
modalities: {
|
|
191
|
-
input:
|
|
192
|
-
output:
|
|
193
|
-
}
|
|
155
|
+
input: inputMods,
|
|
156
|
+
output: outputMods
|
|
157
|
+
},
|
|
158
|
+
tool_call: raw.tools === true && rawId !== 'nomnom' // NomNom: no tools
|
|
194
159
|
};
|
|
195
|
-
//
|
|
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.
|
|
160
|
+
// Model-specific limits
|
|
209
161
|
if (rawId.includes('nova')) {
|
|
210
|
-
modelObj.limit = {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
};
|
|
214
|
-
//
|
|
215
|
-
modelObj.variants.bedrock_safe = { options: { maxTokens: 8000 } };
|
|
216
|
-
}
|
|
217
|
-
// NOMNOM FIX: User reported error if max_tokens is missing.
|
|
218
|
-
// Also it is a 'Gemini-scrape' model, so we treat it similar to Gemini but with strict limit.
|
|
219
|
-
if (rawId.includes('nomnom') || rawId.includes('scrape')) {
|
|
220
|
-
modelObj.limit = {
|
|
221
|
-
output: 2048, // User used 1500 successfully
|
|
222
|
-
context: 32768
|
|
223
|
-
};
|
|
162
|
+
modelObj.limit = { output: 8000, context: 128000 };
|
|
163
|
+
}
|
|
164
|
+
if (rawId === 'nomnom') {
|
|
165
|
+
modelObj.limit = { output: 2048, context: 32768 };
|
|
166
|
+
modelObj.tool_call = false; // NomNom is a router, no external tools
|
|
224
167
|
}
|
|
225
|
-
if (rawId.includes('
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
168
|
+
if (rawId.includes('chicky') || rawId.includes('mistral')) {
|
|
169
|
+
modelObj.limit = { output: 4096, context: 8192 };
|
|
170
|
+
modelObj.options = { maxTokens: 4096 };
|
|
229
171
|
}
|
|
172
|
+
log(`[Mapped] ${modelObj.id} → ${modelObj.name} | tools=${modelObj.tool_call} | modalities=${JSON.stringify(modelObj.modalities)}`);
|
|
230
173
|
return modelObj;
|
|
231
174
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface PollinationsModel {
|
|
2
|
+
name: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
type?: string;
|
|
5
|
+
tools?: boolean;
|
|
6
|
+
reasoning?: boolean;
|
|
7
|
+
context?: number;
|
|
8
|
+
context_window?: number;
|
|
9
|
+
input_modalities?: string[];
|
|
10
|
+
output_modalities?: string[];
|
|
11
|
+
paid_only?: boolean;
|
|
12
|
+
vision?: boolean;
|
|
13
|
+
audio?: boolean;
|
|
14
|
+
community?: boolean;
|
|
15
|
+
censored?: boolean;
|
|
16
|
+
[key: string]: any;
|
|
17
|
+
}
|
|
18
|
+
export declare const FREE_MODELS_SEED: PollinationsModel[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const FREE_MODELS_SEED = [
|
|
2
|
+
{
|
|
3
|
+
"name": "gemini",
|
|
4
|
+
"description": "Gemini 2.5 Flash Lite",
|
|
5
|
+
"tier": "anonymous",
|
|
6
|
+
"tools": true,
|
|
7
|
+
"input_modalities": ["text", "image"],
|
|
8
|
+
"output_modalities": ["text"],
|
|
9
|
+
"vision": true
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "mistral",
|
|
13
|
+
"description": "Mistral Small 3.2 24B",
|
|
14
|
+
"tier": "anonymous",
|
|
15
|
+
"tools": true,
|
|
16
|
+
"input_modalities": ["text"],
|
|
17
|
+
"output_modalities": ["text"],
|
|
18
|
+
"vision": false
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "openai-fast",
|
|
22
|
+
"description": "GPT-OSS 20B Reasoning LLM (OVH)",
|
|
23
|
+
"tier": "anonymous",
|
|
24
|
+
"tools": true,
|
|
25
|
+
"input_modalities": ["text"],
|
|
26
|
+
"output_modalities": ["text"],
|
|
27
|
+
"vision": false,
|
|
28
|
+
"reasoning": true
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "bidara",
|
|
32
|
+
"description": "BIDARA (Biomimetic Designer)",
|
|
33
|
+
"tier": "anonymous",
|
|
34
|
+
"community": true,
|
|
35
|
+
"input_modalities": ["text", "image"],
|
|
36
|
+
"output_modalities": ["text"],
|
|
37
|
+
"vision": true
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "chickytutor",
|
|
41
|
+
"description": "ChickyTutor AI Language Tutor",
|
|
42
|
+
"tier": "anonymous",
|
|
43
|
+
"community": true,
|
|
44
|
+
"input_modalities": ["text"],
|
|
45
|
+
"output_modalities": ["text"]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "midijourney",
|
|
49
|
+
"description": "MIDIjourney",
|
|
50
|
+
"tier": "anonymous",
|
|
51
|
+
"community": true,
|
|
52
|
+
"input_modalities": ["text"],
|
|
53
|
+
"output_modalities": ["text"]
|
|
54
|
+
}
|
|
55
|
+
];
|
package/dist/server/proxy.js
CHANGED
|
@@ -153,6 +153,11 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
153
153
|
const config = loadConfig();
|
|
154
154
|
// DEBUG: Trace Config State for Hot Reload verification
|
|
155
155
|
log(`[Proxy Request] Config Loaded. Mode: ${config.mode}, HasKey: ${!!config.apiKey}, KeyLength: ${config.apiKey ? config.apiKey.length : 0}`);
|
|
156
|
+
// SPY LOGGING
|
|
157
|
+
try {
|
|
158
|
+
fs.appendFileSync('/tmp/opencode_spy.log', `\n\n=== REQUEST ${new Date().toISOString()} ===\nMODEL: ${body.model}\nBODY:\n${JSON.stringify(body, null, 2)}\n==========================\n`);
|
|
159
|
+
}
|
|
160
|
+
catch (e) { }
|
|
156
161
|
// 0. COMMAND HANDLING
|
|
157
162
|
if (body.messages && body.messages.length > 0) {
|
|
158
163
|
const lastMsg = body.messages[body.messages.length - 1];
|
|
@@ -211,15 +216,16 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
211
216
|
// LOAD QUOTA FOR SAFETY CHECKS
|
|
212
217
|
const { getQuotaStatus, formatQuotaForToast } = await import('./quota.js');
|
|
213
218
|
const quota = await getQuotaStatus(false);
|
|
214
|
-
// A. Resolve Base Target
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
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[-/]/, '');
|
|
218
223
|
}
|
|
219
|
-
else if (actualModel.startsWith('free/')) {
|
|
220
|
-
|
|
221
|
-
actualModel = actualModel.replace('free/', '');
|
|
224
|
+
else if (actualModel.startsWith('free/') || actualModel.startsWith('free-')) {
|
|
225
|
+
actualModel = actualModel.replace(/^free[-/]/, '');
|
|
222
226
|
}
|
|
227
|
+
// v6.0: Everything is enterprise now (requires API key)
|
|
228
|
+
isEnterprise = true;
|
|
223
229
|
// A.1 PAID MODEL ENFORCEMENT (V5.5 Strategy)
|
|
224
230
|
// Check dynamic list saved by generate-config.ts
|
|
225
231
|
if (isEnterprise) {
|
|
@@ -331,24 +337,16 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
331
337
|
}
|
|
332
338
|
}
|
|
333
339
|
}
|
|
334
|
-
// C. Construct URL & Headers
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
343
|
-
authHeader = `Bearer ${config.apiKey}`;
|
|
344
|
-
log(`Routing to ENTERPRISE: ${actualModel}`);
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
|
|
348
|
-
authHeader = undefined;
|
|
349
|
-
log(`Routing to FREE: ${actualModel} ${isFallbackActive ? '(FALLBACK)' : ''}`);
|
|
350
|
-
// 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;
|
|
351
346
|
}
|
|
347
|
+
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
348
|
+
authHeader = `Bearer ${config.apiKey}`;
|
|
349
|
+
log(`Routing to gen.pollinations.ai: ${actualModel}`);
|
|
352
350
|
// NOTIFY SWITCH
|
|
353
351
|
if (isFallbackActive) {
|
|
354
352
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
@@ -416,13 +414,32 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
416
414
|
// Restore Tools but REMOVE conflicting ones (Search)
|
|
417
415
|
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
418
416
|
// Handles: "tools" vs "grounding" conflicts, and "infinite loops" via Stop Sequences.
|
|
417
|
+
// GLOBAL BEDROCK FIX (All Models)
|
|
418
|
+
// Check if history has tools but current request misses tools definition.
|
|
419
|
+
// This happens when OpenCode sends the Tool Result (optimisation),
|
|
420
|
+
// but Bedrock requires toolConfig to validate the history.
|
|
421
|
+
const hasToolHistory = proxyBody.messages?.some((m) => m.role === 'tool' || m.tool_calls);
|
|
422
|
+
if (hasToolHistory && (!proxyBody.tools || proxyBody.tools.length === 0)) {
|
|
423
|
+
// Inject Shim Tool to satisfy Bedrock
|
|
424
|
+
proxyBody.tools = [{
|
|
425
|
+
type: 'function',
|
|
426
|
+
function: {
|
|
427
|
+
name: '_bedrock_compatibility_shim',
|
|
428
|
+
description: 'Internal system tool to satisfy Bedrock strict toolConfig requirement. Do not use.',
|
|
429
|
+
parameters: { type: 'object', properties: {} }
|
|
430
|
+
}
|
|
431
|
+
}];
|
|
432
|
+
log(`[Proxy] Bedrock Fix: Injected shim tool for ${actualModel} (History has tools, Request missing tools)`);
|
|
433
|
+
}
|
|
419
434
|
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
420
435
|
// Fixes "Multiple tools" error (Vertex) and "JSON body validation failed" (v5.3.5 regression)
|
|
421
|
-
|
|
436
|
+
// Added ChickyTutor (Claude/Gemini based) to fix "toolConfig must be defined" error.
|
|
437
|
+
else if (actualModel.includes("gemini") || actualModel.includes("chickytutor")) {
|
|
422
438
|
let hasFunctions = false;
|
|
423
439
|
if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
|
|
424
440
|
hasFunctions = proxyBody.tools.some((t) => t.type === 'function' || t.function);
|
|
425
441
|
}
|
|
442
|
+
// Old Shim logic removed (moved up)
|
|
426
443
|
if (hasFunctions) {
|
|
427
444
|
// 1. Strict cleanup of 'google_search' tool
|
|
428
445
|
proxyBody.tools = proxyBody.tools.filter((t) => {
|
|
@@ -453,6 +470,14 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
453
470
|
log(`[Proxy] Gemini Logic: Tools=${proxyBody.tools ? proxyBody.tools.length : 'REMOVED'}, Stops NOT Injected.`);
|
|
454
471
|
}
|
|
455
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
|
+
}
|
|
456
481
|
// C. GEMINI ID BACKTRACKING & SIGNATURE
|
|
457
482
|
if ((actualModel.includes("gemini") || actualModel === "nomnom") && proxyBody.messages) {
|
|
458
483
|
const lastMsg = proxyBody.messages[proxyBody.messages.length - 1];
|
|
@@ -561,10 +586,10 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
561
586
|
// 2. Notify
|
|
562
587
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
563
588
|
emitLogToast('warning', `Recovering from ${fetchRes.status} -> Switching to ${actualModel}`, 'Safety Net');
|
|
564
|
-
// 3. Re-Prepare Request
|
|
565
|
-
targetUrl = 'https://
|
|
589
|
+
// 3. Re-Prepare Request - v6.0: Stay on gen.pollinations.ai
|
|
590
|
+
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
566
591
|
const retryHeaders = { ...headers };
|
|
567
|
-
|
|
592
|
+
// Keep Authorization for gen.pollinations.ai
|
|
568
593
|
const retryBody = { ...proxyBody, model: actualModel };
|
|
569
594
|
// 4. Retry Fetch
|
|
570
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.19",
|
|
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
|
+
}
|
package/dist/debug_check.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import * as https from 'https';
|
|
2
|
-
|
|
3
|
-
function checkEndpoint(ep, key) {
|
|
4
|
-
return new Promise((resolve) => {
|
|
5
|
-
console.log(`Checking ${ep}...`);
|
|
6
|
-
const req = https.request({
|
|
7
|
-
hostname: 'gen.pollinations.ai',
|
|
8
|
-
path: ep,
|
|
9
|
-
method: 'GET',
|
|
10
|
-
headers: { 'Authorization': `Bearer ${key}` }
|
|
11
|
-
}, (res) => {
|
|
12
|
-
console.log(`Status Code: ${res.statusCode}`);
|
|
13
|
-
let data = '';
|
|
14
|
-
res.on('data', chunk => data += chunk);
|
|
15
|
-
res.on('end', () => {
|
|
16
|
-
console.log(`Headers:`, res.headers);
|
|
17
|
-
console.log(`Body Full: ${data}`);
|
|
18
|
-
if (res.statusCode === 200) resolve({ ok: true, body: data });
|
|
19
|
-
else resolve({ ok: false, status: res.statusCode, body: data });
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
req.on('error', (e) => {
|
|
23
|
-
console.log(`Error: ${e.message}`);
|
|
24
|
-
resolve({ ok: false, status: e.message || 'Error' });
|
|
25
|
-
});
|
|
26
|
-
req.setTimeout(10000, () => req.destroy());
|
|
27
|
-
req.end();
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const KEY = "plln_sk_F7a4RcBG4AVCeBSo6lnS36EKwm0nPn1O";
|
|
32
|
-
|
|
33
|
-
(async () => {
|
|
34
|
-
const res = await checkEndpoint('/account/profile', KEY);
|
|
35
|
-
console.log('Result:', res);
|
|
36
|
-
})();
|
package/dist/provider.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const createPollinationsFetch: (apiKey: string) => (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
package/dist/provider.js
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
// Removed invalid imports
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
// --- Sanitization Helpers (Ported from Gateway/Upstream) ---
|
|
4
|
-
function safeId(id) {
|
|
5
|
-
if (!id)
|
|
6
|
-
return id;
|
|
7
|
-
if (id.length > 30)
|
|
8
|
-
return id.substring(0, 30);
|
|
9
|
-
return id;
|
|
10
|
-
}
|
|
11
|
-
function logDebug(message, data) {
|
|
12
|
-
try {
|
|
13
|
-
const timestamp = new Date().toISOString();
|
|
14
|
-
let logMsg = `[${timestamp}] ${message}`;
|
|
15
|
-
if (data) {
|
|
16
|
-
logMsg += `\n${JSON.stringify(data, null, 2)}`;
|
|
17
|
-
}
|
|
18
|
-
fs.appendFileSync('/tmp/opencode_pollinations_debug.log', logMsg + '\n\n');
|
|
19
|
-
}
|
|
20
|
-
catch (e) {
|
|
21
|
-
// ignore logging errors
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function sanitizeTools(tools) {
|
|
25
|
-
if (!Array.isArray(tools))
|
|
26
|
-
return tools;
|
|
27
|
-
const cleanSchema = (schema) => {
|
|
28
|
-
if (!schema || typeof schema !== "object")
|
|
29
|
-
return;
|
|
30
|
-
if (schema.optional !== undefined)
|
|
31
|
-
delete schema.optional;
|
|
32
|
-
if (schema.ref !== undefined)
|
|
33
|
-
delete schema.ref;
|
|
34
|
-
if (schema["$ref"] !== undefined)
|
|
35
|
-
delete schema["$ref"];
|
|
36
|
-
if (schema.properties) {
|
|
37
|
-
for (const key in schema.properties)
|
|
38
|
-
cleanSchema(schema.properties[key]);
|
|
39
|
-
}
|
|
40
|
-
if (schema.items)
|
|
41
|
-
cleanSchema(schema.items);
|
|
42
|
-
};
|
|
43
|
-
return tools.map((tool) => {
|
|
44
|
-
const newTool = { ...tool };
|
|
45
|
-
if (newTool.function && newTool.function.parameters) {
|
|
46
|
-
cleanSchema(newTool.function.parameters);
|
|
47
|
-
}
|
|
48
|
-
return newTool;
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
function filterTools(tools, maxCount = 120) {
|
|
52
|
-
if (!Array.isArray(tools))
|
|
53
|
-
return [];
|
|
54
|
-
if (tools.length <= maxCount)
|
|
55
|
-
return tools;
|
|
56
|
-
const priorities = [
|
|
57
|
-
"bash", "read", "write", "edit", "webfetch", "glob", "grep",
|
|
58
|
-
"searxng_remote_search", "deepsearch_deep_search", "google_search",
|
|
59
|
-
"task", "todowrite"
|
|
60
|
-
];
|
|
61
|
-
const priorityTools = tools.filter((t) => priorities.includes(t.function.name));
|
|
62
|
-
const otherTools = tools.filter((t) => !priorities.includes(t.function.name));
|
|
63
|
-
const slotsLeft = maxCount - priorityTools.length;
|
|
64
|
-
const othersKept = otherTools.slice(0, Math.max(0, slotsLeft));
|
|
65
|
-
logDebug(`[POLLI-PLUGIN] Filtering tools: ${tools.length} -> ${priorityTools.length + othersKept.length}`);
|
|
66
|
-
return [...priorityTools, ...othersKept];
|
|
67
|
-
}
|
|
68
|
-
// --- Fetch Implementation ---
|
|
69
|
-
export const createPollinationsFetch = (apiKey) => async (input, init) => {
|
|
70
|
-
let url = input.toString();
|
|
71
|
-
const options = init || {};
|
|
72
|
-
let body = null;
|
|
73
|
-
if (options.body && typeof options.body === "string") {
|
|
74
|
-
try {
|
|
75
|
-
body = JSON.parse(options.body);
|
|
76
|
-
}
|
|
77
|
-
catch (e) {
|
|
78
|
-
// Not JSON, ignore
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
// --- INTERCEPTION & SANITIZATION ---
|
|
82
|
-
if (body) {
|
|
83
|
-
let model = body.model || "";
|
|
84
|
-
// 0. Model Name Normalization
|
|
85
|
-
if (typeof model === "string" && model.startsWith("pollinations/enter/")) {
|
|
86
|
-
body.model = model.replace("pollinations/enter/", "");
|
|
87
|
-
model = body.model;
|
|
88
|
-
}
|
|
89
|
-
// FIX: Remove stream_options (causes 400 on some OpenAI proxies)
|
|
90
|
-
if (body.stream_options) {
|
|
91
|
-
delete body.stream_options;
|
|
92
|
-
}
|
|
93
|
-
// 1. Azure Tool Limit Fix
|
|
94
|
-
if ((model.includes("openai") || model.includes("gpt")) && body.tools) {
|
|
95
|
-
if (body.tools.length > 120) {
|
|
96
|
-
body.tools = filterTools(body.tools, 120);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
// 2. Vertex/Gemini Schema Fix
|
|
100
|
-
if (model.includes("gemini") && body.tools) {
|
|
101
|
-
body.tools = sanitizeTools(body.tools);
|
|
102
|
-
}
|
|
103
|
-
// Re-serialize body
|
|
104
|
-
options.body = JSON.stringify(body);
|
|
105
|
-
}
|
|
106
|
-
// Ensure Headers
|
|
107
|
-
const headers = new Headers(options.headers || {});
|
|
108
|
-
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
109
|
-
headers.set("Content-Type", "application/json");
|
|
110
|
-
options.headers = headers;
|
|
111
|
-
logDebug(`Req: ${url}`, body);
|
|
112
|
-
try {
|
|
113
|
-
const response = await global.fetch(url, options);
|
|
114
|
-
// Log response status
|
|
115
|
-
// We clone to read text for debugging errors
|
|
116
|
-
if (!response.ok) {
|
|
117
|
-
try {
|
|
118
|
-
const clone = response.clone();
|
|
119
|
-
const text = await clone.text();
|
|
120
|
-
logDebug(`Res (Error): ${response.status}`, text);
|
|
121
|
-
}
|
|
122
|
-
catch (e) {
|
|
123
|
-
logDebug(`Res (Error): ${response.status} (Read failed)`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
logDebug(`Res (OK): ${response.status}`);
|
|
128
|
-
}
|
|
129
|
-
return response;
|
|
130
|
-
}
|
|
131
|
-
catch (e) {
|
|
132
|
-
logDebug(`Fetch Error: ${e.message}`);
|
|
133
|
-
throw e;
|
|
134
|
-
}
|
|
135
|
-
};
|
package/dist/provider_v1.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const createPollinationsFetch: (apiKey: string) => (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
package/dist/provider_v1.js
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
// Removed invalid imports
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
// --- Sanitization Helpers (Ported from Gateway/Upstream) ---
|
|
4
|
-
function safeId(id) {
|
|
5
|
-
if (!id)
|
|
6
|
-
return id;
|
|
7
|
-
if (id.length > 30)
|
|
8
|
-
return id.substring(0, 30);
|
|
9
|
-
return id;
|
|
10
|
-
}
|
|
11
|
-
function logDebug(message, data) {
|
|
12
|
-
try {
|
|
13
|
-
const timestamp = new Date().toISOString();
|
|
14
|
-
let logMsg = `[${timestamp}] ${message}`;
|
|
15
|
-
if (data) {
|
|
16
|
-
logMsg += `\n${JSON.stringify(data, null, 2)}`;
|
|
17
|
-
}
|
|
18
|
-
fs.appendFileSync('/tmp/opencode_pollinations_debug.log', logMsg + '\n\n');
|
|
19
|
-
}
|
|
20
|
-
catch (e) {
|
|
21
|
-
// ignore logging errors
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function sanitizeTools(tools) {
|
|
25
|
-
if (!Array.isArray(tools))
|
|
26
|
-
return tools;
|
|
27
|
-
const cleanSchema = (schema) => {
|
|
28
|
-
if (!schema || typeof schema !== "object")
|
|
29
|
-
return;
|
|
30
|
-
if (schema.optional !== undefined)
|
|
31
|
-
delete schema.optional;
|
|
32
|
-
if (schema.ref !== undefined)
|
|
33
|
-
delete schema.ref;
|
|
34
|
-
if (schema["$ref"] !== undefined)
|
|
35
|
-
delete schema["$ref"];
|
|
36
|
-
if (schema.properties) {
|
|
37
|
-
for (const key in schema.properties)
|
|
38
|
-
cleanSchema(schema.properties[key]);
|
|
39
|
-
}
|
|
40
|
-
if (schema.items)
|
|
41
|
-
cleanSchema(schema.items);
|
|
42
|
-
};
|
|
43
|
-
return tools.map((tool) => {
|
|
44
|
-
const newTool = { ...tool };
|
|
45
|
-
if (newTool.function && newTool.function.parameters) {
|
|
46
|
-
cleanSchema(newTool.function.parameters);
|
|
47
|
-
}
|
|
48
|
-
return newTool;
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
function filterTools(tools, maxCount = 120) {
|
|
52
|
-
if (!Array.isArray(tools))
|
|
53
|
-
return [];
|
|
54
|
-
if (tools.length <= maxCount)
|
|
55
|
-
return tools;
|
|
56
|
-
const priorities = [
|
|
57
|
-
"bash", "read", "write", "edit", "webfetch", "glob", "grep",
|
|
58
|
-
"searxng_remote_search", "deepsearch_deep_search", "google_search",
|
|
59
|
-
"task", "todowrite"
|
|
60
|
-
];
|
|
61
|
-
const priorityTools = tools.filter((t) => priorities.includes(t.function.name));
|
|
62
|
-
const otherTools = tools.filter((t) => !priorities.includes(t.function.name));
|
|
63
|
-
const slotsLeft = maxCount - priorityTools.length;
|
|
64
|
-
const othersKept = otherTools.slice(0, Math.max(0, slotsLeft));
|
|
65
|
-
logDebug(`[POLLI-PLUGIN] Filtering tools: ${tools.length} -> ${priorityTools.length + othersKept.length}`);
|
|
66
|
-
return [...priorityTools, ...othersKept];
|
|
67
|
-
}
|
|
68
|
-
// --- Fetch Implementation ---
|
|
69
|
-
export const createPollinationsFetch = (apiKey) => async (input, init) => {
|
|
70
|
-
let url = input.toString();
|
|
71
|
-
const options = init || {};
|
|
72
|
-
let body = null;
|
|
73
|
-
if (options.body && typeof options.body === "string") {
|
|
74
|
-
try {
|
|
75
|
-
body = JSON.parse(options.body);
|
|
76
|
-
}
|
|
77
|
-
catch (e) {
|
|
78
|
-
// Not JSON, ignore
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
// --- INTERCEPTION & SANITIZATION ---
|
|
82
|
-
if (body) {
|
|
83
|
-
let model = body.model || "";
|
|
84
|
-
// 0. Model Name Normalization
|
|
85
|
-
if (typeof model === "string" && model.startsWith("pollinations/enter/")) {
|
|
86
|
-
body.model = model.replace("pollinations/enter/", "");
|
|
87
|
-
model = body.model;
|
|
88
|
-
}
|
|
89
|
-
// FIX: Remove stream_options (causes 400 on some OpenAI proxies)
|
|
90
|
-
if (body.stream_options) {
|
|
91
|
-
delete body.stream_options;
|
|
92
|
-
}
|
|
93
|
-
// 1. Azure Tool Limit Fix
|
|
94
|
-
if ((model.includes("openai") || model.includes("gpt")) && body.tools) {
|
|
95
|
-
if (body.tools.length > 120) {
|
|
96
|
-
body.tools = filterTools(body.tools, 120);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
// 2. Vertex/Gemini Schema Fix
|
|
100
|
-
if (model.includes("gemini") && body.tools) {
|
|
101
|
-
body.tools = sanitizeTools(body.tools);
|
|
102
|
-
}
|
|
103
|
-
// Re-serialize body
|
|
104
|
-
options.body = JSON.stringify(body);
|
|
105
|
-
}
|
|
106
|
-
// Ensure Headers
|
|
107
|
-
const headers = new Headers(options.headers || {});
|
|
108
|
-
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
109
|
-
headers.set("Content-Type", "application/json");
|
|
110
|
-
options.headers = headers;
|
|
111
|
-
logDebug(`Req: ${url}`, body);
|
|
112
|
-
try {
|
|
113
|
-
const response = await global.fetch(url, options);
|
|
114
|
-
// Log response status
|
|
115
|
-
// We clone to read text for debugging errors
|
|
116
|
-
if (!response.ok) {
|
|
117
|
-
try {
|
|
118
|
-
const clone = response.clone();
|
|
119
|
-
const text = await clone.text();
|
|
120
|
-
logDebug(`Res (Error): ${response.status}`, text);
|
|
121
|
-
}
|
|
122
|
-
catch (e) {
|
|
123
|
-
logDebug(`Res (Error): ${response.status} (Read failed)`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
logDebug(`Res (OK): ${response.status}`);
|
|
128
|
-
}
|
|
129
|
-
return response;
|
|
130
|
-
}
|
|
131
|
-
catch (e) {
|
|
132
|
-
logDebug(`Fetch Error: ${e.message}`);
|
|
133
|
-
throw e;
|
|
134
|
-
}
|
|
135
|
-
};
|
package/dist/test-require.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { createRequire } from 'module';
|
|
2
|
-
const require = createRequire(import.meta.url);
|
|
3
|
-
try {
|
|
4
|
-
const pkg = require('../package.json');
|
|
5
|
-
console.log("SUCCESS: Loaded version " + pkg.version);
|
|
6
|
-
} catch (e) {
|
|
7
|
-
console.error("FAILURE:", e.message);
|
|
8
|
-
process.exit(1);
|
|
9
|
-
}
|