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.
Files changed (96) hide show
  1. package/README.md +242 -62
  2. package/dist/index.js +68 -159
  3. package/dist/server/commands.d.ts +6 -0
  4. package/dist/server/commands.js +400 -71
  5. package/dist/server/config.d.ts +32 -23
  6. package/dist/server/config.js +183 -99
  7. package/dist/server/connect-response.d.ts +2 -0
  8. package/dist/server/connect-response.js +141 -0
  9. package/dist/server/generate-config.d.ts +3 -30
  10. package/dist/server/generate-config.js +164 -106
  11. package/dist/server/index.d.ts +2 -1
  12. package/dist/server/index.js +124 -149
  13. package/dist/server/logger.d.ts +8 -0
  14. package/dist/server/logger.js +36 -0
  15. package/dist/server/models/cache.d.ts +35 -0
  16. package/dist/server/models/cache.js +160 -0
  17. package/dist/server/models/fetcher.d.ts +18 -0
  18. package/dist/server/models/fetcher.js +150 -0
  19. package/dist/server/models/index.d.ts +6 -0
  20. package/dist/server/models/index.js +5 -0
  21. package/dist/server/models/manual.d.ts +15 -0
  22. package/dist/server/models/manual.js +92 -0
  23. package/dist/server/models/types.d.ts +55 -0
  24. package/dist/server/models/types.js +7 -0
  25. package/dist/server/models/worker.d.ts +21 -0
  26. package/dist/server/models/worker.js +97 -0
  27. package/dist/server/pollinations-api.d.ts +11 -0
  28. package/dist/server/pollinations-api.js +21 -8
  29. package/dist/server/proxy.js +223 -160
  30. package/dist/server/quota.d.ts +2 -0
  31. package/dist/server/quota.js +89 -86
  32. package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
  33. package/dist/server/scripts/pollinations_pricing.js +246 -0
  34. package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
  35. package/dist/server/scripts/test_cost_endpoints.js +61 -0
  36. package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
  37. package/dist/server/scripts/test_dynamic_pricing.js +39 -0
  38. package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
  39. package/dist/server/scripts/test_freetier_audit.js +215 -0
  40. package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
  41. package/dist/server/scripts/test_parallel_cost.js +104 -0
  42. package/dist/server/toast.d.ts +7 -1
  43. package/dist/server/toast.js +43 -10
  44. package/dist/tools/design/gen_diagram.d.ts +2 -0
  45. package/dist/tools/design/gen_diagram.js +94 -0
  46. package/dist/tools/design/gen_palette.d.ts +2 -0
  47. package/dist/tools/design/gen_palette.js +182 -0
  48. package/dist/tools/design/gen_qrcode.d.ts +2 -0
  49. package/dist/tools/design/gen_qrcode.js +50 -0
  50. package/dist/tools/ffmpeg.d.ts +24 -0
  51. package/dist/tools/ffmpeg.js +54 -0
  52. package/dist/tools/index.d.ts +24 -0
  53. package/dist/tools/index.js +83 -0
  54. package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
  55. package/dist/tools/pollinations/beta_discovery.js +197 -0
  56. package/dist/tools/pollinations/cost-guard.d.ts +38 -0
  57. package/dist/tools/pollinations/cost-guard.js +141 -0
  58. package/dist/tools/pollinations/deepsearch.d.ts +7 -0
  59. package/dist/tools/pollinations/deepsearch.js +80 -0
  60. package/dist/tools/pollinations/gen_audio.d.ts +18 -0
  61. package/dist/tools/pollinations/gen_audio.js +246 -0
  62. package/dist/tools/pollinations/gen_image.d.ts +11 -0
  63. package/dist/tools/pollinations/gen_image.js +225 -0
  64. package/dist/tools/pollinations/gen_music.d.ts +14 -0
  65. package/dist/tools/pollinations/gen_music.js +180 -0
  66. package/dist/tools/pollinations/gen_video.d.ts +16 -0
  67. package/dist/tools/pollinations/gen_video.js +256 -0
  68. package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
  69. package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
  70. package/dist/tools/pollinations/polli_status.d.ts +2 -0
  71. package/dist/tools/pollinations/polli_status.js +31 -0
  72. package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
  73. package/dist/tools/pollinations/polli_web_search.js +164 -0
  74. package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
  75. package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
  76. package/dist/tools/pollinations/shared.d.ts +165 -0
  77. package/dist/tools/pollinations/shared.js +665 -0
  78. package/dist/tools/pollinations/test_estimators.d.ts +1 -0
  79. package/dist/tools/pollinations/test_estimators.js +22 -0
  80. package/dist/tools/pollinations/transcribe_audio.d.ts +13 -0
  81. package/dist/tools/pollinations/transcribe_audio.js +194 -0
  82. package/dist/tools/power/extract_audio.d.ts +2 -0
  83. package/dist/tools/power/extract_audio.js +179 -0
  84. package/dist/tools/power/extract_frames.d.ts +2 -0
  85. package/dist/tools/power/extract_frames.js +237 -0
  86. package/dist/tools/power/file_to_url.d.ts +2 -0
  87. package/dist/tools/power/file_to_url.js +217 -0
  88. package/dist/tools/power/remove_background.d.ts +2 -0
  89. package/dist/tools/power/remove_background.js +366 -0
  90. package/dist/tools/power/rmbg_keys.d.ts +2 -0
  91. package/dist/tools/power/rmbg_keys.js +79 -0
  92. package/dist/tools/shared.d.ts +30 -0
  93. package/dist/tools/shared.js +80 -0
  94. package/package.json +10 -4
  95. package/dist/server/models-seed.d.ts +0 -18
  96. package/dist/server/models-seed.js +0 -55
