opencode-pollinations-plugin 6.1.0-beta.8 → 6.2.0

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 (109) hide show
  1. package/README.de.md +130 -0
  2. package/README.es.md +130 -0
  3. package/README.fr.md +130 -0
  4. package/README.it.md +130 -0
  5. package/README.md +87 -73
  6. package/dist/index.js +52 -161
  7. package/dist/locales/de.json +374 -0
  8. package/dist/locales/en.json +373 -0
  9. package/dist/locales/es.json +374 -0
  10. package/dist/locales/fr.json +373 -0
  11. package/dist/locales/index.d.ts +1 -0
  12. package/dist/locales/index.js +37 -0
  13. package/dist/locales/it.json +374 -0
  14. package/dist/server/commands.d.ts +6 -0
  15. package/dist/server/commands.js +394 -125
  16. package/dist/server/config.d.ts +34 -23
  17. package/dist/server/config.js +200 -108
  18. package/dist/server/connect-response.d.ts +2 -0
  19. package/dist/server/connect-response.js +59 -0
  20. package/dist/server/generate-config.d.ts +3 -30
  21. package/dist/server/generate-config.js +164 -106
  22. package/dist/server/index.d.ts +2 -1
  23. package/dist/server/index.js +124 -149
  24. package/dist/server/logger.d.ts +8 -0
  25. package/dist/server/logger.js +38 -0
  26. package/dist/server/models/cache.d.ts +35 -0
  27. package/dist/server/models/cache.js +160 -0
  28. package/dist/server/models/fetcher.d.ts +18 -0
  29. package/dist/server/models/fetcher.js +194 -0
  30. package/dist/server/models/index.d.ts +6 -0
  31. package/dist/server/models/index.js +5 -0
  32. package/dist/server/models/manual.d.ts +15 -0
  33. package/dist/server/models/manual.js +92 -0
  34. package/dist/server/models/types.d.ts +55 -0
  35. package/dist/server/models/types.js +7 -0
  36. package/dist/server/models/worker.d.ts +22 -0
  37. package/dist/server/models/worker.js +174 -0
  38. package/dist/server/pollinations-api.d.ts +11 -0
  39. package/dist/server/pollinations-api.js +21 -8
  40. package/dist/server/proxy.js +222 -293
  41. package/dist/server/quota.d.ts +2 -0
  42. package/dist/server/quota.js +89 -86
  43. package/dist/server/scripts/pollinations_pricing.d.ts +8 -0
  44. package/dist/server/scripts/pollinations_pricing.js +246 -0
  45. package/dist/server/scripts/test_cost_endpoints.d.ts +1 -0
  46. package/dist/server/scripts/test_cost_endpoints.js +61 -0
  47. package/dist/server/scripts/test_dynamic_pricing.d.ts +1 -0
  48. package/dist/server/scripts/test_dynamic_pricing.js +39 -0
  49. package/dist/server/scripts/test_freetier_audit.d.ts +11 -0
  50. package/dist/server/scripts/test_freetier_audit.js +215 -0
  51. package/dist/server/scripts/test_parallel_cost.d.ts +1 -0
  52. package/dist/server/scripts/test_parallel_cost.js +104 -0
  53. package/dist/server/toast.d.ts +7 -1
  54. package/dist/server/toast.js +43 -10
  55. package/dist/tools/design/gen_diagram.d.ts +2 -0
  56. package/dist/tools/design/gen_diagram.js +94 -0
  57. package/dist/tools/design/gen_palette.d.ts +2 -0
  58. package/dist/tools/design/gen_palette.js +182 -0
  59. package/dist/tools/design/gen_qrcode.d.ts +2 -0
  60. package/dist/tools/design/gen_qrcode.js +50 -0
  61. package/dist/tools/ffmpeg.d.ts +24 -0
  62. package/dist/tools/ffmpeg.js +54 -0
  63. package/dist/tools/index.d.ts +25 -0
  64. package/dist/tools/index.js +86 -0
  65. package/dist/tools/pollinations/beta_discovery.d.ts +9 -0
  66. package/dist/tools/pollinations/beta_discovery.js +201 -0
  67. package/dist/tools/pollinations/cost-guard.d.ts +38 -0
  68. package/dist/tools/pollinations/cost-guard.js +136 -0
  69. package/dist/tools/pollinations/deepsearch.d.ts +7 -0
  70. package/dist/tools/pollinations/deepsearch.js +80 -0
  71. package/dist/tools/pollinations/gen_audio.d.ts +18 -0
  72. package/dist/tools/pollinations/gen_audio.js +220 -0
  73. package/dist/tools/pollinations/gen_image.d.ts +11 -0
  74. package/dist/tools/pollinations/gen_image.js +211 -0
  75. package/dist/tools/pollinations/gen_music.d.ts +14 -0
  76. package/dist/tools/pollinations/gen_music.js +157 -0
  77. package/dist/tools/pollinations/gen_video.d.ts +16 -0
  78. package/dist/tools/pollinations/gen_video.js +249 -0
  79. package/dist/tools/pollinations/polli_config.d.ts +2 -0
  80. package/dist/tools/pollinations/polli_config.js +95 -0
  81. package/dist/tools/pollinations/polli_gen_confirm.d.ts +2 -0
  82. package/dist/tools/pollinations/polli_gen_confirm.js +48 -0
  83. package/dist/tools/pollinations/polli_status.d.ts +2 -0
  84. package/dist/tools/pollinations/polli_status.js +31 -0
  85. package/dist/tools/pollinations/polli_web_search.d.ts +15 -0
  86. package/dist/tools/pollinations/polli_web_search.js +126 -0
  87. package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
  88. package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
  89. package/dist/tools/pollinations/shared.d.ts +181 -0
  90. package/dist/tools/pollinations/shared.js +758 -0
  91. package/dist/tools/pollinations/test_estimators.d.ts +1 -0
  92. package/dist/tools/pollinations/test_estimators.js +22 -0
  93. package/dist/tools/pollinations/transcribe_audio.d.ts +13 -0
  94. package/dist/tools/pollinations/transcribe_audio.js +171 -0
  95. package/dist/tools/power/extract_audio.d.ts +2 -0
  96. package/dist/tools/power/extract_audio.js +179 -0
  97. package/dist/tools/power/extract_frames.d.ts +2 -0
  98. package/dist/tools/power/extract_frames.js +237 -0
  99. package/dist/tools/power/file_to_url.d.ts +2 -0
  100. package/dist/tools/power/file_to_url.js +217 -0
  101. package/dist/tools/power/remove_background.d.ts +2 -0
  102. package/dist/tools/power/remove_background.js +404 -0
  103. package/dist/tools/power/rmbg_keys.d.ts +2 -0
  104. package/dist/tools/power/rmbg_keys.js +79 -0
  105. package/dist/tools/shared.d.ts +30 -0
  106. package/dist/tools/shared.js +80 -0
  107. package/package.json +10 -4
  108. package/dist/server/models-seed.d.ts +0 -18
  109. package/dist/server/models-seed.js +0 -55
