opencode-pollinations-plugin 6.1.0-beta.2 → 6.1.0-beta.22
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 +242 -62
- package/dist/index.js +68 -159
- package/dist/server/commands.d.ts +6 -0
- package/dist/server/commands.js +400 -71
- package/dist/server/config.d.ts +32 -23
- package/dist/server/config.js +183 -99
- 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 +223 -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 +24 -0
- package/dist/tools/index.js +83 -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_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 +366 -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,59 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
208
258
|
}
|
|
209
259
|
}
|
|
210
260
|
log(`Incoming Model (OpenCode ID): ${body.model}`);
|
|
261
|
+
// 0. TEST 4: Virtual Model Handler for Commands
|
|
262
|
+
if (body.model === 'pollinations/pollimock-handler' || body.model === 'pollimock-handler') {
|
|
263
|
+
const mockContent = "🚀 **[TEST 4] Modèle Virtuel de Commande !**\n\nCe texte n'a jamais quitté ton ordinateur. La commande `/pollimock` a demandé à OpenCode de se brancher temporairement sur le modèle virtuel `pollimock-handler`.\nLe proxy a intercepté cet appel et répondu instantanément.\n\n✅ L'historique affiche bien le message du Chat, **mais la requête LLM est totalement court-circuitée**.\nC'est la méthode ultime pour créer des vues de configuration via commandes (`/pollinations-config` par ex) sans polluer le crédit ou les LLM tiers !";
|
|
264
|
+
res.writeHead(200, {
|
|
265
|
+
'Content-Type': 'text/event-stream',
|
|
266
|
+
'Cache-Control': 'no-cache',
|
|
267
|
+
'Connection': 'keep-alive'
|
|
268
|
+
});
|
|
269
|
+
const chunk = JSON.stringify({
|
|
270
|
+
id: 'mock-' + Date.now(),
|
|
271
|
+
object: 'chat.completion.chunk',
|
|
272
|
+
created: Math.floor(Date.now() / 1000),
|
|
273
|
+
model: body.model,
|
|
274
|
+
choices: [{ index: 0, delta: { role: 'assistant', content: mockContent }, finish_reason: null }]
|
|
275
|
+
});
|
|
276
|
+
res.write(`data: ${chunk}\n\n`);
|
|
277
|
+
const chunkEnd = JSON.stringify({
|
|
278
|
+
id: 'mock-' + Date.now(),
|
|
279
|
+
object: 'chat.completion.chunk',
|
|
280
|
+
created: Math.floor(Date.now() / 1000),
|
|
281
|
+
model: body.model,
|
|
282
|
+
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }]
|
|
283
|
+
});
|
|
284
|
+
res.write(`data: ${chunkEnd}\n\n`);
|
|
285
|
+
res.write('data: [DONE]\n\n');
|
|
286
|
+
res.end();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// 0. SPECIAL: pollinations/connect (Guide & Status)
|
|
290
|
+
const CONNECT_MODEL_IDS = ['pollinations/connect', 'free/pollinations/connect', 'enter/pollinations/connect', 'connect-pollinations'];
|
|
291
|
+
if (CONNECT_MODEL_IDS.includes(body.model)) {
|
|
292
|
+
const guideContent = await buildConnectResponse(config);
|
|
293
|
+
res.writeHead(200, {
|
|
294
|
+
'Content-Type': 'text/event-stream',
|
|
295
|
+
'Cache-Control': 'no-cache',
|
|
296
|
+
'Connection': 'keep-alive'
|
|
297
|
+
});
|
|
298
|
+
const chunk = JSON.stringify({
|
|
299
|
+
id: 'connect-' + Date.now(),
|
|
300
|
+
object: 'chat.completion.chunk',
|
|
301
|
+
created: Math.floor(Date.now() / 1000),
|
|
302
|
+
model: 'pollinations/connect',
|
|
303
|
+
choices: [{
|
|
304
|
+
index: 0,
|
|
305
|
+
delta: { role: 'assistant', content: guideContent },
|
|
306
|
+
finish_reason: 'stop' // Instant finish
|
|
307
|
+
}]
|
|
308
|
+
});
|
|
309
|
+
res.write(`data: ${chunk}\n\n`);
|
|
310
|
+
res.write(`data: [DONE]\n\n`);
|
|
311
|
+
res.end();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
211
314
|
// 1. STRICT ROUTING & SAFETY NET LOGIC (V5)
|
|
212
315
|
let actualModel = body.model || "openai";
|
|
213
316
|
let isEnterprise = false;
|
|
@@ -216,16 +319,15 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
216
319
|
// LOAD QUOTA FOR SAFETY CHECKS
|
|
217
320
|
const { getQuotaStatus, formatQuotaForToast } = await import('./quota.js');
|
|
218
321
|
const quota = await getQuotaStatus(false);
|
|
219
|
-
// A. Resolve Base Target
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
actualModel = actualModel.replace(
|
|
322
|
+
// A. Resolve Base Target
|
|
323
|
+
if (actualModel.startsWith('enter/')) {
|
|
324
|
+
isEnterprise = true;
|
|
325
|
+
actualModel = actualModel.replace('enter/', '');
|
|
223
326
|
}
|
|
224
|
-
else if (actualModel.startsWith('free/')
|
|
225
|
-
|
|
327
|
+
else if (actualModel.startsWith('free/')) {
|
|
328
|
+
isEnterprise = false;
|
|
329
|
+
actualModel = actualModel.replace('free/', '');
|
|
226
330
|
}
|
|
227
|
-
// v6.0: Everything is enterprise now (requires API key)
|
|
228
|
-
isEnterprise = true;
|
|
229
331
|
// A.1 PAID MODEL ENFORCEMENT (V5.5 Strategy)
|
|
230
332
|
// Check dynamic list saved by generate-config.ts
|
|
231
333
|
if (isEnterprise) {
|
|
@@ -245,8 +347,13 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
245
347
|
if (quota.walletBalance <= 0.001) { // Floating point safety
|
|
246
348
|
log(`[SafetyNet] Paid Only Model (${actualModel}) requested but Wallet is Empty ($${quota.walletBalance}). BLOCKING.`);
|
|
247
349
|
// Immediate Block or Fallback?
|
|
248
|
-
//
|
|
249
|
-
|
|
350
|
+
// Text says: "💎 Paid Only models require purchased pollen only"
|
|
351
|
+
// Blocking is safer/clearer than falling back to a free model which might not be what the user expects for a "Pro" feature?
|
|
352
|
+
// Actually, Fallback to Free is usually better for UX if configured, BUT for specific "Paid Only" requests, the user explicitly chose a powerful model.
|
|
353
|
+
// Falling back to Mistral might be confusing if they asked for Gemini-Large.
|
|
354
|
+
// BUT we are failing gracefully.
|
|
355
|
+
// Let's Fallback to Free Default and Warn.
|
|
356
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
250
357
|
isEnterprise = false;
|
|
251
358
|
isFallbackActive = true;
|
|
252
359
|
fallbackReason = "Paid Only Model requires purchased credits";
|
|
@@ -272,127 +379,99 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
272
379
|
// WE DO NOT RETURN 403. WE ALLOW THE REQUEST.
|
|
273
380
|
// Since config.mode is now 'manual', the next checks (alwaysfree/pro) will be skipped.
|
|
274
381
|
}
|
|
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') {
|
|
382
|
+
if (config.mode === 'alwaysfree') {
|
|
280
383
|
if (isEnterprise) {
|
|
281
|
-
//
|
|
384
|
+
// Paid Only Check: BLOCK (not fallback) in AlwaysFree mode
|
|
282
385
|
try {
|
|
283
386
|
const homedir = process.env.HOME || '/tmp';
|
|
284
387
|
const standardPaidPath = path.join(homedir, '.pollinations', 'pollinations-paid-models.json');
|
|
285
388
|
if (fs.existsSync(standardPaidPath)) {
|
|
286
389
|
const paidModels = JSON.parse(fs.readFileSync(standardPaidPath, 'utf-8'));
|
|
287
390
|
if (paidModels.includes(actualModel)) {
|
|
288
|
-
log(`[
|
|
289
|
-
emitStatusToast('
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
391
|
+
log(`[AlwaysFree] BLOCKED: Paid Only Model (${actualModel}).`);
|
|
392
|
+
emitStatusToast('warning', `🚫 Modèle payant bloqué: ${actualModel}`, 'AlwaysFree Mode');
|
|
393
|
+
const blockMsg = {
|
|
394
|
+
id: `chatcmpl-block-${Date.now()}`,
|
|
395
|
+
object: 'chat.completion',
|
|
396
|
+
created: Math.floor(Date.now() / 1000),
|
|
397
|
+
model: actualModel,
|
|
398
|
+
choices: [{
|
|
399
|
+
index: 0,
|
|
400
|
+
message: {
|
|
401
|
+
role: 'assistant',
|
|
402
|
+
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`
|
|
403
|
+
},
|
|
404
|
+
finish_reason: 'stop'
|
|
405
|
+
}],
|
|
406
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
407
|
+
};
|
|
408
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
409
|
+
res.end(JSON.stringify(blockMsg));
|
|
297
410
|
return;
|
|
298
411
|
}
|
|
299
412
|
}
|
|
300
413
|
}
|
|
301
|
-
catch (e) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
actualModel = config.fallbacks.economy || 'nova-fast';
|
|
414
|
+
catch (e) { }
|
|
415
|
+
if (!isFallbackActive && quota.tier === 'error') {
|
|
416
|
+
// Network error or unknown error (but NOT auth_limited, handled above)
|
|
417
|
+
log(`[SafetyNet] AlwaysFree Mode: Quota Check Failed. Switching to Free Fallback.`);
|
|
418
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
419
|
+
isEnterprise = false;
|
|
308
420
|
isFallbackActive = true;
|
|
309
421
|
fallbackReason = "Quota Unreachable (Safety)";
|
|
310
422
|
}
|
|
311
423
|
else {
|
|
312
424
|
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
425
|
if (tierRatio <= (config.thresholds.tier / 100)) {
|
|
328
|
-
log(`[
|
|
329
|
-
actualModel = config.fallbacks.
|
|
426
|
+
log(`[SafetyNet] AlwaysFree Mode: Tier (${(tierRatio * 100).toFixed(1)}%) <= Threshold (${config.thresholds.tier}%). Switching.`);
|
|
427
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
428
|
+
isEnterprise = false;
|
|
330
429
|
isFallbackActive = true;
|
|
331
|
-
fallbackReason = `Tier < ${config.thresholds.tier}% (
|
|
430
|
+
fallbackReason = `Daily Tier < ${config.thresholds.tier}% (Wallet Protected)`;
|
|
332
431
|
}
|
|
333
432
|
}
|
|
334
433
|
}
|
|
335
434
|
}
|
|
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
435
|
else if (config.mode === 'pro') {
|
|
342
436
|
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
437
|
if (quota.tier === 'error') {
|
|
349
|
-
|
|
350
|
-
|
|
438
|
+
// Network error or unknown
|
|
439
|
+
log(`[SafetyNet] Pro Mode: Quota Unreachable. Switching to Free Fallback.`);
|
|
440
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
441
|
+
isEnterprise = false;
|
|
351
442
|
isFallbackActive = true;
|
|
352
443
|
fallbackReason = "Quota Unreachable (Safety)";
|
|
353
444
|
}
|
|
354
445
|
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';
|
|
446
|
+
const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
|
|
447
|
+
if (quota.walletBalance < config.thresholds.wallet && tierRatio <= (config.thresholds.tier / 100)) {
|
|
448
|
+
log(`[SafetyNet] Pro Mode: Wallet < $${config.thresholds.wallet} AND Tier < ${config.thresholds.tier}%. Switching.`);
|
|
449
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
450
|
+
isEnterprise = false;
|
|
376
451
|
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');
|
|
452
|
+
fallbackReason = `Wallet & Tier Critical`;
|
|
382
453
|
}
|
|
383
454
|
}
|
|
384
455
|
}
|
|
385
456
|
}
|
|
386
|
-
// C. Construct URL & Headers
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
457
|
+
// C. Construct URL & Headers
|
|
458
|
+
if (isEnterprise) {
|
|
459
|
+
if (!config.apiKey) {
|
|
460
|
+
emitLogToast('error', "Missing API Key for Enterprise Model", 'Proxy Error');
|
|
461
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
462
|
+
res.end(JSON.stringify({ error: { message: "API Key required for Enterprise models." } }));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
466
|
+
authHeader = `Bearer ${config.apiKey}`;
|
|
467
|
+
log(`Routing to ENTERPRISE: ${actualModel}`);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
|
|
471
|
+
authHeader = undefined;
|
|
472
|
+
log(`Routing to FREE: ${actualModel} ${isFallbackActive ? '(FALLBACK)' : ''}`);
|
|
473
|
+
// emitLogToast('info', `Routing to: FREE UNIVERSE (${actualModel})`, 'Pollinations Routing'); // Too noisy
|
|
392
474
|
}
|
|
393
|
-
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
394
|
-
authHeader = `Bearer ${config.apiKey}`;
|
|
395
|
-
log(`Routing to gen.pollinations.ai: ${actualModel}`);
|
|
396
475
|
// NOTIFY SWITCH
|
|
397
476
|
if (isFallbackActive) {
|
|
398
477
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
@@ -424,17 +503,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
424
503
|
// LOGIC BLOCK: MODEL SPECIFIC ADAPTATIONS
|
|
425
504
|
// =========================================================
|
|
426
505
|
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.
|
|
506
|
+
// B0. KIMI / MOONSHOT SURGICAL FIX
|
|
429
507
|
if (actualModel.includes("kimi") || actualModel.includes("moonshot")) {
|
|
430
|
-
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops.`);
|
|
508
|
+
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops/sanitization.`);
|
|
431
509
|
proxyBody.frequency_penalty = 1.1;
|
|
432
510
|
proxyBody.presence_penalty = 0.4;
|
|
433
511
|
proxyBody.stop = ["<|endoftext|>", "User:", "\nUser", "User :"];
|
|
512
|
+
// KIMI FIX: Remove 'title' from schema
|
|
513
|
+
proxyBody.tools = proxyBody.tools.map((t) => {
|
|
514
|
+
if (t.function && t.function.parameters) {
|
|
515
|
+
t.function.parameters = sanitizeSchemaForKimi(t.function.parameters);
|
|
516
|
+
}
|
|
517
|
+
return t;
|
|
518
|
+
});
|
|
434
519
|
}
|
|
435
|
-
// A. AZURE/OPENAI FIXES
|
|
436
|
-
if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure")) {
|
|
437
|
-
|
|
520
|
+
// A. AZURE/OPENAI FIXES + MIDJOURNEY + GROK
|
|
521
|
+
if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure") || actualModel.includes("midijourney") || actualModel.includes("grok")) {
|
|
522
|
+
const limit = (actualModel.includes("midijourney") || actualModel.includes("grok")) ? 128 : 120;
|
|
523
|
+
proxyBody.tools = truncateTools(proxyBody.tools, limit);
|
|
438
524
|
if (proxyBody.messages) {
|
|
439
525
|
proxyBody.messages.forEach((m) => {
|
|
440
526
|
if (m.tool_calls) {
|
|
@@ -449,6 +535,11 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
449
535
|
});
|
|
450
536
|
}
|
|
451
537
|
}
|
|
538
|
+
// BEDROCK FIX (Claude / Nova / ChickyTutor)
|
|
539
|
+
if (actualModel.includes("claude") || actualModel.includes("nova") || actualModel.includes("bedrock") || actualModel.includes("chickytutor")) {
|
|
540
|
+
log(`[Proxy] Bedrock: Sanitizing tools description.`);
|
|
541
|
+
proxyBody.tools = sanitizeToolsForBedrock(proxyBody.tools);
|
|
542
|
+
}
|
|
452
543
|
// B1. NOMNOM SPECIAL (Disable Grounding, KEEP Search Tool)
|
|
453
544
|
if (actualModel === "nomnom") {
|
|
454
545
|
proxyBody.tools_config = { google_search_retrieval: { disable: true } };
|
|
@@ -456,36 +547,12 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
456
547
|
proxyBody.tools = sanitizeToolsForVertex(proxyBody.tools || []);
|
|
457
548
|
log(`[Proxy] Nomnom Fix: Grounding Disabled, Search Tool KEPT.`);
|
|
458
549
|
}
|
|
459
|
-
// B2. GEMINI FREE / FAST (CRASH FIX: STRICT SANITIZATION)
|
|
460
|
-
// Restore Tools but REMOVE conflicting ones (Search)
|
|
461
|
-
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
462
|
-
// Handles: "tools" vs "grounding" conflicts, and "infinite loops" via Stop Sequences.
|
|
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
550
|
// B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
|
|
481
|
-
|
|
482
|
-
// Added ChickyTutor (Claude/Gemini based) to fix "toolConfig must be defined" error.
|
|
483
|
-
else if (actualModel.includes("gemini") || actualModel.includes("chickytutor")) {
|
|
551
|
+
else if (actualModel.includes("gemini")) {
|
|
484
552
|
let hasFunctions = false;
|
|
485
553
|
if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
|
|
486
554
|
hasFunctions = proxyBody.tools.some((t) => t.type === 'function' || t.function);
|
|
487
555
|
}
|
|
488
|
-
// Old Shim logic removed (moved up)
|
|
489
556
|
if (hasFunctions) {
|
|
490
557
|
// 1. Strict cleanup of 'google_search' tool
|
|
491
558
|
proxyBody.tools = proxyBody.tools.filter((t) => {
|
|
@@ -516,14 +583,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
516
583
|
log(`[Proxy] Gemini Logic: Tools=${proxyBody.tools ? proxyBody.tools.length : 'REMOVED'}, Stops NOT Injected.`);
|
|
517
584
|
}
|
|
518
585
|
}
|
|
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
586
|
// C. GEMINI ID BACKTRACKING & SIGNATURE
|
|
528
587
|
if ((actualModel.includes("gemini") || actualModel === "nomnom") && proxyBody.messages) {
|
|
529
588
|
const lastMsg = proxyBody.messages[proxyBody.messages.length - 1];
|
|
@@ -609,14 +668,18 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
609
668
|
if ((isEnterpriseFallback || isGeminiToolsFallback) && config.mode !== 'manual') {
|
|
610
669
|
log(`[SafetyNet] Upstream Rejection (${fetchRes.status}). Triggering Transparent Fallback.`);
|
|
611
670
|
if (isEnterpriseFallback) {
|
|
612
|
-
// 1a. Enterprise -> Fallback
|
|
613
|
-
actualModel = config.
|
|
614
|
-
? (config.fallbacks.pro || 'qwen-coder')
|
|
615
|
-
: (config.fallbacks.economy || 'nova-fast');
|
|
671
|
+
// 1a. Enterprise -> Free Fallback
|
|
672
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
616
673
|
isEnterprise = false;
|
|
617
674
|
isFallbackActive = true;
|
|
618
|
-
if (fetchRes.status === 402)
|
|
675
|
+
if (fetchRes.status === 402) {
|
|
619
676
|
fallbackReason = "Insufficient Funds (Upstream 402)";
|
|
677
|
+
// Force refresh quota cache so next pre-flight check is accurate
|
|
678
|
+
try {
|
|
679
|
+
await getQuotaStatus(true);
|
|
680
|
+
}
|
|
681
|
+
catch (e) { }
|
|
682
|
+
}
|
|
620
683
|
else if (fetchRes.status === 429)
|
|
621
684
|
fallbackReason = "Rate Limit (Upstream 429)";
|
|
622
685
|
else if (fetchRes.status === 401)
|
|
@@ -634,10 +697,10 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
634
697
|
// 2. Notify
|
|
635
698
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
636
699
|
emitLogToast('warning', `Recovering from ${fetchRes.status} -> Switching to ${actualModel}`, 'Safety Net');
|
|
637
|
-
// 3. Re-Prepare Request
|
|
638
|
-
targetUrl = 'https://
|
|
700
|
+
// 3. Re-Prepare Request
|
|
701
|
+
targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
|
|
639
702
|
const retryHeaders = { ...headers };
|
|
640
|
-
//
|
|
703
|
+
delete retryHeaders['Authorization']; // Free = No Auth
|
|
641
704
|
const retryBody = { ...proxyBody, model: actualModel };
|
|
642
705
|
// 4. Retry Fetch
|
|
643
706
|
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;
|