opencode-pollinations-plugin 6.0.0 → 6.1.0-beta.10
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 +140 -87
- package/dist/index.js +33 -154
- package/dist/server/commands.d.ts +2 -0
- package/dist/server/commands.js +84 -25
- package/dist/server/config.d.ts +6 -0
- package/dist/server/config.js +4 -1
- package/dist/server/generate-config.d.ts +3 -30
- package/dist/server/generate-config.js +172 -100
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +124 -149
- package/dist/server/pollinations-api.d.ts +11 -0
- package/dist/server/pollinations-api.js +20 -0
- package/dist/server/proxy.js +158 -72
- package/dist/server/quota.d.ts +8 -0
- package/dist/server/quota.js +106 -61
- package/dist/server/toast.d.ts +3 -0
- package/dist/server/toast.js +16 -0
- 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/index.d.ts +22 -0
- package/dist/tools/index.js +81 -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 +204 -0
- package/dist/tools/pollinations/gen_image.d.ts +13 -0
- package/dist/tools/pollinations/gen_image.js +239 -0
- package/dist/tools/pollinations/gen_music.d.ts +14 -0
- package/dist/tools/pollinations/gen_music.js +139 -0
- package/dist/tools/pollinations/gen_video.d.ts +16 -0
- package/dist/tools/pollinations/gen_video.js +222 -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 +170 -0
- package/dist/tools/pollinations/shared.js +454 -0
- package/dist/tools/pollinations/transcribe_audio.d.ts +17 -0
- package/dist/tools/pollinations/transcribe_audio.js +235 -0
- package/dist/tools/power/extract_audio.d.ts +2 -0
- package/dist/tools/power/extract_audio.js +180 -0
- package/dist/tools/power/extract_frames.d.ts +2 -0
- package/dist/tools/power/extract_frames.js +240 -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 +365 -0
- package/dist/tools/power/rmbg_keys.d.ts +2 -0
- package/dist/tools/power/rmbg_keys.js +78 -0
- package/dist/tools/shared.d.ts +30 -0
- package/dist/tools/shared.js +74 -0
- package/package.json +9 -3
- package/dist/server/models-seed.d.ts +0 -18
- package/dist/server/models-seed.js +0 -55
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];
|
|
@@ -208,6 +265,27 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
208
265
|
}
|
|
209
266
|
}
|
|
210
267
|
log(`Incoming Model (OpenCode ID): ${body.model}`);
|
|
268
|
+
// 0. SPECIAL: connect-pollinations fallback model
|
|
269
|
+
if (body.model === 'connect-pollinations') {
|
|
270
|
+
const connectMsg = {
|
|
271
|
+
id: `chatcmpl-connect-${Date.now()}`,
|
|
272
|
+
object: 'chat.completion',
|
|
273
|
+
created: Math.floor(Date.now() / 1000),
|
|
274
|
+
model: 'connect-pollinations',
|
|
275
|
+
choices: [{
|
|
276
|
+
index: 0,
|
|
277
|
+
message: {
|
|
278
|
+
role: 'assistant',
|
|
279
|
+
content: `🔗 **Se connecter à Pollinations**\n\nAccédez à 30+ modèles IA de pointe !\n\n📍 **Étapes :**\n1. Visitez https://enter.pollinations.ai\n2. Créez un compte gratuit\n3. Copiez votre API Key\n4. Exécutez: \`/pollinations config apiKey YOUR_KEY\`\n5. Redémarrez OpenCode\n\n✅ **Bénéfices :**\n• 30+ modèles avancés (GPT-5, Claude, Gemini...)\n• Crédits gratuits selon votre tier\n• Stabilité garantie`
|
|
280
|
+
},
|
|
281
|
+
finish_reason: 'stop'
|
|
282
|
+
}],
|
|
283
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
284
|
+
};
|
|
285
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
286
|
+
res.end(JSON.stringify(connectMsg));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
211
289
|
// 1. STRICT ROUTING & SAFETY NET LOGIC (V5)
|
|
212
290
|
let actualModel = body.model || "openai";
|
|
213
291
|
let isEnterprise = false;
|
|
@@ -216,16 +294,15 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
216
294
|
// LOAD QUOTA FOR SAFETY CHECKS
|
|
217
295
|
const { getQuotaStatus, formatQuotaForToast } = await import('./quota.js');
|
|
218
296
|
const quota = await getQuotaStatus(false);
|
|
219
|
-
// A. Resolve Base Target
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
actualModel = actualModel.replace(
|
|
297
|
+
// A. Resolve Base Target
|
|
298
|
+
if (actualModel.startsWith('enter/')) {
|
|
299
|
+
isEnterprise = true;
|
|
300
|
+
actualModel = actualModel.replace('enter/', '');
|
|
223
301
|
}
|
|
224
|
-
else if (actualModel.startsWith('free/')
|
|
225
|
-
|
|
302
|
+
else if (actualModel.startsWith('free/')) {
|
|
303
|
+
isEnterprise = false;
|
|
304
|
+
actualModel = actualModel.replace('free/', '');
|
|
226
305
|
}
|
|
227
|
-
// v6.0: Everything is enterprise now (requires API key)
|
|
228
|
-
isEnterprise = true;
|
|
229
306
|
// A.1 PAID MODEL ENFORCEMENT (V5.5 Strategy)
|
|
230
307
|
// Check dynamic list saved by generate-config.ts
|
|
231
308
|
if (isEnterprise) {
|
|
@@ -279,18 +356,33 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
279
356
|
}
|
|
280
357
|
if (config.mode === 'alwaysfree') {
|
|
281
358
|
if (isEnterprise) {
|
|
282
|
-
//
|
|
359
|
+
// Paid Only Check: BLOCK (not fallback) in AlwaysFree mode
|
|
283
360
|
try {
|
|
284
361
|
const homedir = process.env.HOME || '/tmp';
|
|
285
362
|
const standardPaidPath = path.join(homedir, '.pollinations', 'pollinations-paid-models.json');
|
|
286
363
|
if (fs.existsSync(standardPaidPath)) {
|
|
287
364
|
const paidModels = JSON.parse(fs.readFileSync(standardPaidPath, 'utf-8'));
|
|
288
365
|
if (paidModels.includes(actualModel)) {
|
|
289
|
-
log(`[
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
366
|
+
log(`[AlwaysFree] BLOCKED: Paid Only Model (${actualModel}).`);
|
|
367
|
+
emitStatusToast('warning', `🚫 Modèle payant bloqué: ${actualModel}`, 'AlwaysFree Mode');
|
|
368
|
+
const blockMsg = {
|
|
369
|
+
id: `chatcmpl-block-${Date.now()}`,
|
|
370
|
+
object: 'chat.completion',
|
|
371
|
+
created: Math.floor(Date.now() / 1000),
|
|
372
|
+
model: actualModel,
|
|
373
|
+
choices: [{
|
|
374
|
+
index: 0,
|
|
375
|
+
message: {
|
|
376
|
+
role: 'assistant',
|
|
377
|
+
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`
|
|
378
|
+
},
|
|
379
|
+
finish_reason: 'stop'
|
|
380
|
+
}],
|
|
381
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
382
|
+
};
|
|
383
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
384
|
+
res.end(JSON.stringify(blockMsg));
|
|
385
|
+
return;
|
|
294
386
|
}
|
|
295
387
|
}
|
|
296
388
|
}
|
|
@@ -337,16 +429,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
337
429
|
}
|
|
338
430
|
}
|
|
339
431
|
}
|
|
340
|
-
// C. Construct URL & Headers
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
432
|
+
// C. Construct URL & Headers
|
|
433
|
+
if (isEnterprise) {
|
|
434
|
+
if (!config.apiKey) {
|
|
435
|
+
emitLogToast('error', "Missing API Key for Enterprise Model", 'Proxy Error');
|
|
436
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
437
|
+
res.end(JSON.stringify({ error: { message: "API Key required for Enterprise models." } }));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
441
|
+
authHeader = `Bearer ${config.apiKey}`;
|
|
442
|
+
log(`Routing to ENTERPRISE: ${actualModel}`);
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
|
|
446
|
+
authHeader = undefined;
|
|
447
|
+
log(`Routing to FREE: ${actualModel} ${isFallbackActive ? '(FALLBACK)' : ''}`);
|
|
448
|
+
// emitLogToast('info', `Routing to: FREE UNIVERSE (${actualModel})`, 'Pollinations Routing'); // Too noisy
|
|
346
449
|
}
|
|
347
|
-
targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
|
|
348
|
-
authHeader = `Bearer ${config.apiKey}`;
|
|
349
|
-
log(`Routing to gen.pollinations.ai: ${actualModel}`);
|
|
350
450
|
// NOTIFY SWITCH
|
|
351
451
|
if (isFallbackActive) {
|
|
352
452
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
@@ -378,17 +478,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
378
478
|
// LOGIC BLOCK: MODEL SPECIFIC ADAPTATIONS
|
|
379
479
|
// =========================================================
|
|
380
480
|
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.
|
|
481
|
+
// B0. KIMI / MOONSHOT SURGICAL FIX
|
|
383
482
|
if (actualModel.includes("kimi") || actualModel.includes("moonshot")) {
|
|
384
|
-
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops.`);
|
|
483
|
+
log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops/sanitization.`);
|
|
385
484
|
proxyBody.frequency_penalty = 1.1;
|
|
386
485
|
proxyBody.presence_penalty = 0.4;
|
|
387
486
|
proxyBody.stop = ["<|endoftext|>", "User:", "\nUser", "User :"];
|
|
487
|
+
// KIMI FIX: Remove 'title' from schema
|
|
488
|
+
proxyBody.tools = proxyBody.tools.map((t) => {
|
|
489
|
+
if (t.function && t.function.parameters) {
|
|
490
|
+
t.function.parameters = sanitizeSchemaForKimi(t.function.parameters);
|
|
491
|
+
}
|
|
492
|
+
return t;
|
|
493
|
+
});
|
|
388
494
|
}
|
|
389
|
-
// A. AZURE/OPENAI FIXES
|
|
390
|
-
if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure")) {
|
|
391
|
-
|
|
495
|
+
// A. AZURE/OPENAI FIXES + MIDJOURNEY + GROK
|
|
496
|
+
if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure") || actualModel.includes("midijourney") || actualModel.includes("grok")) {
|
|
497
|
+
const limit = (actualModel.includes("midijourney") || actualModel.includes("grok")) ? 128 : 120;
|
|
498
|
+
proxyBody.tools = truncateTools(proxyBody.tools, limit);
|
|
392
499
|
if (proxyBody.messages) {
|
|
393
500
|
proxyBody.messages.forEach((m) => {
|
|
394
501
|
if (m.tool_calls) {
|
|
@@ -403,6 +510,11 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
403
510
|
});
|
|
404
511
|
}
|
|
405
512
|
}
|
|
513
|
+
// BEDROCK FIX (Claude / Nova / ChickyTutor)
|
|
514
|
+
if (actualModel.includes("claude") || actualModel.includes("nova") || actualModel.includes("bedrock") || actualModel.includes("chickytutor")) {
|
|
515
|
+
log(`[Proxy] Bedrock: Sanitizing tools description.`);
|
|
516
|
+
proxyBody.tools = sanitizeToolsForBedrock(proxyBody.tools);
|
|
517
|
+
}
|
|
406
518
|
// B1. NOMNOM SPECIAL (Disable Grounding, KEEP Search Tool)
|
|
407
519
|
if (actualModel === "nomnom") {
|
|
408
520
|
proxyBody.tools_config = { google_search_retrieval: { disable: true } };
|
|
@@ -410,36 +522,12 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
410
522
|
proxyBody.tools = sanitizeToolsForVertex(proxyBody.tools || []);
|
|
411
523
|
log(`[Proxy] Nomnom Fix: Grounding Disabled, Search Tool KEPT.`);
|
|
412
524
|
}
|
|
413
|
-
// B2. GEMINI FREE / FAST (CRASH FIX: STRICT SANITIZATION)
|
|
414
|
-
// Restore Tools but REMOVE conflicting ones (Search)
|
|
415
525
|
// 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")) {
|
|
526
|
+
else if (actualModel.includes("gemini")) {
|
|
438
527
|
let hasFunctions = false;
|
|
439
528
|
if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
|
|
440
529
|
hasFunctions = proxyBody.tools.some((t) => t.type === 'function' || t.function);
|
|
441
530
|
}
|
|
442
|
-
// Old Shim logic removed (moved up)
|
|
443
531
|
if (hasFunctions) {
|
|
444
532
|
// 1. Strict cleanup of 'google_search' tool
|
|
445
533
|
proxyBody.tools = proxyBody.tools.filter((t) => {
|
|
@@ -470,14 +558,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
470
558
|
log(`[Proxy] Gemini Logic: Tools=${proxyBody.tools ? proxyBody.tools.length : 'REMOVED'}, Stops NOT Injected.`);
|
|
471
559
|
}
|
|
472
560
|
}
|
|
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
561
|
// C. GEMINI ID BACKTRACKING & SIGNATURE
|
|
482
562
|
if ((actualModel.includes("gemini") || actualModel === "nomnom") && proxyBody.messages) {
|
|
483
563
|
const lastMsg = proxyBody.messages[proxyBody.messages.length - 1];
|
|
@@ -567,8 +647,14 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
567
647
|
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
568
648
|
isEnterprise = false;
|
|
569
649
|
isFallbackActive = true;
|
|
570
|
-
if (fetchRes.status === 402)
|
|
650
|
+
if (fetchRes.status === 402) {
|
|
571
651
|
fallbackReason = "Insufficient Funds (Upstream 402)";
|
|
652
|
+
// Force refresh quota cache so next pre-flight check is accurate
|
|
653
|
+
try {
|
|
654
|
+
await getQuotaStatus(true);
|
|
655
|
+
}
|
|
656
|
+
catch (e) { }
|
|
657
|
+
}
|
|
572
658
|
else if (fetchRes.status === 429)
|
|
573
659
|
fallbackReason = "Rate Limit (Upstream 429)";
|
|
574
660
|
else if (fetchRes.status === 401)
|
|
@@ -586,10 +672,10 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
586
672
|
// 2. Notify
|
|
587
673
|
emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
|
|
588
674
|
emitLogToast('warning', `Recovering from ${fetchRes.status} -> Switching to ${actualModel}`, 'Safety Net');
|
|
589
|
-
// 3. Re-Prepare Request
|
|
590
|
-
targetUrl = 'https://
|
|
675
|
+
// 3. Re-Prepare Request
|
|
676
|
+
targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
|
|
591
677
|
const retryHeaders = { ...headers };
|
|
592
|
-
//
|
|
678
|
+
delete retryHeaders['Authorization']; // Free = No Auth
|
|
593
679
|
const retryBody = { ...proxyBody, model: actualModel };
|
|
594
680
|
// 4. Retry Fetch
|
|
595
681
|
const retryRes = await fetchWithRetry(targetUrl, {
|
package/dist/server/quota.d.ts
CHANGED
package/dist/server/quota.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
2
3
|
import * as https from 'https'; // Use Native HTTPS
|
|
4
|
+
import * as crypto from 'crypto';
|
|
3
5
|
import { loadConfig } from './config.js';
|
|
4
|
-
// === CACHE ===
|
|
6
|
+
// === CACHE & CONSTANTS ===
|
|
5
7
|
const CACHE_TTL = 30000; // 30 secondes
|
|
6
8
|
let cachedQuota = null;
|
|
7
9
|
let lastQuotaFetch = 0;
|
|
10
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
const HISTORY_RETENTION_MS = 48 * 60 * 60 * 1000; // 48h history
|
|
8
12
|
// === TIER LIMITS ===
|
|
9
13
|
const TIER_LIMITS = {
|
|
14
|
+
microbe: { pollen: 0.1, emoji: '🦠' },
|
|
10
15
|
spore: { pollen: 1, emoji: '🦠' },
|
|
11
16
|
seed: { pollen: 3, emoji: '🌱' },
|
|
12
17
|
flower: { pollen: 10, emoji: '🌸' },
|
|
@@ -19,24 +24,71 @@ function logQuota(msg) {
|
|
|
19
24
|
}
|
|
20
25
|
catch (e) { }
|
|
21
26
|
}
|
|
22
|
-
// ===
|
|
27
|
+
// === HISTORY MANAGER (JSON) ===
|
|
28
|
+
function getHistoryFilePath() {
|
|
29
|
+
const homedir = process.env.HOME || '/tmp';
|
|
30
|
+
const historyDir = path.join(homedir, '.pollinations');
|
|
31
|
+
if (!fs.existsSync(historyDir)) {
|
|
32
|
+
try {
|
|
33
|
+
fs.mkdirSync(historyDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
catch (e) { }
|
|
36
|
+
}
|
|
37
|
+
return path.join(historyDir, 'usage_history.json');
|
|
38
|
+
}
|
|
39
|
+
function computeEntrySignature(entry) {
|
|
40
|
+
// Unique signature per transaction: timestamp + model + cost + source
|
|
41
|
+
return crypto.createHash('md5').update(`${entry.timestamp}|${entry.model}|${entry.cost_usd}|${entry.meter_source}`).digest('hex');
|
|
42
|
+
}
|
|
43
|
+
function updateLocalHistory(newEntries) {
|
|
44
|
+
const filePath = getHistoryFilePath();
|
|
45
|
+
let history = [];
|
|
46
|
+
// 1. Load existing
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(filePath)) {
|
|
49
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
50
|
+
history = JSON.parse(raw);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
logQuota(`Failed to load history: ${e}`);
|
|
55
|
+
history = [];
|
|
56
|
+
}
|
|
57
|
+
// 2. Merge (Deduplication via Signature)
|
|
58
|
+
const existingSignatures = new Set(history.map(computeEntrySignature));
|
|
59
|
+
let addedCount = 0;
|
|
60
|
+
for (const entry of newEntries) {
|
|
61
|
+
const sig = computeEntrySignature(entry);
|
|
62
|
+
if (!existingSignatures.has(sig)) {
|
|
63
|
+
history.push(entry);
|
|
64
|
+
existingSignatures.add(sig);
|
|
65
|
+
addedCount++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// 3. Prune (> 48h)
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const beforePrune = history.length;
|
|
71
|
+
history = history.filter(e => {
|
|
72
|
+
const entryTime = new Date(e.timestamp.replace(' ', 'T') + 'Z').getTime();
|
|
73
|
+
return (now - entryTime) < HISTORY_RETENTION_MS;
|
|
74
|
+
});
|
|
75
|
+
// 4. Sort (Newest first)
|
|
76
|
+
history.sort((a, b) => new Date(b.timestamp.replace(' ', 'T') + 'Z').getTime() - new Date(a.timestamp.replace(' ', 'T') + 'Z').getTime());
|
|
77
|
+
// 5. Save
|
|
78
|
+
try {
|
|
79
|
+
fs.writeFileSync(filePath, JSON.stringify(history, null, 2));
|
|
80
|
+
logQuota(`History Update: Added ${addedCount}, Pruned ${beforePrune - history.length}, Total ${history.length} entries.`);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
logQuota(`Failed to save history: ${e}`);
|
|
84
|
+
}
|
|
85
|
+
return history;
|
|
86
|
+
}
|
|
87
|
+
// === MAIN QUOTA FUNCTION ===
|
|
23
88
|
export async function getQuotaStatus(forceRefresh = false) {
|
|
24
89
|
const config = loadConfig();
|
|
25
90
|
if (!config.apiKey) {
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
tierRemaining: 0,
|
|
29
|
-
tierUsed: 0,
|
|
30
|
-
tierLimit: 0,
|
|
31
|
-
walletBalance: 0,
|
|
32
|
-
nextResetAt: new Date(),
|
|
33
|
-
timeUntilReset: 0,
|
|
34
|
-
canUseEnterprise: false,
|
|
35
|
-
isUsingWallet: false,
|
|
36
|
-
needsAlert: false,
|
|
37
|
-
tier: 'none',
|
|
38
|
-
tierEmoji: '❌'
|
|
39
|
-
};
|
|
91
|
+
return createDefaultQuota('none', 0);
|
|
40
92
|
}
|
|
41
93
|
const now = Date.now();
|
|
42
94
|
if (!forceRefresh && cachedQuota && (now - lastQuotaFetch) < CACHE_TTL) {
|
|
@@ -44,27 +96,33 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
44
96
|
}
|
|
45
97
|
try {
|
|
46
98
|
logQuota("Fetching Quota Data...");
|
|
99
|
+
// 1. Fetch API
|
|
47
100
|
// SEQUENTIAL FETCH (Avoid Rate Limits)
|
|
48
|
-
// We fetch one by one. If one fails, we catch and return fallback.
|
|
49
101
|
const profileRes = await fetchAPI('/account/profile', config.apiKey);
|
|
50
102
|
const balanceRes = await fetchAPI('/account/balance', config.apiKey);
|
|
51
103
|
const usageRes = await fetchAPI('/account/usage', config.apiKey);
|
|
52
104
|
logQuota(`Fetch Success. Tier: ${profileRes.tier}, Balance: ${balanceRes.balance}`);
|
|
53
105
|
const profile = profileRes;
|
|
54
106
|
const balance = balanceRes.balance;
|
|
55
|
-
|
|
56
|
-
const
|
|
107
|
+
// 2. Update Local History (The Source of Truth)
|
|
108
|
+
const fullHistory = updateLocalHistory(usageRes.usage || []);
|
|
109
|
+
const tierInfo = TIER_LIMITS[profile.tier] || { pollen: 1, emoji: '❓' };
|
|
57
110
|
const tierLimit = tierInfo.pollen;
|
|
58
|
-
//
|
|
111
|
+
// 3. Calculate Reset & Usage from History
|
|
59
112
|
const resetInfo = calculateResetInfo(profile.nextResetAt);
|
|
60
|
-
|
|
61
|
-
|
|
113
|
+
const { tierUsed } = calculateCurrentPeriodUsage(fullHistory, resetInfo);
|
|
114
|
+
// 4. Calculate Balances
|
|
62
115
|
const tierRemaining = Math.max(0, tierLimit - tierUsed);
|
|
63
116
|
// Fix rounding errors
|
|
64
117
|
const cleanTierRemaining = Math.max(0, parseFloat(tierRemaining.toFixed(4)));
|
|
65
118
|
// Le wallet c'est le reste (balance totale - ce qu'il reste du tier gratuit non consommé)
|
|
119
|
+
// Formula: Pollinations Balance = Wallet + TierRemaining.
|
|
66
120
|
const walletBalance = Math.max(0, balance - cleanTierRemaining);
|
|
67
121
|
const cleanWalletBalance = Math.max(0, parseFloat(walletBalance.toFixed(4)));
|
|
122
|
+
// needsAlert: check BOTH tier threshold AND wallet threshold
|
|
123
|
+
const tierAlertPercent = tierLimit > 0 ? (cleanTierRemaining / tierLimit * 100) : 0;
|
|
124
|
+
const tierNeedsAlert = tierLimit > 0 && tierAlertPercent <= config.thresholds.tier;
|
|
125
|
+
const walletNeedsAlert = cleanWalletBalance > 0 && cleanWalletBalance < (config.thresholds.wallet || 0.5);
|
|
68
126
|
cachedQuota = {
|
|
69
127
|
tierRemaining: cleanTierRemaining,
|
|
70
128
|
tierUsed,
|
|
@@ -74,7 +132,7 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
74
132
|
timeUntilReset: resetInfo.timeUntilReset,
|
|
75
133
|
canUseEnterprise: cleanTierRemaining > 0.05 || cleanWalletBalance > 0.05,
|
|
76
134
|
isUsingWallet: cleanTierRemaining <= 0.05 && cleanWalletBalance > 0.05,
|
|
77
|
-
needsAlert:
|
|
135
|
+
needsAlert: tierNeedsAlert || walletNeedsAlert,
|
|
78
136
|
tier: profile.tier,
|
|
79
137
|
tierEmoji: tierInfo.emoji
|
|
80
138
|
};
|
|
@@ -84,30 +142,29 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
84
142
|
catch (e) {
|
|
85
143
|
logQuota(`ERROR fetching quota: ${e.message}`);
|
|
86
144
|
let errorType = 'unknown';
|
|
87
|
-
if (e.message && e.message.includes('403'))
|
|
145
|
+
if (e.message && e.message.includes('403'))
|
|
88
146
|
errorType = 'auth_limited';
|
|
89
|
-
|
|
90
|
-
else if (e.message && e.message.includes('Network Error')) {
|
|
147
|
+
else if (e.message && e.message.includes('Network Error'))
|
|
91
148
|
errorType = 'network';
|
|
92
|
-
}
|
|
93
|
-
// Retourner le cache ou un état par défaut safe
|
|
94
|
-
return cachedQuota || {
|
|
95
|
-
tierRemaining: 0,
|
|
96
|
-
tierUsed: 0,
|
|
97
|
-
tierLimit: 1,
|
|
98
|
-
walletBalance: 0,
|
|
99
|
-
nextResetAt: new Date(),
|
|
100
|
-
timeUntilReset: 0,
|
|
101
|
-
canUseEnterprise: false,
|
|
102
|
-
isUsingWallet: false,
|
|
103
|
-
needsAlert: true,
|
|
104
|
-
tier: 'error',
|
|
105
|
-
tierEmoji: '⚠️',
|
|
106
|
-
errorType
|
|
107
|
-
};
|
|
149
|
+
return cachedQuota || { ...createDefaultQuota('error', 1), errorType };
|
|
108
150
|
}
|
|
109
151
|
}
|
|
110
|
-
|
|
152
|
+
function createDefaultQuota(tierName, limit) {
|
|
153
|
+
return {
|
|
154
|
+
tierRemaining: 0,
|
|
155
|
+
tierUsed: 0,
|
|
156
|
+
tierLimit: limit,
|
|
157
|
+
walletBalance: 0,
|
|
158
|
+
nextResetAt: new Date(),
|
|
159
|
+
timeUntilReset: 0,
|
|
160
|
+
canUseEnterprise: false,
|
|
161
|
+
isUsingWallet: false,
|
|
162
|
+
needsAlert: false,
|
|
163
|
+
tier: tierName,
|
|
164
|
+
tierEmoji: TIER_LIMITS[tierName]?.emoji || '❌'
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// === HELPERS ===
|
|
111
168
|
function fetchAPI(endpoint, apiKey) {
|
|
112
169
|
return new Promise((resolve, reject) => {
|
|
113
170
|
const options = {
|
|
@@ -146,28 +203,23 @@ function fetchAPI(endpoint, apiKey) {
|
|
|
146
203
|
function calculateResetInfo(nextResetAt) {
|
|
147
204
|
const nextResetFromAPI = new Date(nextResetAt);
|
|
148
205
|
const now = new Date();
|
|
149
|
-
// Extraire l'heure de reset depuis l'API (varie par utilisateur!)
|
|
150
206
|
const resetHour = nextResetFromAPI.getUTCHours();
|
|
151
207
|
const resetMinute = nextResetFromAPI.getUTCMinutes();
|
|
152
208
|
const resetSecond = nextResetFromAPI.getUTCSeconds();
|
|
153
|
-
// Calculer le reset d'aujourd'hui à cette heure
|
|
154
209
|
const todayResetUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), resetHour, resetMinute, resetSecond));
|
|
155
210
|
let lastReset;
|
|
156
211
|
let nextReset;
|
|
157
212
|
if (now >= todayResetUTC) {
|
|
158
|
-
// Le reset d'aujourd'hui est passé
|
|
159
213
|
lastReset = todayResetUTC;
|
|
160
|
-
nextReset = new Date(todayResetUTC.getTime() +
|
|
214
|
+
nextReset = new Date(todayResetUTC.getTime() + ONE_DAY_MS);
|
|
161
215
|
}
|
|
162
216
|
else {
|
|
163
|
-
|
|
164
|
-
lastReset = new Date(todayResetUTC.getTime() - 24 * 60 * 60 * 1000);
|
|
217
|
+
lastReset = new Date(todayResetUTC.getTime() - ONE_DAY_MS);
|
|
165
218
|
nextReset = todayResetUTC;
|
|
166
219
|
}
|
|
167
220
|
const timeUntilReset = nextReset.getTime() - now.getTime();
|
|
168
221
|
const timeSinceReset = now.getTime() - lastReset.getTime();
|
|
169
|
-
const
|
|
170
|
-
const progressPercent = (timeSinceReset / cycleDuration) * 100;
|
|
222
|
+
const progressPercent = (timeSinceReset / ONE_DAY_MS) * 100;
|
|
171
223
|
return {
|
|
172
224
|
nextReset,
|
|
173
225
|
lastReset,
|
|
@@ -182,15 +234,10 @@ function calculateResetInfo(nextResetAt) {
|
|
|
182
234
|
function calculateCurrentPeriodUsage(usage, resetInfo) {
|
|
183
235
|
let tierUsed = 0;
|
|
184
236
|
let packUsed = 0;
|
|
185
|
-
// Parser le timestamp de l'API avec Z pour UTC
|
|
186
|
-
function parseUsageTimestamp(timestamp) {
|
|
187
|
-
// Format: "2026-01-23 01:11:21"
|
|
188
|
-
const isoString = timestamp.replace(' ', 'T') + 'Z';
|
|
189
|
-
return new Date(isoString);
|
|
190
|
-
}
|
|
191
|
-
// FILTRER: Ne garder que les entrées APRÈS le dernier reset
|
|
192
237
|
const entriesAfterReset = usage.filter(entry => {
|
|
193
|
-
|
|
238
|
+
// Safe Parse
|
|
239
|
+
const timestamp = entry.timestamp.replace(' ', 'T') + 'Z';
|
|
240
|
+
const entryTime = new Date(timestamp);
|
|
194
241
|
return entryTime >= resetInfo.lastReset;
|
|
195
242
|
});
|
|
196
243
|
for (const entry of entriesAfterReset) {
|
|
@@ -203,7 +250,6 @@ function calculateCurrentPeriodUsage(usage, resetInfo) {
|
|
|
203
250
|
}
|
|
204
251
|
return { tierUsed, packUsed };
|
|
205
252
|
}
|
|
206
|
-
// === EXPORT POUR LES ALERTES ===
|
|
207
253
|
export function formatQuotaForToast(quota) {
|
|
208
254
|
if (quota.errorType === 'auth_limited') {
|
|
209
255
|
return `🔑 CLE LIMITÉE (Génération Seule) | 💎 Wallet: N/A | ⏰ Reset: N/A`;
|
|
@@ -211,7 +257,6 @@ export function formatQuotaForToast(quota) {
|
|
|
211
257
|
const tierPercent = quota.tierLimit > 0
|
|
212
258
|
? Math.round((quota.tierRemaining / quota.tierLimit) * 100)
|
|
213
259
|
: 0;
|
|
214
|
-
// Format compact: 1h23m
|
|
215
260
|
const ms = quota.timeUntilReset;
|
|
216
261
|
const hours = Math.floor(ms / (1000 * 60 * 60));
|
|
217
262
|
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
|
package/dist/server/toast.d.ts
CHANGED
|
@@ -4,3 +4,6 @@ export declare function emitStatusToast(type: 'info' | 'warning' | 'error' | 'su
|
|
|
4
4
|
export declare function createToastHooks(client: any): {
|
|
5
5
|
'session.idle': ({ event }: any) => Promise<void>;
|
|
6
6
|
};
|
|
7
|
+
export declare function createToolHooks(client: any): {
|
|
8
|
+
'tool.execute.after': (input: any, output: any) => Promise<void>;
|
|
9
|
+
};
|