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 +88 -134
- package/dist/server/commands.js +36 -23
- package/dist/server/config.d.ts +23 -21
- package/dist/server/config.js +49 -20
- package/dist/server/proxy.js +84 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,165 +1,119 @@
|
|
|
1
|
-
# πΈ Pollinations AI Plugin for OpenCode
|
|
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="
|
|
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
|
|
6
|
+
<b>The Bridge between OpenCode and the Pollinations.ai Ecosystem</b>
|
|
7
7
|
<br>
|
|
8
|
-
Access
|
|
8
|
+
Access 25+ AI models with Vision, Reasoning, and Tools support.
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
11
|
<div align="center">
|
|
12
12
|
|
|
13
|
-

|
|
14
14
|

|
|
15
|
-

|
|
16
16
|
|
|
17
|
-
[π
|
|
17
|
+
[π Changelog](./CHANGELOG.md) | [π£οΈ Roadmap](./ROADMAP.md)
|
|
18
18
|
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
---
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
## β¨ Features
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
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
|
-
|
|
31
|
+
---
|
|
31
32
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
49
|
+
## π Quick Start
|
|
64
50
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
- **
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
- **
|
|
161
|
-
- **Discord
|
|
162
|
-
- **
|
|
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
|
|
package/dist/server/commands.js
CHANGED
|
@@ -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
|
|
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 [
|
|
268
|
-
if (!
|
|
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${
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
...config.fallbacks,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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 === '
|
|
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,
|
|
421
|
-
return { handled: true, response: `β
|
|
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,
|
|
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() {
|
package/dist/server/config.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface PollinationsConfigV6 {
|
|
2
2
|
version: string | number;
|
|
3
|
-
mode: 'manual' | '
|
|
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
|
-
|
|
12
|
+
wallet_warn: number;
|
|
13
|
+
wallet_stop: number;
|
|
13
14
|
};
|
|
14
15
|
fallbacks: {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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():
|
|
27
|
-
export declare function saveConfig(updates: Partial<
|
|
26
|
+
export declare function loadConfig(): PollinationsConfigV6;
|
|
27
|
+
export declare function saveConfig(updates: Partial<PollinationsConfigV6>): {
|
|
28
28
|
version: string;
|
|
29
|
-
mode: "manual" | "
|
|
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
|
-
|
|
38
|
+
wallet_warn: number;
|
|
39
|
+
wallet_stop: number;
|
|
39
40
|
};
|
|
40
41
|
fallbacks: {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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;
|
package/dist/server/config.js
CHANGED
|
@@ -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 = '
|
|
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
|
|
21
|
+
const DEFAULT_CONFIG_V6 = {
|
|
22
22
|
version: PKG_VERSION,
|
|
23
|
-
mode: '
|
|
23
|
+
mode: 'economy', // Défaut: economy (protège le wallet)
|
|
24
24
|
gui: { status: 'alert', logs: 'none' },
|
|
25
|
-
thresholds: {
|
|
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
|
-
|
|
28
|
-
|
|
31
|
+
economy: 'nova-fast',
|
|
32
|
+
pro: 'qwen-coder'
|
|
29
33
|
},
|
|
34
|
+
session: {},
|
|
30
35
|
enablePaidTools: false,
|
|
31
|
-
keyHasAccessToProfile: true,
|
|
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 = { ...
|
|
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 };
|
|
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
|
+
}
|
package/dist/server/proxy.js
CHANGED
|
@@ -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
|
-
//
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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(`[
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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(`[
|
|
310
|
-
actualModel = config.fallbacks.
|
|
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 = `
|
|
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
|
-
|
|
322
|
-
|
|
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
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
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 ->
|
|
567
|
-
actualModel = config.
|
|
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.
|
|
4
|
+
"version": "6.1.0-beta.1",
|
|
5
5
|
"description": "Native Pollinations.ai Provider Plugin for OpenCode",
|
|
6
6
|
"publisher": "pollinations",
|
|
7
7
|
"repository": {
|