opencode-pollinations-plugin 6.4.2 → 6.4.4

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.
@@ -656,6 +656,7 @@ ${t('commands.config.table_divider')}
656
656
  | **costThreshold**| \`${config.costThreshold ?? 0.15} 🌻\` | ${t('commands.config.costThreshold_role')} | \`/poll config costThreshold <X>\` |
657
657
  | **cost_estimator**| \`${config.costEstimator ?? true}\` | ${t('commands.config.cost_estimator_role')} | \`/poll config cost_estimator <true/false>\` |
658
658
  | **refillOverride**| \`${config.refillOverride ?? 'auto (déduit)'}\` | Quest Pollen refill horaire | \`/poll config refillOverride <0.01/0.15/0.4/0.8/10>\` |
659
+ | **questStashInFreeMode**| \`${config.questStashInFreeMode ?? true}\` | Compte le stash dans alwaysfree | \`/poll config questStashInFreeMode <true/false>\` |
659
660
  | **fallbacks.free.main** | \`${config.fallbacks?.free?.main || 'free/mistral'}\` | ${t('commands.config.fallback_main_role')} | \`/poll fallback <main> <agent>\` |
660
661
  | **fallbacks.free.agent** | \`${config.fallbacks?.free?.agent || 'free/openai-fast'}\`| ${t('commands.config.fallback_agent_role')} | \`/poll fallback <main> <agent>\` |
661
662
  | **fallbacks.enter.agent** | \`${config.fallbacks?.enter?.agent || 'free/openai-fast'}\`| ${t('commands.config.fallback_enter_role')} | *${t('commands.config.managed_auto')}* |
@@ -764,9 +765,14 @@ ${t('commands.config.table_divider')}
764
765
  saveConfig({ refillOverride: override });
765
766
  return { handled: true, response: `✅ refillOverride = ${override} 🌻/h` };
766
767
  }
768
+ if (key === 'questStashInFreeMode' && value) {
769
+ const enabled = value === 'true';
770
+ saveConfig({ questStashInFreeMode: enabled });
771
+ return { handled: true, response: `✅ questStashInFreeMode = ${enabled}${enabled ? ' (le stash Quest compte comme free)' : ' (seul le refill horaire compte)'}` };
772
+ }
767
773
  return {
768
774
  handled: true,
769
- error: `Clé inconnue: ${key}. Clés: status_gui, logs_gui, threshold_tier, threshold_wallet, status_bar, cost_estimator, enablePaidTools, costThreshold, costConfirmationRequired, refillOverride, lang`
775
+ error: `Clé inconnue: ${key}. Clés: status_gui, logs_gui, threshold_tier, threshold_wallet, status_bar, cost_estimator, enablePaidTools, costThreshold, costConfirmationRequired, refillOverride, questStashInFreeMode, lang`
770
776
  };
771
777
  }
772
778
  function handleHelpCommand() {
@@ -29,6 +29,7 @@ export interface PollinationsConfigV5 {
29
29
  statusBar: boolean;
30
30
  costEstimator: boolean;
31
31
  refillOverride?: number;
32
+ questStashInFreeMode?: boolean;
32
33
  lang?: string;
33
34
  }
34
35
  export declare function loadConfig(): PollinationsConfigV5;
@@ -60,6 +61,7 @@ export declare function saveConfig(updates: Partial<PollinationsConfigV5>): {
60
61
  statusBar: boolean;
61
62
  costEstimator: boolean;
62
63
  refillOverride?: number;
64
+ questStashInFreeMode?: boolean;
63
65
  lang?: string;
64
66
  };
65
67
  export declare function saveKeyToAuthJson(key: string): boolean;
@@ -82,6 +82,7 @@ const DEFAULT_CONFIG_V5 = {
82
82
  keyHasAccessToProfile: true, // Default true for legacy keys
83
83
  statusBar: true,
84
84
  costEstimator: true, // Show cost estimates by default
85
+ questStashInFreeMode: true, // Count quest stash as free in alwaysfree
85
86
  lang: 'en', // Default language is English
86
87
  };
87
88
  import { log as logSystem } from './logger.js';
@@ -87,12 +87,12 @@ export async function generatePollinationsConfig(forceApiKey, forceStrict = fals
87
87
  const enterList = ModelRegistry.list('text');
88
88
  const paidModels = [];
89
89
  enterList.forEach((m) => {
90
- if (m.tools === false)
91
- return; // OpenCode UI chat nécessite explicitement les tools
90
+ if (m.tools !== true || m.community === true)
91
+ return; // Exclude non-tool and community models
92
92
  const mapped = mapModel(m, 'enter/', '');
93
93
  modelsOutput.push(mapped);
94
94
  if (m.paid_only) {
95
- paidModels.push(mapped.id.replace('enter/', '')); // Store bare ID "gemini-large"
95
+ paidModels.push(mapped.id.replace('enter/', ''));
96
96
  }
97
97
  });
98
98
  log(`Total models (Free+Pro): ${modelsOutput.length}`);
@@ -207,7 +207,7 @@ function mapModel(raw, prefix, namePrefix) {
207
207
  }
208
208
  }
209
209
  // 2. REASONING VARIANTS — format @ai-sdk/openai-compatible: { reasoningEffort: "level" }
210
- if (raw.reasoning === true || rawId.includes('thinking') || rawId.includes('reasoning')) {
210
+ if (raw.reasoning === true || raw.capabilities?.includes('reasoning') || rawId.includes('thinking') || rawId.includes('reasoning')) {
211
211
  modelObj.variants = {
212
212
  ...modelObj.variants,
213
213
  low: { reasoningEffort: 'low' },
@@ -58,12 +58,11 @@ export class ToolRegistryWorker {
58
58
  videoTable += `| Modèle | Source I/O | Audio | 1 pollen ≈ | Specs |\n`;
59
59
  videoTable += `|--------|------------|-------|------------|-------|\n`;
60
60
  for (const m of videoModels) {
61
- // Estimation pour une vidéo moyenne de 6 secondes
62
61
  const cost = estimateVideoCost(m.name, 6);
63
62
  const price = cost ? `${per1pollen(cost)} vidéos` : 'inconnu';
64
- // durationRange et aspectRatios arrivent du fetcher (ou fallback)
65
63
  const specs = `${m.durationRange ? m.durationRange.join('-') + 's' : '?s'} / ${m.aspectRatios ? m.aspectRatios.length : '?'} ratios`;
66
- const badge = m.paid_only ? '[💎 Paid]' : '[🌿 Free]';
64
+ const isCommunity = m.community === true || m.name.includes('/');
65
+ const badge = isCommunity ? '[👥]' : (m.paid_only ? '[💎 Paid]' : '[🌿 Free]');
67
66
  videoTable += `| \`${m.name}\` ${badge} | ${m.supportsI2X ? 'T2V/I2V' : 'T2V'} | ${m.output_modalities?.includes('audio') || m.name === 'grok-video' ? '✅' : '❌'} | ${price} | ${specs} |\n`;
