opencode-pollinations-plugin 6.0.0-beta.25 β†’ 6.1.0-beta.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,165 +1,119 @@
1
- # 🌸 Pollinations AI Plugin for OpenCode (v5.6.0)
1
+ # 🌸 Pollinations AI Plugin for OpenCode
2
2
 
3
3
  <div align="center">
4
- <img src="https://avatars.githubusercontent.com/u/88394740?s=400&v=4" alt="Pollinations.ai Logo" width="200">
4
+ <img src="https://avatars.githubusercontent.com/u/88394740?s=400&v=4" alt="Pollinations.ai Logo" width="150">
5
5
  <br>
6
- <b>The Bridge between OpenCode and the Pollinations.ai Ecosystem.</b>
6
+ <b>The Bridge between OpenCode and the Pollinations.ai Ecosystem</b>
7
7
  <br>
8
- Access unlimited free AI models or premium enterprise models directly within your editor.
8
+ Access 25+ AI models with Vision, Reasoning, and Tools support.
9
9
  </div>
10
10
 
11
11
  <div align="center">
12
12
 
13
- ![Version](https://img.shields.io/badge/version-5.8.4--beta.15-orange.svg)
13
+ ![Version](https://img.shields.io/badge/version-6.0.0--beta.25-blue.svg)
14
14
  ![License](https://img.shields.io/badge/license-MIT-green.svg)
15
- ![Status](https://img.shields.io/badge/status-Beta-yellow.svg)
15
+ ![Status](https://img.shields.io/badge/status-Stable-brightgreen.svg)
16
16
 
17
- [πŸ“œ View Changelog](./CHANGELOG.md) | [πŸ›£οΈ Roadmap](./ROADMAP.md)
17
+ [πŸ“œ Changelog](./CHANGELOG.md) | [πŸ›£οΈ Roadmap](./ROADMAP.md)
18
18
 
19
19
  </div>
20
20
 
21
- ## πŸ“– Philosophy: Open AI for Creators
21
+ ---
22
22
 
23
- > **"No closed doors, no corporate hoops β€” just good tools and good people."**
23
+ ## ✨ Features
24
24
 
25
- Pollinations.ai is an open-source platform built by and for the community. We provide a unified API for image, text, audio, and video generation.
26
- - **Transparent**: Our code, roadmap, and discussions are open.
27
- - **Community Driven**: Features are prioritized based on what *you* need.
28
- - **Fair**: One single currency (**Pollen**) for all models. No complex subscriptions.
25
+ - **πŸ‘οΈ Vision Support**: Send images to AI models (OpenAI, Claude, Gemini, Kimi)
26
+ - **🧠 Reasoning Models**: Access advanced thinking models (DeepSeek, Kimi, Perplexity)
27
+ - **πŸ› οΈ Native Tools**: Full function calling support for compatible models
28
+ - **πŸ” Secure Auth**: Native OpenCode `/connect` integration
29
+ - **πŸ”„ Hot-Reload**: Change API keys without restarting OpenCode
29
30
 
30
- ## πŸ“Έ Gallery
31
+ ---
31
32
 
32
- <p align="center">
33
- <img src="https://github.com/fkom13/opencode-pollinations-plugin/raw/main/docs/images/connect.png" alt="Connect Command" width="800">
34
- <br>
35
- <em>Easy Connection with /connect or /pollinations config apiKey</em>
36
- </p>
33
+ ## 🐝 Understanding Pollen & Tiers
37
34
 
38
- <p align="center">
39
- <img src="https://github.com/fkom13/opencode-pollinations-plugin/raw/main/docs/images/usage_dashboard.png" alt="Usage Dashboard" width="800">
40
- <br>
41
- <em>Integrated Usage Dashboard (/pollinations usage)</em>
42
- </p>
35
+ **Pollen** is Pollinations' unified credit system. $1 β‰ˆ 1 Pollen.
43
36
 
44
- <p align="center">
45
- <img src="https://github.com/fkom13/opencode-pollinations-plugin/raw/main/docs/images/models.png" alt="Models" width="800">
46
- <br>
47
- <em>Wide Range of Models (Mistral, OpenAI, Gemini, Claude)</em>
48
- </p>
37
+ | Tier | Daily Grant | Requirement |
38
+ |:-----|:------------|:------------|
39
+ | 🦠 **Microbe** | 0.1 Pollen | Flagged accounts |
40
+ | 🌱 **Spore** | 1 Pollen | Sign up |
41
+ | 🌿 **Seed** | 3 Pollen | Active GitHub dev (8+ points) |
42
+ | 🌸 **Flower** | 10 Pollen | Published app |
43
+ | 🍯 **Nectar** | 20 Pollen | Major contributor |
49
44
 
50
- <p align="center">
51
- <img src="https://github.com/fkom13/opencode-pollinations-plugin/raw/main/docs/images/free_add.png" alt="Free Chat Example" width="800">
52
- <br>
53
- <em>Free Universe Chat (Supported by Pollinations Ads)</em>
54
- </p>
45
+ > πŸ’‘ This plugin was published to the OpenCode ecosystem, granting its author **Flower** tier (10 Pollen/day).
55
46
 
56
- <p align="center">
57
- <img src="https://github.com/fkom13/opencode-pollinations-plugin/raw/main/docs/images/plan_1.png" alt="Plan Build Step 1" width="400">
58
- <img src="https://github.com/fkom13/opencode-pollinations-plugin/raw/main/docs/images/plan_2.png" alt="Plan Build Step 2" width="400">
59
- <br>
60
- <em>Integrated Plan Building Workflow</em>
61
- </p>
47
+ ---
62
48
 
63
- ## ✨ Features
49
+ ## πŸš€ Quick Start
64
50
 
65
- - **🌍 Free Universe**: Access generic models (`openai`, `mistral`, `gemini`) for **FREE**, unlimited time, no API key required.
66
- - **πŸš€ Pro Mode**: Connect your Pollinations API Key to access Premium Models (`claude-3-opus`, `gpt-4o`, `deepseek-coder`).
67
- - **πŸ” Limited Key Support (v5.6.0)**: Use keys restricted to "Generation Only". The plugin automatically disables Advanced Features (Dashboard/Quota) but allows full generation in Manual Mode.
68
- - **πŸ›‘οΈ Safety Net V5**: never get blocked.
69
- - **Transparent Fallback**: If your Pro quota runs out mid-chat, the plugin automatically switches to a free model instantly. No errors, just a seamless experience.
70
- - **Smart Mode Switching**: Prevents Limited Keys from entering "Pro" mode to avoid confusing errors.
71
- - **πŸ“Š Real-time Dashboard**: Track your **Pollen** usage, Tier Status, and Wallet Balance inside OpenCode.
72
- - **πŸ”‡ Stealth Mode (v5.4.7)**: Status notifications are now strictly limited to Pollinations Enter (Paid) sessions. No more cluttered notifications when using other providers like Nvidia or Google AI.
51
+ ### 1. Install Plugin
52
+ ```bash
53
+ npm install -g opencode-pollinations-plugin
54
+ npx opencode-pollinations-plugin
55
+ ```
73
56
 
74
- ## 🐝 Understanding Pollen & Tiers
57
+ ### 2. Connect Your Account
58
+ ```
59
+ /connect
60
+ ```
61
+ Select **pollinations** β†’ Enter your API key from [enter.pollinations.ai](https://enter.pollinations.ai)
62
+
63
+ ### 3. Start Using
64
+ Select any `pollinations/*` model in OpenCode and chat!
65
+
66
+ ---
67
+
68
+ ## πŸ‘οΈ Vision Support
75
69
 
76
- **Pollen** is our unified credit system. $1 β‰ˆ 1 Pollen.
77
- You spend it to verify API calls on premium models.
78
-
79
- ### Tiers (Free Daily Grants during Beta)
80
-
81
- | Tier | Grant | Requirement |
82
- | :--- | :--- | :--- |
83
- | **🦠 Spore** | **1 Pollen/day** | Just Sign Up! |
84
- | **🌱 Seed** | **3 Pollen/day** | Active GitHub Developer (8+ points) |
85
- | **🌸 Flower** | **10 Pollen/day** | **Publish an App** (Like this Plugin!) |
86
- | **🍯 Nectar** | **20 Pollen/day** | Major Contributors (Coming Soon) |
87
-
88
- > 🎁 **Beta Bonus**: Buy one Pollen pack, get one free!
89
-
90
- ### 🐧 Platform Support & Dynamic Ports (v5.4.6+)
91
- This plugin is **true Cross-Platform** (Windows, macOS, Linux).
92
- - **Dynamic Port Allocation**: No more port conflicts! The plugin automatically finds an available port on startup.
93
- - **Tools Support**: Using tools with Gemini (Free) triggers an **Automatic Intelligent Fallback** to OpenAI to ensure your workflow never breaks.
94
-
95
- > **Note**: Legacy static port (10001) logic has been replaced with system-assigned ports (0). This eliminates "Address in use" errors and effectively removes the need for Linux-specific `fuser` commands, making the plugin fully **Cross-Platform**.
96
-
97
- This plugin is part of the **OpenCode Ecosystem**.
98
-
99
- ### Option 1: NPM (Instant Setup) (Recommended)
100
- This method automatically configures OpenCode to load the plugin.
101
-
102
- 1. Install global:
103
- ```bash
104
- npm install -g opencode-pollinations-plugin
105
- ```
106
- 2. Run the Auto-Setup (Magic):
107
- ```bash
108
- npx opencode-pollinations-plugin
109
- ```
110
- *This detects your OpenCode config and injects the plugin path automatically.*
111
-
112
- ### Option 2: Manual Configuration
113
- 1. Install globally as above.
114
- 2. Edit `~/.config/opencode/opencode.json`:
115
- ```json
116
- {
117
- "plugin": [
118
- "opencode-pollinations-plugin"
119
- ]
120
- }
121
- ```
122
- *Note: If OpenCode fails to find it, use the absolute path to the global install.*
123
-
124
- ## πŸš€ Publication (The "Registry")
125
- OpenCode uses NPM as its registry. To publish:
126
-
127
- 1. **Publish to NPM**:
128
- ```bash
129
- npm login
130
- npm publish
131
- ```
132
- 2. **Join Ecosystem**: Submit a Pull Request to [OpenCode Ecosystem](https://github.com/opencode-ai/ecosystem) to list your plugin officially.
133
- *Once accepted, users can find it via documentation or future registry commands.*
134
-
135
- ## πŸš€ Getting Started
136
-
137
- ### 1. The Basics (Free Mode)
138
- Just type in the chat. You are in **Manual Mode** by default.
139
- - Model: `openai` (GPT-4o Mini equivalent)
140
- - Model: `mistral` (Mistral Nemo)
141
-
142
- ### πŸ”‘ Configuration (API Key)
143
-
144
- 1. Run the setup command:
145
- ```bash
146
- /connect
147
- ```
148
- 2. Choose "pollinations" and enter your key if you have one (or leave blank for free tier).
149
- 3. **IMPORTANT**: You must **restart OpenCode** for the model list to update with your new tier (e.g. to see Paid models).
150
-
151
- ### πŸ€– Models
152
-
153
- ### πŸ”‘ Types de ClΓ©s SupportΓ©s
154
- - **Clés Standard (`sk-...`)**: Accès complet (Modèles + Dashboard Usage + Quota).
155
- - **Clés Limitées**: Accès Génération uniquement. Le dashboard affichera une alerte de restriction (v5.4.11).
156
- - **Support Legacy**: Les anciennes clΓ©s (`sk_...`) sont aussi acceptΓ©es.
70
+ The following models support image input:
71
+
72
+ | Model | Vision | Reasoning | Tools |
73
+ |-------|:------:|:---------:|:-----:|
74
+ | `openai` / `openai-large` | βœ… | - | βœ… |
75
+ | `gemini` / `gemini-search` | βœ… | - | βœ… |
76
+ | `claude` / `claude-large` | βœ… | - | βœ… |
77
+ | `kimi` | βœ… | βœ… | - |
78
+ | `openai-audio` | βœ… | - | βœ… |
79
+
80
+ **Usage**: Simply paste an image in the chat or use an image URL. The plugin handles encoding automatically.
81
+
82
+ ---
83
+
84
+ ## πŸ“Š Available Models (25+)
85
+
86
+ ### Text/Chat Models
87
+ - **OpenAI**: `openai`, `openai-fast`, `openai-large`
88
+ - **Gemini**: `gemini`, `gemini-fast`, `gemini-search`, `gemini-large` πŸ’Ž
89
+ - **Claude**: `claude-fast`, `claude`, `claude-large` πŸ’Ž
90
+ - **Reasoning**: `deepseek`, `kimi`, `perplexity-reasoning`
91
+ - **Code**: `qwen-coder`, `mistral`
92
+ - **Fast**: `nova-fast`, `grok`
93
+
94
+ ### Paid-Only Models (πŸ’Ž)
95
+ These require purchased credits (not daily grants):
96
+ - `claude-large`, `gemini-large`, `veo`, `seedream-pro`
97
+
98
+ ---
99
+
100
+ ## πŸ”§ Commands
101
+
102
+ ```
103
+ /pollinations usage # View Pollen balance
104
+ /pollinations usage full # Detailed model breakdown
105
+ /pollinations status # Plugin health check
106
+ ```
107
+
108
+ ---
157
109
 
158
110
  ## πŸ”— Links
159
111
 
160
- - **Pollinations Website**: [pollinations.ai](https://pollinations.ai)
161
- - **Discord Community**: [Join us!](https://discord.gg/pollinations-ai-885844321461485618)
162
- - **OpenCode Ecosystem**: [opencode.ai](https://opencode.ai/docs/ecosystem#plugins)
112
+ - **Get API Key**: [enter.pollinations.ai](https://enter.pollinations.ai)
113
+ - **Discord**: [Join Community](https://discord.gg/pollinations-ai-885844321461485618)
114
+ - **GitHub**: [Plugin Repository](https://github.com/fkom13/opencode-pollinations-plugin)
115
+
116
+ ---
163
117
 
164
118
  ## πŸ“œ License
165
119
 
@@ -202,7 +202,7 @@ async function handleModeCommand(args) {
202
202
  return { handled: true, error: `❌ Erreur de vΓ©rification: ${e.message}` };
203
203
  }
204
204
  }
205
- // Allow switch (if alwaysfree or manual, or verified pro)
205
+ // Allow switch (if economy or manual, or verified pro)
206
206
  saveConfig({ mode: mode });
207
207
  const config = loadConfig();
208
208
  if (config.gui.status !== 'none') {
@@ -264,32 +264,36 @@ async function handleUsageCommand(args) {
264
264
  }
265
265
  }
266
266
  function handleFallbackCommand(args) {
267
- const [main, agent] = args;
268
- if (!main) {
267
+ const [mode, model] = args;
268
+ if (!mode) {
269
269
  const config = loadConfig();
270
- const freeConfig = `Free: main=${config.fallbacks.free.main}, agent=${config.fallbacks.free.agent}`;
271
- const enterConfig = `Enter: agent=${config.fallbacks.enter.agent}`;
272
270
  return {
273
271
  handled: true,
274
- response: `Fallbacks actuels:\n${freeConfig}\n${enterConfig}`
272
+ response: `Fallbacks actuels:\n- Economy: ${config.fallbacks.economy}\n- Pro: ${config.fallbacks.pro}`
275
273
  };
276
274
  }
277
- // Default behavior for "/poll fallback <model> <agent>" is setting FREE fallbacks
278
- // User needs to use commands (maybe add /poll fallback enter ...) later
279
- // For now, map to Free Fallback as it's the primary Safety Net
280
275
  const config = loadConfig();
281
- saveConfig({
282
- fallbacks: {
283
- ...config.fallbacks,
284
- free: {
285
- main: main,
286
- agent: agent || config.fallbacks.free.agent
287
- }
288
- }
289
- });
276
+ if (mode === 'economy' && model) {
277
+ saveConfig({
278
+ fallbacks: { ...config.fallbacks, economy: model }
279
+ });
280
+ return {
281
+ handled: true,
282
+ response: `βœ… Fallback Economy configurΓ©: ${model}`
283
+ };
284
+ }
285
+ if (mode === 'pro' && model) {
286
+ saveConfig({
287
+ fallbacks: { ...config.fallbacks, pro: model }
288
+ });
289
+ return {
290
+ handled: true,
291
+ response: `βœ… Fallback Pro configurΓ©: ${model}`
292
+ };
293
+ }
290
294
  return {
291
295
  handled: true,
292
- response: `βœ… Fallback (Free) configurΓ©: main=${main}, agent=${agent || config.fallbacks.free.agent}`
296
+ error: `Usage: /pollinations fallback [economy|pro] <modèle>`
293
297
  };
294
298
  }
295
299
  async function handleConnectCommand(args) {
@@ -411,14 +415,23 @@ function handleConfigCommand(args) {
411
415
  saveConfig({ thresholds: { ...config.thresholds, tier: threshold } });
412
416
  return { handled: true, response: `βœ… threshold_tier = ${threshold}%` };
413
417
  }
414
- if (key === 'threshold_wallet' && value) {
418
+ if (key === 'threshold_wallet_warn' && value) {
415
419
  const threshold = parseInt(value);
416
420
  if (isNaN(threshold) || threshold < 0 || threshold > 100) {
417
421
  return { handled: true, error: 'Valeur entre 0 et 100 requise' };
418
422
  }
419
423
  const config = loadConfig();
420
- saveConfig({ thresholds: { ...config.thresholds, wallet: threshold } });
421
- return { handled: true, response: `βœ… threshold_wallet = ${threshold}%` };
424
+ saveConfig({ thresholds: { ...config.thresholds, wallet_warn: threshold } });
425
+ return { handled: true, response: `βœ… threshold_wallet_warn = ${threshold}%` };
426
+ }
427
+ if (key === 'threshold_wallet_stop' && value) {
428
+ const stopValue = parseFloat(value);
429
+ if (isNaN(stopValue) || stopValue < 0) {
430
+ return { handled: true, error: 'Valeur $ positive requise' };
431
+ }
432
+ const config = loadConfig();
433
+ saveConfig({ thresholds: { ...config.thresholds, wallet_stop: stopValue } });
434
+ return { handled: true, response: `βœ… threshold_wallet_stop = $${stopValue}` };
422
435
  }
423
436
  if (key === 'status_bar' && value) {
424
437
  const enabled = value === 'true';
@@ -427,7 +440,7 @@ function handleConfigCommand(args) {
427
440
  }
428
441
  return {
429
442
  handled: true,
430
- error: `ClΓ© inconnue: ${key}. ClΓ©s: status_gui, logs_gui, threshold_tier, threshold_wallet, status_bar`
443
+ error: `ClΓ© inconnue: ${key}. ClΓ©s: status_gui, logs_gui, threshold_tier, threshold_wallet_warn, threshold_wallet_stop, status_bar`
431
444
  };
432
445
  }
433
446
  function handleHelpCommand() {
@@ -1,6 +1,6 @@
1
- export interface PollinationsConfigV5 {
1
+ export interface PollinationsConfigV6 {
2
2
  version: string | number;
3
- mode: 'manual' | 'alwaysfree' | 'pro';
3
+ mode: 'manual' | 'economy' | 'pro';
4
4
  apiKey?: string;
5
5
  keyHasAccessToProfile?: boolean;
6
6
  gui: {
@@ -9,24 +9,24 @@ export interface PollinationsConfigV5 {
9
9
  };
10
10
  thresholds: {
11
11
  tier: number;
12
- wallet: number;
12
+ wallet_warn: number;
13
+ wallet_stop: number;
13
14
  };
14
15
  fallbacks: {
15
- free: {
16
- main: string;
17
- agent: string;
18
- };
19
- enter: {
20
- agent: string;
21
- };
16
+ economy: string;
17
+ pro: string;
18
+ };
19
+ session: {
20
+ wallet_initial?: number;
21
+ session_start?: string;
22
22
  };
23
23
  enablePaidTools: boolean;
24
24
  statusBar: boolean;
25
25
  }
26
- export declare function loadConfig(): PollinationsConfigV5;
27
- export declare function saveConfig(updates: Partial<PollinationsConfigV5>): {
26
+ export declare function loadConfig(): PollinationsConfigV6;
27
+ export declare function saveConfig(updates: Partial<PollinationsConfigV6>): {
28
28
  version: string;
29
- mode: "manual" | "alwaysfree" | "pro";
29
+ mode: "manual" | "economy" | "pro";
30
30
  apiKey?: string;
31
31
  keyHasAccessToProfile?: boolean;
32
32
  gui: {
@@ -35,17 +35,19 @@ export declare function saveConfig(updates: Partial<PollinationsConfigV5>): {
35
35
  };
36
36
  thresholds: {
37
37
  tier: number;
38
- wallet: number;
38
+ wallet_warn: number;
39
+ wallet_stop: number;
39
40
  };
40
41
  fallbacks: {
41
- free: {
42
- main: string;
43
- agent: string;
44
- };
45
- enter: {
46
- agent: string;
47
- };
42
+ economy: string;
43
+ pro: string;
44
+ };
45
+ session: {
46
+ wallet_initial?: number;
47
+ session_start?: string;
48
48
  };
49
49
  enablePaidTools: boolean;
50
50
  statusBar: boolean;
51
51
  };
52
+ export declare function initSessionWallet(walletBalance: number): void;
53
+ export declare function getWalletWarnPercent(): number;
@@ -9,7 +9,7 @@ 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.2.0';
12
+ let PKG_VERSION = '6.1.0';
13
13
  try {
14
14
  const pkgPath = path.join(__dirname, '../../package.json');
15
15
  if (fs.existsSync(pkgPath)) {
@@ -18,17 +18,22 @@ try {
18
18
  }
19
19
  }
20
20
  catch (e) { }
21
- const DEFAULT_CONFIG_V5 = {
21
+ const DEFAULT_CONFIG_V6 = {
22
22
  version: PKG_VERSION,
23
- mode: 'manual',
23
+ mode: 'economy', // Défaut: economy (protège le wallet)
24
24
  gui: { status: 'alert', logs: 'none' },
25
- thresholds: { tier: 10, wallet: 5 },
25
+ thresholds: {
26
+ tier: 20, // 20% tier restant β†’ alerte/fallback
27
+ wallet_warn: 20, // 20% wallet restant β†’ alerte (pro)
28
+ wallet_stop: 0.50 // $0.50 β†’ stop absolu (pro)
29
+ },
26
30
  fallbacks: {
27
- free: { main: 'free/mistral', agent: 'free/openai-fast' },
28
- enter: { agent: 'free/openai-fast' }
31
+ economy: 'nova-fast',
32
+ pro: 'qwen-coder'
29
33
  },
34
+ session: {},
30
35
  enablePaidTools: false,
31
- keyHasAccessToProfile: true, // Default true for legacy keys
36
+ keyHasAccessToProfile: true,
32
37
  statusBar: true
33
38
  };
34
39
  function logConfig(msg) {
@@ -40,17 +45,28 @@ function logConfig(msg) {
40
45
  }
41
46
  catch (e) { }
42
47
  }
48
+ // MIGRATION: alwaysfree β†’ economy
49
+ function migrateConfig(config) {
50
+ if (config.mode === 'alwaysfree') {
51
+ config.mode = 'economy';
52
+ logConfig('[Migration] alwaysfree β†’ economy');
53
+ }
54
+ // Migrate old fallbacks structure
55
+ if (config.fallbacks?.free?.main) {
56
+ config.fallbacks.economy = config.fallbacks.free.main.replace('free/', '');
57
+ delete config.fallbacks.free;
58
+ logConfig('[Migration] fallbacks.free β†’ fallbacks.economy');
59
+ }
60
+ return config;
61
+ }
43
62
  // SIMPLE LOAD (Direct Disk Read - No Caching, No Watchers)
44
- // This ensures the Proxy ALWAYS sees the latest state from auth.json
45
63
  export function loadConfig() {
46
64
  return readConfigFromDisk();
47
65
  }
48
66
  function readConfigFromDisk() {
49
- let config = { ...DEFAULT_CONFIG_V5 };
67
+ let config = { ...DEFAULT_CONFIG_V6 };
50
68
  let finalKey = undefined;
51
69
  let source = 'none';
52
- // TIMESTAMP BASED PRIORITY LOGIC
53
- // We want the most recently updated Valid Key to win.
54
70
  let configTime = 0;
55
71
  let authTime = 0;
56
72
  try {
@@ -69,7 +85,7 @@ function readConfigFromDisk() {
69
85
  try {
70
86
  const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
71
87
  const custom = JSON.parse(raw);
72
- config = { ...config, ...custom }; // Helper: We load the rest of config anyway
88
+ config = { ...config, ...migrateConfig(custom) };
73
89
  if (custom.apiKey && custom.apiKey.length > 5)
74
90
  configKey = custom.apiKey;
75
91
  }
@@ -90,7 +106,6 @@ function readConfigFromDisk() {
90
106
  catch (e) { }
91
107
  }
92
108
  // 2. DETERMINE WINNER
93
- // If both exist, newest wins. If one exists, it wins.
94
109
  if (configKey && authKey) {
95
110
  if (configTime >= authTime) {
96
111
  finalKey = configKey;
@@ -128,16 +143,9 @@ function readConfigFromDisk() {
128
143
  // 4. APPLY
129
144
  if (finalKey) {
130
145
  config.apiKey = finalKey;
131
- // config.mode = 'pro'; // REMOVED: Mode is decoupled from Key presence.
132
146
  }
133
147
  else {
134
- // Ensure no phantom key remains
135
148
  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.
141
149
  }
142
150
  return { ...config, version: PKG_VERSION };
143
151
  }
@@ -149,6 +157,7 @@ export function saveConfig(updates) {
149
157
  fs.mkdirSync(CONFIG_DIR_POLLI, { recursive: true });
150
158
  }
151
159
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2));
160
+ logConfig(`[SaveConfig] Updated: ${Object.keys(updates).join(', ')}`);
152
161
  return updated;
153
162
  }
154
163
  catch (e) {
@@ -156,3 +165,23 @@ export function saveConfig(updates) {
156
165
  throw e;
157
166
  }
158
167
  }
168
+ // SESSION WALLET TRACKING
169
+ export function initSessionWallet(walletBalance) {
170
+ const config = loadConfig();
171
+ if (!config.session.wallet_initial) {
172
+ saveConfig({
173
+ session: {
174
+ wallet_initial: walletBalance,
175
+ session_start: new Date().toISOString()
176
+ }
177
+ });
178
+ logConfig(`[Session] Wallet initial stockΓ©: $${walletBalance}`);
179
+ }
180
+ }
181
+ export function getWalletWarnPercent() {
182
+ const config = loadConfig();
183
+ const initial = config.session.wallet_initial;
184
+ if (!initial || initial <= 0)
185
+ return 100; // No reference = no warning
186
+ return config.thresholds.wallet_warn;
187
+ }
@@ -245,13 +245,8 @@ export async function handleChatCompletion(req, res, bodyRaw) {
245
245
  if (quota.walletBalance <= 0.001) { // Floating point safety
246
246
  log(`[SafetyNet] Paid Only Model (${actualModel}) requested but Wallet is Empty ($${quota.walletBalance}). BLOCKING.`);
247
247
  // Immediate Block or Fallback?
248
- // Text says: "πŸ’Ž Paid Only models require purchased pollen only"
249
- // Blocking is safer/clearer than falling back to a free model which might not be what the user expects for a "Pro" feature?
250
- // Actually, Fallback to Free is usually better for UX if configured, BUT for specific "Paid Only" requests, the user explicitly chose a powerful model.
251
- // Falling back to Mistral might be confusing if they asked for Gemini-Large.
252
- // BUT we are failing gracefully.
253
- // Let's Fallback to Free Default and Warn.
254
- actualModel = config.fallbacks.free.main.replace('free/', '');
248
+ // Fallback based on current mode
249
+ actualModel = config.fallbacks.economy || 'nova-fast';
255
250
  isEnterprise = false;
256
251
  isFallbackActive = true;
257
252
  fallbackReason = "Paid Only Model requires purchased credits";
@@ -277,62 +272,113 @@ export async function handleChatCompletion(req, res, bodyRaw) {
277
272
  // WE DO NOT RETURN 403. WE ALLOW THE REQUEST.
278
273
  // Since config.mode is now 'manual', the next checks (alwaysfree/pro) will be skipped.
279
274
  }
280
- if (config.mode === 'alwaysfree') {
275
+ // === MODE ECONOMY ===
276
+ // - Paid models BLOCKED (not fallback, BLOCK with message)
277
+ // - tier < threshold β†’ fallback economy
278
+ // - tier = 0 β†’ STOP (no wallet access)
279
+ if (config.mode === 'economy') {
281
280
  if (isEnterprise) {
282
- // NEW: Paid Only Check for Always Free
281
+ // 1. BLOCK Paid Models
283
282
  try {
284
283
  const homedir = process.env.HOME || '/tmp';
285
284
  const standardPaidPath = path.join(homedir, '.pollinations', 'pollinations-paid-models.json');
286
285
  if (fs.existsSync(standardPaidPath)) {
287
286
  const paidModels = JSON.parse(fs.readFileSync(standardPaidPath, 'utf-8'));
288
287
  if (paidModels.includes(actualModel)) {
289
- log(`[SafetyNet] alwaysfree Mode: Request for Paid Only Model (${actualModel}). FALLBACK.`);
290
- actualModel = config.fallbacks.free.main.replace('free/', '');
291
- isEnterprise = false;
292
- isFallbackActive = true;
293
- fallbackReason = "Mode AlwaysFree actif: Ce modèle payant consomme du wallet. Passez en mode PRO.";
288
+ log(`[Economy] BLOCKED: Paid model ${actualModel} not allowed in Economy mode`);
289
+ emitStatusToast('error', `πŸ’Ž ModΓ¨le payant interdit en mode Economy: ${actualModel}`, 'Mode Economy');
290
+ res.writeHead(403, { 'Content-Type': 'application/json' });
291
+ res.end(JSON.stringify({
292
+ error: {
293
+ message: `πŸ’Ž Paid model "${actualModel}" is not allowed in Economy mode. Switch to Pro mode or use a free model.`,
294
+ code: 'ECONOMY_PAID_BLOCKED'
295
+ }
296
+ }));
297
+ return;
294
298
  }
295
299
  }
296
300
  }
297
- catch (e) { }
298
- if (!isFallbackActive && quota.tier === 'error') {
299
- // Network error or unknown error (but NOT auth_limited, handled above)
300
- log(`[SafetyNet] AlwaysFree Mode: Quota Check Failed. Switching to Free Fallback.`);
301
- actualModel = config.fallbacks.free.main.replace('free/', '');
302
- isEnterprise = false;
301
+ catch (e) {
302
+ log(`[Economy] Error checking paid models: ${e}`);
303
+ }
304
+ // 2. Check Tier
305
+ if (quota.tier === 'error') {
306
+ log(`[Economy] Quota unreachable, switching to fallback`);
307
+ actualModel = config.fallbacks.economy || 'nova-fast';
303
308
  isFallbackActive = true;
304
309
  fallbackReason = "Quota Unreachable (Safety)";
305
310
  }
306
311
  else {
307
312
  const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
313
+ // 3. STOP if tier = 0
314
+ if (quota.tierRemaining <= 0.01) {
315
+ log(`[Economy] STOP: Tier exhausted, no wallet access in Economy mode`);
316
+ emitStatusToast('error', `πŸ›‘ Quota Γ©puisΓ©! Attendez le reset ou passez en mode Pro.`, 'Mode Economy');
317
+ res.writeHead(429, { 'Content-Type': 'application/json' });
318
+ res.end(JSON.stringify({
319
+ error: {
320
+ message: `πŸ›‘ Daily quota exhausted. Wait for reset or switch to Pro mode to use wallet credits.`,
321
+ code: 'ECONOMY_TIER_EXHAUSTED'
322
+ }
323
+ }));
324
+ return;
325
+ }
326
+ // 4. Fallback if tier < threshold
308
327
  if (tierRatio <= (config.thresholds.tier / 100)) {
309
- log(`[SafetyNet] AlwaysFree Mode: Tier (${(tierRatio * 100).toFixed(1)}%) <= Threshold (${config.thresholds.tier}%). Switching.`);
310
- actualModel = config.fallbacks.free.main.replace('free/', '');
311
- isEnterprise = false;
328
+ log(`[Economy] Tier ${(tierRatio * 100).toFixed(1)}% <= ${config.thresholds.tier}%, switching to fallback`);
329
+ actualModel = config.fallbacks.economy || 'nova-fast';
312
330
  isFallbackActive = true;
313
- fallbackReason = `Daily Tier < ${config.thresholds.tier}% (Wallet Protected)`;
331
+ fallbackReason = `Tier < ${config.thresholds.tier}% (Economie active)`;
314
332
  }
315
333
  }
316
334
  }
317
335
  }
336
+ // === MODE PRO ===
337
+ // - Paid models ALLOWED
338
+ // - wallet < wallet_stop $ β†’ STOP
339
+ // - wallet < wallet_warn % β†’ fallback pro
340
+ // - tier exhausted β†’ info toast (continue on wallet)
318
341
  else if (config.mode === 'pro') {
319
342
  if (isEnterprise) {
343
+ // Init session wallet tracking (first request)
344
+ if (!config.session?.wallet_initial && quota.walletBalance > 0) {
345
+ const { initSessionWallet } = await import('./config.js');
346
+ initSessionWallet(quota.walletBalance);
347
+ }
320
348
  if (quota.tier === 'error') {
321
- // Network error or unknown
322
- log(`[SafetyNet] Pro Mode: Quota Unreachable. Switching to Free Fallback.`);
323
- actualModel = config.fallbacks.free.main.replace('free/', '');
324
- isEnterprise = false;
349
+ log(`[Pro] Quota unreachable, switching to fallback`);
350
+ actualModel = config.fallbacks.pro || 'qwen-coder';
325
351
  isFallbackActive = true;
326
352
  fallbackReason = "Quota Unreachable (Safety)";
327
353
  }
328
354
  else {
329
- const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
330
- if (quota.walletBalance < config.thresholds.wallet && tierRatio <= (config.thresholds.tier / 100)) {
331
- log(`[SafetyNet] Pro Mode: Wallet < $${config.thresholds.wallet} AND Tier < ${config.thresholds.tier}%. Switching.`);
332
- actualModel = config.fallbacks.free.main.replace('free/', '');
333
- isEnterprise = false;
355
+ const walletStop = config.thresholds.wallet_stop || 0.50;
356
+ const walletWarnPercent = config.thresholds.wallet_warn || 20;
357
+ const walletInitial = config.session?.wallet_initial || quota.walletBalance;
358
+ const walletPercent = walletInitial > 0 ? (quota.walletBalance / walletInitial) * 100 : 100;
359
+ // 1. STOP if wallet < wallet_stop $
360
+ if (quota.walletBalance < walletStop) {
361
+ log(`[Pro] STOP: Wallet $${quota.walletBalance} < limit $${walletStop}`);
362
+ emitStatusToast('error', `πŸ›‘ Wallet $${quota.walletBalance.toFixed(2)} sous limite $${walletStop}`, 'Mode Pro');
363
+ res.writeHead(429, { 'Content-Type': 'application/json' });
364
+ res.end(JSON.stringify({
365
+ error: {
366
+ message: `πŸ›‘ Wallet ($${quota.walletBalance.toFixed(2)}) below hard limit ($${walletStop}). Add credits or adjust wallet_stop threshold.`,
367
+ code: 'PRO_WALLET_LIMIT'
368
+ }
369
+ }));
370
+ return;
371
+ }
372
+ // 2. Fallback if wallet% < wallet_warn%
373
+ if (walletPercent <= walletWarnPercent) {
374
+ log(`[Pro] Wallet ${walletPercent.toFixed(1)}% <= ${walletWarnPercent}%, switching to fallback`);
375
+ actualModel = config.fallbacks.pro || 'qwen-coder';
334
376
  isFallbackActive = true;
335
- fallbackReason = `Wallet & Tier Critical`;
377
+ fallbackReason = `Wallet < ${walletWarnPercent}% (${quota.walletBalance.toFixed(2)}$)`;
378
+ }
379
+ // 3. Info if tier exhausted (continue on wallet)
380
+ if (quota.tierRemaining <= 0.01 && !isFallbackActive) {
381
+ emitStatusToast('info', `ℹ️ Tier Γ©puisΓ©, utilisation du wallet ($${quota.walletBalance.toFixed(2)})`, 'Mode Pro');
336
382
  }
337
383
  }
338
384
  }
@@ -563,8 +609,10 @@ export async function handleChatCompletion(req, res, bodyRaw) {
563
609
  if ((isEnterpriseFallback || isGeminiToolsFallback) && config.mode !== 'manual') {
564
610
  log(`[SafetyNet] Upstream Rejection (${fetchRes.status}). Triggering Transparent Fallback.`);
565
611
  if (isEnterpriseFallback) {
566
- // 1a. Enterprise -> Free Fallback
567
- actualModel = config.fallbacks.free.main.replace('free/', '');
612
+ // 1a. Enterprise -> Fallback based on mode
613
+ actualModel = config.mode === 'pro'
614
+ ? (config.fallbacks.pro || 'qwen-coder')
615
+ : (config.fallbacks.economy || 'nova-fast');
568
616
  isEnterprise = false;
569
617
  isFallbackActive = true;
570
618
  if (fetchRes.status === 402)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-pollinations-plugin",
3
3
  "displayName": "Pollinations AI (V5.6)",
4
- "version": "6.0.0-beta.25",
4
+ "version": "6.1.0-beta.1",
5
5
  "description": "Native Pollinations.ai Provider Plugin for OpenCode",
6
6
  "publisher": "pollinations",
7
7
  "repository": {