opencode-pollinations-plugin 6.0.0-beta.19 → 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 +25 -116
- 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
|
@@ -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
|
}
|
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];
|
|
@@ -216,16 +273,15 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
216
273
|
// LOAD QUOTA FOR SAFETY CHECKS
|
|
217
274
|
const { getQuotaStatus, formatQuotaForToast } = await import('./quota.js');
|
|
218
275
|
const quota = await getQuotaStatus(false);
|
|
219
|
-
// A. Resolve Base Target
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
actualModel = actualModel.replace(
|
|
276
|
+
// A. Resolve Base Target
|
|
277
|
+
if (actualModel.startsWith('enter/')) {
|
|
278
|
+
isEnterprise = true;
|
|
279
|
+
actualModel = actualModel.replace('enter/', '');
|
|
223
280
|
}
|
|
224
|
-
else if (actualModel.startsWith('free/')
|
|
225
|
-
|
|
281
|
+
else if (actualModel.startsWith('free/')) {
|
|
282
|
+
isEnterprise = false;
|
|
283
|
+
actualModel = actualModel.replace('free/', '');
|
|
226
284
|
}
|
|
227
|
-
// v6.0: Everything is enterprise now (requires API key)
|
|
228
|
-
isEnterprise = true;
|
|
229
285
|
// A.1 PAID MODEL ENFORCEMENT (V5.5 Strategy)
|
|
230
286
|
// Check dynamic list saved by generate-config.ts
|
|
231
287
|
if (isEnterprise) {
|
|
@@ -337,16 +393,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
337
393
|
}
|
|
338
394
|
}
|
|
339
395
|
}
|
|
340
|
-
// C. Construct URL & Headers
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
396
|
+
// C. Construct URL & Headers
|
|
397
|
+
if (isEnterprise) {
|
|
398
|
+
if (!config.apiKey) {
|
|
399
|
+
emitLogToast('error', "Missing API Key for Enterprise Model", 'Proxy Error');
|
|
400
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
401
|
+
res.end(JSON.stringify({ error: { message: "API Key required for Enterprise models." } }));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
405
|
+
authHeader = `Bearer ${config.apiKey}`;
|
|
406
|
+
log(`Routing to ENTERPRISE: ${actualModel}`);
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
|
|
410
|
+
authHeader = undefined;
|
|
411
|
+
log(`Routing to FREE: ${actualModel} ${isFallbackActive ? '(FALLBACK)' : ''}`);
|
|
412
|
+
// emitLogToast('info', `Routing to: FREE UNIVERSE (${actualModel})`, 'Pollinations Routing'); // Too noisy
|
|
346
413
|
}
|
|
347
|
-
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
348
|
-
authHeader = `Bearer ${config.apiKey}`;
|
|
349
|
-
log(`Routing to gen.pollinations.ai: ${actualModel}`);
|
|
350
414
|
// NOTIFY SWITCH
|
|
351
415
|
if (isFallbackActive) {
|
|
352
416
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
@@ -378,17 +442,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
378
442
|
// LOGIC BLOCK: MODEL SPECIFIC ADAPTATIONS
|
|
379
443
|
// =========================================================
|
|
380
444
|
if (proxyBody.tools && Array.isArray(proxyBody.tools) && proxyBody.tools.length > 0) {
|
|
381
|
-
// B0. KIMI / MOONSHOT SURGICAL FIX
|
|
382
|
-
// Tools are ENABLED. We rely on penalties and strict stops to fight loops.
|
|
445
|
+
// B0. KIMI / MOONSHOT SURGICAL FIX
|
|
383
446
|
if (actualModel.includes("kimi") || actualModel.includes("moonshot")) {
|
|
384
|
-
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops.`);
|
|
447
|
+
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops/sanitization.`);
|
|
385
448
|
proxyBody.frequency_penalty = 1.1;
|
|
386
449
|
proxyBody.presence_penalty = 0.4;
|
|
387
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
|
+
});
|
|
388
458
|
}
|
|
389
|
-
// A. AZURE/OPENAI FIXES
|
|
390
|
-
if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure")) {
|
|
391
|
-
|
|
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);
|
|
392
463
|
if (proxyBody.messages) {
|
|
393
464
|
proxyBody.messages.forEach((m) => {
|
|
394
465
|
if (m.tool_calls) {
|
|
@@ -403,6 +474,11 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
403
474
|
});
|
|
404
475
|
}
|
|
405
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
|
+
}
|
|
406
482
|
// B1. NOMNOM SPECIAL (Disable Grounding, KEEP Search Tool)
|
|
407
483
|
if (actualModel === "nomnom") {
|
|
408
484
|
proxyBody.tools_config = { google_search_retrieval: { disable: true } };
|
|
@@ -410,36 +486,12 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
410
486
|
proxyBody.tools = sanitizeToolsForVertex(proxyBody.tools || []);
|
|
411
487
|
log(`[Proxy] Nomnom Fix: Grounding Disabled, Search Tool KEPT.`);
|
|
412
488
|
}
|
|
413
|
-
// B2. GEMINI FREE / FAST (CRASH FIX: STRICT SANITIZATION)
|
|
414
|
-
// Restore Tools but REMOVE conflicting ones (Search)
|
|
415
489
|
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
416
|
-
|
|
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
|
-
}
|
|
434
|
-
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
435
|
-
// Fixes "Multiple tools" error (Vertex) and "JSON body validation failed" (v5.3.5 regression)
|
|
436
|
-
// Added ChickyTutor (Claude/Gemini based) to fix "toolConfig must be defined" error.
|
|
437
|
-
else if (actualModel.includes("gemini") || actualModel.includes("chickytutor")) {
|
|
490
|
+
else if (actualModel.includes("gemini")) {
|
|
438
491
|
let hasFunctions = false;
|
|
439
492
|
if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
|
|
440
493
|
hasFunctions = proxyBody.tools.some((t) => t.type === 'function' || t.function);
|
|
441
494
|
}
|
|
442
|
-
// Old Shim logic removed (moved up)
|
|
443
495
|
if (hasFunctions) {
|
|
444
496
|
// 1. Strict cleanup of 'google_search' tool
|
|
445
497
|
proxyBody.tools = proxyBody.tools.filter((t) => {
|
|
@@ -470,14 +522,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
470
522
|
log(`[Proxy] Gemini Logic: Tools=${proxyBody.tools ? proxyBody.tools.length : 'REMOVED'}, Stops NOT Injected.`);
|
|
471
523
|
}
|
|
472
524
|
}
|
|
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
|
-
}
|
|
481
525
|
// C. GEMINI ID BACKTRACKING & SIGNATURE
|
|
482
526
|
if ((actualModel.includes("gemini") || actualModel === "nomnom") && proxyBody.messages) {
|
|
483
527
|
const lastMsg = proxyBody.messages[proxyBody.messages.length - 1];
|
|
@@ -586,10 +630,10 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
586
630
|
// 2. Notify
|
|
587
631
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
588
632
|
emitLogToast('warning', `Recovering from ${fetchRes.status} -> Switching to ${actualModel}`, 'Safety Net');
|
|
589
|
-
// 3. Re-Prepare Request
|
|
590
|
-
targetUrl = 'https://
|
|
633
|
+
// 3. Re-Prepare Request
|
|
634
|
+
targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
|
|
591
635
|
const retryHeaders = { ...headers };
|
|
592
|
-
//
|
|
636
|
+
delete retryHeaders['Authorization']; // Free = No Auth
|
|
593
637
|
const retryBody = { ...proxyBody, model: actualModel };
|
|
594
638
|
// 4. Retry Fetch
|
|
595
639
|
const retryRes = await fetchWithRetry(targetUrl, {
|