opencode-pollinations-plugin 5.8.3 → 5.8.4-beta.10

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/dist/index.js CHANGED
@@ -100,11 +100,13 @@ export const PollinationsPlugin = async (ctx) => {
100
100
  const version = require('../package.json').version;
101
101
  config.provider['pollinations'] = {
102
102
  id: 'pollinations',
103
+ npm: require('../package.json').name,
103
104
  name: `Pollinations AI (v${version})`,
104
105
  options: { baseURL: localBaseUrl },
105
106
  models: modelsObj
106
107
  };
107
108
  log(`[Hook] Registered ${Object.keys(modelsObj).length} models.`);
109
+ log(`[Hook] Keys: ${Object.keys(modelsObj).join(', ')}`);
108
110
  },
109
111
  ...toastHooks,
110
112
  ...createStatusHooks(ctx.client),
@@ -1,8 +1,6 @@
1
1
  interface OpenCodeModel {
2
2
  id: string;
3
3
  name: string;
4
- object: string;
5
- variants?: any;
6
4
  options?: any;
7
5
  limit?: {
8
6
  context?: number;
@@ -12,6 +10,7 @@ interface OpenCodeModel {
12
10
  input?: string[];
13
11
  output?: string[];
14
12
  };
13
+ tool_call?: boolean;
15
14
  }
16
15
  export declare function generatePollinationsConfig(forceApiKey?: string, forceStrict?: boolean): Promise<OpenCodeModel[]>;
17
16
  export {};
@@ -7,15 +7,8 @@ const HOMEDIR = os.homedir();
7
7
  const CONFIG_DIR_POLLI = path.join(HOMEDIR, '.pollinations');
8
8
  const CACHE_FILE = path.join(CONFIG_DIR_POLLI, 'models-cache.json');
9
9
  // --- CONSTANTS ---
10
- // Seed from _archs/debug_free.json
11
- const DEFAULT_FREE_MODELS = [
12
- { "name": "gemini", "description": "Gemini 2.5 Flash Lite", "tier": "anonymous", "tools": true, "input_modalities": ["text", "image"], "output_modalities": ["text"], "vision": true },
13
- { "name": "mistral", "description": "Mistral Small 3.2 24B", "tier": "anonymous", "tools": true, "input_modalities": ["text"], "output_modalities": ["text"], "vision": false },
14
- { "name": "openai-fast", "description": "GPT-OSS 20B Reasoning LLM (OVH)", "tier": "anonymous", "tools": true, "input_modalities": ["text"], "output_modalities": ["text"], "vision": false, "reasoning": true },
15
- { "name": "bidara", "description": "BIDARA (Biomimetic Designer)", "tier": "anonymous", "community": true, "input_modalities": ["text", "image"], "output_modalities": ["text"], "vision": true },
16
- { "name": "chickytutor", "description": "ChickyTutor AI Language Tutor", "tier": "anonymous", "community": true, "input_modalities": ["text"], "output_modalities": ["text"] },
17
- { "name": "midijourney", "description": "MIDIjourney", "tier": "anonymous", "community": true, "input_modalities": ["text"], "output_modalities": ["text"] }
18
- ];
10
+ // Seed from models-seed.ts
11
+ import { FREE_MODELS_SEED } from './models-seed.js';
19
12
  // --- LOGGING ---
20
13
  const LOG_FILE = '/tmp/opencode_pollinations_config.log';
21
14
  function log(msg) {
@@ -28,23 +21,39 @@ function log(msg) {
28
21
  catch (e) { }
29
22
  }
30
23
  // --- NETWORK HELPER ---
24
+ function fetchHead(url) {
25
+ return new Promise((resolve) => {
26
+ // Use Node.js native https check for minimal overhead
27
+ const req = https.request(url, { method: 'HEAD', timeout: 5000 }, (res) => {
28
+ resolve(res.headers['etag'] || null);
29
+ });
30
+ req.on('error', () => resolve(null));
31
+ req.on('timeout', () => { req.destroy(); resolve(null); });
32
+ req.end();
33
+ });
34
+ }
31
35
  function fetchJson(url, headers = {}) {
32
36
  return new Promise((resolve, reject) => {
33
37
  const finalHeaders = {
34
38
  ...headers,
35
- 'User-Agent': 'Mozilla/5.0 (compatible; OpenCode/5.8.2; +https://opencode.ai)'
39
+ 'User-Agent': 'Mozilla/5.0 (compatible; OpenCode/5.8.4; +https://opencode.ai)'
36
40
  };
37
41
  const req = https.get(url, { headers: finalHeaders }, (res) => {
42
+ const etag = res.headers['etag'];
38
43
  let data = '';
39
44
  res.on('data', chunk => data += chunk);
40
45
  res.on('end', () => {
41
46
  try {
42
47
  const json = JSON.parse(data);
48
+ // HACK: Attach ETag to the object to pass it up
49
+ if (etag && typeof json === 'object') {
50
+ Object.defineProperty(json, '_etag', { value: etag, enumerable: false, writable: true });
51
+ }
43
52
  resolve(json);
44
53
  }
45
54
  catch (e) {
46
55
  log(`JSON Parse Error for ${url}: ${e}`);
47
- resolve([]); // Fail safe -> empty list to trigger fallback logic
56
+ resolve([]); // Fail safe
48
57
  }
49
58
  });
50
59
  });
@@ -58,7 +67,6 @@ function fetchJson(url, headers = {}) {
58
67
  });
59
68
  });
60
69
  }
61
- // --- CACHE MANAGER ---
62
70
  function loadCache() {
63
71
  try {
64
72
  if (fs.existsSync(CACHE_FILE)) {
@@ -71,10 +79,11 @@ function loadCache() {
71
79
  }
72
80
  return null;
73
81
  }
74
- function saveCache(models) {
82
+ function saveCache(models, etag) {
75
83
  try {
76
84
  const data = {
77
85
  timestamp: Date.now(),
86
+ etag: etag,
78
87
  models: models
79
88
  };
80
89
  if (!fs.existsSync(CONFIG_DIR_POLLI))
@@ -89,25 +98,43 @@ function saveCache(models) {
89
98
  export async function generatePollinationsConfig(forceApiKey, forceStrict = false) {
90
99
  const config = loadConfig();
91
100
  const modelsOutput = [];
92
- log(`Starting Configuration (v5.8.2-Robust)...`);
101
+ log(`Starting Configuration (v5.8.4-Debug-Tools)...`);
93
102
  const effectiveKey = forceApiKey || config.apiKey;
94
- // 1. FREE UNIVERSE (Cache System)
103
+ // 1. FREE UNIVERSE (Smart Cache System)
95
104
  let freeModelsList = [];
96
105
  let isOffline = false;
97
106
  let cache = loadCache();
98
107
  const CACHE_TTL = 7 * 24 * 3600 * 1000; // 7 days
99
- // Decision: Fetch or Cache?
108
+ // Decision Logic
100
109
  const now = Date.now();
101
110
  let shouldFetch = !cache || (now - cache.timestamp > CACHE_TTL);
111
+ // ETag Check: If cache is valid but we want to be proactive
112
+ if (!shouldFetch && cache && cache.etag) {
113
+ try {
114
+ log('Smart Refresh: Checking for updates (HEAD)...');
115
+ const remoteEtag = await fetchHead('https://text.pollinations.ai/models');
116
+ if (remoteEtag && remoteEtag !== cache.etag) {
117
+ log(`Update Detected! (Remote: ${remoteEtag} != Local: ${cache.etag}). Forcing refresh.`);
118
+ shouldFetch = true;
119
+ }
120
+ else {
121
+ log('Cache is clean (ETag match). No refresh needed.');
122
+ }
123
+ }
124
+ catch (e) {
125
+ log(`Smart Refresh check failed: ${e}. Ignoring.`);
126
+ }
127
+ }
102
128
  if (shouldFetch) {
103
- log('Attempting to fetch fresh Free models...');
129
+ log('Fetching fresh Free models...');
104
130
  try {
105
131
  const raw = await fetchJson('https://text.pollinations.ai/models');
106
132
  const list = Array.isArray(raw) ? raw : (raw.data || []);
133
+ const newEtag = raw._etag; // Get hidden ETag
107
134
  if (list.length > 0) {
108
135
  freeModelsList = list;
109
- saveCache(list);
110
- log(`Fetched and cached ${list.length} models.`);
136
+ saveCache(list, newEtag);
137
+ log(`Fetched and cached ${list.length} models (ETag: ${newEtag || 'N/A'}).`);
111
138
  }
112
139
  else {
113
140
  throw new Error('API returned empty list');
@@ -116,29 +143,27 @@ export async function generatePollinationsConfig(forceApiKey, forceStrict = fals
116
143
  catch (e) {
117
144
  log(`Fetch failed: ${e}.`);
118
145
  isOffline = true;
119
- // Fallback to Cache or Default
146
+ // Fallback Logic
120
147
  if (cache && cache.models.length > 0) {
121
148
  log('Using cached models (Offline).');
122
149
  freeModelsList = cache.models;
123
150
  }
124
151
  else {
125
152
  log('Using DEFAULT SEED models (Offline + No Cache).');
126
- freeModelsList = DEFAULT_FREE_MODELS;
153
+ freeModelsList = FREE_MODELS_SEED;
127
154
  }
128
155
  }
129
156
  }
130
157
  else {
131
- log('Cache is recent. Using cached models.');
158
+ log('Using cached models (Skipped fetch).');
132
159
  freeModelsList = cache.models;
133
160
  }
134
161
  // Map Free Models
135
162
  freeModelsList.forEach((m) => {
136
- // Appending (Offline) if we are in offline mode due to error,
137
- // OR (Cache) if we just used cache? User said: "rajoutant dans les noms de modeles à la fin (down)"
138
- // when valid list is reached but date > 8 days (deprecated) or fallback used?
139
- // Let's mark it only if we tried to fetch and failed.
163
+ // Tag (Offline) only if we explicitly failed a fetch attempt or are using Fallback SEED when fetch failed.
164
+ // If we use cache because it's valid (Skipped fetch), we don't tag (Offline).
140
165
  const suffix = isOffline ? ' (Offline)' : '';
141
- const mapped = mapModel(m, 'free/', `[Free] `, suffix);
166
+ const mapped = mapModel(m, 'free-', `[Free] `, suffix);
142
167
  modelsOutput.push(mapped);
143
168
  });
144
169
  // 2. ENTERPRISE UNIVERSE
@@ -151,17 +176,17 @@ export async function generatePollinationsConfig(forceApiKey, forceStrict = fals
151
176
  enterList.forEach((m) => {
152
177
  if (m.tools === false)
153
178
  return;
154
- const mapped = mapModel(m, 'enter/', '[Enter] ');
179
+ const mapped = mapModel(m, 'enter-', '[Enter] ');
155
180
  modelsOutput.push(mapped);
156
181
  });
157
182
  log(`Total models (Free+Pro): ${modelsOutput.length}`);
183
+ log(`Generated IDs: ${modelsOutput.map(m => m.id).join(', ')}`);
158
184
  }
159
185
  catch (e) {
160
186
  log(`Error fetching Enterprise models: ${e}`);
161
187
  if (forceStrict)
162
188
  throw e;
163
- // Fallback Enter (could be cached too in future)
164
- modelsOutput.push({ id: "enter/gpt-4o", name: "[Enter] GPT-4o (Fallback)", object: "model", variants: {} });
189
+ // STRICT: No Fallback for Enterprise. If API is down, we have 0 Enter models.
165
190
  }
166
191
  }
167
192
  return modelsOutput;
@@ -209,14 +234,16 @@ function mapModel(raw, prefix, namePrefix, nameSuffix = '') {
209
234
  const modelObj = {
210
235
  id: fullId,
211
236
  name: finalName,
212
- object: 'model',
213
- variants: {},
237
+ // object: 'model',
238
+ // variants: {}, // POTENTIAL SCHEMA VIOLATION
214
239
  modalities: {
215
240
  input: raw.input_modalities || ['text'],
216
241
  output: raw.output_modalities || ['text']
217
- }
242
+ },
243
+ tool_call: false // FORCE DEBUG DISABLE
218
244
  };
219
245
  // Enrichissements
246
+ /*
220
247
  if (raw.reasoning === true || rawId.includes('thinking') || rawId.includes('reasoning')) {
221
248
  modelObj.variants = { ...modelObj.variants, high_reasoning: { options: { reasoningEffort: "high", budgetTokens: 16000 } } };
222
249
  }
@@ -228,16 +255,24 @@ function mapModel(raw, prefix, namePrefix, nameSuffix = '') {
228
255
  if (rawId.includes('claude') || rawId.includes('mistral') || rawId.includes('llama')) {
229
256
  modelObj.variants.safe_tokens = { options: { maxTokens: 8000 } };
230
257
  }
258
+ */
231
259
  if (rawId.includes('nova')) {
232
- modelObj.limit = { output: 8000, context: 128000 };
233
- }
234
- if (rawId.includes('nomnom') || rawId.includes('scrape')) {
235
- modelObj.limit = { output: 2048, context: 32768 };
236
- }
260
+ if (rawId.includes('nova')) {
261
+ modelObj.limit = { output: 8000, context: 128000 };
262
+ }
263
+ if (rawId.includes('nomnom') || rawId.includes('scrape')) {
264
+ modelObj.limit = { output: 2048, context: 32768 };
265
+ }
266
+ if (rawId.includes('chicky')) {
267
+ modelObj.limit = { output: 8192, context: 8192 };
268
+ }
269
+ /*
237
270
  if (rawId.includes('fast') || rawId.includes('flash')) {
238
271
  if (!rawId.includes('gemini')) {
239
272
  modelObj.variants.speed = { options: { thinking: { disabled: true } } };
240
273
  }
241
274
  }
275
+ */
276
+ }
242
277
  return modelObj;
243
278
  }
@@ -0,0 +1,18 @@
1
+ export interface PollinationsModel {
2
+ name: string;
3
+ description?: string;
4
+ type?: string;
5
+ tools?: boolean;
6
+ reasoning?: boolean;
7
+ context?: number;
8
+ context_window?: number;
9
+ input_modalities?: string[];
10
+ output_modalities?: string[];
11
+ paid_only?: boolean;
12
+ vision?: boolean;
13
+ audio?: boolean;
14
+ community?: boolean;
15
+ censored?: boolean;
16
+ [key: string]: any;
17
+ }
18
+ export declare const FREE_MODELS_SEED: PollinationsModel[];
@@ -0,0 +1,55 @@
1
+ export const FREE_MODELS_SEED = [
2
+ {
3
+ "name": "gemini",
4
+ "description": "Gemini 2.5 Flash Lite",
5
+ "tier": "anonymous",
6
+ "tools": true,
7
+ "input_modalities": ["text", "image"],
8
+ "output_modalities": ["text"],
9
+ "vision": true
10
+ },
11
+ {
12
+ "name": "mistral",
13
+ "description": "Mistral Small 3.2 24B",
14
+ "tier": "anonymous",
15
+ "tools": true,
16
+ "input_modalities": ["text"],
17
+ "output_modalities": ["text"],
18
+ "vision": false
19
+ },
20
+ {
21
+ "name": "openai-fast",
22
+ "description": "GPT-OSS 20B Reasoning LLM (OVH)",
23
+ "tier": "anonymous",
24
+ "tools": true,
25
+ "input_modalities": ["text"],
26
+ "output_modalities": ["text"],
27
+ "vision": false,
28
+ "reasoning": true
29
+ },
30
+ {
31
+ "name": "bidara",
32
+ "description": "BIDARA (Biomimetic Designer)",
33
+ "tier": "anonymous",
34
+ "community": true,
35
+ "input_modalities": ["text", "image"],
36
+ "output_modalities": ["text"],
37
+ "vision": true
38
+ },
39
+ {
40
+ "name": "chickytutor",
41
+ "description": "ChickyTutor AI Language Tutor",
42
+ "tier": "anonymous",
43
+ "community": true,
44
+ "input_modalities": ["text"],
45
+ "output_modalities": ["text"]
46
+ },
47
+ {
48
+ "name": "midijourney",
49
+ "description": "MIDIjourney",
50
+ "tier": "anonymous",
51
+ "community": true,
52
+ "input_modalities": ["text"],
53
+ "output_modalities": ["text"]
54
+ }
55
+ ];
@@ -153,6 +153,11 @@ export async function handleChatCompletion(req, res, bodyRaw) {
153
153
  const config = loadConfig();
154
154
  // DEBUG: Trace Config State for Hot Reload verification
155
155
  log(`[Proxy Request] Config Loaded. Mode: ${config.mode}, HasKey: ${!!config.apiKey}, KeyLength: ${config.apiKey ? config.apiKey.length : 0}`);
156
+ // SPY LOGGING
157
+ try {
158
+ fs.appendFileSync('/tmp/opencode_spy.log', `\n\n=== REQUEST ${new Date().toISOString()} ===\nMODEL: ${body.model}\nBODY:\n${JSON.stringify(body, null, 2)}\n==========================\n`);
159
+ }
160
+ catch (e) { }
156
161
  // 0. COMMAND HANDLING
157
162
  if (body.messages && body.messages.length > 0) {
158
163
  const lastMsg = body.messages[body.messages.length - 1];
@@ -416,13 +421,32 @@ export async function handleChatCompletion(req, res, bodyRaw) {
416
421
  // Restore Tools but REMOVE conflicting ones (Search)
417
422
  // B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
418
423
  // Handles: "tools" vs "grounding" conflicts, and "infinite loops" via Stop Sequences.
424
+ // GLOBAL BEDROCK FIX (All Models)
425
+ // Check if history has tools but current request misses tools definition.
426
+ // This happens when OpenCode sends the Tool Result (optimisation),
427
+ // but Bedrock requires toolConfig to validate the history.
428
+ const hasToolHistory = proxyBody.messages?.some((m) => m.role === 'tool' || m.tool_calls);
429
+ if (hasToolHistory && (!proxyBody.tools || proxyBody.tools.length === 0)) {
430
+ // Inject Shim Tool to satisfy Bedrock
431
+ proxyBody.tools = [{
432
+ type: 'function',
433
+ function: {
434
+ name: '_bedrock_compatibility_shim',
435
+ description: 'Internal system tool to satisfy Bedrock strict toolConfig requirement. Do not use.',
436
+ parameters: { type: 'object', properties: {} }
437
+ }
438
+ }];
439
+ log(`[Proxy] Bedrock Fix: Injected shim tool for ${actualModel} (History has tools, Request missing tools)`);
440
+ }
419
441
  // B. GEMINI UNIFIED FIX (Free, Fast, Pro, Enterprise, Legacy)
420
442
  // Fixes "Multiple tools" error (Vertex) and "JSON body validation failed" (v5.3.5 regression)
421
- else if (actualModel.includes("gemini")) {
443
+ // Added ChickyTutor (Claude/Gemini based) to fix "toolConfig must be defined" error.
444
+ else if (actualModel.includes("gemini") || actualModel.includes("chickytutor")) {
422
445
  let hasFunctions = false;
423
446
  if (proxyBody.tools && Array.isArray(proxyBody.tools)) {
424
447
  hasFunctions = proxyBody.tools.some((t) => t.type === 'function' || t.function);
425
448
  }
449
+ // Old Shim logic removed (moved up)
426
450
  if (hasFunctions) {
427
451
  // 1. Strict cleanup of 'google_search' tool
428
452
  proxyBody.tools = proxyBody.tools.filter((t) => {
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": "5.8.3",
4
+ "version": "5.8.4-beta.10",
5
5
  "description": "Native Pollinations.ai Provider Plugin for OpenCode",
6
6
  "publisher": "pollinations",
7
7
  "repository": {
@@ -55,4 +55,4 @@
55
55
  "@types/node": "^20.0.0",
56
56
  "typescript": "^5.0.0"
57
57
  }
58
- }
58
+ }