opencode-pollinations-plugin 6.1.0-beta.3 → 6.1.0-beta.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +257 -61
  2. package/dist/index.js +53 -161
  3. package/dist/server/commands.d.ts +6 -0
  4. package/dist/server/commands.js +404 -73
  5. package/dist/server/config.d.ts +32 -23
  6. package/dist/server/config.js +183 -104
  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 +195 -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 +25 -0
  53. package/dist/tools/index.js +86 -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_config.d.ts +2 -0
  69. package/dist/tools/pollinations/polli_config.js +88 -0
  70. package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
  71. package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
  72. package/dist/tools/pollinations/polli_status.d.ts +2 -0
  73. package/dist/tools/pollinations/polli_status.js +31 -0
  74. package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
  75. package/dist/tools/pollinations/polli_web_search.js +164 -0
  76. package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
  77. package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
  78. package/dist/tools/pollinations/shared.d.ts +165 -0
  79. package/dist/tools/pollinations/shared.js +665 -0
  80. package/dist/tools/pollinations/test_estimators.d.ts +1 -0
  81. package/dist/tools/pollinations/test_estimators.js +22 -0
  82. package/dist/tools/pollinations/transcribe_audio.d.ts +13 -0
  83. package/dist/tools/pollinations/transcribe_audio.js +194 -0
  84. package/dist/tools/power/extract_audio.d.ts +2 -0
  85. package/dist/tools/power/extract_audio.js +179 -0
  86. package/dist/tools/power/extract_frames.d.ts +2 -0
  87. package/dist/tools/power/extract_frames.js +237 -0
  88. package/dist/tools/power/file_to_url.d.ts +2 -0
  89. package/dist/tools/power/file_to_url.js +217 -0
  90. package/dist/tools/power/remove_background.d.ts +2 -0
  91. package/dist/tools/power/remove_background.js +392 -0
  92. package/dist/tools/power/rmbg_keys.d.ts +2 -0
  93. package/dist/tools/power/rmbg_keys.js +79 -0
  94. package/dist/tools/shared.d.ts +30 -0
  95. package/dist/tools/shared.js +80 -0
  96. package/package.json +10 -4
  97. package/dist/server/models-seed.d.ts +0 -18
  98. 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,31 @@ export async function handleChatCompletion(req, res, bodyRaw) {
208
258
  }
209
259
  }
210
260
  log(`Incoming Model (OpenCode ID): ${body.model}`);
261
+ // 0. SPECIAL: pollinations/connect (Guide & Status)
262
+ const CONNECT_MODEL_IDS = ['pollinations/connect', 'free/pollinations/connect', 'enter/pollinations/connect', 'connect-pollinations'];
263
+ if (CONNECT_MODEL_IDS.includes(body.model)) {
264
+ const guideContent = await buildConnectResponse(config);
265
+ res.writeHead(200, {
266
+ 'Content-Type': 'text/event-stream',
267
+ 'Cache-Control': 'no-cache',
268
+ 'Connection': 'keep-alive'
269
+ });
270
+ const chunk = JSON.stringify({
271
+ id: 'connect-' + Date.now(),
272
+ object: 'chat.completion.chunk',
273
+ created: Math.floor(Date.now() / 1000),
274
+ model: 'pollinations/connect',
275
+ choices: [{
276
+ index: 0,
277
+ delta: { role: 'assistant', content: guideContent },
278
+ finish_reason: 'stop' // Instant finish
279
+ }]
280
+ });
281
+ res.write(`data: ${chunk}\n\n`);
282
+ res.write(`data: [DONE]\n\n`);
283
+ res.end();
284
+ return;
285
+ }
211
286
  // 1. STRICT ROUTING & SAFETY NET LOGIC (V5)
212
287
  let actualModel = body.model || "openai";
213
288
  let isEnterprise = false;
