opencode-pollinations-plugin 6.4.10 → 6.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.de.md +67 -55
  2. package/README.es.md +79 -67
  3. package/README.fr.md +70 -58
  4. package/README.it.md +78 -66
  5. package/README.md +33 -29
  6. package/README.zh.md +77 -65
  7. package/dist/locales/de.json +82 -50
  8. package/dist/locales/en.json +84 -52
  9. package/dist/locales/es.json +82 -50
  10. package/dist/locales/fr.json +81 -49
  11. package/dist/locales/it.json +82 -50
  12. package/dist/locales/zh.json +82 -50
  13. package/dist/server/commands.js +112 -110
  14. package/dist/server/config.d.ts +24 -6
  15. package/dist/server/config.js +45 -4
  16. package/dist/server/connect-response.js +7 -7
  17. package/dist/server/generate-config.d.ts +7 -0
  18. package/dist/server/generate-config.js +22 -16
  19. package/dist/server/models/cache.d.ts +23 -10
  20. package/dist/server/models/cache.js +46 -24
  21. package/dist/server/models/fetcher.js +9 -0
  22. package/dist/server/models/types.d.ts +8 -0
  23. package/dist/server/models/worker.js +32 -8
  24. package/dist/server/proxy.d.ts +6 -0
  25. package/dist/server/proxy.js +297 -210
  26. package/dist/server/quota.d.ts +23 -32
  27. package/dist/server/quota.js +45 -185
  28. package/dist/server/status.js +1 -2
  29. package/dist/server/toast.js +1 -1
  30. package/dist/tools/index.d.ts +2 -1
  31. package/dist/tools/index.js +8 -3
  32. package/dist/tools/pollinations/artifact-core.d.ts +53 -0
  33. package/dist/tools/pollinations/artifact-core.js +159 -0
  34. package/dist/tools/pollinations/beta_discovery.js +2 -1
  35. package/dist/tools/pollinations/cost-guard.d.ts +2 -2
  36. package/dist/tools/pollinations/error-parser.d.ts +38 -0
  37. package/dist/tools/pollinations/error-parser.js +112 -0
  38. package/dist/tools/pollinations/gen_3d.d.ts +17 -0
  39. package/dist/tools/pollinations/gen_3d.js +207 -0
  40. package/dist/tools/pollinations/gen_image.js +29 -10
  41. package/dist/tools/pollinations/gen_music.js +3 -2
  42. package/dist/tools/pollinations/gen_video.js +13 -2
  43. package/dist/tools/pollinations/polli_config.js +18 -21
  44. package/dist/tools/pollinations/polli_gen_confirm.js +2 -0
  45. package/dist/tools/pollinations/shared.d.ts +1 -1
  46. package/dist/tools/pollinations/shared.js +53 -142
  47. package/dist/tools/pollinations/timeout-policy.d.ts +80 -0
  48. package/dist/tools/pollinations/timeout-policy.js +124 -0
  49. package/dist/tools/pollinations/tool-capability-registry.d.ts +51 -0
  50. package/dist/tools/pollinations/tool-capability-registry.js +215 -0
  51. package/dist/tools/pollinations/transcribe_audio.js +5 -24
  52. package/package.json +6 -4
  53. package/dist/server/tier-info.d.ts +0 -36
  54. package/dist/server/tier-info.js +0 -107
@@ -3,33 +3,46 @@
3
3
  *
4
4
  * Central access point for all model metadata. Backed by the fetcher
5
5
  * with a configurable TTL. Falls back to static data if fetch fails.
6
+ *
7
+ * v6.5: every read path (get/list/all) triggers a coalesced, non-blocking
8
+ * freshness check, so long sessions always converge to the live catalog.
6
9
  */
7
10
  import type { PollinationsModel, ModelCategory, ModelRegistryInterface } from './types.js';