68
67
  }
69
68
  if (!polliGenVideoTool.description.includes('**🎬 Modèles Vidéo Détectés')) {
@@ -84,7 +83,8 @@ export class ToolRegistryWorker {
84
83
  for (const m of imageModels.slice(0, 20)) {
85
84
  const cost = estimateImageCost(m.name);
86
85
  const price = cost ? `${per1pollen(cost)} images` : 'inconnu';
87
- const badge = m.paid_only ? '[💎 Paid]' : '[🌿 Free]';
86
+ const isCommunity = m.community === true || m.name.includes('/');
87
+ const badge = isCommunity ? '[👥]' : (m.paid_only ? '[💎 Paid]' : '[🌿 Free]');
88
88
  imageTable += `| \`${m.name}\` ${badge} | ${m.supportsI2X ? '✅' : '❌'} | Standard | ${price} |\n`;
89
89
  }
90
90
  if (imageModels.length > 20) {
@@ -105,9 +105,9 @@ export class ToolRegistryWorker {
105
105
  audioTable += `| Modèle | Durée max | Qualité |\n`;
106
106
  audioTable += `|--------|-----------|---------|\n`;
107
107
  for (const m of audioModels) {
108
- const badge = m.paid_only ? '[💎 Paid]' : '[🌿 Free]';
108
+ const isCommunity = m.community === true || m.name.includes('/');
109
+ const badge = isCommunity ? '[👥]' : (m.paid_only ? '[💎 Paid]' : '[🌿 Free]');
109
110
  const duration = m.durationRange ? `${m.durationRange.join('-')}s` : 'Standard';
110
- // STT Whispers or TTS or Music
111
111
  audioTable += `| \`${m.name}\` ${badge} | ${duration} | Standard |\n`;
112
112
  }
113
113
  if (!polliGenAudioTool.description.includes('**🎵 Modèles Audio/Music Détectés')) {
@@ -138,7 +138,8 @@ export class ToolRegistryWorker {
138
138
  searchTable += `| Modèle | Description / Specs |\n`;
139
139
  searchTable += `|--------|---------------------|\n`;
140
140
  for (const m of searchModels) {
141
- const badge = m.paid_only ? '[💎 Paid]' : '[🌿 Free]';
141
+ const isCommunity = m.community === true || m.name.includes('/');
142
+ const badge = isCommunity ? '[👥]' : (m.paid_only ? '[💎 Paid]' : '[🌿 Free]');
142
143
  // Clean markdown piping conflicts
143
144
  let cleanDesc = m.description.replace(/\|/g, '-');
144
145
  // Adding "Specialized" hint
@@ -446,7 +446,13 @@ export async function handleChatCompletion(req, res, bodyRaw) {
446
446
  fallbackReason = t('proxy.warnings.quota_unreachable_msg');
447
447
  }
448
448
  else {
449
- const tierRatio = quota.tierLimit > 0 ? (quota.tierRemaining / quota.tierLimit) : 0;
449
+ const effectiveFree = config.questStashInFreeMode !== false
450
+ ? quota.tierRemaining + (quota.questStash || 0)
451
+ : quota.tierRemaining;
452
+ const effectiveLimit = config.questStashInFreeMode !== false
453
+ ? quota.tierLimit + (quota.questStash || 0)
454
+ : quota.tierLimit;
455
+ const tierRatio = effectiveLimit > 0 ? (effectiveFree / effectiveLimit) : 0;
450
456
  if (tierRatio <= (config.thresholds.tier / 100)) {
451
457
  log(`[SafetyNet] AlwaysFree Mode: Tier (${(tierRatio * 100).toFixed(1)}%) <= Threshold (${config.thresholds.tier}%). Switching.`);
452
458
  emitStatusToast('warning', t('proxy.warnings.tier_limit_title', { threshold: config.thresholds.tier }), 'AlwaysFree Mode');
@@ -3,6 +3,7 @@ export interface QuotaStatus {
3
3
  tierRemaining: number;
4
4
  tierUsed: number;
5
5
  tierLimit: number;
6
+ questStash: number;
6
7
  walletBalance: number;
7
8
  nextResetAt: Date;
8
9
  timeUntilReset: number;
@@ -16,3 +17,8 @@ export interface QuotaStatus {
16
17
  export declare function fetchUsageForPeriod(apiKey: string, lastReset: Date): Promise<DetailedUsageEntry[]>;
17
18
  export declare function getQuotaStatus(forceRefresh?: boolean): Promise<QuotaStatus>;
18
19
  export declare function formatQuotaForToast(quota: QuotaStatus): string;
20
+ export declare function fetchQuestStash(apiKey: string): Promise<{
21
+ questStash: number;
22
+ claimedQuestTier: number;
23
+ tierConsumedSinceClaim: number;
24
+ }>;
@@ -4,6 +4,9 @@ import { loadConfig } from './config.js';
4
4
  const CACHE_TTL = 30000;
5
5
  let cachedQuota = null;
6
6
  let lastQuotaFetch = 0;
7
+ const STASH_CACHE_TTL = 5 * 60 * 1000; // 5 min — le stash Quest change rarement
8
+ let cachedStash = null;
9
+ let lastStashFetch = 0;
7
10
  const ONE_HOUR_MS = 60 * 60 * 1000;
8
11
  const KNOWN_REFILLS = [
9
12
  { pollen: 0, emoji: '👤', label: 'anonymous' },
@@ -177,10 +180,21 @@ export async function getQuotaStatus(forceRefresh = false) {
177
180
  const tierNeedsAlert = tierLimit > 0 && tierAlertPercent <= config.thresholds.tier;
178
181
  const walletNeedsAlert = cleanWalletBalance > 0 && cleanWalletBalance < (config.thresholds.wallet || 0.5);
179
182
  logQuota(`Fetch Success. Allowance: ${allowance}, Paid: ${cleanWalletBalance}, Total: ${totalBalance}`);
183
+ // Fetch quest stash (cached 5 min)
184
+ const nowStash = Date.now();
185
+ if (!cachedStash || (nowStash - lastStashFetch) > STASH_CACHE_TTL) {
186
+ try {
187
+ cachedStash = await fetchQuestStash(config.apiKey);
188
+ lastStashFetch = nowStash;
189
+ }
190
+ catch { /* keep previous */ }
191
+ }
192
+ const questStash = cachedStash?.questStash ?? 0;
180
193
  cachedQuota = {
181
194
  tierRemaining: cleanTierRemaining,
182
195
  tierUsed,
183
196
  tierLimit,
197
+ questStash,
184
198
  walletBalance: cleanWalletBalance,
185
199
  nextResetAt: resetInfo.nextReset,
186
200
  timeUntilReset: resetInfo.timeUntilReset,
@@ -209,6 +223,7 @@ function createDefaultQuota(tierName, limit) {
209
223
  tierRemaining: 0,
210
224
  tierUsed: 0,
211
225
  tierLimit: limit,
226
+ questStash: 0,
212
227
  walletBalance: 0,
213
228
  nextResetAt: new Date(),
214
229
  timeUntilReset: 0,
@@ -302,5 +317,37 @@ export function formatQuotaForToast(quota) {
302
317
  const hours = Math.floor(ms / (1000 * 60 * 60));
303
318
  const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
304
319
  const resetIn = `${hours}h${minutes}m`;
305
- return `${quota.tierEmoji} Quest: ${quota.tierRemaining.toFixed(2)}/${quota.tierLimit} (${tierPercent}%) | 💎 Paid: $${quota.walletBalance.toFixed(2)} | ⏰ Reset: ${resetIn}`;
320
+ const stashStr = quota.questStash > 0
321
+ ? ` | 🎁 ~${quota.questStash.toFixed(2)} (stash)`
322
+ : '';
323
+ return `${quota.tierEmoji} ${quota.tierRemaining.toFixed(2)}/${quota.tierLimit} (${tierPercent}%)${stashStr} | 💎 $${quota.walletBalance.toFixed(2)} | ⏰ ${resetIn}`;
324
+ }
325
+ export async function fetchQuestStash(apiKey) {
326
+ let claimedQuestTier = 0;
327
+ let firstClaimMs = Infinity;
328
+ let tierConsumedSinceClaim = 0;
329
+ try {
330
+ const qres = await fetchAPI('/account/quests', apiKey);
331
+ for (const q of (qres?.quests || [])) {
332
+ const r = q.reward;
333
+ if (r && r.claimedAt && r.balanceBucket === 'tier') {
334
+ claimedQuestTier += (r.pollenAmount || 0);
335
+ const cms = new Date(r.claimedAt).getTime();
336
+ if (!isNaN(cms) && cms < firstClaimMs)
337
+ firstClaimMs = cms;
338
+ }
339
+ }
340
+ if (claimedQuestTier > 0 && isFinite(firstClaimMs)) {
341
+ const ures = await fetchAPI('/account/usage?limit=500', apiKey);
342
+ for (const e of (ures?.usage || [])) {
343
+ const ts = new Date(String(e.timestamp).replace(' ', 'T') + (String(e.timestamp).includes('Z') ? '' : 'Z')).getTime();
344
+ if (e.meter_source === 'tier' && !isNaN(ts) && ts >= firstClaimMs) {
345
+ tierConsumedSinceClaim += (e.cost_usd || 0);
346
+ }
347
+ }
348
+ }
349
+ }
350
+ catch { /* fallback gracefully */ }
351
+ const questStash = Math.max(0, claimedQuestTier - tierConsumedSinceClaim);
352
+ return { questStash, claimedQuestTier, tierConsumedSinceClaim };
306
353
  }
@@ -37,7 +37,8 @@ Use 'action=update' to change these. NEVER confuse Chat Mode with Tools Protecti
37
37
  thresholdsTier: tool.schema.number().optional().describe('Warning threshold PERCENTAGE (e.g. 10 for 10%) for Free Tier.'),
38
38
  thresholdsWallet: tool.schema.number().optional().describe('Warning threshold PERCENTAGE (e.g. 50 for 50%) for Wallet balance.'),
39
39
  lang: tool.schema.enum(['en', 'fr', 'es', 'de', 'it', 'zh']).optional().describe('Plugin language for commands and toasts (en, fr, es, de, it, zh).'),
40
- refillOverride: tool.schema.number().optional().describe('Manual Quest Pollen hourly refill override (0.01, 0.15, 0.4, 0.8, or 10). Set to 0 for auto-deduction.')
40
+ refillOverride: tool.schema.number().optional().describe('Manual Quest Pollen hourly refill override (0.01, 0.15, 0.4, 0.8, or 10). Set to 0 for auto-deduction.'),
41
+ questStashInFreeMode: tool.schema.boolean().optional().describe('Count accumulated quest stash as free pollen in alwaysfree Safety Net (default: true).')
41
42
  },
42
43
  async execute(args, context) {
43
44
  if (args.action === 'view') {
@@ -67,6 +68,8 @@ Use 'action=update' to change these. NEVER confuse Chat Mode with Tools Protecti
67
68
  if (args.refillOverride !== undefined) {
68
69
  updates.refillOverride = args.refillOverride === 0 ? undefined : args.refillOverride;
69
70
  }
71
+ if (args.questStashInFreeMode !== undefined)
72
+ updates.questStashInFreeMode = args.questStashInFreeMode;
70
73
  if (args.lang !== undefined)
71
74
  updates.lang = args.lang;
72
75
  if (args.thresholdsTier !== undefined || args.thresholdsWallet !== undefined) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-pollinations-plugin",
3
3
  "displayName": "Pollinations",
4
- "version": "6.4.2",
4
+ "version": "6.4.4",
5
5
  "description": "Native Pollinations.ai Provider Plugin for OpenCode",
6
6
  "publisher": "pollinations",
7
7
  "repository": {