opencode-pollinations-plugin 6.1.0-beta.3 → 6.1.0-beta.30
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 +257 -61
- package/dist/index.js +53 -161
- package/dist/server/commands.d.ts +6 -0
- package/dist/server/commands.js +404 -73
- package/dist/server/config.d.ts +32 -23
- package/dist/server/config.js +183 -104
- package/dist/server/connect-response.d.ts +2 -0
- package/dist/server/connect-response.js +141 -0
- package/dist/server/generate-config.d.ts +3 -30
- package/dist/server/generate-config.js +164 -106
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +124 -149
- package/dist/server/logger.d.ts +8 -0
- package/dist/server/logger.js +36 -0
- package/dist/server/models/cache.d.ts +35 -0
- package/dist/server/models/cache.js +160 -0
- package/dist/server/models/fetcher.d.ts +18 -0
- package/dist/server/models/fetcher.js +150 -0
- package/dist/server/models/index.d.ts +6 -0
- package/dist/server/models/index.js +5 -0
- package/dist/server/models/manual.d.ts +15 -0
- package/dist/server/models/manual.js +92 -0
- package/dist/server/models/types.d.ts +55 -0
- package/dist/server/models/types.js +7 -0
- package/dist/server/models/worker.d.ts +21 -0
- package/dist/server/models/worker.js +97 -0
- package/dist/server/pollinations-api.d.ts +11 -0
- package/dist/server/pollinations-api.js +21 -8
- package/dist/server/proxy.js +195 -160
- package/dist/server/quota.d.ts +2 -0
- package/dist/server/quota.js +89 -86
- package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
- package/dist/server/scripts/pollinations_pricing.js +246 -0
- package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
- package/dist/server/scripts/test_cost_endpoints.js +61 -0
- package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
- package/dist/server/scripts/test_dynamic_pricing.js +39 -0
- package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
- package/dist/server/scripts/test_freetier_audit.js +215 -0
- package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
- package/dist/server/scripts/test_parallel_cost.js +104 -0
- package/dist/server/toast.d.ts +7 -1
- package/dist/server/toast.js +43 -10
- package/dist/tools/design/gen_diagram.d.ts +2 -0
- package/dist/tools/design/gen_diagram.js +94 -0
- package/dist/tools/design/gen_palette.d.ts +2 -0
- package/dist/tools/design/gen_palette.js +182 -0
- package/dist/tools/design/gen_qrcode.d.ts +2 -0
- package/dist/tools/design/gen_qrcode.js +50 -0
- package/dist/tools/ffmpeg.d.ts +24 -0
- package/dist/tools/ffmpeg.js +54 -0
- package/dist/tools/index.d.ts +25 -0
- package/dist/tools/index.js +86 -0
- package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
- package/dist/tools/pollinations/beta_discovery.js +197 -0
- package/dist/tools/pollinations/cost-guard.d.ts +38 -0
- package/dist/tools/pollinations/cost-guard.js +141 -0
- package/dist/tools/pollinations/deepsearch.d.ts +7 -0
- package/dist/tools/pollinations/deepsearch.js +80 -0
- package/dist/tools/pollinations/gen_audio.d.ts +18 -0
- package/dist/tools/pollinations/gen_audio.js +246 -0
- package/dist/tools/pollinations/gen_image.d.ts +11 -0
- package/dist/tools/pollinations/gen_image.js +225 -0
- package/dist/tools/pollinations/gen_music.d.ts +14 -0
- package/dist/tools/pollinations/gen_music.js +180 -0
- package/dist/tools/pollinations/gen_video.d.ts +16 -0
- package/dist/tools/pollinations/gen_video.js +256 -0
- package/dist/tools/pollinations/polli_config.d.ts +2 -0
- package/dist/tools/pollinations/polli_config.js +88 -0
- package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
- package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
- package/dist/tools/pollinations/polli_status.d.ts +2 -0
- package/dist/tools/pollinations/polli_status.js +31 -0
- package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
- package/dist/tools/pollinations/polli_web_search.js +164 -0
- package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
- package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
- package/dist/tools/pollinations/shared.d.ts +165 -0
- package/dist/tools/pollinations/shared.js +665 -0
- package/dist/tools/pollinations/test_estimators.d.ts +1 -0
- package/dist/tools/pollinations/test_estimators.js +22 -0
- package/dist/tools/pollinations/transcribe_audio.d.ts +13 -0
- package/dist/tools/pollinations/transcribe_audio.js +194 -0
- package/dist/tools/power/extract_audio.d.ts +2 -0
- package/dist/tools/power/extract_audio.js +179 -0
- package/dist/tools/power/extract_frames.d.ts +2 -0
- package/dist/tools/power/extract_frames.js +237 -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 +392 -0
- package/dist/tools/power/rmbg_keys.d.ts +2 -0
- package/dist/tools/power/rmbg_keys.js +79 -0
- package/dist/tools/shared.d.ts +30 -0
- package/dist/tools/shared.js +80 -0
- package/package.json +10 -4
- package/dist/server/models-seed.d.ts +0 -18
- package/dist/server/models-seed.js +0 -55
package/dist/server/proxy.js
CHANGED
|
@@ -3,20 +3,13 @@ import * as path from 'path';
|
|
|
3
3
|
import { loadConfig, saveConfig } from './config.js';
|
|
4
4
|
import { handleCommand } from './commands.js';
|
|
5
5
|
import { emitStatusToast, emitLogToast } from './toast.js';
|
|
6
|
+
import { buildConnectResponse } from './connect-response.js';
|
|
7
|
+
import { log } from './logger.js';
|
|
8
|
+
import { getConfigDir } from './config.js';
|
|
6
9
|
// --- PERSISTENCE: SIGNATURE MAP (Multi-Round Support) ---
|
|
7
|
-
const SIG_FILE = path.join(
|
|
10
|
+
const SIG_FILE = path.join(getConfigDir(), 'pollinations-signature.json');
|
|
8
11
|
let signatureMap = {};
|
|
9
12
|
let lastSignature = null; // V1 Fallback Global
|
|
10
|
-
function log(msg) {
|
|
11
|
-
try {
|
|
12
|
-
const ts = new Date().toISOString();
|
|
13
|
-
if (!fs.existsSync('/tmp/opencode_pollinations_debug.log')) {
|
|
14
|
-
fs.writeFileSync('/tmp/opencode_pollinations_debug.log', '');
|
|
15
|
-
}
|
|
16
|
-
fs.appendFileSync('/tmp/opencode_pollinations_debug.log', `[Proxy] ${ts} ${msg}\n`);
|
|
17
|
-
}
|
|
18
|
-
catch (e) { }
|
|
19
|
-
}
|
|
20
13
|
try {
|
|
21
14
|
if (fs.existsSync(SIG_FILE)) {
|
|
22
15
|
signatureMap = JSON.parse(fs.readFileSync(SIG_FILE, 'utf-8'));
|
|
@@ -78,6 +71,23 @@ function dereferenceSchema(schema, rootDefs) {
|
|
|
78
71
|
schema.description = (schema.description || "") + " [Ref Failed]";
|
|
79
72
|
}
|
|
80
73
|
}
|
|
74
|
+
// VERTEX FIX: 'const' not supported -> convert to 'enum'
|
|
75
|
+
if (schema.const !== undefined) {
|
|
76
|
+
schema.enum = [schema.const];
|
|
77
|
+
delete schema.const;
|
|
78
|
+
}
|
|
79
|
+
// VERTEX FIX: 'anyOf' must be exclusive (no other siblings)
|
|
80
|
+
if (schema.anyOf || schema.oneOf) {
|
|
81
|
+
// Vertex demands strict exclusivity.
|
|
82
|
+
// We keep 'definitions'/'$defs' if present at root (though unlikely here)
|
|
83
|
+
// But for a property node, we must strip EVERYTHING else.
|
|
84
|
+
const keys = Object.keys(schema);
|
|
85
|
+
keys.forEach(k => {
|
|
86
|
+
if (k !== 'anyOf' && k !== 'oneOf' && k !== 'definitions' && k !== '$defs') {
|
|
87
|
+
delete schema[k];
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
81
91
|
if (schema.properties) {
|
|
82
92
|
for (const key in schema.properties) {
|
|
83
93
|
schema.properties[key] = dereferenceSchema(schema.properties[key], rootDefs);
|
|
@@ -86,6 +96,15 @@ function dereferenceSchema(schema, rootDefs) {
|
|
|
86
96
|
if (schema.items) {
|
|
87
97
|
schema.items = dereferenceSchema(schema.items, rootDefs);
|
|
88
98
|
}
|
|
99
|
+
if (schema.anyOf) {
|
|
100
|
+
schema.anyOf = schema.anyOf.map((s) => dereferenceSchema(s, rootDefs));
|
|
101
|
+
}
|
|
102
|
+
if (schema.oneOf) {
|
|
103
|
+
schema.oneOf = schema.oneOf.map((s) => dereferenceSchema(s, rootDefs));
|
|
104
|
+
}
|
|
105
|
+
if (schema.allOf) {
|
|
106
|
+
schema.allOf = schema.allOf.map((s) => dereferenceSchema(s, rootDefs));
|
|
107
|
+
}
|
|
89
108
|
if (schema.optional !== undefined)
|
|
90
109
|
delete schema.optional;
|
|
91
110
|
if (schema.title)
|
|
@@ -107,6 +126,38 @@ function sanitizeToolsForVertex(tools) {
|
|
|
107
126
|
return tool;
|
|
108
127
|
});
|
|
109
128
|
}
|
|
129
|
+
function sanitizeToolsForBedrock(tools) {
|
|
130
|
+
return tools.map(tool => {
|
|
131
|
+
if (tool.function) {
|
|
132
|
+
if (!tool.function.description || tool.function.description.length === 0) {
|
|
133
|
+
tool.function.description = " "; // Force non-empty string
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return tool;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function sanitizeSchemaForKimi(schema) {
|
|
140
|
+
if (!schema || typeof schema !== 'object')
|
|
141
|
+
return schema;
|
|
142
|
+
// Kimi Fixes
|
|
143
|
+
if (schema.title)
|
|
144
|
+
delete schema.title;
|
|
145
|
+
// Fix empty objects "{}" which Kimi hates.
|
|
146
|
+
// If it's an empty object without type, assume string or object?
|
|
147
|
+
// Often happens with "additionalProperties: {}"
|
|
148
|
+
if (Object.keys(schema).length === 0) {
|
|
149
|
+
schema.type = "string"; // Fallback to safe type
|
|
150
|
+
schema.description = "Any value";
|
|
151
|
+
}
|
|
152
|
+
if (schema.properties) {
|
|
153
|
+
for (const key in schema.properties) {
|
|
154
|
+
schema.properties[key] = sanitizeSchemaForKimi(schema.properties[key]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (schema.items)
|
|
158
|
+
sanitizeSchemaForKimi(schema.items);
|
|
159
|
+
return schema;
|
|
160
|
+
}
|
|
110
161
|
function truncateTools(tools, limit = 120) {
|
|
111
162
|
if (!tools || tools.length <= limit)
|
|
112
163
|
return tools;
|
|
@@ -114,12 +165,16 @@ function truncateTools(tools, limit = 120) {
|
|
|
114
165
|
}
|
|
115
166
|
const MAX_RETRIES = 3;
|
|
116
167
|
const RETRY_DELAY_MS = 1000;
|
|
168
|
+
const FETCH_TIMEOUT_MS = 600000; // 10 Minutes global timeout
|
|
117
169
|
function sleep(ms) {
|
|
118
170
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
119
171
|
}
|
|
120
172
|
async function fetchWithRetry(url, options, retries = MAX_RETRIES) {
|
|
121
173
|
try {
|
|
122
|
-
const
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
176
|
+
const response = await fetch(url, { ...options, signal: controller.signal });
|
|
177
|
+
clearTimeout(timeoutId);
|
|
123
178
|
if (response.ok)
|
|
124
179
|
return response;
|
|
125
180
|
if (response.status === 404 || response.status === 401 || response.status === 400) {
|
|
@@ -153,11 +208,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
153
208
|
const config = loadConfig();
|
|
154
209
|
// DEBUG: Trace Config State for Hot Reload verification
|
|
155
210
|
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
211
|
// 0. COMMAND HANDLING
|
|
162
212
|
if (body.messages && body.messages.length > 0) {
|
|
163
213
|
const lastMsg = body.messages[body.messages.length - 1];
|
|
@@ -208,6 +258,31 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
208
258
|
}
|
|
209
259
|
}
|
|
210
260
|
log(`Incoming Model (OpenCode ID): ${body.model}`);
|
|
261
|
+
// 0. SPECIAL: pollinations/connect (Guide & Status)
|
|
262
|
+
const CONNECT_MODEL_IDS = ['pollinations/connect', 'free/pollinations/connect', 'enter/pollinations/connect', 'connect-pollinations'];
|
|
263
|
+
if (CONNECT_MODEL_IDS.includes(body.model)) {
|
|
264
|
+
const guideContent = await buildConnectResponse(config);
|
|
265
|
+
res.writeHead(200, {
|
|
266
|
+
'Content-Type': 'text/event-stream',
|
|
267
|
+
'Cache-Control': 'no-cache',
|
|
268
|
+
'Connection': 'keep-alive'
|
|
269
|
+
});
|
|
270
|
+
const chunk = JSON.stringify({
|
|
271
|
+
id: 'connect-' + Date.now(),
|
|
272
|
+
object: 'chat.completion.chunk',
|
|
273
|
+
created: Math.floor(Date.now() / 1000),
|
|
274
|
+
model: 'pollinations/connect',
|
|
275
|
+
choices: [{
|
|
276
|
+
index: 0,
|
|
277
|
+
delta: { role: 'assistant', content: guideContent },
|
|
278
|
+
finish_reason: 'stop' // Instant finish
|
|
279
|
+
}]
|
|
280
|
+
});
|
|
281
|
+
res.write(`data: ${chunk}\n\n`);
|
|
282
|
+
res.write(`data: [DONE]\n\n`);
|
|
283
|
+
res.end();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
211
286
|
// 1. STRICT ROUTING & SAFETY NET LOGIC (V5)
|
|
212
287
|
let actualModel = body.model || "openai";
|
|
213
288
|
let isEnterprise = false;
|
|
@@ -216,16 +291,15 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
216
291
|
// LOAD QUOTA FOR SAFETY CHECKS
|
|
217
292
|
const { getQuotaStatus, formatQuotaForToast } = await import('./quota.js');
|
|
218
293
|
const quota = await getQuotaStatus(false);
|
|
219
|
-
// A. Resolve Base Target
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
actualModel = actualModel.replace(
|
|
294
|
+
// A. Resolve Base Target
|
|
295
|
+
if (actualModel.startsWith('enter/')) {
|
|
296
|
+
isEnterprise = true;
|
|
297
|
+
actualModel = actualModel.replace('enter/', '');
|
|
223
298
|
}
|
|
224
|
-
else if (actualModel.startsWith('free/')
|
|
225
|
-
|
|
299
|
+
else if (actualModel.startsWith('free/')) {
|
|
300
|
+
isEnterprise = false;
|
|
301
|
+
actualModel = actualModel.replace('free/', '');
|
|
226
302
|
}
|
|
227
|
-
// v6.0: Everything is enterprise now (requires API key)
|
|
228
|
-
isEnterprise = true;
|
|
229
303
|
// A.1 PAID MODEL ENFORCEMENT (V5.5 Strategy)
|
|
230
304
|
// Check dynamic list saved by generate-config.ts
|
|
231
305
|
if (isEnterprise) {
|
|
@@ -245,8 +319,13 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
245
319
|
if (quota.walletBalance <= 0.001) { // Floating point safety
|
|
246
320
|
log(`[SafetyNet] Paid Only Model (${actualModel}) requested but Wallet is Empty ($${quota.walletBalance}). BLOCKING.`);
|
|
247
321
|
// Immediate Block or Fallback?
|
|
248
|
-
//
|
|
249
|
-
|
|
322
|
+
// Text says: "💎 Paid Only models require purchased pollen only"
|
|
323
|
+
// Blocking is safer/clearer than falling back to a free model which might not be what the user expects for a "Pro" feature?
|
|
324
|
+
// Actually, Fallback to Free is usually better for UX if configured, BUT for specific "Paid Only" requests, the user explicitly chose a powerful model.
|
|
325
|
+
// Falling back to Mistral might be confusing if they asked for Gemini-Large.
|
|
326
|
+
// BUT we are failing gracefully.
|
|
327
|
+
// Let's Fallback to Free Default and Warn.
|
|
328
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
250
329
|
isEnterprise = false;
|
|
251
330
|
isFallbackActive = true;
|
|
252
331
|
fallbackReason = "Paid Only Model requires purchased credits";
|
|
@@ -272,127 +351,99 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
272
351
|
// WE DO NOT RETURN 403. WE ALLOW THE REQUEST.
|
|
273
352
|
// Since config.mode is now 'manual', the next checks (alwaysfree/pro) will be skipped.
|
|
274
353
|
}
|
|
275
|
-
|
|
276
|
-
// - Paid models BLOCKED (not fallback, BLOCK with message)
|
|
277
|
-
// - tier < threshold → fallback economy
|
|
278
|
-
// - tier = 0 → STOP (no wallet access)
|
|
279
|
-
if (config.mode === 'economy') {
|
|
354
|
+
if (config.mode === 'alwaysfree') {
|
|
280
355
|
if (isEnterprise) {
|
|
281
|
-
//
|
|
356
|
+
// Paid Only Check: BLOCK (not fallback) in AlwaysFree mode
|
|
282
357
|
try {
|
|
283
358
|
const homedir = process.env.HOME || '/tmp';
|
|
284
359
|
const standardPaidPath = path.join(homedir, '.pollinations', 'pollinations-paid-models.json');
|
|
285
360
|
if (fs.existsSync(standardPaidPath)) {
|
|
286
361
|
const paidModels = JSON.parse(fs.readFileSync(standardPaidPath, 'utf-8'));
|
|
287
362
|
if (paidModels.includes(actualModel)) {
|
|
288
|
-
log(`[
|
|
289
|
-
emitStatusToast('
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
363
|
+
log(`[AlwaysFree] BLOCKED: Paid Only Model (${actualModel}).`);
|
|
364
|
+
emitStatusToast('warning', `🚫 Modèle payant bloqué: ${actualModel}`, 'AlwaysFree Mode');
|
|
365
|
+
const blockMsg = {
|
|
366
|
+
id: `chatcmpl-block-${Date.now()}`,
|
|
367
|
+
object: 'chat.completion',
|
|
368
|
+
created: Math.floor(Date.now() / 1000),
|
|
369
|
+
model: actualModel,
|
|
370
|
+
choices: [{
|
|
371
|
+
index: 0,
|
|
372
|
+
message: {
|
|
373
|
+
role: 'assistant',
|
|
374
|
+
content: `🚫 **Modèle payant non disponible en mode AlwaysFree**\n\nLe modèle \`${actualModel}\` consomme directement votre wallet (💎 Paid Only).\n\n**Solutions :**\n• \`/pollinations config mode pro\` — Autorise les modèles payants avec protection wallet\n• \`/pollinations config mode manual\` — Aucune restriction, contrôle total`
|
|
375
|
+
},
|
|
376
|
+
finish_reason: 'stop'
|
|
377
|
+
}],
|
|
378
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
379
|
+
};
|
|
380
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
381
|
+
res.end(JSON.stringify(blockMsg));
|
|
297
382
|
return;
|
|
298
383
|
}
|
|
299
384
|
}
|
|
300
385
|
}
|
|
301
|
-
catch (e) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
actualModel = config.fallbacks.economy || 'nova-fast';
|
|
386
|
+
catch (e) { }
|
|
387
|
+
if (!isFallbackActive && quota.tier === 'error') {
|
|
388
|
+
// Network error or unknown error (but NOT auth_limited, handled above)
|
|
389
|
+
log(`[SafetyNet] AlwaysFree Mode: Quota Check Failed. Switching to Free Fallback.`);
|
|
390
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
391
|
+
isEnterprise = false;
|
|
308
392
|
isFallbackActive = true;
|
|
309
393
|
fallbackReason = "Quota Unreachable (Safety)";
|
|
310
394
|
}
|
|
311
395
|
else {
|
|
312
396
|
const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
|
|
313
|
-
// 3. STOP if tier = 0
|
|
314
|
-
if (quota.tierRemaining <= 0.01) {
|
|
315
|
-
log(`[Economy] STOP: Tier exhausted, no wallet access in Economy mode`);
|
|
316
|
-
emitStatusToast('error', `🛑 Quota épuisé! Attendez le reset ou passez en mode Pro.`, 'Mode Economy');
|
|
317
|
-
res.writeHead(429, { 'Content-Type': 'application/json' });
|
|
318
|
-
res.end(JSON.stringify({
|
|
319
|
-
error: {
|
|
320
|
-
message: `🛑 Daily quota exhausted. Wait for reset or switch to Pro mode to use wallet credits.`,
|
|
321
|
-
code: 'ECONOMY_TIER_EXHAUSTED'
|
|
322
|
-
}
|
|
323
|
-
}));
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
// 4. Fallback if tier < threshold
|
|
327
397
|
if (tierRatio <= (config.thresholds.tier / 100)) {
|
|
328
|
-
log(`[
|
|
329
|
-
actualModel = config.fallbacks.
|
|
398
|
+
log(`[SafetyNet] AlwaysFree Mode: Tier (${(tierRatio * 100).toFixed(1)}%) <= Threshold (${config.thresholds.tier}%). Switching.`);
|
|
399
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
400
|
+
isEnterprise = false;
|
|
330
401
|
isFallbackActive = true;
|
|
331
|
-
fallbackReason = `Tier < ${config.thresholds.tier}% (
|
|
402
|
+
fallbackReason = `Daily Tier < ${config.thresholds.tier}% (Wallet Protected)`;
|
|
332
403
|
}
|
|
333
404
|
}
|
|
334
405
|
}
|
|
335
406
|
}
|
|
336
|
-
// === MODE PRO ===
|
|
337
|
-
// - Paid models ALLOWED
|
|
338
|
-
// - wallet < wallet_stop $ → STOP
|
|
339
|
-
// - wallet < wallet_warn % → fallback pro
|
|
340
|
-
// - tier exhausted → info toast (continue on wallet)
|
|
341
407
|
else if (config.mode === 'pro') {
|
|
342
408
|
if (isEnterprise) {
|
|
343
|
-
// Init session wallet tracking (first request)
|
|
344
|
-
if (!config.session?.wallet_initial && quota.walletBalance > 0) {
|
|
345
|
-
const { initSessionWallet } = await import('./config.js');
|
|
346
|
-
initSessionWallet(quota.walletBalance);
|
|
347
|
-
}
|
|
348
409
|
if (quota.tier === 'error') {
|
|
349
|
-
|
|
350
|
-
|
|
410
|
+
// Network error or unknown
|
|
411
|
+
log(`[SafetyNet] Pro Mode: Quota Unreachable. Switching to Free Fallback.`);
|
|
412
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
413
|
+
isEnterprise = false;
|
|
351
414
|
isFallbackActive = true;
|
|
352
415
|
fallbackReason = "Quota Unreachable (Safety)";
|
|
353
416
|
}
|
|
354
417
|
else {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (quota.walletBalance < walletStop) {
|
|
361
|
-
log(`[Pro] STOP: Wallet $${quota.walletBalance} < limit $${walletStop}`);
|
|
362
|
-
emitStatusToast('error', `🛑 Wallet $${quota.walletBalance.toFixed(2)} sous limite $${walletStop}`, 'Mode Pro');
|
|
363
|
-
res.writeHead(429, { 'Content-Type': 'application/json' });
|
|
364
|
-
res.end(JSON.stringify({
|
|
365
|
-
error: {
|
|
366
|
-
message: `🛑 Wallet ($${quota.walletBalance.toFixed(2)}) below hard limit ($${walletStop}). Add credits or adjust wallet_stop threshold.`,
|
|
367
|
-
code: 'PRO_WALLET_LIMIT'
|
|
368
|
-
}
|
|
369
|
-
}));
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
// 2. Fallback if wallet% < wallet_warn%
|
|
373
|
-
if (walletPercent <= walletWarnPercent) {
|
|
374
|
-
log(`[Pro] Wallet ${walletPercent.toFixed(1)}% <= ${walletWarnPercent}%, switching to fallback`);
|
|
375
|
-
actualModel = config.fallbacks.pro || 'qwen-coder';
|
|
418
|
+
const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
|
|
419
|
+
if (quota.walletBalance < config.thresholds.wallet && tierRatio <= (config.thresholds.tier / 100)) {
|
|
420
|
+
log(`[SafetyNet] Pro Mode: Wallet < $${config.thresholds.wallet} AND Tier < ${config.thresholds.tier}%. Switching.`);
|
|
421
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
422
|
+
isEnterprise = false;
|
|
376
423
|
isFallbackActive = true;
|
|
377
|
-
fallbackReason = `Wallet
|
|
378
|
-
}
|
|
379
|
-
// 3. Info if tier exhausted (continue on wallet)
|
|
380
|
-
if (quota.tierRemaining <= 0.01 && !isFallbackActive) {
|
|
381
|
-
emitStatusToast('info', `ℹ️ Tier épuisé, utilisation du wallet ($${quota.walletBalance.toFixed(2)})`, 'Mode Pro');
|
|
424
|
+
fallbackReason = `Wallet & Tier Critical`;
|
|
382
425
|
}
|
|
383
426
|
}
|
|
384
427
|
}
|
|
385
428
|
}
|
|
386
|
-
// C. Construct URL & Headers
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
429
|
+
// C. Construct URL & Headers
|
|
430
|
+
if (isEnterprise) {
|
|
431
|
+
if (!config.apiKey) {
|
|
432
|
+
emitLogToast('error', "Missing API Key for Enterprise Model", 'Proxy Error');
|
|
433
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
434
|
+
res.end(JSON.stringify({ error: { message: "API Key required for Enterprise models." } }));
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
438
|
+
authHeader = `Bearer ${config.apiKey}`;
|
|
439
|
+
log(`Routing to ENTERPRISE: ${actualModel}`);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
|
|
443
|
+
authHeader = undefined;
|
|
444
|
+
log(`Routing to FREE: ${actualModel} ${isFallbackActive ? '(FALLBACK)' : ''}`);
|
|
445
|
+
// emitLogToast('info', `Routing to: FREE UNIVERSE (${actualModel})`, 'Pollinations Routing'); // Too noisy
|
|
392
446
|
}
|
|
393
|
-
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
394
|
-
authHeader = `Bearer ${config.apiKey}`;
|
|
395
|
-
log(`Routing to gen.pollinations.ai: ${actualModel}`);
|
|
396
447
|
// NOTIFY SWITCH
|
|
397
448
|
if (isFallbackActive) {
|
|
398
449
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
@@ -424,17 +475,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
424
475
|
// LOGIC BLOCK: MODEL SPECIFIC ADAPTATIONS
|
|
425
476
|
// =========================================================
|
|
426
477
|
if (proxyBody.tools && Array.isArray(proxyBody.tools) && proxyBody.tools.length > 0) {
|
|
427
|
-
// B0. KIMI / MOONSHOT SURGICAL FIX
|
|
428
|
-
// Tools are ENABLED. We rely on penalties and strict stops to fight loops.
|
|
478
|
+
// B0. KIMI / MOONSHOT SURGICAL FIX
|
|
429
479
|
if (actualModel.includes("kimi") || actualModel.includes("moonshot")) {
|
|
430
|
-
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops.`);
|
|
480
|
+
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops/sanitization.`);
|
|
431
481
|
proxyBody.frequency_penalty = 1.1;
|
|
432
482
|
proxyBody.presence_penalty = 0.4;
|
|
433
483
|
proxyBody.stop = ["<|endoftext|>", "User:", "\nUser", "User :"];
|
|
484
|
+
// KIMI FIX: Remove 'title' from schema
|
|
485
|
+
proxyBody.tools = proxyBody.tools.map((t) => {
|
|
486
|
+
if (t.function && t.function.parameters) {
|
|
487
|
+
t.function.parameters = sanitizeSchemaForKimi(t.function.parameters);
|
|
488
|
+
}
|
|
489
|
+
return t;
|
|
490
|
+
});
|
|
434
491
|
}
|
|
435
|
-
// A. AZURE/OPENAI FIXES
|
|
436
|
-
if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure")) {
|
|
437
|
-
|
|
492
|
+
// A. AZURE/OPENAI FIXES + MIDJOURNEY + GROK
|
|
493
|
+
if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure") || actualModel.includes("midijourney") || actualModel.includes("grok")) {
|
|
494
|
+
const limit = (actualModel.includes("midijourney") || actualModel.includes("grok")) ? 128 : 120;
|
|
495
|
+
proxyBody.tools = truncateTools(proxyBody.tools, limit);
|
|
438
496
|
if (proxyBody.messages) {
|
|
439
497
|
proxyBody.messages.forEach((m) => {
|
|
440
498
|
if (m.tool_calls) {
|
|
@@ -449,6 +507,11 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
449
507
|
});
|
|
450
508
|
}
|
|
451
509
|
}
|
|
510
|
+
// BEDROCK FIX (Claude / Nova / ChickyTutor)
|
|
511
|
+
if (actualModel.includes("claude") || actualModel.includes("nova") || actualModel.includes("bedrock") || actualModel.includes("chickytutor")) {
|
|
512
|
+
log(`[Proxy] Bedrock: Sanitizing tools description.`);
|
|
513
|
+
proxyBody.tools = sanitizeToolsForBedrock(proxyBody.tools);
|
|
514
|
+
}
|
|
452
515
|
// B1. NOMNOM SPECIAL (Disable Grounding, KEEP Search Tool)
|
|
453
516
|
if (actualModel === "nomnom") {
|
|
454
517
|
proxyBody.tools_config = { google_search_retrieval: { disable: true } };
|
|
@@ -456,36 +519,12 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
456
519
|
proxyBody.tools = sanitizeToolsForVertex(proxyBody.tools || []);
|
|
457
520
|
log(`[Proxy] Nomnom Fix: Grounding Disabled, Search Tool KEPT.`);
|
|
458
521
|
}
|
|
459
|
-
// B2. GEMINI FREE / FAST (CRASH FIX: STRICT SANITIZATION)
|
|
460
|
-
// Restore Tools but REMOVE conflicting ones (Search)
|
|
461
522
|
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
462
|
-
|
|
463
|
-
// GLOBAL BEDROCK FIX (All Models)
|
|
464
|
-
// Check if history has tools but current request misses tools definition.
|
|
465
|
-
// This happens when OpenCode sends the Tool Result (optimisation),
|
|
466
|
-
// but Bedrock requires toolConfig to validate the history.
|
|
467
|
-
const hasToolHistory = proxyBody.messages?.some((m) => m.role === 'tool' || m.tool_calls);
|
|
468
|
-
if (hasToolHistory && (!proxyBody.tools || proxyBody.tools.length === 0)) {
|
|
469
|
-
// Inject Shim Tool to satisfy Bedrock
|
|
470
|
-
proxyBody.tools = [{
|
|
471
|
-
type: 'function',
|
|
472
|
-
function: {
|
|
473
|
-
name: '_bedrock_compatibility_shim',
|
|
474
|
-
description: 'Internal system tool to satisfy Bedrock strict toolConfig requirement. Do not use.',
|
|
475
|
-
parameters: { type: 'object', properties: {} }
|
|
476
|
-
}
|
|
477
|
-
}];
|
|
478
|
-
log(`[Proxy] Bedrock Fix: Injected shim tool for ${actualModel} (History has tools, Request missing tools)`);
|
|
479
|
-
}
|
|
480
|
-
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
481
|
-
// Fixes "Multiple tools" error (Vertex) and "JSON body validation failed" (v5.3.5 regression)
|
|
482
|
-
// Added ChickyTutor (Claude/Gemini based) to fix "toolConfig must be defined" error.
|
|
483
|
-
else if (actualModel.includes("gemini") || actualModel.includes("chickytutor")) {
|
|
523
|
+
else if (actualModel.includes("gemini")) {
|
|
484
524
|
let hasFunctions = false;
|
|
485
525
|
if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
|
|
486
526
|
hasFunctions = proxyBody.tools.some((t) => t.type === 'function' || t.function);
|
|
487
527
|
}
|
|
488
|
-
// Old Shim logic removed (moved up)
|
|
489
528
|
if (hasFunctions) {
|
|
490
529
|
// 1. Strict cleanup of 'google_search' tool
|
|
491
530
|
proxyBody.tools = proxyBody.tools.filter((t) => {
|
|
@@ -516,14 +555,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
516
555
|
log(`[Proxy] Gemini Logic: Tools=${proxyBody.tools ? proxyBody.tools.length : 'REMOVED'}, Stops NOT Injected.`);
|
|
517
556
|
}
|
|
518
557
|
}
|
|
519
|
-
// B5. BEDROCK TOKEN LIMIT FIX
|
|
520
|
-
if (actualModel.includes("chicky") || actualModel.includes("mistral")) {
|
|
521
|
-
// Force max_tokens if not present or too high (Bedrock outputs usually max 4k, context 8k+ but strict check)
|
|
522
|
-
if (!proxyBody.max_tokens || proxyBody.max_tokens > 4096) {
|
|
523
|
-
proxyBody.max_tokens = 4096;
|
|
524
|
-
log(`[Proxy] Enforcing max_tokens=4096 for ${actualModel} (Bedrock Limit)`);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
558
|
// C. GEMINI ID BACKTRACKING & SIGNATURE
|
|
528
559
|
if ((actualModel.includes("gemini") || actualModel === "nomnom") && proxyBody.messages) {
|
|
529
560
|
const lastMsg = proxyBody.messages[proxyBody.messages.length - 1];
|
|
@@ -609,14 +640,18 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
609
640
|
if ((isEnterpriseFallback || isGeminiToolsFallback) && config.mode !== 'manual') {
|
|
610
641
|
log(`[SafetyNet] Upstream Rejection (${fetchRes.status}). Triggering Transparent Fallback.`);
|
|
611
642
|
if (isEnterpriseFallback) {
|
|
612
|
-
// 1a. Enterprise -> Fallback
|
|
613
|
-
actualModel = config.
|
|
614
|
-
? (config.fallbacks.pro || 'qwen-coder')
|
|
615
|
-
: (config.fallbacks.economy || 'nova-fast');
|
|
643
|
+
// 1a. Enterprise -> Free Fallback
|
|
644
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
616
645
|
isEnterprise = false;
|
|
617
646
|
isFallbackActive = true;
|
|
618
|
-
if (fetchRes.status === 402)
|
|
647
|
+
if (fetchRes.status === 402) {
|
|
619
648
|
fallbackReason = "Insufficient Funds (Upstream 402)";
|
|
649
|
+
// Force refresh quota cache so next pre-flight check is accurate
|
|
650
|
+
try {
|
|
651
|
+
await getQuotaStatus(true);
|
|
652
|
+
}
|
|
653
|
+
catch (e) { }
|
|
654
|
+
}
|
|
620
655
|
else if (fetchRes.status === 429)
|
|
621
656
|
fallbackReason = "Rate Limit (Upstream 429)";
|
|
622
657
|
else if (fetchRes.status === 401)
|
|
@@ -634,10 +669,10 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
634
669
|
// 2. Notify
|
|
635
670
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
636
671
|
emitLogToast('warning', `Recovering from ${fetchRes.status} -> Switching to ${actualModel}`, 'Safety Net');
|
|
637
|
-
// 3. Re-Prepare Request
|
|
638
|
-
targetUrl = 'https://
|
|
672
|
+
// 3. Re-Prepare Request
|
|
673
|
+
targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
|
|
639
674
|
const retryHeaders = { ...headers };
|
|
640
|
-
//
|
|
675
|
+
delete retryHeaders['Authorization']; // Free = No Auth
|
|
641
676
|
const retryBody = { ...proxyBody, model: actualModel };
|
|
642
677
|
// 4. Retry Fetch
|
|
643
678
|
const retryRes = await fetchWithRetry(targetUrl, {
|
package/dist/server/quota.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DetailedUsageEntry } from './pollinations-api.js';
|
|
1
2
|
export interface QuotaStatus {
|
|
2
3
|
tierRemaining: number;
|
|
3
4
|
tierUsed: number;
|
|
@@ -12,5 +13,6 @@ export interface QuotaStatus {
|
|
|
12
13
|
tierEmoji: string;
|
|
13
14
|
errorType?: 'auth_limited' | 'network' | 'unknown';
|
|
14
15
|
}
|
|
16
|
+
export declare function fetchUsageForPeriod(apiKey: string, lastReset: Date): Promise<DetailedUsageEntry[]>;
|
|
15
17
|
export declare function getQuotaStatus(forceRefresh?: boolean): Promise<QuotaStatus>;
|
|
16
18
|
export declare function formatQuotaForToast(quota: QuotaStatus): string;
|