8
- declare class ModelRegistryImpl implements ModelRegistryInterface {
11
+ export type RegistryFetcher = (apiKey?: string) => Promise<PollinationsModel[]>;
12
+ export interface RegistryOptions {
13
+ ttlMs?: number;
14
+ fetcher?: RegistryFetcher;
15
+ diskCache?: boolean;
16
+ }
17
+ export declare class ModelRegistryImpl implements ModelRegistryInterface {
9
18
  private models;
10
19
  private lastRefresh;
11
20
  private ttl;
12
21
  private ready;
13
22
  private refreshing;
14
- constructor();
15
- /** Get a single model by category and name */
23
+ private fetcher;
24
+ private useDiskCache;
25
+ constructor(options?: RegistryOptions);
26
+ /** Get a single model by category and name. Triggers lazy freshness check. */
16
27
  get(category: ModelCategory, name: string): PollinationsModel | undefined;
17
- /** Also search by alias */
28
+ /** Also search by alias. Triggers lazy freshness check. */
18
29
  getByNameOrAlias(category: ModelCategory, name: string): PollinationsModel | undefined;
19
- /** List all models in a category */
30
+ /** List all models in a category. Triggers lazy freshness check. */
20
31
  list(category: ModelCategory): PollinationsModel[];
21
32
  /** Check if registry has been populated */
22
33
  isReady(): boolean;
23
34
  /** Check if cache is stale */
24
35
  isStale(): boolean;
25
- /** Force refresh from API */
36
+ /** Timestamp of last successful refresh (for tests/diagnostics). */
37
+ lastRefreshAt(): number;
38
+ /** Force refresh from API. Concurrent calls are coalesced on one fetch. */
26
39
  refresh(apiKey?: string): Promise<void>;
27
- /** Get all models across all categories */
40
+ private performRefresh;
41
+ /** Get all models across all categories. Triggers lazy freshness check. */
28
42
  all(): PollinationsModel[];
29
- /** Auto-refresh if stale (non-blocking) */
30
- ensureFresh(): void;
43
+ /** Auto-refresh if stale (non-blocking). Returns the refresh promise for tests. */
44
+ ensureFresh(): Promise<void>;
31
45
  /** Get count per category (for logging) */
32
46
  stats(): Record<ModelCategory, number>;
33
47
  }
34
48
  export declare const ModelRegistry: ModelRegistryImpl;
35
- export {};
@@ -3,6 +3,9 @@
3
3
  *
4
4
  * Central access point for all model metadata. Backed by the fetcher
5
5
  * with a configurable TTL. Falls back to static data if fetch fails.
6
+ *
7
+ * v6.5: every read path (get/list/all) triggers a coalesced, non-blocking
8
+ * freshness check, so long sessions always converge to the live catalog.
6
9
  */
7
10
  import * as fs from 'fs';
8
11
  import * as path from 'path';
@@ -60,31 +63,41 @@ function saveCacheToDisk(models, timestamp) {
60
63
  }
61
64
  }
62
65
  // ─── Registry Implementation ─────────────────────────────────────────────