@@ -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(process.env.HOME || '/tmp', '.config/opencode/pollinations-signature.json');
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 response = await fetch(url, options);
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 - v6.0: All models go to gen.pollinations.ai
220
- // Strip any legacy prefixes for backwards compatibility
221
- if (actualModel.startsWith('enter/') || actualModel.startsWith('enter-')) {
222
- actualModel = actualModel.replace(/^enter[-/]/, '');
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/') || actualModel.startsWith('free-')) {
225
- actualModel = actualModel.replace(/^free[-/]/, '');
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
- // Fallback based on current mode
249
- actualModel = config.fallbacks.economy || 'nova-fast';
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
- // === MODE ECONOMY ===
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
- // 1. BLOCK Paid Models
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(`[Economy] BLOCKED: Paid model ${actualModel} not allowed in Economy mode`);
289
- emitStatusToast('error', `💎 Modèle payant interdit en mode Economy: ${actualModel}`, 'Mode Economy');
290
- res.writeHead(403, { 'Content-Type': 'application/json' });
291
- res.end(JSON.stringify({
292
- error: {
293
- message: `💎 Paid model "${actualModel}" is not allowed in Economy mode. Switch to Pro mode or use a free model.`,
294
- code: 'ECONOMY_PAID_BLOCKED'
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
- log(`[Economy] Error checking paid models: ${e}`);
303
- }
304
- // 2. Check Tier
305
- if (quota.tier === 'error') {
306
- log(`[Economy] Quota unreachable, switching to fallback`);
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(`[Economy] Tier ${(tierRatio * 100).toFixed(1)}% <= ${config.thresholds.tier}%, switching to fallback`);
329
- actualModel = config.fallbacks.economy || 'nova-fast';
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}% (Economie active)`;
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
- log(`[Pro] Quota unreachable, switching to fallback`);
350
- actualModel = config.fallbacks.pro || 'qwen-coder';
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 walletStop = config.thresholds.wallet_stop || 0.50;
356
- const walletWarnPercent = config.thresholds.wallet_warn || 20;
357
- const walletInitial = config.session?.wallet_initial || quota.walletBalance;
358
- const walletPercent = walletInitial > 0 ? (quota.walletBalance / walletInitial) * 100 : 100;
359
- // 1. STOP if wallet < wallet_stop $
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 < ${walletWarnPercent}% (${quota.walletBalance.toFixed(2)}$)`;
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 - v6.0: Always gen.pollinations.ai
387
- if (!config.apiKey) {
388
- emitLogToast('error', "Missing API Key - Use /pollinations connect <key>", 'Proxy Error');
389
- res.writeHead(401, { 'Content-Type': 'application/json' });
390
- res.end(JSON.stringify({ error: { message: "API Key required. Use /pollinations connect <your_key> to connect." } }));
391
- return;
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 (Restored for Debug)
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
- proxyBody.tools = truncateTools(proxyBody.tools, 120);
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
- // 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")) {
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 based on mode
613
- actualModel = config.mode === 'pro'
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 - v6.0: Stay on gen.pollinations.ai
638
- targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
700
+ // 3. Re-Prepare Request
701
+ targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
639
702
  const retryHeaders = { ...headers };
640
- // Keep Authorization for gen.pollinations.ai
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, {
@@ -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;