@@ -216,16 +291,15 @@ export async function handleChatCompletion(req, res, bodyRaw) {
216
291
  // LOAD QUOTA FOR SAFETY CHECKS
217
292
  const { getQuotaStatus, formatQuotaForToast } = await import('./quota.js');
218
293
  const quota = await getQuotaStatus(false);
219
- // A. Resolve Base Target - 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[-/]/, '');
294
+ // A. Resolve Base Target
295
+ if (actualModel.startsWith('enter/')) {
296
+ isEnterprise = true;
297
+ actualModel = actualModel.replace('enter/', '');
223
298
  }
224
- else if (actualModel.startsWith('free/') || actualModel.startsWith('free-')) {
225
- actualModel = actualModel.replace(/^free[-/]/, '');
299
+ else if (actualModel.startsWith('free/')) {
300
+ isEnterprise = false;
301
+ actualModel = actualModel.replace('free/', '');
226
302
  }
227
- // v6.0: Everything is enterprise now (requires API key)
228
- isEnterprise = true;
229
303
  // A.1 PAID MODEL ENFORCEMENT (V5.5 Strategy)
230
304
  // Check dynamic list saved by generate-config.ts
231
305
  if (isEnterprise) {
@@ -245,8 +319,13 @@ export async function handleChatCompletion(req, res, bodyRaw) {
245
319
  if (quota.walletBalance <= 0.001) { // Floating point safety
246
320
  log(`[SafetyNet] Paid Only Model (${actualModel}) requested but Wallet is Empty ($${quota.walletBalance}). BLOCKING.`);
247
321
  // Immediate Block or Fallback?
248
- // Fallback based on current mode
249
- actualModel = config.fallbacks.economy || 'nova-fast';
322
+ // Text says: "💎 Paid Only models require purchased pollen only"
323
+ // Blocking is safer/clearer than falling back to a free model which might not be what the user expects for a "Pro" feature?
324
+ // Actually, Fallback to Free is usually better for UX if configured, BUT for specific "Paid Only" requests, the user explicitly chose a powerful model.
325
+ // Falling back to Mistral might be confusing if they asked for Gemini-Large.
326
+ // BUT we are failing gracefully.
327
+ // Let's Fallback to Free Default and Warn.
328
+ actualModel = config.fallbacks.free.main.replace('free/', '');
250
329
  isEnterprise = false;
251
330
  isFallbackActive = true;
252
331
  fallbackReason = "Paid Only Model requires purchased credits";
@@ -272,127 +351,99 @@ export async function handleChatCompletion(req, res, bodyRaw) {
272
351
  // WE DO NOT RETURN 403. WE ALLOW THE REQUEST.
273
352
  // Since config.mode is now 'manual', the next checks (alwaysfree/pro) will be skipped.
274
353
  }
275
- // === 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') {
354
+ if (config.mode === 'alwaysfree') {
280
355
  if (isEnterprise) {
281
- // 1. BLOCK Paid Models
356
+ // Paid Only Check: BLOCK (not fallback) in AlwaysFree mode
282
357
  try {
283
358
  const homedir = process.env.HOME || '/tmp';
284
359
  const standardPaidPath = path.join(homedir, '.pollinations', 'pollinations-paid-models.json');
285
360
  if (fs.existsSync(standardPaidPath)) {
286
361
  const paidModels = JSON.parse(fs.readFileSync(standardPaidPath, 'utf-8'));
287
362
  if (paidModels.includes(actualModel)) {
288
- log(`[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
- }));
363
+ log(`[AlwaysFree] BLOCKED: Paid Only Model (${actualModel}).`);
364
+ emitStatusToast('warning', `🚫 Modèle payant bloqué: ${actualModel}`, 'AlwaysFree Mode');
365
+ const blockMsg = {
366
+ id: `chatcmpl-block-${Date.now()}`,
367
+ object: 'chat.completion',
368
+ created: Math.floor(Date.now() / 1000),
369
+ model: actualModel,
370
+ choices: [{
371
+ index: 0,
372
+ message: {
373
+ role: 'assistant',
374
+ content: `🚫 **Modèle payant non disponible en mode AlwaysFree**\n\nLe modèle \`${actualModel}\` consomme directement votre wallet (💎 Paid Only).\n\n**Solutions :**\n• \`/pollinations config mode pro\` — Autorise les modèles payants avec protection wallet\n• \`/pollinations config mode manual\` — Aucune restriction, contrôle total`
375
+ },
376
+ finish_reason: 'stop'
377
+ }],
378
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
379
+ };
380
+ res.writeHead(200, { 'Content-Type': 'application/json' });
381
+ res.end(JSON.stringify(blockMsg));
297
382
  return;
298
383
  }
299
384
  }
300
385
  }
301
- catch (e) {
302
- 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';
386
+ catch (e) { }
387
+ if (!isFallbackActive && quota.tier === 'error') {
388
+ // Network error or unknown error (but NOT auth_limited, handled above)
389
+ log(`[SafetyNet] AlwaysFree Mode: Quota Check Failed. Switching to Free Fallback.`);
390
+ actualModel = config.fallbacks.free.main.replace('free/', '');
391
+ isEnterprise = false;
308
392
  isFallbackActive = true;
309
393
  fallbackReason = "Quota Unreachable (Safety)";
310
394
  }
311
395
  else {
312
396
  const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
313
- // 3. STOP if tier = 0
314
- if (quota.tierRemaining <= 0.01) {
315
- log(`[Economy] STOP: Tier exhausted, no wallet access in Economy mode`);
316
- emitStatusToast('error', `🛑 Quota épuisé! Attendez le reset ou passez en mode Pro.`, 'Mode Economy');
317
- res.writeHead(429, { 'Content-Type': 'application/json' });
318
- res.end(JSON.stringify({
319
- error: {
320
- message: `🛑 Daily quota exhausted. Wait for reset or switch to Pro mode to use wallet credits.`,
321
- code: 'ECONOMY_TIER_EXHAUSTED'
322
- }
323
- }));
324
- return;
325
- }
326
- // 4. Fallback if tier < threshold
327
397
  if (tierRatio <= (config.thresholds.tier / 100)) {
328
- log(`[Economy] Tier ${(tierRatio * 100).toFixed(1)}% <= ${config.thresholds.tier}%, switching to fallback`);
329
- actualModel = config.fallbacks.economy || 'nova-fast';
398
+ log(`[SafetyNet] AlwaysFree Mode: Tier (${(tierRatio * 100).toFixed(1)}%) <= Threshold (${config.thresholds.tier}%). Switching.`);
399
+ actualModel = config.fallbacks.free.main.replace('free/', '');
400
+ isEnterprise = false;
330
401
  isFallbackActive = true;
331
- fallbackReason = `Tier < ${config.thresholds.tier}% (Economie active)`;
402
+ fallbackReason = `Daily Tier < ${config.thresholds.tier}% (Wallet Protected)`;
332
403
  }
333
404
  }
334
405
  }
335
406
  }
336
- // === MODE PRO ===
337
- // - Paid models ALLOWED
338
- // - wallet < wallet_stop $ → STOP
339
- // - wallet < wallet_warn % → fallback pro
340
- // - tier exhausted → info toast (continue on wallet)
341
407
  else if (config.mode === 'pro') {
342
408
  if (isEnterprise) {
343
- // Init session wallet tracking (first request)
344
- if (!config.session?.wallet_initial && quota.walletBalance > 0) {
345
- const { initSessionWallet } = await import('./config.js');
346
- initSessionWallet(quota.walletBalance);
347
- }
348
409
  if (quota.tier === 'error') {
349
- log(`[Pro] Quota unreachable, switching to fallback`);
350
- actualModel = config.fallbacks.pro || 'qwen-coder';
410
+ // Network error or unknown
411
+ log(`[SafetyNet] Pro Mode: Quota Unreachable. Switching to Free Fallback.`);
412
+ actualModel = config.fallbacks.free.main.replace('free/', '');
413
+ isEnterprise = false;
351
414
  isFallbackActive = true;
352
415
  fallbackReason = "Quota Unreachable (Safety)";
353
416
  }
354
417
  else {
355
- const 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';
418
+ const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
419
+ if (quota.walletBalance < config.thresholds.wallet && tierRatio <= (config.thresholds.tier / 100)) {
420
+ log(`[SafetyNet] Pro Mode: Wallet < $${config.thresholds.wallet} AND Tier < ${config.thresholds.tier}%. Switching.`);
421
+ actualModel = config.fallbacks.free.main.replace('free/', '');
422
+ isEnterprise = false;
376
423
  isFallbackActive = true;
377
- fallbackReason = `Wallet < ${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');
424
+ fallbackReason = `Wallet & Tier Critical`;
382
425
  }
383
426
  }
384
427
  }
385
428
  }
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;
429
+ // C. Construct URL & Headers
430
+ if (isEnterprise) {
431
+ if (!config.apiKey) {
432
+ emitLogToast('error', "Missing API Key for Enterprise Model", 'Proxy Error');
433
+ res.writeHead(401, { 'Content-Type': 'application/json' });
434
+ res.end(JSON.stringify({ error: { message: "API Key required for Enterprise models." } }));
435
+ return;
436
+ }
437
+ targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
438
+ authHeader = `Bearer ${config.apiKey}`;
439
+ log(`Routing to ENTERPRISE: ${actualModel}`);
440
+ }
441
+ else {
442
+ targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
443
+ authHeader = undefined;
444
+ log(`Routing to FREE: ${actualModel} ${isFallbackActive ? '(FALLBACK)' : ''}`);
445
+ // emitLogToast('info', `Routing to: FREE UNIVERSE (${actualModel})`, 'Pollinations Routing'); // Too noisy
392
446
  }
393
- targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
394
- authHeader = `Bearer ${config.apiKey}`;
395
- log(`Routing to gen.pollinations.ai: ${actualModel}`);
396
447
  // NOTIFY SWITCH
397
448
  if (isFallbackActive) {
398
449
  emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
@@ -424,17 +475,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
424
475
  // LOGIC BLOCK: MODEL SPECIFIC ADAPTATIONS
425
476
  // =========================================================
426
477
  if (proxyBody.tools && Array.isArray(proxyBody.tools) && proxyBody.tools.length > 0) {
427
- // B0. KIMI / MOONSHOT SURGICAL FIX (Restored for Debug)
428
- // Tools are ENABLED. We rely on penalties and strict stops to fight loops.
478
+ // B0. KIMI / MOONSHOT SURGICAL FIX
429
479
  if (actualModel.includes("kimi") || actualModel.includes("moonshot")) {
430
- log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops.`);
480
+ log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops/sanitization.`);
431
481
  proxyBody.frequency_penalty = 1.1;
432
482
  proxyBody.presence_penalty = 0.4;
433
483
  proxyBody.stop = ["<|endoftext|>", "User:", "\nUser", "User :"];
484
+ // KIMI FIX: Remove 'title' from schema
485
+ proxyBody.tools = proxyBody.tools.map((t) => {
486
+ if (t.function && t.function.parameters) {
487
+ t.function.parameters = sanitizeSchemaForKimi(t.function.parameters);
488
+ }
489
+ return t;
490
+ });
434
491
  }
435
- // A. AZURE/OPENAI FIXES
436
- if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure")) {
437
- proxyBody.tools = truncateTools(proxyBody.tools, 120);
492
+ // A. AZURE/OPENAI FIXES + MIDJOURNEY + GROK
493
+ if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure") || actualModel.includes("midijourney") || actualModel.includes("grok")) {
494
+ const limit = (actualModel.includes("midijourney") || actualModel.includes("grok")) ? 128 : 120;
495
+ proxyBody.tools = truncateTools(proxyBody.tools, limit);
438
496
  if (proxyBody.messages) {
439
497
  proxyBody.messages.forEach((m) => {
440
498
  if (m.tool_calls) {
@@ -449,6 +507,11 @@ export async function handleChatCompletion(req, res, bodyRaw) {
449
507
  });
450
508
  }
451
509
  }
510
+ // BEDROCK FIX (Claude / Nova / ChickyTutor)
511
+ if (actualModel.includes("claude") || actualModel.includes("nova") || actualModel.includes("bedrock") || actualModel.includes("chickytutor")) {
512
+ log(`[Proxy] Bedrock: Sanitizing tools description.`);
513
+ proxyBody.tools = sanitizeToolsForBedrock(proxyBody.tools);
514
+ }
452
515
  // B1. NOMNOM SPECIAL (Disable Grounding, KEEP Search Tool)
453
516
  if (actualModel === "nomnom") {
454
517
  proxyBody.tools_config = { google_search_retrieval: { disable: true } };
@@ -456,36 +519,12 @@ export async function handleChatCompletion(req, res, bodyRaw) {
456
519
  proxyBody.tools = sanitizeToolsForVertex(proxyBody.tools || []);
457
520
  log(`[Proxy] Nomnom Fix: Grounding Disabled, Search Tool KEPT.`);
458
521
  }
459
- // B2. GEMINI FREE / FAST (CRASH FIX: STRICT SANITIZATION)
460
- // Restore Tools but REMOVE conflicting ones (Search)
461
522
  // B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
462
- // 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
- // B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
481
- // Fixes "Multiple tools" error (Vertex) and "JSON body validation failed" (v5.3.5 regression)
482
- // Added ChickyTutor (Claude/Gemini based) to fix "toolConfig must be defined" error.
483
- else if (actualModel.includes("gemini") || actualModel.includes("chickytutor")) {
523
+ else if (actualModel.includes("gemini")) {
484
524
  let hasFunctions = false;
485
525
  if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
486
526
  hasFunctions = proxyBody.tools.some((t) => t.type === 'function' || t.function);
487
527
  }
488
- // Old Shim logic removed (moved up)
489
528
  if (hasFunctions) {
490
529
  // 1. Strict cleanup of 'google_search' tool
491
530
  proxyBody.tools = proxyBody.tools.filter((t) => {
@@ -516,14 +555,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
516
555
  log(`[Proxy] Gemini Logic: Tools=${proxyBody.tools ? proxyBody.tools.length : 'REMOVED'}, Stops NOT Injected.`);
517
556
  }
518
557
  }
519
- // B5. BEDROCK TOKEN LIMIT FIX
520
- if (actualModel.includes("chicky") || actualModel.includes("mistral")) {
521
- // Force max_tokens if not present or too high (Bedrock outputs usually max 4k, context 8k+ but strict check)
522
- if (!proxyBody.max_tokens || proxyBody.max_tokens > 4096) {
523
- proxyBody.max_tokens = 4096;
524
- log(`[Proxy] Enforcing max_tokens=4096 for ${actualModel} (Bedrock Limit)`);
525
- }
526
- }
527
558
  // C. GEMINI ID BACKTRACKING & SIGNATURE
528
559
  if ((actualModel.includes("gemini") || actualModel === "nomnom") && proxyBody.messages) {
529
560
  const lastMsg = proxyBody.messages[proxyBody.messages.length - 1];
@@ -609,14 +640,18 @@ export async function handleChatCompletion(req, res, bodyRaw) {
609
640
  if ((isEnterpriseFallback || isGeminiToolsFallback) && config.mode !== 'manual') {
610
641
  log(`[SafetyNet] Upstream Rejection (${fetchRes.status}). Triggering Transparent Fallback.`);
611
642
  if (isEnterpriseFallback) {
612
- // 1a. Enterprise -> Fallback based on mode
613
- actualModel = config.mode === 'pro'
614
- ? (config.fallbacks.pro || 'qwen-coder')
615
- : (config.fallbacks.economy || 'nova-fast');
643
+ // 1a. Enterprise -> Free Fallback
644
+ actualModel = config.fallbacks.free.main.replace('free/', '');
616
645
  isEnterprise = false;
617
646
  isFallbackActive = true;
618
- if (fetchRes.status === 402)
647
+ if (fetchRes.status === 402) {
619
648
  fallbackReason = "Insufficient Funds (Upstream 402)";
649
+ // Force refresh quota cache so next pre-flight check is accurate
650
+ try {
651
+ await getQuotaStatus(true);
652
+ }
653
+ catch (e) { }
654
+ }
620
655
  else if (fetchRes.status === 429)
621
656
  fallbackReason = "Rate Limit (Upstream 429)";
622
657
  else if (fetchRes.status === 401)
@@ -634,10 +669,10 @@ export async function handleChatCompletion(req, res, bodyRaw) {
634
669
  // 2. Notify
635
670
  emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
636
671
  emitLogToast('warning', `Recovering from ${fetchRes.status} -> Switching to ${actualModel}`, 'Safety Net');
637
- // 3. Re-Prepare Request - v6.0: Stay on gen.pollinations.ai
638
- targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
672
+ // 3. Re-Prepare Request
673
+ targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
639
674
  const retryHeaders = { ...headers };
640
- // Keep Authorization for gen.pollinations.ai
675
+ delete retryHeaders['Authorization']; // Free = No Auth
641
676
  const retryBody = { ...proxyBody, model: actualModel };
642
677
  // 4. Retry Fetch
643
678
  const retryRes = await fetchWithRetry(targetUrl, {
@@ -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;