63
- class ModelRegistryImpl {
66
+ export class ModelRegistryImpl {
64
67
  models = [];
65
68
  lastRefresh = 0;
66
69
  ttl = DEFAULT_TTL;
67
70
  ready = false;
68
- refreshing = false;
69
- constructor() {
70
- const diskCache = loadCacheFromDisk();
71
- if (diskCache && (Date.now() - diskCache.timestamp) < this.ttl) {
72
- this.models = diskCache.models;
73
- this.lastRefresh = diskCache.timestamp;
74
- this.ready = true;
75
- log(`[ModelRegistry] Loaded ${this.models.length} models from disk cache.`);
71
+ refreshing = null;
72
+ fetcher;
73
+ useDiskCache;
74
+ constructor(options = {}) {
75
+ this.ttl = options.ttlMs ?? DEFAULT_TTL;
76
+ this.fetcher = options.fetcher ?? ((apiKey) => fetchAllModels(apiKey));
77
+ this.useDiskCache = options.diskCache ?? true;
78
+ if (this.useDiskCache) {
79
+ const diskCache = loadCacheFromDisk();
80
+ if (diskCache && (Date.now() - diskCache.timestamp) < this.ttl) {
81
+ this.models = diskCache.models;
82
+ this.lastRefresh = diskCache.timestamp;
83
+ this.ready = true;
84
+ log(`[ModelRegistry] Loaded ${this.models.length} models from disk cache.`);
85
+ }
76
86
  }
77
87
  }
78
- /** Get a single model by category and name */
88
+ /** Get a single model by category and name. Triggers lazy freshness check. */
79
89
  get(category, name) {
90
+ this.ensureFresh().catch(() => { });
80
91
  return this.models.find(m => m.category === category && m.name === name);
81
92
  }
82
- /** Also search by alias */
93
+ /** Also search by alias. Triggers lazy freshness check. */
83
94
  getByNameOrAlias(category, name) {
95
+ this.ensureFresh().catch(() => { });
84
96
  return this.models.find(m => m.category === category && (m.name === name || m.aliases.includes(name)));
85
97
  }
86
- /** List all models in a category */
98
+ /** List all models in a category. Triggers lazy freshness check. */
87
99
  list(category) {
100
+ this.ensureFresh().catch(() => { });
88
101
  return this.models.filter(m => m.category === category);
89
102
  }
90
103
  /** Check if registry has been populated */
@@ -95,19 +108,29 @@ class ModelRegistryImpl {
95
108
  isStale() {
96
109
  return Date.now() - this.lastRefresh > this.ttl;
97
110
  }
98
- /** Force refresh from API */
99
- async refresh(apiKey) {
111
+ /** Timestamp of last successful refresh (for tests/diagnostics). */
112
+ lastRefreshAt() {
113
+ return this.lastRefresh;
114
+ }
115
+ /** Force refresh from API. Concurrent calls are coalesced on one fetch. */
116
+ refresh(apiKey) {
100
117
  if (this.refreshing)
101
- return; // Prevent concurrent refreshes
102
- this.refreshing = true;
118
+ return this.refreshing; // coalesce concurrent refreshes
119
+ this.refreshing = this.performRefresh(apiKey).finally(() => {
120
+ this.refreshing = null;
121
+ });
122
+ return this.refreshing;
123
+ }
124
+ async performRefresh(apiKey) {
103
125
  try {
104
126
  const key = apiKey || loadConfig().apiKey;
105
- const fetched = await fetchAllModels(key);
127
+ const fetched = await this.fetcher(key);
106
128
  if (fetched.length > 0) {
107
129
  this.models = fetched;
108
130
  this.lastRefresh = Date.now();
109
131
  this.ready = true;
110
- saveCacheToDisk(this.models, this.lastRefresh);
132
+ if (this.useDiskCache)
133
+ saveCacheToDisk(this.models, this.lastRefresh);
111
134
  log(`[ModelRegistry] Refreshed: ${this.models.length} models cached to disk.`);
112
135
  }
113
136
  else {
@@ -132,19 +155,18 @@ class ModelRegistryImpl {
132
155
  log(`[ModelRegistry] Refresh failed, keeping cache: ${e}`);
133
156
  }
134
157
  }
135
- finally {
136
- this.refreshing = false;
137
- }
138
158
  }
139
- /** Get all models across all categories */
159
+ /** Get all models across all categories. Triggers lazy freshness check. */
140
160
  all() {
161
+ this.ensureFresh().catch(() => { });
141
162
  return [...this.models];
142
163
  }
143
- /** Auto-refresh if stale (non-blocking) */
164
+ /** Auto-refresh if stale (non-blocking). Returns the refresh promise for tests. */
144
165
  ensureFresh() {
145
166
  if (this.isStale()) {
146
- this.refresh().catch(() => { }); // Fire-and-forget
167
+ return this.refresh(); // coalesced; refresh() never rejects (handles offline)
147
168
  }
169
+ return Promise.resolve();
148
170
  }
149
171
  /** Get count per category (for logging) */
150
172
  stats() {
@@ -93,6 +93,9 @@ function mapRawToModel(raw, fallbackCategory, averageCost) {
93
93
  voices: raw.voices,
94
94
  tools: raw.tools,
95
95
  reasoning: raw.reasoning,
96
+ reasoning_options: raw.reasoning_options,
97
+ interleaved: raw.interleaved,
98
+ capabilities: raw.capabilities,
96
99
  is_specialized: raw.is_specialized,
97
100
  context_window: raw.context_window || raw.context_length,
98
101
  averageCost: averageCost !== undefined && !isNaN(averageCost) ? averageCost : undefined,
@@ -179,6 +182,12 @@ export async function fetchAllModels(apiKey) {
179
182
  item.tools = v1Item.tools;
180
183
  if (v1Item.reasoning !== undefined)
181
184
  item.reasoning = v1Item.reasoning;
185
+ if (v1Item.reasoning_options !== undefined)
186
+ item.reasoning_options = v1Item.reasoning_options;
187
+ if (v1Item.interleaved !== undefined)
188
+ item.interleaved = v1Item.interleaved;
189
+ if (v1Item.capabilities !== undefined)
190
+ item.capabilities = v1Item.capabilities;
182
191
  }
183
192
  const model = mapRawToModel(item, res.fallbackCategory, avgCost);
184
193
  const uniqueId = model.name;
@@ -33,6 +33,14 @@ export interface PollinationsModel {
33
33
  voices?: string[];
34
34
  tools?: boolean;
35
35
  reasoning?: boolean;
36
+ reasoning_options?: Array<{
37
+ type: 'effort' | 'toggle';
38
+ values?: string[];
39
+ }>;
40
+ interleaved?: {
41
+ field: 'reasoning' | 'reasoning_content' | 'reasoning_details';
42
+ };
43
+ capabilities?: string[];
36
44
  is_specialized?: boolean;
37
45
  context_window?: number;
38
46
  input_modalities: string[];
@@ -2,6 +2,7 @@ import { polliGenImageTool } from '../../tools/pollinations/gen_image.js';
2
2
  import { polliGenVideoTool } from '../../tools/pollinations/gen_video.js';
3
3
  import { polliGenAudioTool } from '../../tools/pollinations/gen_audio.js';
4
4
  import { polliGenMusicTool } from '../../tools/pollinations/gen_music.js';
5
+ import { polliGen3dTool } from '../../tools/pollinations/gen_3d.js';
5
6
  import { polliWebSearchTool } from '../../tools/pollinations/polli_web_search.js';
6
7
  import { ModelRegistry } from './index.js';
7
8
  import { log } from '../logger.js';
@@ -54,7 +55,7 @@ export class ToolRegistryWorker {
54
55
  const videoModels = ModelRegistry.list('video');
55
56
  if (videoModels.length > 0 && polliGenVideoTool.description) {
56
57
  let videoTable = `\n\n**🎬 Modèles Vidéo Détectés (${videoModels.length}) :**\n`;
57
- videoTable += `*(Légende: [💎 Paid] = Enter Tier (Consomme le Wallet USD), [🌿 Free] = Free Tier (Consomme d'abord le Quota Pollen Gratuit Journalier))*\n`;
58
+ videoTable += `*(Légende: [💎 Paid] = Pollen acheté (consomme le Wallet USD), [🌿 Quest] = appel éligible au Quest; en mode quest_only, fallback Free avant le plancher Quest)*\n`;
58
59
  videoTable += `| Modèle | Source I/O | Audio | 1 pollen ≈ | Specs |\n`;
59
60
  videoTable += `|--------|------------|-------|------------|-------|\n`;
60
61
  for (const m of videoModels) {
@@ -62,7 +63,7 @@ export class ToolRegistryWorker {
62
63
  const price = cost ? `${per1pollen(cost)} vidéos` : 'inconnu';
63
64
  const specs = `${m.durationRange ? m.durationRange.join('-') + 's' : '?s'} / ${m.aspectRatios ? m.aspectRatios.length : '?'} ratios`;
64
65
  const isCommunity = m.community === true || m.name.includes('/');
65
- const badge = isCommunity ? '[👥]' : (m.paid_only ? '[💎 Paid]' : '[🌿 Free]');
66
+ const badge = isCommunity ? '[👥]' : (m.paid_only ? '[💎 Paid]' : '[🌿 Quest]');
66
67
  videoTable += `| \`${m.name}\` ${badge} | ${m.supportsI2X ? 'T2V/I2V' : 'T2V'} | ${m.output_modalities?.includes('audio') || m.name === 'grok-video' ? '✅' : '❌'} | ${price} | ${specs} |\n`;
67
68
  }
68
69
  if (!polliGenVideoTool.description.includes('**🎬 Modèles Vidéo Détectés')) {
@@ -76,7 +77,7 @@ export class ToolRegistryWorker {
76
77
  const imageModels = ModelRegistry.list('image');
77
78
  if (imageModels.length > 0 && polliGenImageTool.description) {
78
79
  let imageTable = `\n\n**🎨 Modèles Image Détectés (${imageModels.length}) :**\n`;
79
- imageTable += `*(Légende: [💎 Paid] = Enter Tier (Consomme le Wallet USD), [🌿 Free] = Free Tier (Consomme d'abord le Quota Pollen Gratuit Journalier))*\n`;
80
+ imageTable += `*(Légende: [💎 Paid] = Pollen acheté (consomme le Wallet USD), [🌿 Quest] = appel éligible au Quest; en mode quest_only, fallback Free avant le plancher Quest)*\n`;
80
81
  imageTable += `| Modèle | I2I | Qualité | 1 pollen ≈ |\n`;
81
82
  imageTable += `|--------|-----|---------|------------|\n`;
82
83
  // Afficher max 20 modèles pour éviter de saturer le prompt LLM
@@ -84,7 +85,7 @@ export class ToolRegistryWorker {
84
85
  const cost = estimateImageCost(m.name);
85
86
  const price = cost ? `${per1pollen(cost)} images` : 'inconnu';
86
87
  const isCommunity = m.community === true || m.name.includes('/');
87
- const badge = isCommunity ? '[👥]' : (m.paid_only ? '[💎 Paid]' : '[🌿 Free]');
88
+ const badge = isCommunity ? '[👥]' : (m.paid_only ? '[💎 Paid]' : '[🌿 Quest]');
88
89
  imageTable += `| \`${m.name}\` ${badge} | ${m.supportsI2X ? '✅' : '❌'} | Standard | ${price} |\n`;
89
90
  }
90
91
  if (imageModels.length > 20) {
@@ -101,12 +102,12 @@ export class ToolRegistryWorker {
101
102
  const audioModels = ModelRegistry.list('audio');
102
103
  if (audioModels.length > 0 && polliGenAudioTool.description && polliGenMusicTool.description) {
103
104
  let audioTable = `\n\n**🎵 Modèles Audio/Music Détectés (${audioModels.length}) :**\n`;
104
- audioTable += `*(Légende: [💎 Paid] = Enter Tier (Consomme le Wallet USD), [🌿 Free] = Free Tier (Consomme d'abord le Quota Pollen Gratuit Journalier))*\n`;
105
+ audioTable += `*(Légende: [💎 Paid] = Pollen acheté (consomme le Wallet USD), [🌿 Quest] = appel éligible au Quest; en mode quest_only, fallback Free avant le plancher Quest)*\n`;
105
106
  audioTable += `| Modèle | Durée max | Qualité |\n`;
106
107
  audioTable += `|--------|-----------|---------|\n`;
107
108
  for (const m of audioModels) {
108
109
  const isCommunity = m.community === true || m.name.includes('/');
109
- const badge = isCommunity ? '[👥]' : (m.paid_only ? '[💎 Paid]' : '[🌿 Free]');
110
+ const badge = isCommunity ? '[👥]' : (m.paid_only ? '[💎 Paid]' : '[🌿 Quest]');
110
111
  const duration = m.durationRange ? `${m.durationRange.join('-')}s` : 'Standard';
111
112
  audioTable += `| \`${m.name}\` ${badge} | ${duration} | Standard |\n`;
112
113
  }
@@ -123,6 +124,29 @@ export class ToolRegistryWorker {
123
124
  }
124
125
  }
125
126
  }
127
+ // -- Patch 3D Tool (fully dynamic from ModelRegistry) --
128
+ const threeDModels = ModelRegistry.list('3d');
129
+ if (threeDModels.length > 0 && polliGen3dTool.description) {
130
+ let threeDTable = `\n\n**🧊 Live 3D models (${threeDModels.length})**\n`;
131
+ threeDTable += `| Model | Access | Inputs | Output | Estimated cost |\n`;
132
+ threeDTable += `|-------|--------|--------|--------|----------------|\n`;
133
+ for (const m of threeDModels) {
134
+ const access = m.paid_only ? 'Paid only' : 'Quest eligible';
135
+ const inputs = m.input_modalities.join(', ') || 'text';
136
+ const cost = m.averageCost
137
+ ?? m.pricing.completionImageTokens
138
+ ?? m.pricing.completionTextTokens;
139
+ const costText = typeof cost === 'number' ? `${cost.toFixed(4)} pollen` : 'live pricing';
140
+ threeDTable += `| \`${m.name}\` | ${access} | ${inputs} | GLB | ${costText} |\n`;
141
+ }
142
+ const sectionMarker = '**🧊 Live 3D models';
143
+ if (!polliGen3dTool.description.includes(sectionMarker)) {
144
+ polliGen3dTool.description += threeDTable;
145
+ }
146
+ else {
147
+ polliGen3dTool.description = polliGen3dTool.description.split(sectionMarker)[0] + threeDTable;
148
+ }
149
+ }
126
150
  // -- Patch WEB SEARCH Tool --
127
151
  const textModels = ModelRegistry.list('text');
128
152
  if (textModels.length > 0 && polliWebSearchTool.description) {
@@ -134,12 +158,12 @@ export class ToolRegistryWorker {
134
158
  });
135
159
  if (searchModels.length > 0) {
136
160
  let searchTable = `\n\n**🌍 Modèles de Recherche & Grounding Détectés (${searchModels.length}) :**\n`;
137
- searchTable += `*(Légende: [💎 Paid] = Enter Tier, [🌿 Free] = Quota Gratuit. Obligatoire de choisir un modèle exact au lieu de 'deep' ou 'rapid' !)*\n`;
161
+ searchTable += `*(Légende: [💎 Paid] = Pollen acheté, [🌿 Quest] = appel éligible au Quest; quest_only protège le wallet Paid. Obligatoire de choisir un modèle exact au lieu de 'deep' ou 'rapid' !)*\n`;
138
162
  searchTable += `| Modèle | Description / Specs |\n`;
139
163
  searchTable += `|--------|---------------------|\n`;
140
164
  for (const m of searchModels) {
141
165
  const isCommunity = m.community === true || m.name.includes('/');
142
- const badge = isCommunity ? '[👥]' : (m.paid_only ? '[💎 Paid]' : '[🌿 Free]');
166
+ const badge = isCommunity ? '[👥]' : (m.paid_only ? '[💎 Paid]' : '[🌿 Quest]');
143
167
  // Clean markdown piping conflicts
144
168
  let cleanDesc = m.description.replace(/\|/g, '-');
145
169
  // Adding "Specialized" hint
@@ -1,2 +1,8 @@
1
1
  import * as http from 'http';
2
+ type RetrySignal = 'abort' | 'network' | number;
3
+ export declare function classifyRetry(signal: RetrySignal): 'RETRY' | 'NO_RETRY';
4
+ export declare function fetchWithRetry(url: string, options: any, retries?: number): Promise<Response>;
5
+ /** Normalize a single raw SSE `data:` payload line (JSON). Non-JSON passes through. */
6
+ export declare function normalizeChunkLine(payload: string): string;
2
7
  export declare function handleChatCompletion(req: http.IncomingMessage, res: http.ServerResponse, bodyRaw: string): Promise<void>;
8
+ export {};