opencode-pollinations-plugin 6.0.0 → 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.
@@ -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",
4
+ "version": "6.1.0-beta.1",
5
5
  "description": "Native Pollinations.ai Provider Plugin for OpenCode",
6
6
  "publisher": "pollinations",
7
7
  "repository": {