opencode-pollinations-plugin 5.5.0-debug.3 → 5.5.1

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 CHANGED
@@ -1,4 +1,4 @@
1
- # 🌸 Pollinations AI Plugin for OpenCode (v5.4.6)
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
- ![Version](https://img.shields.io/badge/version-5.4.6-blue.svg)
13
+ ![Version](https://img.shields.io/badge/version-5.4.16-blue.svg)
14
14
  ![License](https://img.shields.io/badge/license-MIT-green.svg)
15
15
  ![Status](https://img.shields.io/badge/status-Stable-success.svg)
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.
@@ -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 config = loadConfig();
123
- if (config.gui.status !== 'none') {
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
- const usageData = await getDetailedUsage(config.apiKey);
148
- if (usageData && usageData.usage) {
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
- response += `\n> ⚠️ *Impossible de récupérer l'historique détaillé.*\n`;
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 [primary, subagent] = args;
178
- if (!primary) {
226
+ const [main, agent] = args;
227
+ if (!main) {
179
228
  const config = loadConfig();
180
- const freeConfig = `Free Fallbacks: primary=${config.fallbacks.free.primary}, subagent=${config.fallbacks.free.subagent}`;
181
- const enterConfig = `Enter Targets: subagent=${config.fallbacks.enter.subagent}`;
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: `Configuration des Modèles:\n${freeConfig}\n${enterConfig}`
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
- primary: primary,
193
- subagent: subagent || config.fallbacks.free.subagent
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: `✅ Fallbacks configurés: primary=${primary}, subagent=${subagent || config.fallbacks.free.subagent}`
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 <primary> [subagent]\`**: Configure le Safety Net (Free).
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).
@@ -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
- primary: string;
16
- subagent: string;
16
+ main: string;
17
+ agent: string;
17
18
  };
18
19
  enter: {
19
- subagent: string;
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
- primary: string;
41
- subagent: string;
42
+ main: string;
43
+ agent: string;
42
44
  };
43
45
  enter: {
44
- subagent: string;
46
+ agent: string;
45
47
  };
46
48
  };
47
49
  enablePaidTools: boolean;
@@ -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.5.0';
12
+ let PKG_VERSION = '5.2.0';
13
13
  try {
14
- const pkgPath = new URL('../../package.json', import.meta.url);
15
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
16
- PKG_VERSION = pkg.version;
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: { primary: 'free/mistral', subagent: 'free/openai-fast' },
26
- enter: { subagent: 'default' }
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 keyFound = false;
47
- // 1. Custom Config
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
- // MIGRATION LOGIC (v5.5.0)
53
- if (custom.fallbacks) {
54
- if (custom.fallbacks.free) {
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
- catch (e) {
75
- logConfig(`Error loading config: ${e}`);
76
- }
77
- // 2. Auth Store (Priority)
78
- if (!keyFound) {
78
+ let authKey = undefined;
79
+ if (fs.existsSync(AUTH_FILE)) {
79
80
  try {
80
- if (fs.existsSync(AUTH_FILE)) {
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 key = (typeof entry === 'object' && entry.key) ? entry.key : entry;
86
- if (key && typeof key === 'string' && key.length > 10) {
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
- logConfig(`Error reading auth.json: ${e}`);
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
- // 3. OpenCode Config (Fallback)
99
- if (!keyFound) {
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
- config.apiKey = nativeKey;
108
- config.mode = 'pro';
109
- keyFound = true;
121
+ finalKey = nativeKey;
122
+ source = 'opencode.json';
110
123
  }
111
124
  }
112
125
  }
113
126
  catch (e) { }
114
127
  }
115
- if (!keyFound && config.mode === 'pro') {
116
- config.mode = 'manual';
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
- // Sometimes the API list changes or is cached weirdly. We force vital models.
78
- const hasGemini = modelsOutput.find(m => m.id === 'free/gemini');
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/text/models', {
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
- const finalName = `${namePrefix}${baseName}`;
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,
@@ -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
- // --- AGENT DETECTION (v5.5.0 Simplified) ---
155
- const systemMsg = body.messages?.find((m) => m.role === 'system')?.content || "";
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
- // B. SPECIALIZED TARGETS (Subagents in Enter Universe)
231
- if (isEnterprise && isSubagent) {
232
- const specialized = config.fallbacks.enter.subagent;
233
- if (specialized && specialized !== 'default') {
234
- actualModel = specialized.replace('enter/', '').replace('free/', '');
235
- if (specialized.startsWith('free/'))
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
- log(`[Route] Subagent specialized to: ${actualModel}`);
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
- // C. SAFETY NETS (The Core V5 Logic)
241
- if (config.mode === 'alwaysfree' || config.mode === 'pro') {
300
+ else if (config.mode === 'pro') {
242
301
  if (isEnterprise) {
243
- if (!quota.canUseEnterprise || quota.tier === 'error') {
244
- const fallbackModel = isSubagent ? config.fallbacks.free.subagent : config.fallbacks.free.primary;
245
- log(`[SafetyNet] ${config.mode.toUpperCase()} Limit reached. Switching to: ${fallbackModel}`);
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 = `${config.mode.toUpperCase()} Limit Reached`;
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
- // v5.4.1: MUST send tools_config to avoid 401 Unauthorized on Vertex
355
- proxyBody.tools_config = { google_search_retrieval: { disable: true } };
356
- proxyBody.tools = sanitizeToolsForVertex(proxyBody.tools);
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
- if (isEnterpriseFallback || isGeminiToolsFallback) {
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.primary.replace('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 (isEnterprise) {
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
  }
@@ -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;
@@ -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: hasAvailableTier || hasAvailableWallet,
83
- isUsingWallet: !hasAvailableTier && hasAvailableWallet,
84
- needsAlert: tierLimit > 0 ? (cleanTierRemaining <= tierThresholdPollen) : false,
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-pollinations-plugin",
3
3
  "displayName": "Pollinations AI (V5.1)",
4
- "version": "5.5.0-debug.3",
4
+ "version": "5.5.1",
5
5
  "description": "Native Pollinations.ai Provider Plugin for OpenCode",
6
6
  "publisher": "pollinations",
7
7
  "repository": {