opencode-pollinations-plugin 5.8.4-beta.9 → 5.9.1
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 +5 -3
- package/dist/index.js +0 -2
- package/dist/server/generate-config.d.ts +2 -1
- package/dist/server/generate-config.js +118 -156
- package/dist/server/proxy.js +82 -37
- package/package.json +3 -3
- 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">
|
|
@@ -134,8 +134,9 @@ OpenCode uses NPM as its registry. To publish:
|
|
|
134
134
|
|
|
135
135
|
### 1. The Basics (Free Mode)
|
|
136
136
|
Just type in the chat. You are in **Manual Mode** by default.
|
|
137
|
-
- Model: `openai` (GPT-
|
|
138
|
-
- Model: `mistral` (Mistral
|
|
137
|
+
- Model: `openai-fast` (GPT-OSS 20b)
|
|
138
|
+
- Model: `mistral` (Mistral Small 3.1)
|
|
139
|
+
- ...
|
|
139
140
|
|
|
140
141
|
### 🔑 Configuration (API Key)
|
|
141
142
|
|
|
@@ -155,6 +156,7 @@ Just type in the chat. You are in **Manual Mode** by default.
|
|
|
155
156
|
|
|
156
157
|
## 🔗 Links
|
|
157
158
|
|
|
159
|
+
- **Sign up Pollinations Beta (more and best free tiers access and paids models)**: [pollinations.ai](https://enter.pollinations.ai)
|
|
158
160
|
- **Pollinations Website**: [pollinations.ai](https://pollinations.ai)
|
|
159
161
|
- **Discord Community**: [Join us!](https://discord.gg/pollinations-ai-885844321461485618)
|
|
160
162
|
- **OpenCode Ecosystem**: [opencode.ai](https://opencode.ai/docs/ecosystem#plugins)
|
package/dist/index.js
CHANGED
|
@@ -100,13 +100,11 @@ export const PollinationsPlugin = async (ctx) => {
|
|
|
100
100
|
const version = require('../package.json').version;
|
|
101
101
|
config.provider['pollinations'] = {
|
|
102
102
|
id: 'pollinations',
|
|
103
|
-
npm: require('../package.json').name,
|
|
104
103
|
name: `Pollinations AI (v${version})`,
|
|
105
104
|
options: { baseURL: localBaseUrl },
|
|
106
105
|
models: modelsObj
|
|
107
106
|
};
|
|
108
107
|
log(`[Hook] Registered ${Object.keys(modelsObj).length} models.`);
|
|
109
|
-
log(`[Hook] Keys: ${Object.keys(modelsObj).join(', ')}`);
|
|
110
108
|
},
|
|
111
109
|
...toastHooks,
|
|
112
110
|
...createStatusHooks(ctx.client),
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
interface OpenCodeModel {
|
|
2
2
|
id: string;
|
|
3
3
|
name: string;
|
|
4
|
+
object: string;
|
|
5
|
+
variants?: any;
|
|
4
6
|
options?: any;
|
|
5
7
|
limit?: {
|
|
6
8
|
context?: number;
|
|
@@ -10,7 +12,6 @@ interface OpenCodeModel {
|
|
|
10
12
|
input?: string[];
|
|
11
13
|
output?: string[];
|
|
12
14
|
};
|
|
13
|
-
tool_call?: boolean;
|
|
14
15
|
}
|
|
15
16
|
export declare function generatePollinationsConfig(forceApiKey?: string, forceStrict?: boolean): Promise<OpenCodeModel[]>;
|
|
16
17
|
export {};
|
|
@@ -5,10 +5,7 @@ import * as path from 'path';
|
|
|
5
5
|
import { loadConfig } from './config.js';
|
|
6
6
|
const HOMEDIR = os.homedir();
|
|
7
7
|
const CONFIG_DIR_POLLI = path.join(HOMEDIR, '.pollinations');
|
|
8
|
-
const
|
|
9
|
-
// --- CONSTANTS ---
|
|
10
|
-
// Seed from models-seed.ts
|
|
11
|
-
import { FREE_MODELS_SEED } from './models-seed.js';
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR_POLLI, 'config.json');
|
|
12
9
|
// --- LOGGING ---
|
|
13
10
|
const LOG_FILE = '/tmp/opencode_pollinations_config.log';
|
|
14
11
|
function log(msg) {
|
|
@@ -19,41 +16,22 @@ function log(msg) {
|
|
|
19
16
|
fs.appendFileSync(LOG_FILE, `[ConfigGen] ${ts} ${msg}\n`);
|
|
20
17
|
}
|
|
21
18
|
catch (e) { }
|
|
19
|
+
// Force output to stderr for CLI visibility if needed, but clean.
|
|
22
20
|
}
|
|
23
|
-
//
|
|
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
|
-
}
|
|
21
|
+
// Fetch Helper
|
|
35
22
|
function fetchJson(url, headers = {}) {
|
|
36
23
|
return new Promise((resolve, reject) => {
|
|
37
|
-
const
|
|
38
|
-
...headers,
|
|
39
|
-
'User-Agent': 'Mozilla/5.0 (compatible; OpenCode/5.8.4; +https://opencode.ai)'
|
|
40
|
-
};
|
|
41
|
-
const req = https.get(url, { headers: finalHeaders }, (res) => {
|
|
42
|
-
const etag = res.headers['etag'];
|
|
24
|
+
const req = https.get(url, { headers }, (res) => {
|
|
43
25
|
let data = '';
|
|
44
26
|
res.on('data', chunk => data += chunk);
|
|
45
27
|
res.on('end', () => {
|
|
46
28
|
try {
|
|
47
29
|
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
30
|
resolve(json);
|
|
53
31
|
}
|
|
54
32
|
catch (e) {
|
|
55
33
|
log(`JSON Parse Error for ${url}: ${e}`);
|
|
56
|
-
resolve([]); // Fail safe
|
|
34
|
+
resolve([]); // Fail safe -> empty list
|
|
57
35
|
}
|
|
58
36
|
});
|
|
59
37
|
});
|
|
@@ -61,167 +39,138 @@ function fetchJson(url, headers = {}) {
|
|
|
61
39
|
log(`Network Error for ${url}: ${e.message}`);
|
|
62
40
|
reject(e);
|
|
63
41
|
});
|
|
64
|
-
req.setTimeout(
|
|
42
|
+
req.setTimeout(5000, () => {
|
|
65
43
|
req.destroy();
|
|
66
44
|
reject(new Error('Timeout'));
|
|
67
45
|
});
|
|
68
46
|
});
|
|
69
47
|
}
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
}
|
|
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;
|
|
96
54
|
}
|
|
97
|
-
// --- GENERATOR
|
|
55
|
+
// --- MAIN GENERATOR logic ---
|
|
56
|
+
// --- MAIN GENERATOR logic ---
|
|
98
57
|
export async function generatePollinationsConfig(forceApiKey, forceStrict = false) {
|
|
99
58
|
const config = loadConfig();
|
|
100
59
|
const modelsOutput = [];
|
|
101
|
-
log(`Starting Configuration (
|
|
60
|
+
log(`Starting Configuration (V5.1.22 Hot-Reload)...`);
|
|
61
|
+
// Use forced key (from Hook) or cached key
|
|
102
62
|
const effectiveKey = forceApiKey || config.apiKey;
|
|
103
|
-
// 1. FREE UNIVERSE
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
log('Smart Refresh: Checking for updates (HEAD)...');
|
|
115
|
-
const remoteEtag = await fetchHead('https://text.pollinations.ai/models');
|
|
116
|
-
if (remoteEtag && remoteEtag !== cache.etag) {
|
|
117
|
-
log(`Update Detected! (Remote: ${remoteEtag} != Local: ${cache.etag}). Forcing refresh.`);
|
|
118
|
-
shouldFetch = true;
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
log('Cache is clean (ETag match). No refresh needed.');
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
catch (e) {
|
|
125
|
-
log(`Smart Refresh check failed: ${e}. Ignoring.`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (shouldFetch) {
|
|
129
|
-
log('Fetching fresh Free models...');
|
|
130
|
-
try {
|
|
131
|
-
const raw = await fetchJson('https://text.pollinations.ai/models');
|
|
132
|
-
const list = Array.isArray(raw) ? raw : (raw.data || []);
|
|
133
|
-
const newEtag = raw._etag; // Get hidden ETag
|
|
134
|
-
if (list.length > 0) {
|
|
135
|
-
freeModelsList = list;
|
|
136
|
-
saveCache(list, newEtag);
|
|
137
|
-
log(`Fetched and cached ${list.length} models (ETag: ${newEtag || 'N/A'}).`);
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
throw new Error('API returned empty list');
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
catch (e) {
|
|
144
|
-
log(`Fetch failed: ${e}.`);
|
|
145
|
-
isOffline = true;
|
|
146
|
-
// Fallback Logic
|
|
147
|
-
if (cache && cache.models.length > 0) {
|
|
148
|
-
log('Using cached models (Offline).');
|
|
149
|
-
freeModelsList = cache.models;
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
log('Using DEFAULT SEED models (Offline + No Cache).');
|
|
153
|
-
freeModelsList = FREE_MODELS_SEED;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
log('Using cached models (Skipped fetch).');
|
|
159
|
-
freeModelsList = cache.models;
|
|
63
|
+
// 1. FREE UNIVERSE
|
|
64
|
+
try {
|
|
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] ');
|
|
70
|
+
modelsOutput.push(mapped);
|
|
71
|
+
});
|
|
72
|
+
log(`Fetched ${modelsOutput.length} Free models.`);
|
|
160
73
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
74
|
+
catch (e) {
|
|
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
|
+
// }
|
|
169
93
|
// 2. ENTERPRISE UNIVERSE
|
|
170
94
|
if (effectiveKey && effectiveKey.length > 5 && effectiveKey !== 'dummy') {
|
|
171
95
|
try {
|
|
96
|
+
// Use /text/models for full metadata (input_modalities, tools, reasoning, pricing)
|
|
172
97
|
const enterListRaw = await fetchJson('https://gen.pollinations.ai/text/models', {
|
|
173
98
|
'Authorization': `Bearer ${effectiveKey}`
|
|
174
99
|
});
|
|
175
100
|
const enterList = Array.isArray(enterListRaw) ? enterListRaw : (enterListRaw.data || []);
|
|
101
|
+
const paidModels = [];
|
|
176
102
|
enterList.forEach((m) => {
|
|
177
103
|
if (m.tools === false)
|
|
178
104
|
return;
|
|
179
|
-
const mapped = mapModel(m, 'enter
|
|
105
|
+
const mapped = mapModel(m, 'enter/', '[Enter] ');
|
|
180
106
|
modelsOutput.push(mapped);
|
|
107
|
+
if (m.paid_only) {
|
|
108
|
+
paidModels.push(mapped.id.replace('enter/', '')); // Store bare ID "gemini-large"
|
|
109
|
+
}
|
|
181
110
|
});
|
|
182
111
|
log(`Total models (Free+Pro): ${modelsOutput.length}`);
|
|
183
|
-
|
|
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
|
+
}
|
|
184
123
|
}
|
|
185
124
|
catch (e) {
|
|
186
125
|
log(`Error fetching Enterprise models: ${e}`);
|
|
126
|
+
// STRICT MODE (Validation): Do not return fake fallback models.
|
|
187
127
|
if (forceStrict)
|
|
188
128
|
throw e;
|
|
189
|
-
//
|
|
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: {} });
|
|
190
134
|
}
|
|
191
135
|
}
|
|
192
136
|
return modelsOutput;
|
|
193
137
|
}
|
|
194
|
-
// ---
|
|
138
|
+
// --- CAPABILITY ICONS ---
|
|
195
139
|
function getCapabilityIcons(raw) {
|
|
196
140
|
const icons = [];
|
|
197
|
-
|
|
141
|
+
// Vision: accepts images
|
|
142
|
+
if (raw.input_modalities?.includes('image'))
|
|
198
143
|
icons.push('👁️');
|
|
199
|
-
|
|
144
|
+
// Audio Input
|
|
145
|
+
if (raw.input_modalities?.includes('audio'))
|
|
200
146
|
icons.push('🎙️');
|
|
147
|
+
// Audio Output
|
|
201
148
|
if (raw.output_modalities?.includes('audio'))
|
|
202
149
|
icons.push('🔊');
|
|
150
|
+
// Reasoning capability
|
|
203
151
|
if (raw.reasoning === true)
|
|
204
152
|
icons.push('🧠');
|
|
205
|
-
|
|
153
|
+
// Web Search (from description)
|
|
154
|
+
if (raw.description?.toLowerCase().includes('search') ||
|
|
155
|
+
raw.name?.includes('search') ||
|
|
156
|
+
raw.name?.includes('perplexity')) {
|
|
206
157
|
icons.push('🔍');
|
|
158
|
+
}
|
|
159
|
+
// Tool/Function calling
|
|
207
160
|
if (raw.tools === true)
|
|
208
161
|
icons.push('💻');
|
|
209
162
|
return icons.length > 0 ? ` ${icons.join('')}` : '';
|
|
210
163
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
clean = clean.replace(/\b\w/g, l => l.toUpperCase());
|
|
214
|
-
if (!censored)
|
|
215
|
-
clean += " (Uncensored)";
|
|
216
|
-
return clean;
|
|
217
|
-
}
|
|
218
|
-
function mapModel(raw, prefix, namePrefix, nameSuffix = '') {
|
|
164
|
+
// --- MAPPING ENGINE ---
|
|
165
|
+
function mapModel(raw, prefix, namePrefix) {
|
|
219
166
|
const rawId = raw.id || raw.name;
|
|
220
|
-
const fullId = prefix + rawId;
|
|
167
|
+
const fullId = prefix + rawId; // ex: "free/gemini" or "enter/nomnom" (prefix passed is "enter/")
|
|
221
168
|
let baseName = raw.description;
|
|
222
169
|
if (!baseName || baseName === rawId) {
|
|
223
170
|
baseName = formatName(rawId, raw.censored !== false);
|
|
224
171
|
}
|
|
172
|
+
// CLEANUP: Simple Truncation Rule (Requested by User)
|
|
173
|
+
// "Start from left, find ' - ', delete everything after."
|
|
225
174
|
if (baseName && baseName.includes(' - ')) {
|
|
226
175
|
baseName = baseName.split(' - ')[0].trim();
|
|
227
176
|
}
|
|
@@ -229,21 +178,21 @@ function mapModel(raw, prefix, namePrefix, nameSuffix = '') {
|
|
|
229
178
|
if (raw.paid_only) {
|
|
230
179
|
namePrefixFinal = namePrefix.replace('[Enter]', '[💎 Paid]');
|
|
231
180
|
}
|
|
181
|
+
// Get capability icons from API metadata
|
|
232
182
|
const capabilityIcons = getCapabilityIcons(raw);
|
|
233
|
-
const finalName = `${namePrefixFinal}${baseName}${
|
|
183
|
+
const finalName = `${namePrefixFinal}${baseName}${capabilityIcons}`;
|
|
234
184
|
const modelObj = {
|
|
235
185
|
id: fullId,
|
|
236
186
|
name: finalName,
|
|
237
|
-
|
|
238
|
-
|
|
187
|
+
object: 'model',
|
|
188
|
+
variants: {},
|
|
189
|
+
// Declare modalities for OpenCode vision support
|
|
239
190
|
modalities: {
|
|
240
191
|
input: raw.input_modalities || ['text'],
|
|
241
192
|
output: raw.output_modalities || ['text']
|
|
242
|
-
}
|
|
243
|
-
tool_call: false // FORCE DEBUG DISABLE
|
|
193
|
+
}
|
|
244
194
|
};
|
|
245
|
-
//
|
|
246
|
-
/*
|
|
195
|
+
// --- ENRICHISSEMENT ---
|
|
247
196
|
if (raw.reasoning === true || rawId.includes('thinking') || rawId.includes('reasoning')) {
|
|
248
197
|
modelObj.variants = { ...modelObj.variants, high_reasoning: { options: { reasoningEffort: "high", budgetTokens: 16000 } } };
|
|
249
198
|
}
|
|
@@ -255,22 +204,35 @@ function mapModel(raw, prefix, namePrefix, nameSuffix = '') {
|
|
|
255
204
|
if (rawId.includes('claude') || rawId.includes('mistral') || rawId.includes('llama')) {
|
|
256
205
|
modelObj.variants.safe_tokens = { options: { maxTokens: 8000 } };
|
|
257
206
|
}
|
|
258
|
-
|
|
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.
|
|
259
209
|
if (rawId.includes('nova')) {
|
|
260
|
-
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
|
+
};
|
|
261
223
|
}
|
|
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.
|
|
262
226
|
if (rawId.includes('nomnom') || rawId.includes('scrape')) {
|
|
263
|
-
modelObj.limit = {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
227
|
+
modelObj.limit = {
|
|
228
|
+
output: 2048, // User used 1500 successfully
|
|
229
|
+
context: 32768
|
|
230
|
+
};
|
|
267
231
|
}
|
|
268
|
-
|
|
269
|
-
if (rawId.includes('
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
}
|
|
272
236
|
}
|
|
273
|
-
}
|
|
274
|
-
*/
|
|
275
237
|
return modelObj;
|
|
276
238
|
}
|
package/dist/server/proxy.js
CHANGED
|
@@ -78,6 +78,23 @@ function dereferenceSchema(schema, rootDefs) {
|
|
|
78
78
|
schema.description = (schema.description || "") + " [Ref Failed]";
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
// VERTEX FIX: 'const' not supported -> convert to 'enum'
|
|
82
|
+
if (schema.const !== undefined) {
|
|
83
|
+
schema.enum = [schema.const];
|
|
84
|
+
delete schema.const;
|
|
85
|
+
}
|
|
86
|
+
// VERTEX FIX: 'anyOf' must be exclusive (no other siblings)
|
|
87
|
+
if (schema.anyOf || schema.oneOf) {
|
|
88
|
+
// Vertex demands strict exclusivity.
|
|
89
|
+
// We keep 'definitions'/'$defs' if present at root (though unlikely here)
|
|
90
|
+
// But for a property node, we must strip EVERYTHING else.
|
|
91
|
+
const keys = Object.keys(schema);
|
|
92
|
+
keys.forEach(k => {
|
|
93
|
+
if (k !== 'anyOf' && k !== 'oneOf' && k !== 'definitions' && k !== '$defs') {
|
|
94
|
+
delete schema[k];
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
81
98
|
if (schema.properties) {
|
|
82
99
|
for (const key in schema.properties) {
|
|
83
100
|
schema.properties[key] = dereferenceSchema(schema.properties[key], rootDefs);
|
|
@@ -86,6 +103,15 @@ function dereferenceSchema(schema, rootDefs) {
|
|
|
86
103
|
if (schema.items) {
|
|
87
104
|
schema.items = dereferenceSchema(schema.items, rootDefs);
|
|
88
105
|
}
|
|
106
|
+
if (schema.anyOf) {
|
|
107
|
+
schema.anyOf = schema.anyOf.map((s) => dereferenceSchema(s, rootDefs));
|
|
108
|
+
}
|
|
109
|
+
if (schema.oneOf) {
|
|
110
|
+
schema.oneOf = schema.oneOf.map((s) => dereferenceSchema(s, rootDefs));
|
|
111
|
+
}
|
|
112
|
+
if (schema.allOf) {
|
|
113
|
+
schema.allOf = schema.allOf.map((s) => dereferenceSchema(s, rootDefs));
|
|
114
|
+
}
|
|
89
115
|
if (schema.optional !== undefined)
|
|
90
116
|
delete schema.optional;
|
|
91
117
|
if (schema.title)
|
|
@@ -107,6 +133,38 @@ function sanitizeToolsForVertex(tools) {
|
|
|
107
133
|
return tool;
|
|
108
134
|
});
|
|
109
135
|
}
|
|
136
|
+
function sanitizeToolsForBedrock(tools) {
|
|
137
|
+
return tools.map(tool => {
|
|
138
|
+
if (tool.function) {
|
|
139
|
+
if (!tool.function.description || tool.function.description.length === 0) {
|
|
140
|
+
tool.function.description = " "; // Force non-empty string
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return tool;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function sanitizeSchemaForKimi(schema) {
|
|
147
|
+
if (!schema || typeof schema !== 'object')
|
|
148
|
+
return schema;
|
|
149
|
+
// Kimi Fixes
|
|
150
|
+
if (schema.title)
|
|
151
|
+
delete schema.title;
|
|
152
|
+
// Fix empty objects "{}" which Kimi hates.
|
|
153
|
+
// If it's an empty object without type, assume string or object?
|
|
154
|
+
// Often happens with "additionalProperties: {}"
|
|
155
|
+
if (Object.keys(schema).length === 0) {
|
|
156
|
+
schema.type = "string"; // Fallback to safe type
|
|
157
|
+
schema.description = "Any value";
|
|
158
|
+
}
|
|
159
|
+
if (schema.properties) {
|
|
160
|
+
for (const key in schema.properties) {
|
|
161
|
+
schema.properties[key] = sanitizeSchemaForKimi(schema.properties[key]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (schema.items)
|
|
165
|
+
sanitizeSchemaForKimi(schema.items);
|
|
166
|
+
return schema;
|
|
167
|
+
}
|
|
110
168
|
function truncateTools(tools, limit = 120) {
|
|
111
169
|
if (!tools || tools.length <= limit)
|
|
112
170
|
return tools;
|
|
@@ -114,12 +172,16 @@ function truncateTools(tools, limit = 120) {
|
|
|
114
172
|
}
|
|
115
173
|
const MAX_RETRIES = 3;
|
|
116
174
|
const RETRY_DELAY_MS = 1000;
|
|
175
|
+
const FETCH_TIMEOUT_MS = 600000; // 10 Minutes global timeout
|
|
117
176
|
function sleep(ms) {
|
|
118
177
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
119
178
|
}
|
|
120
179
|
async function fetchWithRetry(url, options, retries = MAX_RETRIES) {
|
|
121
180
|
try {
|
|
122
|
-
const
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
183
|
+
const response = await fetch(url, { ...options, signal: controller.signal });
|
|
184
|
+
clearTimeout(timeoutId);
|
|
123
185
|
if (response.ok)
|
|
124
186
|
return response;
|
|
125
187
|
if (response.status === 404 || response.status === 401 || response.status === 400) {
|
|
@@ -153,11 +215,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
153
215
|
const config = loadConfig();
|
|
154
216
|
// DEBUG: Trace Config State for Hot Reload verification
|
|
155
217
|
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) { }
|
|
161
218
|
// 0. COMMAND HANDLING
|
|
162
219
|
if (body.messages && body.messages.length > 0) {
|
|
163
220
|
const lastMsg = body.messages[body.messages.length - 1];
|
|
@@ -385,17 +442,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
385
442
|
// LOGIC BLOCK: MODEL SPECIFIC ADAPTATIONS
|
|
386
443
|
// =========================================================
|
|
387
444
|
if (proxyBody.tools && Array.isArray(proxyBody.tools) && proxyBody.tools.length > 0) {
|
|
388
|
-
// B0. KIMI / MOONSHOT SURGICAL FIX
|
|
389
|
-
// Tools are ENABLED. We rely on penalties and strict stops to fight loops.
|
|
445
|
+
// B0. KIMI / MOONSHOT SURGICAL FIX
|
|
390
446
|
if (actualModel.includes("kimi") || actualModel.includes("moonshot")) {
|
|
391
|
-
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops.`);
|
|
447
|
+
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops/sanitization.`);
|
|
392
448
|
proxyBody.frequency_penalty = 1.1;
|
|
393
449
|
proxyBody.presence_penalty = 0.4;
|
|
394
450
|
proxyBody.stop = ["<|endoftext|>", "User:", "\nUser", "User :"];
|
|
451
|
+
// KIMI FIX: Remove 'title' from schema
|
|
452
|
+
proxyBody.tools = proxyBody.tools.map((t) => {
|
|
453
|
+
if (t.function && t.function.parameters) {
|
|
454
|
+
t.function.parameters = sanitizeSchemaForKimi(t.function.parameters);
|
|
455
|
+
}
|
|
456
|
+
return t;
|
|
457
|
+
});
|
|
395
458
|
}
|
|
396
|
-
// A. AZURE/OPENAI FIXES
|
|
397
|
-
if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure")) {
|
|
398
|
-
|
|
459
|
+
// A. AZURE/OPENAI FIXES + MIDJOURNEY + GROK
|
|
460
|
+
if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure") || actualModel.includes("midijourney") || actualModel.includes("grok")) {
|
|
461
|
+
const limit = (actualModel.includes("midijourney") || actualModel.includes("grok")) ? 128 : 120;
|
|
462
|
+
proxyBody.tools = truncateTools(proxyBody.tools, limit);
|
|
399
463
|
if (proxyBody.messages) {
|
|
400
464
|
proxyBody.messages.forEach((m) => {
|
|
401
465
|
if (m.tool_calls) {
|
|
@@ -410,6 +474,11 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
410
474
|
});
|
|
411
475
|
}
|
|
412
476
|
}
|
|
477
|
+
// BEDROCK FIX (Claude / Nova / ChickyTutor)
|
|
478
|
+
if (actualModel.includes("claude") || actualModel.includes("nova") || actualModel.includes("bedrock") || actualModel.includes("chickytutor")) {
|
|
479
|
+
log(`[Proxy] Bedrock: Sanitizing tools description.`);
|
|
480
|
+
proxyBody.tools = sanitizeToolsForBedrock(proxyBody.tools);
|
|
481
|
+
}
|
|
413
482
|
// B1. NOMNOM SPECIAL (Disable Grounding, KEEP Search Tool)
|
|
414
483
|
if (actualModel === "nomnom") {
|
|
415
484
|
proxyBody.tools_config = { google_search_retrieval: { disable: true } };
|
|
@@ -417,36 +486,12 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
417
486
|
proxyBody.tools = sanitizeToolsForVertex(proxyBody.tools || []);
|
|
418
487
|
log(`[Proxy] Nomnom Fix: Grounding Disabled, Search Tool KEPT.`);
|
|
419
488
|
}
|
|
420
|
-
// B2. GEMINI FREE / FAST (CRASH FIX: STRICT SANITIZATION)
|
|
421
|
-
// Restore Tools but REMOVE conflicting ones (Search)
|
|
422
|
-
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
423
|
-
// Handles: "tools" vs "grounding" conflicts, and "infinite loops" via Stop Sequences.
|
|
424
|
-
// GLOBAL BEDROCK FIX (All Models)
|
|
425
|
-
// Check if history has tools but current request misses tools definition.
|
|
426
|
-
// This happens when OpenCode sends the Tool Result (optimisation),
|
|
427
|
-
// but Bedrock requires toolConfig to validate the history.
|
|
428
|
-
const hasToolHistory = proxyBody.messages?.some((m) => m.role === 'tool' || m.tool_calls);
|
|
429
|
-
if (hasToolHistory && (!proxyBody.tools || proxyBody.tools.length === 0)) {
|
|
430
|
-
// Inject Shim Tool to satisfy Bedrock
|
|
431
|
-
proxyBody.tools = [{
|
|
432
|
-
type: 'function',
|
|
433
|
-
function: {
|
|
434
|
-
name: '_bedrock_compatibility_shim',
|
|
435
|
-
description: 'Internal system tool to satisfy Bedrock strict toolConfig requirement. Do not use.',
|
|
436
|
-
parameters: { type: 'object', properties: {} }
|
|
437
|
-
}
|
|
438
|
-
}];
|
|
439
|
-
log(`[Proxy] Bedrock Fix: Injected shim tool for ${actualModel} (History has tools, Request missing tools)`);
|
|
440
|
-
}
|
|
441
489
|
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
442
|
-
|
|
443
|
-
// Added ChickyTutor (Claude/Gemini based) to fix "toolConfig must be defined" error.
|
|
444
|
-
else if (actualModel.includes("gemini") || actualModel.includes("chickytutor")) {
|
|
490
|
+
else if (actualModel.includes("gemini")) {
|
|
445
491
|
let hasFunctions = false;
|
|
446
492
|
if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
|
|
447
493
|
hasFunctions = proxyBody.tools.some((t) => t.type === 'function' || t.function);
|
|
448
494
|
}
|
|
449
|
-
// Old Shim logic removed (moved up)
|
|
450
495
|
if (hasFunctions) {
|
|
451
496
|
// 1. Strict cleanup of 'google_search' tool
|
|
452
497
|
proxyBody.tools = proxyBody.tools.filter((t) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-pollinations-plugin",
|
|
3
|
-
"displayName": "Pollinations AI (V5.
|
|
4
|
-
"version": "5.
|
|
3
|
+
"displayName": "Pollinations AI (V5.9)",
|
|
4
|
+
"version": "5.9.1",
|
|
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
|
+
}
|
|
@@ -1,18 +0,0 @@
|
|
|
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[];
|
|
@@ -1,55 +0,0 @@
|
|
|
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
|
-
];
|