@@ -3,26 +3,22 @@ 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';
9
+ import { t } from '../locales/index.js';
6
10
  // --- PERSISTENCE: SIGNATURE MAP (Multi-Round Support) ---
7
- const SIG_FILE = path.join(process.env.HOME || '/tmp', '.config/opencode/pollinations-signature.json');
11
+ const SIG_FILE = path.join(getConfigDir(), 'pollinations-signature.json');
8
12
  let signatureMap = {};
9
13
  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
14
  try {
21
15
  if (fs.existsSync(SIG_FILE)) {
22
16
  signatureMap = JSON.parse(fs.readFileSync(SIG_FILE, 'utf-8'));
23
17
  }
24
18
  }
25
- catch (e) { }
19
+ catch (e) {
20
+ log(`[Proxy Signature] Error loading: ${e}`);
21
+ }
26
22
  function saveSignatureMap() {
27
23
  try {
28
24
  if (!fs.existsSync(path.dirname(SIG_FILE)))
@@ -78,6 +74,23 @@ function dereferenceSchema(schema, rootDefs) {
78
74
  schema.description = (schema.description || "") + " [Ref Failed]";
79
75
  }
80
76
  }
77
+ // VERTEX FIX: 'const' not supported -> convert to 'enum'
78
+ if (schema.const !== undefined) {
79
+ schema.enum = [schema.const];
80
+ delete schema.const;
81
+ }
82
+ // VERTEX FIX: 'anyOf' must be exclusive (no other siblings)
83
+ if (schema.anyOf || schema.oneOf) {
84
+ // Vertex demands strict exclusivity.
85
+ // We keep 'definitions'/'$defs' if present at root (though unlikely here)
86
+ // But for a property node, we must strip EVERYTHING else.
87
+ const keys = Object.keys(schema);
88
+ keys.forEach(k => {
89
+ if (k !== 'anyOf' && k !== 'oneOf' && k !== 'definitions' && k !== '$defs') {
90
+ delete schema[k];
91
+ }
92
+ });
93
+ }
81
94
  if (schema.properties) {
82
95
  for (const key in schema.properties) {
83
96
  schema.properties[key] = dereferenceSchema(schema.properties[key], rootDefs);
@@ -86,6 +99,15 @@ function dereferenceSchema(schema, rootDefs) {
86
99
  if (schema.items) {
87
100
  schema.items = dereferenceSchema(schema.items, rootDefs);
88
101
  }
102
+ if (schema.anyOf) {
103
+ schema.anyOf = schema.anyOf.map((s) => dereferenceSchema(s, rootDefs));
104
+ }
105
+ if (schema.oneOf) {
106
+ schema.oneOf = schema.oneOf.map((s) => dereferenceSchema(s, rootDefs));
107
+ }
108
+ if (schema.allOf) {
109
+ schema.allOf = schema.allOf.map((s) => dereferenceSchema(s, rootDefs));
110
+ }
89
111
  if (schema.optional !== undefined)
90
112
  delete schema.optional;
91
113
  if (schema.title)
@@ -107,127 +129,55 @@ function sanitizeToolsForVertex(tools) {
107
129
  return tool;
108
130
  });
109
131
  }
110
- function truncateTools(tools, limit = 120) {
111
- if (!tools || tools.length <= limit)
112
- return tools;
113
- return tools.slice(0, limit);
114
- }
115
- // --- BEDROCK COMPATIBILITY SHIM ---
116
- // Bedrock requires:
117
- // 1. toolUseId matching [a-zA-Z0-9_-]+ (no dots, spaces, special chars)
118
- // 2. toolConfig present when toolUse/toolResult in messages
119
- function sanitizeToolUseId(id) {
120
- // Replace any char not in [a-zA-Z0-9_-] with underscore
121
- return id.replace(/[^a-zA-Z0-9_-]/g, '_');
122
- }
123
- function sanitizeForBedrock(body) {
124
- if (!body)
125
- return body;
126
- const sanitized = JSON.parse(JSON.stringify(body)); // Deep clone
127
- // 1. Sanitize toolUseId in all messages AND detect tool history
128
- let hasToolHistory = false;
129
- const toolNamesFromHistory = [];
130
- if (sanitized.messages && Array.isArray(sanitized.messages)) {
131
- for (const msg of sanitized.messages) {
132
- if (msg.content && Array.isArray(msg.content)) {
133
- for (const block of msg.content) {
134
- // Handle toolUse blocks
135
- if (block.toolUse && block.toolUse.toolUseId) {
136
- block.toolUse.toolUseId = sanitizeToolUseId(block.toolUse.toolUseId);
137
- hasToolHistory = true;
138
- if (block.toolUse.name)
139
- toolNamesFromHistory.push(block.toolUse.name);
140
- }
141
- // Handle toolResult blocks
142
- if (block.toolResult && block.toolResult.toolUseId) {
143
- block.toolResult.toolUseId = sanitizeToolUseId(block.toolResult.toolUseId);
144
- hasToolHistory = true;
145
- }
146
- // Handle OpenAI-style tool_use in content array
147
- if (block.type === 'tool_use' && block.id) {
148
- block.id = sanitizeToolUseId(block.id);
149
- hasToolHistory = true;
150
- if (block.name)
151
- toolNamesFromHistory.push(block.name);
152
- }
153
- }
154
- }
155
- // Handle assistant tool_calls array (OpenAI format)
156
- if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
157
- hasToolHistory = true;
158
- for (const tc of msg.tool_calls) {
159
- if (tc.id)
160
- tc.id = sanitizeToolUseId(tc.id);
161
- if (tc.function?.name)
162
- toolNamesFromHistory.push(tc.function.name);
163
- }
164
- }
165
- // Handle tool role message (OpenAI format)
166
- if (msg.role === 'tool') {
167
- hasToolHistory = true;
168
- if (msg.tool_call_id) {
169
- msg.tool_call_id = sanitizeToolUseId(msg.tool_call_id);
170
- }
132
+ function sanitizeToolsForBedrock(tools) {
133
+ return tools.map(tool => {
134
+ if (tool.function) {
135
+ if (!tool.function.description || tool.function.description.length === 0) {
136
+ tool.function.description = " "; // Force non-empty string
171
137
  }
172
138
  }
139
+ return tool;
140
+ });
141
+ }
142
+ function sanitizeSchemaForKimi(schema) {
143
+ if (!schema || typeof schema !== 'object')
144
+ return schema;
145
+ // Kimi Fixes
146
+ if (schema.title)
147
+ delete schema.title;
148
+ // Fix empty objects "{}" which Kimi hates.
149
+ // If it's an empty object without type, assume string or object?
150
+ // Often happens with "additionalProperties: {}"
151
+ if (Object.keys(schema).length === 0) {
152
+ schema.type = "string"; // Fallback to safe type
153
+ schema.description = "Any value";
173
154
  }
174
- // 2. Add toolConfig if tools present
175
- if (sanitized.tools && Array.isArray(sanitized.tools) && sanitized.tools.length > 0) {
176
- if (!sanitized.tool_config && !sanitized.toolConfig) {
177
- // Build toolConfig from tools array (Bedrock Converse format)
178
- sanitized.tool_config = {
179
- tools: sanitized.tools.map((t) => {
180
- if (t.function) {
181
- // OpenAI format -> Bedrock format
182
- return {
183
- toolSpec: {
184
- name: t.function.name,
185
- description: t.function.description || '',
186
- inputSchema: {
187
- json: t.function.parameters || { type: 'object', properties: {} }
188
- }
189
- }
190
- };
191
- }
192
- return t; // Already in Bedrock format
193
- })
194
- };
155
+ if (schema.properties) {
156
+ for (const key in schema.properties) {
157
+ schema.properties[key] = sanitizeSchemaForKimi(schema.properties[key]);
195
158
  }
196
159
  }
197
- // 3. CRITICAL: If no tools BUT history has tool calls, inject shim toolConfig
198
- else if (hasToolHistory && !sanitized.tool_config && !sanitized.toolConfig) {
199
- // Bedrock requires toolConfig when history contains toolUse/toolResult
200
- // Create shim tools from the tool names we found in history
201
- const uniqueToolNames = [...new Set(toolNamesFromHistory)];
202
- sanitized.tool_config = {
203
- tools: uniqueToolNames.length > 0
204
- ? uniqueToolNames.map(name => ({
205
- toolSpec: {
206
- name: name,
207
- description: 'Tool inferred from conversation history (Bedrock compatibility shim)',
208
- inputSchema: { json: { type: 'object', properties: {} } }
209
- }
210
- }))
211
- : [{
212
- toolSpec: {
213
- name: '_bedrock_shim_tool',
214
- description: 'Internal shim to satisfy Bedrock toolConfig requirement',
215
- inputSchema: { json: { type: 'object', properties: {} } }
216
- }
217
- }]
218
- };
219
- log(`[Bedrock Shim] Injected toolConfig with ${uniqueToolNames.length || 1} shim tool(s) for history compatibility`);
220
- }
221
- return sanitized;
160
+ if (schema.items)
161
+ sanitizeSchemaForKimi(schema.items);
162
+ return schema;
163
+ }
164
+ function truncateTools(tools, limit = 120) {
165
+ if (!tools || tools.length <= limit)
166
+ return tools;
167
+ return tools.slice(0, limit);
222
168
  }
223
169
  const MAX_RETRIES = 3;
224
170
  const RETRY_DELAY_MS = 1000;
171
+ const FETCH_TIMEOUT_MS = 600000; // 10 Minutes global timeout
225
172
  function sleep(ms) {
226
173
  return new Promise(resolve => setTimeout(resolve, ms));
227
174
  }
228
175
  async function fetchWithRetry(url, options, retries = MAX_RETRIES) {
229
176
  try {
230
- const response = await fetch(url, options);
177
+ const controller = new AbortController();
178
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
179
+ const response = await fetch(url, { ...options, signal: controller.signal });
180
+ clearTimeout(timeoutId);
231
181
  if (response.ok)
232
182
  return response;
233
183
  if (response.status === 404 || response.status === 401 || response.status === 400) {
@@ -261,11 +211,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
261
211
  const config = loadConfig();
262
212
  // DEBUG: Trace Config State for Hot Reload verification
263
213
  log(`[Proxy Request] Config Loaded. Mode: ${config.mode}, HasKey: ${!!config.apiKey}, KeyLength: ${config.apiKey ? config.apiKey.length : 0}`);
264
- // SPY LOGGING
265
- try {
266
- fs.appendFileSync('/tmp/opencode_spy.log', `\n\n=== REQUEST ${new Date().toISOString()} ===\nMODEL: ${body.model}\nBODY:\n${JSON.stringify(body, null, 2)}\n==========================\n`);
267
- }
268
- catch (e) { }
269
214
  // 0. COMMAND HANDLING
270
215
  if (body.messages && body.messages.length > 0) {
271
216
  const lastMsg = body.messages[body.messages.length - 1];
@@ -316,6 +261,31 @@ export async function handleChatCompletion(req, res, bodyRaw) {
316
261
  }
317
262
  }
318
263
  log(`Incoming Model (OpenCode ID): ${body.model}`);
264
+ // 0. SPECIAL: pollinations/connect (Guide & Status)
265
+ const CONNECT_MODEL_IDS = ['pollinations/connect', 'free/pollinations/connect', 'enter/pollinations/connect', 'connect-pollinations'];
266
+ if (CONNECT_MODEL_IDS.includes(body.model)) {
267
+ const guideContent = await buildConnectResponse(config);
268
+ res.writeHead(200, {
269
+ 'Content-Type': 'text/event-stream',
270
+ 'Cache-Control': 'no-cache',
271
+ 'Connection': 'keep-alive'
272
+ });
273
+ const chunk = JSON.stringify({
274
+ id: 'connect-' + Date.now(),
275
+ object: 'chat.completion.chunk',
276
+ created: Math.floor(Date.now() / 1000),
277
+ model: 'pollinations/connect',
278
+ choices: [{
279
+ index: 0,
280
+ delta: { role: 'assistant', content: guideContent },
281
+ finish_reason: 'stop' // Instant finish
282
+ }]
283
+ });
284
+ res.write(`data: ${chunk}\n\n`);
285
+ res.write(`data: [DONE]\n\n`);
286
+ res.end();
287
+ return;
288
+ }
319
289
  // 1. STRICT ROUTING & SAFETY NET LOGIC (V5)
320
290
  let actualModel = body.model || "openai";
321
291
  let isEnterprise = false;
@@ -324,16 +294,15 @@ export async function handleChatCompletion(req, res, bodyRaw) {
324
294
  // LOAD QUOTA FOR SAFETY CHECKS
325
295
  const { getQuotaStatus, formatQuotaForToast } = await import('./quota.js');
326
296
  const quota = await getQuotaStatus(false);
327
- // A. Resolve Base Target - v6.0: All models go to gen.pollinations.ai
328
- // Strip any legacy prefixes for backwards compatibility
329
- if (actualModel.startsWith('enter/') || actualModel.startsWith('enter-')) {
330
- actualModel = actualModel.replace(/^enter[-/]/, '');
297
+ // A. Resolve Base Target
298
+ if (actualModel.startsWith('enter/')) {
299
+ isEnterprise = true;
300
+ actualModel = actualModel.replace('enter/', '');
331
301
  }
332
- else if (actualModel.startsWith('free/') || actualModel.startsWith('free-')) {
333
- actualModel = actualModel.replace(/^free[-/]/, '');
302
+ else if (actualModel.startsWith('free/')) {
303
+ isEnterprise = false;
304
+ actualModel = actualModel.replace('free/', '');
334
305
  }
335
- // v6.0: Everything is enterprise now (requires API key)
336
- isEnterprise = true;
337
306
  // A.1 PAID MODEL ENFORCEMENT (V5.5 Strategy)
338
307
  // Check dynamic list saved by generate-config.ts
339
308
  if (isEnterprise) {
@@ -353,8 +322,13 @@ export async function handleChatCompletion(req, res, bodyRaw) {
353
322
  if (quota.walletBalance <= 0.001) { // Floating point safety
354
323
  log(`[SafetyNet] Paid Only Model (${actualModel}) requested but Wallet is Empty ($${quota.walletBalance}). BLOCKING.`);
355
324
  // Immediate Block or Fallback?
356
- // Fallback based on current mode
357
- actualModel = config.fallbacks.economy || 'nova-fast';
325
+ // Text says: "💎 Paid Only models require purchased pollen only"
326
+ // Blocking is safer/clearer than falling back to a free model which might not be what the user expects for a "Pro" feature?
327
+ // Actually, Fallback to Free is usually better for UX if configured, BUT for specific "Paid Only" requests, the user explicitly chose a powerful model.
328
+ // Falling back to Mistral might be confusing if they asked for Gemini-Large.
329
+ // BUT we are failing gracefully.
330
+ // Let's Fallback to Free Default and Warn.
331
+ actualModel = config.fallbacks.free.main.replace('free/', '');
358
332
  isEnterprise = false;
359
333
  isFallbackActive = true;
360
334
  fallbackReason = "Paid Only Model requires purchased credits";
@@ -380,127 +354,121 @@ export async function handleChatCompletion(req, res, bodyRaw) {
380
354
  // WE DO NOT RETURN 403. WE ALLOW THE REQUEST.
381
355
  // Since config.mode is now 'manual', the next checks (alwaysfree/pro) will be skipped.
382
356
  }
383
- // === MODE ECONOMY ===
384
- // - Paid models BLOCKED (not fallback, BLOCK with message)
385
- // - tier < threshold → fallback economy
386
- // - tier = 0 → STOP (no wallet access)
387
- if (config.mode === 'economy') {
357
+ if (config.mode === 'alwaysfree') {
388
358
  if (isEnterprise) {
389
- // 1. BLOCK Paid Models
359
+ // Paid Only Check: BLOCK (not fallback) in AlwaysFree mode
390
360
  try {
391
361
  const homedir = process.env.HOME || '/tmp';
392
362
  const standardPaidPath = path.join(homedir, '.pollinations', 'pollinations-paid-models.json');
393
363
  if (fs.existsSync(standardPaidPath)) {
394
364
  const paidModels = JSON.parse(fs.readFileSync(standardPaidPath, 'utf-8'));
395
365
  if (paidModels.includes(actualModel)) {
396
- log(`[Economy] BLOCKED: Paid model ${actualModel} not allowed in Economy mode`);
397
- emitStatusToast('error', `💎 Modèle payant interdit en mode Economy: ${actualModel}`, 'Mode Economy');
398
- res.writeHead(403, { 'Content-Type': 'application/json' });
399
- res.end(JSON.stringify({
400
- error: {
401
- message: `💎 Paid model "${actualModel}" is not allowed in Economy mode. Switch to Pro mode or use a free model.`,
402
- code: 'ECONOMY_PAID_BLOCKED'
403
- }
404
- }));
366
+ log(`[AlwaysFree] BLOCKED: Paid Only Model (${actualModel}).`);
367
+ emitStatusToast('warning', t('proxy.warnings.paid_blocked_alwaysfree_title', { model: 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: t('proxy.warnings.paid_blocked_alwaysfree_msg', { model: actualModel })
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));
405
385
  return;
406
386
  }
407
387
  }
408
388
  }
409
389
  catch (e) {
410
- log(`[Economy] Error checking paid models: ${e}`);
390
+ log(`[Proxy AlwaysFree] Error checking paid models: ${e}`);
411
391
  }
412
- // 2. Check Tier
413
- if (quota.tier === 'error') {
414
- log(`[Economy] Quota unreachable, switching to fallback`);
415
- actualModel = config.fallbacks.economy || 'nova-fast';
392
+ if (!isFallbackActive && quota.tier === 'error') {
393
+ // Network error or unknown error (but NOT auth_limited, handled above)
394
+ log(`[SafetyNet] AlwaysFree Mode: Quota Check Failed. Switching to Free Fallback.`);
395
+ emitStatusToast('warning', t('proxy.warnings.quota_unreachable_title'), 'AlwaysFree Mode');
396
+ actualModel = config.fallbacks.free.main.replace('free/', '');
397
+ isEnterprise = false;
416
398
  isFallbackActive = true;
417
- fallbackReason = "Quota Unreachable (Safety)";
399
+ fallbackReason = t('proxy.warnings.quota_unreachable_msg');
418
400
  }
419
401
  else {
420
402
  const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
421
- // 3. STOP if tier = 0
422
- if (quota.tierRemaining <= 0.01) {
423
- log(`[Economy] STOP: Tier exhausted, no wallet access in Economy mode`);
424
- emitStatusToast('error', `🛑 Quota épuisé! Attendez le reset ou passez en mode Pro.`, 'Mode Economy');
425
- res.writeHead(429, { 'Content-Type': 'application/json' });
426
- res.end(JSON.stringify({
427
- error: {
428
- message: `🛑 Daily quota exhausted. Wait for reset or switch to Pro mode to use wallet credits.`,
429
- code: 'ECONOMY_TIER_EXHAUSTED'
430
- }
431
- }));
432
- return;
433
- }
434
- // 4. Fallback if tier < threshold
435
403
  if (tierRatio <= (config.thresholds.tier / 100)) {
436
- log(`[Economy] Tier ${(tierRatio * 100).toFixed(1)}% <= ${config.thresholds.tier}%, switching to fallback`);
437
- actualModel = config.fallbacks.economy || 'nova-fast';
404
+ log(`[SafetyNet] AlwaysFree Mode: Tier (${(tierRatio * 100).toFixed(1)}%) <= Threshold (${config.thresholds.tier}%). Switching.`);
405
+ emitStatusToast('warning', t('proxy.warnings.tier_limit_title', { threshold: config.thresholds.tier }), 'AlwaysFree Mode');
406
+ actualModel = config.fallbacks.free.main.replace('free/', '');
407
+ isEnterprise = false;
438
408
  isFallbackActive = true;
439
- fallbackReason = `Tier < ${config.thresholds.tier}% (Economie active)`;
409
+ fallbackReason = t('proxy.warnings.tier_limit_msg', { threshold: config.thresholds.tier });
440
410
  }
441
411
  }
442
412
  }
443
413
  }
444
- // === MODE PRO ===
445
- // - Paid models ALLOWED
446
- // - wallet < wallet_stop $ → STOP
447
- // - wallet < wallet_warn % → fallback pro
448
- // - tier exhausted → info toast (continue on wallet)
449
414
  else if (config.mode === 'pro') {
450
415
  if (isEnterprise) {
451
- // Init session wallet tracking (first request)
452
- if (!config.session?.wallet_initial && quota.walletBalance > 0) {
453
- const { initSessionWallet } = await import('./config.js');
454
- initSessionWallet(quota.walletBalance);
455
- }
456
416
  if (quota.tier === 'error') {
457
- log(`[Pro] Quota unreachable, switching to fallback`);
458
- actualModel = config.fallbacks.pro || 'qwen-coder';
417
+ // Network error or unknown
418
+ log(`[SafetyNet] Pro Mode: Quota Unreachable. Switching to Free Fallback.`);
419
+ emitStatusToast('warning', t('proxy.warnings.quota_unreachable_title'), 'Pro Mode');
420
+ actualModel = config.fallbacks.free.main.replace('free/', '');
421
+ isEnterprise = false;
459
422
  isFallbackActive = true;
460
- fallbackReason = "Quota Unreachable (Safety)";
423
+ fallbackReason = t('proxy.warnings.quota_unreachable_msg');
461
424
  }
462
425
  else {
463
- const walletStop = config.thresholds.wallet_stop || 0.50;
464
- const walletWarnPercent = config.thresholds.wallet_warn || 20;
465
- const walletInitial = config.session?.wallet_initial || quota.walletBalance;
466
- const walletPercent = walletInitial > 0 ? (quota.walletBalance / walletInitial) * 100 : 100;
467
- // 1. STOP if wallet < wallet_stop $
468
- if (quota.walletBalance < walletStop) {
469
- log(`[Pro] STOP: Wallet $${quota.walletBalance} < limit $${walletStop}`);
470
- emitStatusToast('error', `🛑 Wallet $${quota.walletBalance.toFixed(2)} sous limite $${walletStop}`, 'Mode Pro');
471
- res.writeHead(429, { 'Content-Type': 'application/json' });
472
- res.end(JSON.stringify({
473
- error: {
474
- message: `🛑 Wallet ($${quota.walletBalance.toFixed(2)}) below hard limit ($${walletStop}). Add credits or adjust wallet_stop threshold.`,
475
- code: 'PRO_WALLET_LIMIT'
476
- }
477
- }));
478
- return;
426
+ const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
427
+ if (quota.walletBalance < config.thresholds.wallet && tierRatio <= (config.thresholds.tier / 100)) {
428
+ log(`[SafetyNet] Pro Mode: Wallet < $${config.thresholds.wallet} AND Tier < ${config.thresholds.tier}%. Switching.`);
429
+ emitStatusToast('warning', t('proxy.warnings.wallet_tier_critical_title', { wallet: config.thresholds.wallet, tier: config.thresholds.tier }), 'Pro Mode');
430
+ actualModel = config.fallbacks.free.main.replace('free/', '');
431
+ isEnterprise = false;
432
+ isFallbackActive = true;
433
+ fallbackReason = t('proxy.warnings.wallet_tier_critical_msg', { wallet: config.thresholds.wallet, tier: config.thresholds.tier });
479
434
  }
480
- // 2. Fallback if wallet% < wallet_warn%
481
- if (walletPercent <= walletWarnPercent) {
482
- log(`[Pro] Wallet ${walletPercent.toFixed(1)}% <= ${walletWarnPercent}%, switching to fallback`);
483
- actualModel = config.fallbacks.pro || 'qwen-coder';
435
+ else if (quota.walletBalance < config.thresholds.wallet) {
436
+ log(`[SafetyNet] Pro Mode: Wallet < $${config.thresholds.wallet}. Switching.`);
437
+ emitStatusToast('warning', t('proxy.warnings.wallet_limit_title', { wallet: config.thresholds.wallet }), 'Pro Mode');
438
+ actualModel = config.fallbacks.free.main.replace('free/', '');
439
+ isEnterprise = false;
484
440
  isFallbackActive = true;
485
- fallbackReason = `Wallet < ${walletWarnPercent}% (${quota.walletBalance.toFixed(2)}$)`;
441
+ fallbackReason = t('proxy.warnings.wallet_limit_msg', { threshold: config.thresholds.wallet });
486
442
  }
487
- // 3. Info if tier exhausted (continue on wallet)
488
- if (quota.tierRemaining <= 0.01 && !isFallbackActive) {
489
- emitStatusToast('info', `ℹ️ Tier épuisé, utilisation du wallet ($${quota.walletBalance.toFixed(2)})`, 'Mode Pro');
443
+ else if (tierRatio <= (config.thresholds.tier / 100)) {
444
+ log(`[SafetyNet] Pro Mode: Tier < ${config.thresholds.tier}%. Switching.`);
445
+ emitStatusToast('warning', t('proxy.warnings.tier_limit_title', { threshold: config.thresholds.tier }), 'Pro Mode');
446
+ actualModel = config.fallbacks.free.main.replace('free/', '');
447
+ isEnterprise = false;
448
+ isFallbackActive = true;
449
+ fallbackReason = t('proxy.warnings.tier_limit_msg', { threshold: config.thresholds.tier });
490
450
  }
491
451
  }
492
452
  }
493
453
  }
494
- // C. Construct URL & Headers - v6.0: Always gen.pollinations.ai
495
- if (!config.apiKey) {
496
- emitLogToast('error', "Missing API Key - Use /pollinations connect <key>", 'Proxy Error');
497
- res.writeHead(401, { 'Content-Type': 'application/json' });
498
- res.end(JSON.stringify({ error: { message: "API Key required. Use /pollinations connect <your_key> to connect." } }));
499
- return;
454
+ // C. Construct URL & Headers
455
+ if (isEnterprise) {
456
+ if (!config.apiKey) {
457
+ emitLogToast('error', "Missing API Key for Enterprise Model", 'Proxy Error');
458
+ res.writeHead(401, { 'Content-Type': 'application/json' });
459
+ res.end(JSON.stringify({ error: { message: "API Key required for Enterprise models." } }));
460
+ return;
461
+ }
462
+ targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
463
+ authHeader = `Bearer ${config.apiKey}`;
464
+ log(`Routing to ENTERPRISE: ${actualModel}`);
465
+ }
466
+ else {
467
+ targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
468
+ authHeader = undefined;
469
+ log(`Routing to FREE: ${actualModel} ${isFallbackActive ? '(FALLBACK)' : ''}`);
470
+ // emitLogToast('info', `Routing to: FREE UNIVERSE (${actualModel})`, 'Pollinations Routing'); // Too noisy
500
471
  }
501
- targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
502
- authHeader = `Bearer ${config.apiKey}`;
503
- log(`Routing to gen.pollinations.ai: ${actualModel}`);
504
472
  // NOTIFY SWITCH
505
473
  if (isFallbackActive) {
506
474
  emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
@@ -532,17 +500,24 @@ export async function handleChatCompletion(req, res, bodyRaw) {
532
500
  // LOGIC BLOCK: MODEL SPECIFIC ADAPTATIONS
533
501
  // =========================================================
534
502
  if (proxyBody.tools && Array.isArray(proxyBody.tools) && proxyBody.tools.length > 0) {
535
- // B0. KIMI / MOONSHOT SURGICAL FIX (Restored for Debug)
536
- // Tools are ENABLED. We rely on penalties and strict stops to fight loops.
503
+ // B0. KIMI / MOONSHOT SURGICAL FIX
537
504
  if (actualModel.includes("kimi") || actualModel.includes("moonshot")) {
538
- log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops.`);
505
+ log(`[Proxy] Kimi: Tools ENABLED. Applying penalties/stops/sanitization.`);
539
506
  proxyBody.frequency_penalty = 1.1;
540
507
  proxyBody.presence_penalty = 0.4;
541
508
  proxyBody.stop = ["<|endoftext|>", "User:", "\nUser", "User :"];
509
+ // KIMI FIX: Remove 'title' from schema
510
+ proxyBody.tools = proxyBody.tools.map((t) => {
511
+ if (t.function && t.function.parameters) {
512
+ t.function.parameters = sanitizeSchemaForKimi(t.function.parameters);
513
+ }
514
+ return t;
515
+ });
542
516
  }
543
- // A. AZURE/OPENAI FIXES
544
- if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure")) {
545
- proxyBody.tools = truncateTools(proxyBody.tools, 120);
517
+ // A. AZURE/OPENAI FIXES + MIDJOURNEY + GROK
518
+ if (actualModel.includes("gpt") || actualModel.includes("openai") || actualModel.includes("azure") || actualModel.includes("midijourney") || actualModel.includes("grok")) {
519
+ const limit = (actualModel.includes("midijourney") || actualModel.includes("grok")) ? 128 : 120;
520
+ proxyBody.tools = truncateTools(proxyBody.tools, limit);
546
521
  if (proxyBody.messages) {
547
522
  proxyBody.messages.forEach((m) => {
548
523
  if (m.tool_calls) {
@@ -557,6 +532,11 @@ export async function handleChatCompletion(req, res, bodyRaw) {
557
532
  });
558
533
  }
559
534
  }
535
+ // BEDROCK FIX (Claude / Nova / ChickyTutor)
536
+ if (actualModel.includes("claude") || actualModel.includes("nova") || actualModel.includes("bedrock") || actualModel.includes("chickytutor")) {
537
+ log(`[Proxy] Bedrock: Sanitizing tools description.`);
538
+ proxyBody.tools = sanitizeToolsForBedrock(proxyBody.tools);
539
+ }
560
540
  // B1. NOMNOM SPECIAL (Disable Grounding, KEEP Search Tool)
561
541
  if (actualModel === "nomnom") {
562
542
  proxyBody.tools_config = { google_search_retrieval: { disable: true } };
@@ -564,36 +544,12 @@ export async function handleChatCompletion(req, res, bodyRaw) {
564
544
  proxyBody.tools = sanitizeToolsForVertex(proxyBody.tools || []);
565
545
  log(`[Proxy] Nomnom Fix: Grounding Disabled, Search Tool KEPT.`);
566
546
  }
567
- // B2. GEMINI FREE / FAST (CRASH FIX: STRICT SANITIZATION)
568
- // Restore Tools but REMOVE conflicting ones (Search)
569
547
  // B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
570
- // Handles: "tools" vs "grounding" conflicts, and "infinite loops" via Stop Sequences.
571
- // GLOBAL BEDROCK FIX (All Models)
572
- // Check if history has tools but current request misses tools definition.
573
- // This happens when OpenCode sends the Tool Result (optimisation),
574
- // but Bedrock requires toolConfig to validate the history.
575
- const hasToolHistory = proxyBody.messages?.some((m) => m.role === 'tool' || m.tool_calls);
576
- if (hasToolHistory && (!proxyBody.tools || proxyBody.tools.length === 0)) {
577
- // Inject Shim Tool to satisfy Bedrock
578
- proxyBody.tools = [{
579
- type: 'function',
580
- function: {
581
- name: '_bedrock_compatibility_shim',
582
- description: 'Internal system tool to satisfy Bedrock strict toolConfig requirement. Do not use.',
583
- parameters: { type: 'object', properties: {} }
584
- }
585
- }];
586
- log(`[Proxy] Bedrock Fix: Injected shim tool for ${actualModel} (History has tools, Request missing tools)`);
587
- }
588
- // B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
589
- // Fixes "Multiple tools" error (Vertex) and "JSON body validation failed" (v5.3.5 regression)
590
- // Added ChickyTutor (Claude/Gemini based) to fix "toolConfig must be defined" error.
591
- else if (actualModel.includes("gemini") || actualModel.includes("chickytutor")) {
548
+ else if (actualModel.includes("gemini")) {
592
549
  let hasFunctions = false;
593
550
  if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
594
551
  hasFunctions = proxyBody.tools.some((t) => t.type === 'function' || t.function);
595
552
  }
596
- // Old Shim logic removed (moved up)
597
553
  if (hasFunctions) {
598
554
  // 1. Strict cleanup of 'google_search' tool
599
555
  proxyBody.tools = proxyBody.tools.filter((t) => {
@@ -624,14 +580,6 @@ export async function handleChatCompletion(req, res, bodyRaw) {
624
580
  log(`[Proxy] Gemini Logic: Tools=${proxyBody.tools ? proxyBody.tools.length : 'REMOVED'}, Stops NOT Injected.`);
625
581
  }
626
582
  }
627
- // B5. BEDROCK TOKEN LIMIT FIX
628
- if (actualModel.includes("chicky") || actualModel.includes("mistral")) {
629
- // Force max_tokens if not present or too high (Bedrock outputs usually max 4k, context 8k+ but strict check)
630
- if (!proxyBody.max_tokens || proxyBody.max_tokens > 4096) {
631
- proxyBody.max_tokens = 4096;
632
- log(`[Proxy] Enforcing max_tokens=4096 for ${actualModel} (Bedrock Limit)`);
633
- }
634
- }
635
583
  // C. GEMINI ID BACKTRACKING & SIGNATURE
636
584
  if ((actualModel.includes("gemini") || actualModel === "nomnom") && proxyBody.messages) {
637
585
  const lastMsg = proxyBody.messages[proxyBody.messages.length - 1];
@@ -695,18 +643,10 @@ export async function handleChatCompletion(req, res, bodyRaw) {
695
643
  if (authHeader)
696
644
  headers['Authorization'] = authHeader;
697
645
  // 5. Forward (Global Fetch with Retry)
698
- // BEDROCK COMPATIBILITY: Apply transformation BEFORE first request
699
- const isBedrockModel = actualModel.includes('nova') ||
700
- actualModel.includes('amazon') ||
701
- actualModel.includes('chickytutor');
702
- const finalProxyBody = isBedrockModel ? sanitizeForBedrock(proxyBody) : proxyBody;
703
- if (isBedrockModel) {
704
- log(`[Proxy] Applied Bedrock shim for ${actualModel} (Initial Request)`);
705
- }
706
646
  const fetchRes = await fetchWithRetry(targetUrl, {
707
647
  method: 'POST',
708
648
  headers: headers,
709
- body: JSON.stringify(finalProxyBody)
649
+ body: JSON.stringify(proxyBody)
710
650
  });
711
651
  res.statusCode = fetchRes.status;
712
652
  fetchRes.headers.forEach((val, key) => {
@@ -725,14 +665,20 @@ export async function handleChatCompletion(req, res, bodyRaw) {
725
665
  if ((isEnterpriseFallback || isGeminiToolsFallback) && config.mode !== 'manual') {
726
666
  log(`[SafetyNet] Upstream Rejection (${fetchRes.status}). Triggering Transparent Fallback.`);
727
667
  if (isEnterpriseFallback) {
728
- // 1a. Enterprise -> Fallback based on mode
729
- actualModel = config.mode === 'pro'
730
- ? (config.fallbacks.pro || 'qwen-coder')
731
- : (config.fallbacks.economy || 'nova-fast');
668
+ // 1a. Enterprise -> Free Fallback
669
+ actualModel = config.fallbacks.free.main.replace('free/', '');
732
670
  isEnterprise = false;
733
671
  isFallbackActive = true;
734
- if (fetchRes.status === 402)
672
+ if (fetchRes.status === 402) {
735
673
  fallbackReason = "Insufficient Funds (Upstream 402)";
674
+ // Force refresh quota cache so next pre-flight check is accurate
675
+ try {
676
+ await getQuotaStatus(true);
677
+ }
678
+ catch (e) {
679
+ log(`[Proxy Quota] Silent refresh error: ${e}`);
680
+ }
681
+ }
736
682
  else if (fetchRes.status === 429)
737
683
  fallbackReason = "Rate Limit (Upstream 429)";
738
684
  else if (fetchRes.status === 401)
@@ -750,33 +696,16 @@ export async function handleChatCompletion(req, res, bodyRaw) {
750
696
  // 2. Notify
751
697
  emitStatusToast('warning', `⚠️ Safety Net: ${actualModel} (${fallbackReason})`, 'Pollinations Safety');
752
698
  emitLogToast('warning', `Recovering from ${fetchRes.status} -> Switching to ${actualModel}`, 'Safety Net');
753
- // 3. Re-Prepare Request - v6.0: Stay on gen.pollinations.ai
754
- targetUrl = 'https://gen.pollinations.ai/v1/chat/completions';
699
+ // 3. Re-Prepare Request
700
+ targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
755
701
  const retryHeaders = { ...headers };
756
- // Keep Authorization for gen.pollinations.ai
702
+ delete retryHeaders['Authorization']; // Free = No Auth
757
703
  const retryBody = { ...proxyBody, model: actualModel };
758
- // LIMIT MAX_TOKENS for lightweight fallback models (e.g. nova-fast limits)
759
- if (actualModel.includes('nova') || actualModel.includes('fast')) {
760
- // Force max_tokens down if missing or too high
761
- if (!retryBody.max_tokens || retryBody.max_tokens > 4000) {
762
- retryBody.max_tokens = 4000;
763
- log(`[SafetyNet] Adjusted max_tokens to 4000 for ${actualModel}`);
764
- }
765
- }
766
- // BEDROCK COMPATIBILITY SHIM (BUG 1 - Proper Transform)
767
- const isBedrockModel = actualModel.includes('nova') ||
768
- actualModel.includes('amazon') ||
769
- actualModel.includes('chickytutor');
770
- let finalRetryBody = retryBody;
771
- if (isBedrockModel) {
772
- finalRetryBody = sanitizeForBedrock(retryBody);
773
- log(`[SafetyNet] Applied Bedrock shim for ${actualModel}`);
774
- }
775
704
  // 4. Retry Fetch
776
705
  const retryRes = await fetchWithRetry(targetUrl, {
777
706
  method: 'POST',
778
707
  headers: retryHeaders,
779
- body: JSON.stringify(finalRetryBody)
708
+ body: JSON.stringify(retryBody)
780
709
  });
781
710
  if (retryRes.ok) {
782
711
  res.statusCode = retryRes.status;