opencode-pollinations-plugin 5.5.0-debug.3 → 5.5.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.
- package/README.md +26 -2
- package/dist/server/commands.js +154 -24
- package/dist/server/config.d.ts +8 -6
- package/dist/server/config.js +79 -55
- package/dist/server/generate-config.d.ts +1 -1
- package/dist/server/generate-config.js +33 -9
- package/dist/server/proxy.js +111 -31
- package/dist/server/quota.d.ts +1 -0
- package/dist/server/quota.js +25 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 🌸 Pollinations AI Plugin for OpenCode (v5.4.
|
|
1
|
+
# 🌸 Pollinations AI Plugin for OpenCode (v5.4.16)
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
<img src="https://avatars.githubusercontent.com/u/88394740?s=400&v=4" alt="Pollinations.ai Logo" width="200">
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
<div align="center">
|
|
12
12
|
|
|
13
|
-

|
|
14
14
|

|
|
15
15
|

|
|
16
16
|
|
|
@@ -45,6 +45,19 @@ Pollinations.ai is an open-source platform built by and for the community. We pr
|
|
|
45
45
|
<em>Wide Range of Models (Mistral, OpenAI, Gemini, Claude)</em>
|
|
46
46
|
</p>
|
|
47
47
|
|
|
48
|
+
<p align="center">
|
|
49
|
+
<img src="https://github.com/fkom13/opencode-pollinations-plugin/raw/main/docs/images/free_add.png" alt="Free Chat Example" width="800">
|
|
50
|
+
<br>
|
|
51
|
+
<em>Free Universe Chat (Supported by Pollinations Ads)</em>
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
<p align="center">
|
|
55
|
+
<img src="https://github.com/fkom13/opencode-pollinations-plugin/raw/main/docs/images/plan_1.png" alt="Plan Build Step 1" width="400">
|
|
56
|
+
<img src="https://github.com/fkom13/opencode-pollinations-plugin/raw/main/docs/images/plan_2.png" alt="Plan Build Step 2" width="400">
|
|
57
|
+
<br>
|
|
58
|
+
<em>Integrated Plan Building Workflow</em>
|
|
59
|
+
</p>
|
|
60
|
+
|
|
48
61
|
## ✨ Features
|
|
49
62
|
|
|
50
63
|
- **🌍 Free Universe**: Access generic models (`openai`, `mistral`, `gemini`) for **FREE**, unlimited time, no API key required.
|
|
@@ -133,6 +146,11 @@ Just type in the chat. You are in **Manual Mode** by default.
|
|
|
133
146
|
|
|
134
147
|
### 🤖 Models
|
|
135
148
|
|
|
149
|
+
### 🔑 Types de Clés Supportés
|
|
150
|
+
- **Clés Standard (`sk-...`)**: Accès complet (Modèles + Dashboard Usage + Quota).
|
|
151
|
+
- **Clés Limitées**: Accès Génération uniquement. Le dashboard affichera une alerte de restriction (v5.4.11).
|
|
152
|
+
- **Support Legacy**: Les anciennes clés (`sk_...`) sont aussi acceptées.
|
|
153
|
+
|
|
136
154
|
## 🔗 Links
|
|
137
155
|
|
|
138
156
|
- **Pollinations Website**: [pollinations.ai](https://pollinations.ai)
|
|
@@ -142,3 +160,9 @@ Just type in the chat. You are in **Manual Mode** by default.
|
|
|
142
160
|
## 📜 License
|
|
143
161
|
|
|
144
162
|
MIT License. Created by [fkom13](https://github.com/fkom13) & The Pollinations Community.
|
|
163
|
+
### 🛡️ Clés API Limitées
|
|
164
|
+
|
|
165
|
+
Si vous utilisez un token API restreint (ex: création d'une clé **sans** les permissions `Profile`, `Balance` ou `Usage`), le plugin passera automatiquement en mode **Manual**.
|
|
166
|
+
|
|
167
|
+
- **Mode Forcé** : `Manual` (Les modes `Pro` et `AlwaysFree` sont désactivés pour ces clés car ils nécessitent l'accès au quota pour fonctionner).
|
|
168
|
+
- **Conséquence** : Pas de Dashboard, pas de Safety Nets, mais génération fonctionnelle sur les modèles autorisés.
|
package/dist/server/commands.js
CHANGED
|
@@ -2,6 +2,40 @@ import { loadConfig, saveConfig } from './config.js';
|
|
|
2
2
|
import { getQuotaStatus } from './quota.js';
|
|
3
3
|
import { emitStatusToast } from './toast.js';
|
|
4
4
|
import { getDetailedUsage } from './pollinations-api.js';
|
|
5
|
+
import { generatePollinationsConfig } from './generate-config.js';
|
|
6
|
+
import * as https from 'https';
|
|
7
|
+
// --- HELPER: STRICT PERMISSION CHECK ---
|
|
8
|
+
function checkKeyPermissions(key) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
// We need Usage, Profile AND Balance for "Managed Modes"
|
|
11
|
+
// If any of these fail (403), the key is Limited.
|
|
12
|
+
const endpoints = ['/account/profile', '/account/balance', '/account/usage'];
|
|
13
|
+
let successCount = 0;
|
|
14
|
+
let completed = 0;
|
|
15
|
+
endpoints.forEach(ep => {
|
|
16
|
+
const req = https.request({
|
|
17
|
+
hostname: 'gen.pollinations.ai',
|
|
18
|
+
path: ep,
|
|
19
|
+
method: 'GET',
|
|
20
|
+
headers: { 'Authorization': `Bearer ${key}` }
|
|
21
|
+
}, (res) => {
|
|
22
|
+
completed++;
|
|
23
|
+
if (res.statusCode === 200)
|
|
24
|
+
successCount++;
|
|
25
|
+
if (completed === endpoints.length) {
|
|
26
|
+
resolve(successCount === endpoints.length);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
req.on('error', () => {
|
|
30
|
+
completed++;
|
|
31
|
+
if (completed === endpoints.length)
|
|
32
|
+
resolve(successCount === endpoints.length);
|
|
33
|
+
});
|
|
34
|
+
req.setTimeout(5000, () => req.destroy());
|
|
35
|
+
req.end();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
5
39
|
// === CONSTANTS & PRICING ===
|
|
6
40
|
const TIER_LIMITS = {
|
|
7
41
|
spore: { pollen: 1, emoji: '🦠' },
|
|
@@ -89,6 +123,8 @@ export async function handleCommand(command) {
|
|
|
89
123
|
return handleModeCommand(args);
|
|
90
124
|
case 'usage':
|
|
91
125
|
return await handleUsageCommand(args);
|
|
126
|
+
case 'connect':
|
|
127
|
+
return handleConnectCommand(args);
|
|
92
128
|
case 'fallback':
|
|
93
129
|
return handleFallbackCommand(args);
|
|
94
130
|
case 'config':
|
|
@@ -118,9 +154,17 @@ function handleModeCommand(args) {
|
|
|
118
154
|
error: `Mode invalide: ${mode}. Valeurs: manual, alwaysfree, pro`
|
|
119
155
|
};
|
|
120
156
|
}
|
|
157
|
+
const currentConfig = loadConfig();
|
|
158
|
+
// RESTRICTED KEY LOGIC: Block Managed Modes if key is limited
|
|
159
|
+
if (currentConfig.keyHasAccessToProfile === false && (mode === 'alwaysfree' || mode === 'pro')) {
|
|
160
|
+
return {
|
|
161
|
+
handled: true,
|
|
162
|
+
error: `❌ **Mode Refusé**: Votre clé API est "Limitée" (Pas d'accès Profile/Usage).\n\nLes modes gérés (Pro/AlwaysFree) nécessitent un accès au Quota pour fonctionner.\nRestez en mode **Manual** ou utilisez une clé avec permissions complètes.`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
121
165
|
saveConfig({ mode: mode });
|
|
122
|
-
const
|
|
123
|
-
if (
|
|
166
|
+
const newConfig = loadConfig();
|
|
167
|
+
if (newConfig.gui.status !== 'none') {
|
|
124
168
|
emitStatusToast('success', `Mode changé vers: ${mode}`, 'Pollinations Config');
|
|
125
169
|
}
|
|
126
170
|
return {
|
|
@@ -144,21 +188,26 @@ async function handleUsageCommand(args) {
|
|
|
144
188
|
response += `- **Wallet**: $${quota.walletBalance.toFixed(2)}\n`;
|
|
145
189
|
response += `- **Reset**: ${resetDate} (dans ${durationStr})\n`;
|
|
146
190
|
if (isFull && config.apiKey) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const lastReset = calculateResetDate(quota.nextResetAt);
|
|
150
|
-
const stats = calculateCurrentPeriodStats(usageData.usage, lastReset, quota.tierLimit);
|
|
151
|
-
response += `\n### 📊 Détail Période (depuis ${lastReset.toLocaleTimeString()})\n`;
|
|
152
|
-
response += `**Total Requêtes**: ${stats.totalRequests} | **Tokens**: In ${formatTokens(stats.inputTokens)} / Out ${formatTokens(stats.outputTokens)}\n\n`;
|
|
153
|
-
response += `| Modèle | Reqs | Coût | Tokens |\n`;
|
|
154
|
-
response += `| :--- | :---: | :---: | :---: |\n`;
|
|
155
|
-
const sorted = Array.from(stats.models.entries()).sort((a, b) => b[1].cost - a[1].cost);
|
|
156
|
-
for (const [model, data] of sorted) {
|
|
157
|
-
response += `| \`${model}\` | ${data.requests} | ${formatPollen(data.cost)} | ${formatTokens(data.inputTokens + data.outputTokens)} |\n`;
|
|
158
|
-
}
|
|
191
|
+
if (config.keyHasAccessToProfile === false) {
|
|
192
|
+
response += `\n> ⚠️ *Votre clé API ne permet pas l'accès aux détails d'usage (Restriction).*`;
|
|
159
193
|
}
|
|
160
194
|
else {
|
|
161
|
-
|
|
195
|
+
const usageData = await getDetailedUsage(config.apiKey);
|
|
196
|
+
if (usageData && usageData.usage) {
|
|
197
|
+
const lastReset = calculateResetDate(quota.nextResetAt);
|
|
198
|
+
const stats = calculateCurrentPeriodStats(usageData.usage, lastReset, quota.tierLimit);
|
|
199
|
+
response += `\n### 📊 Détail Période (depuis ${lastReset.toLocaleTimeString()})\n`;
|
|
200
|
+
response += `**Total Requêtes**: ${stats.totalRequests} | **Tokens**: In ${formatTokens(stats.inputTokens)} / Out ${formatTokens(stats.outputTokens)}\n\n`;
|
|
201
|
+
response += `| Modèle | Reqs | Coût | Tokens |\n`;
|
|
202
|
+
response += `| :--- | :---: | :---: | :---: |\n`;
|
|
203
|
+
const sorted = Array.from(stats.models.entries()).sort((a, b) => b[1].cost - a[1].cost);
|
|
204
|
+
for (const [model, data] of sorted) {
|
|
205
|
+
response += `| \`${model}\` | ${data.requests} | ${formatPollen(data.cost)} | ${formatTokens(data.inputTokens + data.outputTokens)} |\n`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
response += `\n> ⚠️ *Impossible de récupérer l'historique détaillé.*\n`;
|
|
210
|
+
}
|
|
162
211
|
}
|
|
163
212
|
}
|
|
164
213
|
else if (isFull) {
|
|
@@ -174,31 +223,112 @@ async function handleUsageCommand(args) {
|
|
|
174
223
|
}
|
|
175
224
|
}
|
|
176
225
|
function handleFallbackCommand(args) {
|
|
177
|
-
const [
|
|
178
|
-
if (!
|
|
226
|
+
const [main, agent] = args;
|
|
227
|
+
if (!main) {
|
|
179
228
|
const config = loadConfig();
|
|
180
|
-
const freeConfig = `Free
|
|
181
|
-
const enterConfig = `Enter
|
|
229
|
+
const freeConfig = `Free: main=${config.fallbacks.free.main}, agent=${config.fallbacks.free.agent}`;
|
|
230
|
+
const enterConfig = `Enter: agent=${config.fallbacks.enter.agent}`;
|
|
182
231
|
return {
|
|
183
232
|
handled: true,
|
|
184
|
-
response: `
|
|
233
|
+
response: `Fallbacks actuels:\n${freeConfig}\n${enterConfig}`
|
|
185
234
|
};
|
|
186
235
|
}
|
|
236
|
+
// Default behavior for "/poll fallback <model> <agent>" is setting FREE fallbacks
|
|
237
|
+
// User needs to use commands (maybe add /poll fallback enter ...) later
|
|
238
|
+
// For now, map to Free Fallback as it's the primary Safety Net
|
|
187
239
|
const config = loadConfig();
|
|
188
240
|
saveConfig({
|
|
189
241
|
fallbacks: {
|
|
190
242
|
...config.fallbacks,
|
|
191
243
|
free: {
|
|
192
|
-
|
|
193
|
-
|
|
244
|
+
main: main,
|
|
245
|
+
agent: agent || config.fallbacks.free.agent
|
|
194
246
|
}
|
|
195
247
|
}
|
|
196
248
|
});
|
|
197
249
|
return {
|
|
198
250
|
handled: true,
|
|
199
|
-
response: `✅
|
|
251
|
+
response: `✅ Fallback (Free) configuré: main=${main}, agent=${agent || config.fallbacks.free.agent}`
|
|
200
252
|
};
|
|
201
253
|
}
|
|
254
|
+
async function handleConnectCommand(args) {
|
|
255
|
+
const key = args[0];
|
|
256
|
+
if (!key) {
|
|
257
|
+
return {
|
|
258
|
+
handled: true,
|
|
259
|
+
error: `Utilisation: /pollinations connect <votre_clé_api>`
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
// 1. Universal Validation (No Syntax Check) - Functional Check
|
|
263
|
+
emitStatusToast('info', 'Vérification de la clé...', 'Pollinations Config');
|
|
264
|
+
try {
|
|
265
|
+
const models = await generatePollinationsConfig(key, true);
|
|
266
|
+
// 2. Check if we got Enterprise models
|
|
267
|
+
const enterpriseModels = models.filter(m => m.id.startsWith('enter/'));
|
|
268
|
+
if (enterpriseModels.length > 0) {
|
|
269
|
+
// SUCCESS
|
|
270
|
+
saveConfig({ apiKey: key }); // Don't force mode 'pro'. Let user decide.
|
|
271
|
+
const masked = key.substring(0, 6) + '...';
|
|
272
|
+
// count Paid Only models found
|
|
273
|
+
const diamondCount = enterpriseModels.filter(m => m.name.includes('💎')).length;
|
|
274
|
+
// CHECK RESTRICTIONS: Strict Check (Usage + Profile + Balance)
|
|
275
|
+
let forcedModeMsg = "";
|
|
276
|
+
let isLimited = false;
|
|
277
|
+
try {
|
|
278
|
+
// Strict Probe: Must be able to read ALL accounting data
|
|
279
|
+
const hasFullAccess = await checkKeyPermissions(key);
|
|
280
|
+
isLimited = !hasFullAccess;
|
|
281
|
+
}
|
|
282
|
+
catch (e) {
|
|
283
|
+
isLimited = true;
|
|
284
|
+
}
|
|
285
|
+
// If Limited -> FORCE MANUAL
|
|
286
|
+
if (isLimited) {
|
|
287
|
+
saveConfig({ apiKey: key, mode: 'manual', keyHasAccessToProfile: false });
|
|
288
|
+
forcedModeMsg = "\n⚠️ **Clé Limitée** (Permissions insuffisantes) -> Mode **MANUAL** forcé.\n*Requis pour mode Auto: Profile, Balance & Usage.*";
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
saveConfig({ apiKey: key, keyHasAccessToProfile: true }); // Let user keep current mode or default
|
|
292
|
+
}
|
|
293
|
+
emitStatusToast('success', `Clé Valide! (${enterpriseModels.length} modèles Pro débloqués)`, 'Pollinations Config');
|
|
294
|
+
return {
|
|
295
|
+
handled: true,
|
|
296
|
+
response: `✅ **Connexion Réussie!**\n- Clé: \`${masked}\`\n- Modèles Débloqués: ${enterpriseModels.length} (dont ${diamondCount} 💎 Paid)${forcedModeMsg}`
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
// FAILURE (Valid JSON but no Enterprise models - likely Invalid Key or Free plan only?)
|
|
301
|
+
// If key is invalid, generatePollinationsConfig usually returns fallback free models BUT
|
|
302
|
+
// we specifically checked 'enter/'. If 0 enterprise models found for a *provided* key, it's suspicious.
|
|
303
|
+
// Actually config generator returns Free models + Enter models if key works.
|
|
304
|
+
// If key is BAD, fetchJson throws/logs error, and returns fallbacks (Enter GPT-4o Fallback).
|
|
305
|
+
// Wait, generate-config falls back to providing a list containing "[Enter] GPT-4o (Fallback)" if fetch failed.
|
|
306
|
+
// So we need to detect if it's a "REAL" fetch or a "FALLBACK" fetch.
|
|
307
|
+
// The fallback models have `variants: {}` usually, but real ones might too.
|
|
308
|
+
// A better check: The fallback list is hardcoded in generate-config.ts catch block.
|
|
309
|
+
// Let's modify generate-config to return EMPTY list on error?
|
|
310
|
+
// Or just check if the returned models work?
|
|
311
|
+
// Simplest: If `generatePollinationsConfig` returns any model starting with `enter/` that includes "(Fallback)" in name, we assume failure?
|
|
312
|
+
// "GPT-4o (Fallback)" is the name.
|
|
313
|
+
const isFallback = models.some(m => m.name.includes('(Fallback)') && m.id.startsWith('enter/'));
|
|
314
|
+
if (isFallback) {
|
|
315
|
+
throw new Error("Clé rejetée par l'API (Accès refusé ou invalide).");
|
|
316
|
+
}
|
|
317
|
+
// If we are here, we got no enter models, or empty list?
|
|
318
|
+
// If key is valid but has no access?
|
|
319
|
+
throw new Error("Aucun modèle Enterprise détecté pour cette clé.");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch (e) {
|
|
323
|
+
// 3. FAILURE HANDLING - Revert to FREE
|
|
324
|
+
saveConfig({ apiKey: undefined, mode: 'manual' }); // Clear Key, Set Manual
|
|
325
|
+
emitStatusToast('error', `Clé Invalide. Retour au mode Gratuit.`, 'Pollinations Config');
|
|
326
|
+
return {
|
|
327
|
+
handled: true,
|
|
328
|
+
error: `❌ **Échec Connexion**: ${e.message || e}\n\nLa configuration a été réinitialisée (Mode Gratuit/Manuel).`
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
202
332
|
function handleConfigCommand(args) {
|
|
203
333
|
const [key, value] = args;
|
|
204
334
|
if (!key) {
|
|
@@ -270,7 +400,7 @@ function handleHelpCommand() {
|
|
|
270
400
|
|
|
271
401
|
- **\`/pollinations mode [mode]\`**: Change le mode (manual, alwaysfree, pro).
|
|
272
402
|
- **\`/pollinations usage [full]\`**: Affiche le dashboard (full = détail).
|
|
273
|
-
- **\`/pollinations fallback <
|
|
403
|
+
- **\`/pollinations fallback <main> [agent]\`**: Configure le Safety Net (Free).
|
|
274
404
|
- **\`/pollinations config [key] [value]\`**:
|
|
275
405
|
- \`status_gui\`: none, alert, all (Status Dashboard).
|
|
276
406
|
- \`logs_gui\`: none, error, verbose (Logs Techniques).
|
package/dist/server/config.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export interface PollinationsConfigV5 {
|
|
|
2
2
|
version: string | number;
|
|
3
3
|
mode: 'manual' | 'alwaysfree' | 'pro';
|
|
4
4
|
apiKey?: string;
|
|
5
|
+
keyHasAccessToProfile?: boolean;
|
|
5
6
|
gui: {
|
|
6
7
|
status: 'none' | 'alert' | 'all';
|
|
7
8
|
logs: 'none' | 'error' | 'verbose';
|
|
@@ -12,11 +13,11 @@ export interface PollinationsConfigV5 {
|
|
|
12
13
|
};
|
|
13
14
|
fallbacks: {
|
|
14
15
|
free: {
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
main: string;
|
|
17
|
+
agent: string;
|
|
17
18
|
};
|
|
18
19
|
enter: {
|
|
19
|
-
|
|
20
|
+
agent: string;
|
|
20
21
|
};
|
|
21
22
|
};
|
|
22
23
|
enablePaidTools: boolean;
|
|
@@ -27,6 +28,7 @@ export declare function saveConfig(updates: Partial<PollinationsConfigV5>): {
|
|
|
27
28
|
version: string;
|
|
28
29
|
mode: "manual" | "alwaysfree" | "pro";
|
|
29
30
|
apiKey?: string;
|
|
31
|
+
keyHasAccessToProfile?: boolean;
|
|
30
32
|
gui: {
|
|
31
33
|
status: "none" | "alert" | "all";
|
|
32
34
|
logs: "none" | "error" | "verbose";
|
|
@@ -37,11 +39,11 @@ export declare function saveConfig(updates: Partial<PollinationsConfigV5>): {
|
|
|
37
39
|
};
|
|
38
40
|
fallbacks: {
|
|
39
41
|
free: {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
main: string;
|
|
43
|
+
agent: string;
|
|
42
44
|
};
|
|
43
45
|
enter: {
|
|
44
|
-
|
|
46
|
+
agent: string;
|
|
45
47
|
};
|
|
46
48
|
};
|
|
47
49
|
enablePaidTools: boolean;
|
package/dist/server/config.js
CHANGED
|
@@ -9,11 +9,13 @@ const CONFIG_DIR_OPENCODE = path.join(HOMEDIR, '.config', 'opencode');
|
|
|
9
9
|
const OPENCODE_CONFIG_FILE = path.join(CONFIG_DIR_OPENCODE, 'opencode.json');
|
|
10
10
|
const AUTH_FILE = path.join(HOMEDIR, '.local', 'share', 'opencode', 'auth.json');
|
|
11
11
|
// LOAD PACKAGE VERSION
|
|
12
|
-
let PKG_VERSION = '5.
|
|
12
|
+
let PKG_VERSION = '5.2.0';
|
|
13
13
|
try {
|
|
14
|
-
const pkgPath =
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const pkgPath = path.join(__dirname, '../../package.json');
|
|
15
|
+
if (fs.existsSync(pkgPath)) {
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
17
|
+
PKG_VERSION = pkg.version;
|
|
18
|
+
}
|
|
17
19
|
}
|
|
18
20
|
catch (e) { }
|
|
19
21
|
const DEFAULT_CONFIG_V5 = {
|
|
@@ -22,10 +24,11 @@ const DEFAULT_CONFIG_V5 = {
|
|
|
22
24
|
gui: { status: 'alert', logs: 'none' },
|
|
23
25
|
thresholds: { tier: 10, wallet: 5 },
|
|
24
26
|
fallbacks: {
|
|
25
|
-
free: {
|
|
26
|
-
enter: {
|
|
27
|
+
free: { main: 'free/mistral', agent: 'free/openai-fast' },
|
|
28
|
+
enter: { agent: 'free/openai-fast' }
|
|
27
29
|
},
|
|
28
30
|
enablePaidTools: false,
|
|
31
|
+
keyHasAccessToProfile: true, // Default true for legacy keys
|
|
29
32
|
statusBar: true
|
|
30
33
|
};
|
|
31
34
|
function logConfig(msg) {
|
|
@@ -38,65 +41,76 @@ function logConfig(msg) {
|
|
|
38
41
|
catch (e) { }
|
|
39
42
|
}
|
|
40
43
|
// SIMPLE LOAD (Direct Disk Read - No Caching, No Watchers)
|
|
44
|
+
// This ensures the Proxy ALWAYS sees the latest state from auth.json
|
|
41
45
|
export function loadConfig() {
|
|
42
46
|
return readConfigFromDisk();
|
|
43
47
|
}
|
|
44
48
|
function readConfigFromDisk() {
|
|
45
49
|
let config = { ...DEFAULT_CONFIG_V5 };
|
|
46
|
-
let
|
|
47
|
-
|
|
50
|
+
let finalKey = undefined;
|
|
51
|
+
let source = 'none';
|
|
52
|
+
// TIMESTAMP BASED PRIORITY LOGIC
|
|
53
|
+
// We want the most recently updated Valid Key to win.
|
|
54
|
+
let configTime = 0;
|
|
55
|
+
let authTime = 0;
|
|
48
56
|
try {
|
|
49
|
-
if (fs.existsSync(CONFIG_FILE))
|
|
57
|
+
if (fs.existsSync(CONFIG_FILE))
|
|
58
|
+
configTime = fs.statSync(CONFIG_FILE).mtime.getTime();
|
|
59
|
+
}
|
|
60
|
+
catch (e) { }
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(AUTH_FILE))
|
|
63
|
+
authTime = fs.statSync(AUTH_FILE).mtime.getTime();
|
|
64
|
+
}
|
|
65
|
+
catch (e) { }
|
|
66
|
+
// 1. EXTRACT KEYS
|
|
67
|
+
let configKey = undefined;
|
|
68
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
69
|
+
try {
|
|
50
70
|
const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
51
71
|
const custom = JSON.parse(raw);
|
|
52
|
-
//
|
|
53
|
-
if (custom.
|
|
54
|
-
|
|
55
|
-
if (custom.fallbacks.free.main) {
|
|
56
|
-
custom.fallbacks.free.primary = custom.fallbacks.free.main;
|
|
57
|
-
delete custom.fallbacks.free.main;
|
|
58
|
-
}
|
|
59
|
-
if (custom.fallbacks.free.agent) {
|
|
60
|
-
custom.fallbacks.free.subagent = custom.fallbacks.free.agent;
|
|
61
|
-
delete custom.fallbacks.free.agent;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (custom.fallbacks.enter && custom.fallbacks.enter.agent) {
|
|
65
|
-
custom.fallbacks.enter.subagent = custom.fallbacks.enter.agent;
|
|
66
|
-
delete custom.fallbacks.enter.agent;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
config = { ...config, ...custom };
|
|
70
|
-
if (config.apiKey)
|
|
71
|
-
keyFound = true;
|
|
72
|
+
config = { ...config, ...custom }; // Helper: We load the rest of config anyway
|
|
73
|
+
if (custom.apiKey && custom.apiKey.length > 5)
|
|
74
|
+
configKey = custom.apiKey;
|
|
72
75
|
}
|
|
76
|
+
catch (e) { }
|
|
73
77
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
// 2. Auth Store (Priority)
|
|
78
|
-
if (!keyFound) {
|
|
78
|
+
let authKey = undefined;
|
|
79
|
+
if (fs.existsSync(AUTH_FILE)) {
|
|
79
80
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
config.apiKey = key;
|
|
88
|
-
config.mode = 'pro';
|
|
89
|
-
keyFound = true;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
81
|
+
const raw = fs.readFileSync(AUTH_FILE, 'utf-8');
|
|
82
|
+
const authData = JSON.parse(raw);
|
|
83
|
+
const entry = authData['pollinations'] || authData['pollinations_enter'] || authData['pollinations_api_key'];
|
|
84
|
+
if (entry) {
|
|
85
|
+
const k = (typeof entry === 'object' && entry.key) ? entry.key : entry;
|
|
86
|
+
if (k && typeof k === 'string' && k.length > 10)
|
|
87
|
+
authKey = k;
|
|
92
88
|
}
|
|
93
89
|
}
|
|
94
|
-
catch (e) {
|
|
95
|
-
|
|
90
|
+
catch (e) { }
|
|
91
|
+
}
|
|
92
|
+
// 2. DETERMINE WINNER
|
|
93
|
+
// If both exist, newest wins. If one exists, it wins.
|
|
94
|
+
if (configKey && authKey) {
|
|
95
|
+
if (configTime >= authTime) {
|
|
96
|
+
finalKey = configKey;
|
|
97
|
+
source = 'config.json';
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
finalKey = authKey;
|
|
101
|
+
source = 'auth.json';
|
|
96
102
|
}
|
|
97
103
|
}
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
else if (configKey) {
|
|
105
|
+
finalKey = configKey;
|
|
106
|
+
source = 'config.json';
|
|
107
|
+
}
|
|
108
|
+
else if (authKey) {
|
|
109
|
+
finalKey = authKey;
|
|
110
|
+
source = 'auth.json';
|
|
111
|
+
}
|
|
112
|
+
// 3. Fallback to OpenCode Global Config (Lowest Priority)
|
|
113
|
+
if (!finalKey) {
|
|
100
114
|
try {
|
|
101
115
|
if (fs.existsSync(OPENCODE_CONFIG_FILE)) {
|
|
102
116
|
const raw = fs.readFileSync(OPENCODE_CONFIG_FILE, 'utf-8');
|
|
@@ -104,16 +118,26 @@ function readConfigFromDisk() {
|
|
|
104
118
|
const nativeKey = data?.provider?.pollinations?.options?.apiKey ||
|
|
105
119
|
data?.provider?.pollinations_enter?.options?.apiKey;
|
|
106
120
|
if (nativeKey && nativeKey.length > 5 && nativeKey !== 'dummy') {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
keyFound = true;
|
|
121
|
+
finalKey = nativeKey;
|
|
122
|
+
source = 'opencode.json';
|
|
110
123
|
}
|
|
111
124
|
}
|
|
112
125
|
}
|
|
113
126
|
catch (e) { }
|
|
114
127
|
}
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
// 4. APPLY
|
|
129
|
+
if (finalKey) {
|
|
130
|
+
config.apiKey = finalKey;
|
|
131
|
+
// config.mode = 'pro'; // REMOVED: Mode is decoupled from Key presence.
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// Ensure no phantom key remains
|
|
135
|
+
delete config.apiKey;
|
|
136
|
+
// if (config.mode === 'pro') config.mode = 'manual'; // OPTIONAL: Downgrade if no key? User says "No link".
|
|
137
|
+
// Actually, if I am in PRO mode and lose my key, I am broken. Falling back to manual is safer?
|
|
138
|
+
// User said "Manual mode is like standard API".
|
|
139
|
+
// Let's REMOVE this auto-downgrade too to be strictly "Decoupled".
|
|
140
|
+
// If user is in PRO without key, they get "Missing Key" error, which is correct.
|
|
117
141
|
}
|
|
118
142
|
return { ...config, version: PKG_VERSION };
|
|
119
143
|
}
|
|
@@ -9,5 +9,5 @@ interface OpenCodeModel {
|
|
|
9
9
|
output?: number;
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
-
export declare function generatePollinationsConfig(forceApiKey?: string): Promise<OpenCodeModel[]>;
|
|
12
|
+
export declare function generatePollinationsConfig(forceApiKey?: string, forceStrict?: boolean): Promise<OpenCodeModel[]>;
|
|
13
13
|
export {};
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as https from 'https';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
3
5
|
import { loadConfig } from './config.js';
|
|
6
|
+
const HOMEDIR = os.homedir();
|
|
7
|
+
const CONFIG_DIR_POLLI = path.join(HOMEDIR, '.pollinations');
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR_POLLI, 'config.json');
|
|
4
9
|
// --- LOGGING ---
|
|
5
10
|
const LOG_FILE = '/tmp/opencode_pollinations_config.log';
|
|
6
11
|
function log(msg) {
|
|
@@ -49,7 +54,7 @@ function formatName(id, censored = true) {
|
|
|
49
54
|
}
|
|
50
55
|
// --- MAIN GENERATOR logic ---
|
|
51
56
|
// --- MAIN GENERATOR logic ---
|
|
52
|
-
export async function generatePollinationsConfig(forceApiKey) {
|
|
57
|
+
export async function generatePollinationsConfig(forceApiKey, forceStrict = false) {
|
|
53
58
|
const config = loadConfig();
|
|
54
59
|
const modelsOutput = [];
|
|
55
60
|
log(`Starting Configuration (V5.1.22 Hot-Reload)...`);
|
|
@@ -74,12 +79,8 @@ export async function generatePollinationsConfig(forceApiKey) {
|
|
|
74
79
|
modelsOutput.push({ id: "free/gemini", name: "[Free] Gemini Flash (Fallback)", object: "model", variants: {} });
|
|
75
80
|
}
|
|
76
81
|
// 1.5 FORCE ENSURE CRITICAL MODELS
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
if (!hasGemini) {
|
|
80
|
-
log(`[ConfigGen] Force-injecting free/gemini.`);
|
|
81
|
-
modelsOutput.push({ id: "free/gemini", name: "[Free] Gemini Flash (Force)", object: "model", variants: {} });
|
|
82
|
-
}
|
|
82
|
+
// REMOVED: Duplicate Gemini/Flash injection.
|
|
83
|
+
// The alias below handles standardizing the ID.
|
|
83
84
|
// ALIAS for Full ID matching (Fix ProviderModelNotFoundError) - ALWAYS CHECK SEPARATELY
|
|
84
85
|
const hasGeminiAlias = modelsOutput.find(m => m.id === 'pollinations/free/gemini');
|
|
85
86
|
if (!hasGeminiAlias) {
|
|
@@ -88,22 +89,41 @@ export async function generatePollinationsConfig(forceApiKey) {
|
|
|
88
89
|
// 2. ENTERPRISE UNIVERSE
|
|
89
90
|
if (effectiveKey && effectiveKey.length > 5 && effectiveKey !== 'dummy') {
|
|
90
91
|
try {
|
|
91
|
-
const enterListRaw = await fetchJson('https://gen.pollinations.ai/
|
|
92
|
+
const enterListRaw = await fetchJson('https://gen.pollinations.ai/models', {
|
|
92
93
|
'Authorization': `Bearer ${effectiveKey}`
|
|
93
94
|
});
|
|
94
95
|
const enterList = Array.isArray(enterListRaw) ? enterListRaw : (enterListRaw.data || []);
|
|
96
|
+
const paidModels = [];
|
|
95
97
|
enterList.forEach((m) => {
|
|
96
98
|
if (m.tools === false)
|
|
97
99
|
return;
|
|
98
100
|
const mapped = mapModel(m, 'enter/', '[Enter] ');
|
|
99
101
|
modelsOutput.push(mapped);
|
|
102
|
+
if (m.paid_only) {
|
|
103
|
+
paidModels.push(mapped.id.replace('enter/', '')); // Store bare ID "gemini-large"
|
|
104
|
+
}
|
|
100
105
|
});
|
|
101
106
|
log(`Total models (Free+Pro): ${modelsOutput.length}`);
|
|
107
|
+
// Save Paid Models List for Proxy
|
|
108
|
+
try {
|
|
109
|
+
const paidListPath = path.join(config.gui ? path.dirname(CONFIG_FILE) : '/tmp', 'pollinations-paid-models.json');
|
|
110
|
+
// Ensure dir exists (re-use config dir logic from config.ts if possible, or just assume it exists since config loaded)
|
|
111
|
+
if (fs.existsSync(path.dirname(paidListPath))) {
|
|
112
|
+
fs.writeFileSync(paidListPath, JSON.stringify(paidModels));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
log(`Error saving paid models list: ${e}`);
|
|
117
|
+
}
|
|
102
118
|
}
|
|
103
119
|
catch (e) {
|
|
104
120
|
log(`Error fetching Enterprise models: ${e}`);
|
|
121
|
+
// STRICT MODE (Validation): Do not return fake fallback models.
|
|
122
|
+
if (forceStrict)
|
|
123
|
+
throw e;
|
|
105
124
|
// Fallback Robust for Enterprise (User has Key but discovery failed)
|
|
106
125
|
modelsOutput.push({ id: "enter/gpt-4o", name: "[Enter] GPT-4o (Fallback)", object: "model", variants: {} });
|
|
126
|
+
// ...
|
|
107
127
|
modelsOutput.push({ id: "enter/claude-3-5-sonnet", name: "[Enter] Claude 3.5 Sonnet (Fallback)", object: "model", variants: {} });
|
|
108
128
|
modelsOutput.push({ id: "enter/deepseek-reasoner", name: "[Enter] DeepSeek R1 (Fallback)", object: "model", variants: {} });
|
|
109
129
|
}
|
|
@@ -123,7 +143,11 @@ function mapModel(raw, prefix, namePrefix) {
|
|
|
123
143
|
if (baseName && baseName.includes(' - ')) {
|
|
124
144
|
baseName = baseName.split(' - ')[0].trim();
|
|
125
145
|
}
|
|
126
|
-
|
|
146
|
+
let namePrefixFinal = namePrefix;
|
|
147
|
+
if (raw.paid_only) {
|
|
148
|
+
namePrefixFinal = namePrefix.replace('[Enter]', '[💎 Paid]');
|
|
149
|
+
}
|
|
150
|
+
const finalName = `${namePrefixFinal}${baseName}`;
|
|
127
151
|
const modelObj = {
|
|
128
152
|
id: fullId,
|
|
129
153
|
name: finalName,
|
package/dist/server/proxy.js
CHANGED
|
@@ -151,14 +151,8 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
151
151
|
try {
|
|
152
152
|
const body = JSON.parse(bodyRaw);
|
|
153
153
|
const config = loadConfig();
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
const isSubagent = systemMsg.includes("exploring codebases") ||
|
|
157
|
-
systemMsg.includes("title generator") ||
|
|
158
|
-
systemMsg.includes("researching complex questions") ||
|
|
159
|
-
systemMsg.includes("session summary");
|
|
160
|
-
const agentLabel = isSubagent ? "Subagent" : "Primary";
|
|
161
|
-
log(`[Proxy] Agent Detected: ${agentLabel} (Tools: ${body.tools?.length || 0})`);
|
|
154
|
+
// DEBUG: Trace Config State for Hot Reload verification
|
|
155
|
+
log(`[Proxy Request] Config Loaded. Mode: ${config.mode}, HasKey: ${!!config.apiKey}, KeyLength: ${config.apiKey ? config.apiKey.length : 0}`);
|
|
162
156
|
// 0. COMMAND HANDLING
|
|
163
157
|
if (body.messages && body.messages.length > 0) {
|
|
164
158
|
const lastMsg = body.messages[body.messages.length - 1];
|
|
@@ -226,27 +220,106 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
226
220
|
isEnterprise = false;
|
|
227
221
|
actualModel = actualModel.replace('free/', '');
|
|
228
222
|
}
|
|
223
|
+
// A.1 PAID MODEL ENFORCEMENT (V5.5 Strategy)
|
|
224
|
+
// Check dynamic list saved by generate-config.ts
|
|
225
|
+
if (isEnterprise) {
|
|
226
|
+
try {
|
|
227
|
+
const paidListPath = path.join(config.gui ? path.dirname(path.join(process.env.HOME || '/tmp', '.config/opencode/pollinations-signature.json')) : '/tmp', 'pollinations-paid-models.json');
|
|
228
|
+
// Wait, logic above for config path is messy. Let's use standard path logic:
|
|
229
|
+
// config.ts uses ~/.pollinations/config.json usually.
|
|
230
|
+
// generate-config uses path.join(config.gui ? path.dirname(CONFIG_FILE) : '/tmp')
|
|
231
|
+
// Let's rely on standard ~/.pollinations location if possible, or try both.
|
|
232
|
+
const homedir = process.env.HOME || '/tmp';
|
|
233
|
+
const standardPaidPath = path.join(homedir, '.pollinations', 'pollinations-paid-models.json');
|
|
234
|
+
if (fs.existsSync(standardPaidPath)) {
|
|
235
|
+
const paidModels = JSON.parse(fs.readFileSync(standardPaidPath, 'utf-8'));
|
|
236
|
+
if (paidModels.includes(actualModel)) {
|
|
237
|
+
// IT IS A PAID ONLY MODEL.
|
|
238
|
+
// STRICT CHECK: Wallet > 0 required. (Not just Tier)
|
|
239
|
+
if (quota.walletBalance <= 0.001) { // Floating point safety
|
|
240
|
+
log(`[SafetyNet] Paid Only Model (${actualModel}) requested but Wallet is Empty ($${quota.walletBalance}). BLOCKING.`);
|
|
241
|
+
// Immediate Block or Fallback?
|
|
242
|
+
// Text says: "💎 Paid Only models require purchased pollen only"
|
|
243
|
+
// Blocking is safer/clearer than falling back to a free model which might not be what the user expects for a "Pro" feature?
|
|
244
|
+
// Actually, Fallback to Free is usually better for UX if configured, BUT for specific "Paid Only" requests, the user explicitly chose a powerful model.
|
|
245
|
+
// Falling back to Mistral might be confusing if they asked for Gemini-Large.
|
|
246
|
+
// BUT we are failing gracefully.
|
|
247
|
+
// Let's Fallback to Free Default and Warn.
|
|
248
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
249
|
+
isEnterprise = false;
|
|
250
|
+
isFallbackActive = true;
|
|
251
|
+
fallbackReason = "Paid Only Model requires purchased credits";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
log(`[Proxy] Error checking paid models: ${e}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
229
260
|
// B. SAFETY NETS (The Core V5 Logic)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
261
|
+
if (config.mode === 'alwaysfree') {
|
|
262
|
+
if (isEnterprise) {
|
|
263
|
+
// NEW: Paid Only Check for Always Free
|
|
264
|
+
// If the user asks for a 💎 Paid Only model while in Always Free, we BLOCK it to save wallet
|
|
265
|
+
// and fallback to free specific message.
|
|
266
|
+
try {
|
|
267
|
+
const homedir = process.env.HOME || '/tmp';
|
|
268
|
+
const standardPaidPath = path.join(homedir, '.pollinations', 'pollinations-paid-models.json');
|
|
269
|
+
if (fs.existsSync(standardPaidPath)) {
|
|
270
|
+
const paidModels = JSON.parse(fs.readFileSync(standardPaidPath, 'utf-8'));
|
|
271
|
+
if (paidModels.includes(actualModel)) {
|
|
272
|
+
log(`[SafetyNet] alwaysfree Mode: Request for Paid Only Model (${actualModel}). FALLBACK.`);
|
|
273
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
274
|
+
isEnterprise = false;
|
|
275
|
+
isFallbackActive = true;
|
|
276
|
+
fallbackReason = "Mode AlwaysFree actif: Ce modèle payant consomme du wallet. Passez en mode PRO.";
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (e) { }
|
|
281
|
+
if (!isFallbackActive && quota.tier === 'error') {
|
|
282
|
+
log(`[SafetyNet] AlwaysFree Mode: Quota Check Failed. Switching to Free Fallback.`);
|
|
283
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
236
284
|
isEnterprise = false;
|
|
237
|
-
|
|
285
|
+
isFallbackActive = true;
|
|
286
|
+
fallbackReason = "Quota Unreachable (Safety)";
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
|
|
290
|
+
if (tierRatio <= (config.thresholds.tier / 100)) {
|
|
291
|
+
log(`[SafetyNet] AlwaysFree Mode: Tier (${(tierRatio * 100).toFixed(1)}%) <= Threshold (${config.thresholds.tier}%). Switching.`);
|
|
292
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
293
|
+
isEnterprise = false;
|
|
294
|
+
isFallbackActive = true;
|
|
295
|
+
fallbackReason = `Daily Tier < ${config.thresholds.tier}% (Wallet Protected)`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
238
298
|
}
|
|
239
299
|
}
|
|
240
|
-
|
|
241
|
-
if (config.mode === 'alwaysfree' || config.mode === 'pro') {
|
|
300
|
+
else if (config.mode === 'pro') {
|
|
242
301
|
if (isEnterprise) {
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
actualModel = fallbackModel.replace('free/', '');
|
|
302
|
+
if (quota.tier === 'error') {
|
|
303
|
+
log(`[SafetyNet] Pro Mode: Quota Unreachable. Switching to Free Fallback.`);
|
|
304
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
247
305
|
isEnterprise = false;
|
|
248
306
|
isFallbackActive = true;
|
|
249
|
-
fallbackReason =
|
|
307
|
+
fallbackReason = "Quota Unreachable (Safety)";
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
|
|
311
|
+
// Logic: Fallback if Wallet is Low (< Threshold) AND Tier is Exhausted (< Threshold %)
|
|
312
|
+
// Wait, user wants priority to Free Tier.
|
|
313
|
+
// If Free Tier is available (Ratio > Threshold), we usage it (don't fallback).
|
|
314
|
+
// If Free Tier is exhausted (Ratio <= Threshold), THEN check Wallet.
|
|
315
|
+
// If Wallet also Low, THEN Fallback.
|
|
316
|
+
if (quota.walletBalance < config.thresholds.wallet && tierRatio <= (config.thresholds.tier / 100)) {
|
|
317
|
+
log(`[SafetyNet] Pro Mode: Wallet < $${config.thresholds.wallet} AND Tier < ${config.thresholds.tier}%. Switching.`);
|
|
318
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
319
|
+
isEnterprise = false;
|
|
320
|
+
isFallbackActive = true;
|
|
321
|
+
fallbackReason = `Wallet & Tier Critical`;
|
|
322
|
+
}
|
|
250
323
|
}
|
|
251
324
|
}
|
|
252
325
|
}
|
|
@@ -266,7 +339,7 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
266
339
|
targetUrl = 'https://text.pollinations.ai/openai/chat/completions';
|
|
267
340
|
authHeader = undefined;
|
|
268
341
|
log(`Routing to FREE: ${actualModel} ${isFallbackActive ? '(FALLBACK)' : ''}`);
|
|
269
|
-
emitLogToast('info', `Routing to: FREE UNIVERSE (${actualModel})`, 'Pollinations Routing');
|
|
342
|
+
// emitLogToast('info', `Routing to: FREE UNIVERSE (${actualModel})`, 'Pollinations Routing'); // Too noisy
|
|
270
343
|
}
|
|
271
344
|
// NOTIFY SWITCH
|
|
272
345
|
if (isFallbackActive) {
|
|
@@ -298,7 +371,7 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
298
371
|
// =========================================================
|
|
299
372
|
// LOGIC BLOCK: MODEL SPECIFIC ADAPTATIONS
|
|
300
373
|
// =========================================================
|
|
301
|
-
if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
|
|
374
|
+
if (proxyBody.tools && Array.isArray(proxyBody.tools) && proxyBody.tools.length > 0) {
|
|
302
375
|
// B0. KIMI / MOONSHOT SURGICAL FIX (Restored for Debug)
|
|
303
376
|
// Tools are ENABLED. We rely on penalties and strict stops to fight loops.
|
|
304
377
|
if (actualModel.includes("kimi") || actualModel.includes("moonshot")) {
|
|
@@ -351,9 +424,14 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
351
424
|
});
|
|
352
425
|
// 2. Sanitize & RESTORE GROUNDING CONFIG (Essential for Vertex Auth)
|
|
353
426
|
if (proxyBody.tools.length > 0) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
427
|
+
if (hasFunctions) {
|
|
428
|
+
proxyBody.tools = sanitizeToolsForVertex(proxyBody.tools);
|
|
429
|
+
// ONLY for Free/Vertex: Add tools_config to disable search grounding (required for free tier).
|
|
430
|
+
// For Enterprise, adding this causes 403 Forbidden on some keys.
|
|
431
|
+
if (!isEnterprise) {
|
|
432
|
+
proxyBody.tools_config = { google_search_retrieval: { disable: true } };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
357
435
|
}
|
|
358
436
|
else {
|
|
359
437
|
// 3. If no tools left (or only search was present), DELETE 'tools' entirely
|
|
@@ -448,11 +526,12 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
448
526
|
// 2. Gemini Tools Fix (Gemini + Tools -> 401 -> Fallback to OpenAI)
|
|
449
527
|
const isEnterpriseFallback = (fetchRes.status === 402 || fetchRes.status === 429 || fetchRes.status === 401 || fetchRes.status === 403) && isEnterprise;
|
|
450
528
|
const isGeminiToolsFallback = fetchRes.status === 401 && actualModel.includes('gemini') && !isEnterprise && proxyBody.tools && proxyBody.tools.length > 0;
|
|
451
|
-
|
|
529
|
+
// STRICT MANUAL MODE: Disable "Magic" Fallbacks
|
|
530
|
+
if ((isEnterpriseFallback || isGeminiToolsFallback) && config.mode !== 'manual') {
|
|
452
531
|
log(`[SafetyNet] Upstream Rejection (${fetchRes.status}). Triggering Transparent Fallback.`);
|
|
453
532
|
if (isEnterpriseFallback) {
|
|
454
533
|
// 1a. Enterprise -> Free Fallback
|
|
455
|
-
actualModel = config.fallbacks.free.
|
|
534
|
+
actualModel = config.fallbacks.free.main.replace('free/', '');
|
|
456
535
|
isEnterprise = false;
|
|
457
536
|
isFallbackActive = true;
|
|
458
537
|
if (fetchRes.status === 402)
|
|
@@ -595,8 +674,9 @@ export async function handleChatCompletion(req, res, bodyRaw) {
|
|
|
595
674
|
if (isFallbackActive)
|
|
596
675
|
modeLabel += " (FALLBACK)";
|
|
597
676
|
const fullMsg = `${dashboardMsg} | ⚙️ ${modeLabel}`;
|
|
598
|
-
// Only emit if not silenced AND only for Enterprise/Paid requests
|
|
599
|
-
if
|
|
677
|
+
// Only emit if not silenced AND (only for Enterprise/Paid requests OR if Fallback occurred)
|
|
678
|
+
// We want to know if our Pro request failed.
|
|
679
|
+
if (isEnterprise || isFallbackActive) {
|
|
600
680
|
emitStatusToast('info', fullMsg, 'Pollinations Status');
|
|
601
681
|
}
|
|
602
682
|
}
|
package/dist/server/quota.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface QuotaStatus {
|
|
|
10
10
|
needsAlert: boolean;
|
|
11
11
|
tier: string;
|
|
12
12
|
tierEmoji: string;
|
|
13
|
+
isLimitedKey?: boolean;
|
|
13
14
|
}
|
|
14
15
|
export declare function getQuotaStatus(forceRefresh?: boolean): Promise<QuotaStatus>;
|
|
15
16
|
export declare function formatQuotaForToast(quota: QuotaStatus): string;
|
package/dist/server/quota.js
CHANGED
|
@@ -38,6 +38,25 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
38
38
|
tierEmoji: '❌'
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
// CHECK LIMITED KEY (v5.5)
|
|
42
|
+
// If commands.ts detected this key has no profile access, return specific status immediately.
|
|
43
|
+
// We do NOT attempt to fetch quota to avoid 403 spam.
|
|
44
|
+
if (config.keyHasAccessToProfile === false) {
|
|
45
|
+
return {
|
|
46
|
+
tierRemaining: 0,
|
|
47
|
+
tierUsed: 0,
|
|
48
|
+
tierLimit: 0,
|
|
49
|
+
walletBalance: 0,
|
|
50
|
+
nextResetAt: new Date(),
|
|
51
|
+
timeUntilReset: 0,
|
|
52
|
+
canUseEnterprise: true, // GENERATION IS ALLOWED
|
|
53
|
+
isUsingWallet: false,
|
|
54
|
+
needsAlert: false,
|
|
55
|
+
tier: 'limited',
|
|
56
|
+
tierEmoji: '🗝️',
|
|
57
|
+
isLimitedKey: true
|
|
58
|
+
};
|
|
59
|
+
}
|
|
41
60
|
const now = Date.now();
|
|
42
61
|
if (!forceRefresh && cachedQuota && (now - lastQuotaFetch) < CACHE_TTL) {
|
|
43
62
|
return cachedQuota;
|
|
@@ -66,12 +85,6 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
66
85
|
// Le wallet c'est le reste (balance totale - ce qu'il reste du tier gratuit non consommé)
|
|
67
86
|
const walletBalance = Math.max(0, balance - cleanTierRemaining);
|
|
68
87
|
const cleanWalletBalance = Math.max(0, parseFloat(walletBalance.toFixed(4)));
|
|
69
|
-
// Calculer les marges de sécurité basées sur les seuils configurés
|
|
70
|
-
const tierThresholdPollen = (config.thresholds.tier / 100) * tierLimit;
|
|
71
|
-
const walletThresholdUSD = config.thresholds.wallet; // Wallet threshold is already in $ in config
|
|
72
|
-
// canUseEnterprise: Vrai si on a encore du budget au-dessus du seuil de fallback
|
|
73
|
-
const hasAvailableTier = cleanTierRemaining > tierThresholdPollen;
|
|
74
|
-
const hasAvailableWallet = cleanWalletBalance > walletThresholdUSD;
|
|
75
88
|
cachedQuota = {
|
|
76
89
|
tierRemaining: cleanTierRemaining,
|
|
77
90
|
tierUsed,
|
|
@@ -79,9 +92,9 @@ export async function getQuotaStatus(forceRefresh = false) {
|
|
|
79
92
|
walletBalance: cleanWalletBalance,
|
|
80
93
|
nextResetAt: resetInfo.nextReset,
|
|
81
94
|
timeUntilReset: resetInfo.timeUntilReset,
|
|
82
|
-
canUseEnterprise:
|
|
83
|
-
isUsingWallet:
|
|
84
|
-
needsAlert: tierLimit > 0 ? (cleanTierRemaining
|
|
95
|
+
canUseEnterprise: cleanTierRemaining > 0.05 || cleanWalletBalance > 0.05,
|
|
96
|
+
isUsingWallet: cleanTierRemaining <= 0.05 && cleanWalletBalance > 0.05,
|
|
97
|
+
needsAlert: tierLimit > 0 ? (cleanTierRemaining / tierLimit * 100) <= config.thresholds.tier : false,
|
|
85
98
|
tier: profile.tier,
|
|
86
99
|
tierEmoji: tierInfo.emoji
|
|
87
100
|
};
|
|
@@ -204,6 +217,9 @@ function calculateCurrentPeriodUsage(usage, resetInfo) {
|
|
|
204
217
|
}
|
|
205
218
|
// === EXPORT POUR LES ALERTES ===
|
|
206
219
|
export function formatQuotaForToast(quota) {
|
|
220
|
+
if (quota.isLimitedKey) {
|
|
221
|
+
return "⚠️ Dashboard Limitation: Clé restreinte (Activez Profile/Usage/Balance pour voir le Quota)";
|
|
222
|
+
}
|
|
207
223
|
const tierPercent = quota.tierLimit > 0
|
|
208
224
|
? Math.round((quota.tierRemaining / quota.tierLimit) * 100)
|
|
209
225
|
: 0;
|
package/package.json
CHANGED